Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The wire protocol

You need this chapter if you are writing a client in a language the project does not ship an SDK for, or debugging one that exists.

The IDL is the source of truth. protocol/vitrin-v0.xml defines every interface, and where this book and an IDL <description> disagree, the IDL wins. docs/protocol/00-conventions.md is the normative conventions page this chapter summarises.

Shape

Wayland-influenced and deliberately so: object-oriented, per-connection object ids, requests and events over a Unix socket, file descriptors passed by SCM_RIGHTS. If you have written a Wayland client, this will feel familiar.

What is different is that authority is a first-class object. vitrin_grant is on the wire, with a lifecycle you can observe.

Framing

One message, one frame. All multi-byte integers little-endian. Every frame opens with an 8-byte header:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+---------------------------------------------------------------+
|                        object_id (u32)                        |
+-------------------------------+---------------+---------------+
|           size (u16)          |  opcode (u8)  | fd_count (u8) |
+-------------------------------+---------------+---------------+
|                       argument payload                        |
FieldNotes
object_idThe target. Per-connection; referencing an unknown id is fatal invalid_object.
sizeThe whole frame including this 8-byte header. Below 8, or a payload shorter than declared, is fatal oversized. The u16 ceiling of 65535 binds senders: do not construct a larger frame.
opcodeThe request or event opcode within the target’s interface, at the negotiated version.
fd_countHow many descriptors accompany this frame.

fd_count living in the header is a deliberate framing invariant, not an accident of the current signatures: a receiver can drop a frame it cannot interpret and still consume exactly the right number of descriptors, so an unknown message never desynchronises the fd stream. If the header’s fd_count disagrees with the target message’s signature, that is fatal.

At most one fd per message. Nothing in v0 needs two.

Argument types

TypeEncoding
intsigned 32-bit LE
uintunsigned 32-bit LE
objectu32 object id — may be 0 only where the IDL declares allow-null
new_idu32 id the sender allocates for the object being created
stringlength-prefixed, see the conventions page for padding
fdnot in the payload — carried out-of-band, counted by fd_count

Two connection classes, one wire format

ClassReaches the core byInterfaces
Agent principalConnecting to the core’s listening socket and authenticatingvitrin_handshake, vitrin_principal, vitrin_realm, vitrin_grant, vitrin_consent, vitrin_view, vitrin_actuator_*
ShimInheriting a socketpair from the core across fork/execvitrin_shim_session, vitrin_shim_surface, vitrin_shim_seat

The classes are mutually unreachable. A message using the other class’s opcodes dies as fatal invalid_opcode, with no special-casing anywhere.

Note what a shim’s identity is: holding the inherited descriptor. There is no shim credential and no shim handshake — the socketpair is the authentication, because the core created both ends itself.

The interfaces

InterfacePurpose
vitrin_handshakeVersion + identity hello, resolving to a bound principal
vitrin_principalThe authenticated principal — root of the connection’s authority chain
vitrin_realmRealm address; an authority-free scope handle (v0 serves the well-known realm-0)
vitrin_grantCapability handle — the wire projection of one grant-table row
vitrin_consentConsent-prompt visibility for one petition (events only, no authority)
vitrin_viewObservation facet — poll-model frame capture
vitrin_actuator_pointerPointer actuation facet
vitrin_actuator_textText actuation facet
vitrin_shim_sessionShim connection bootstrap
vitrin_shim_surfaceShim-to-core buffer path
vitrin_shim_seatInput delivery to the shim (events only, origin-tagged)

Each has a prose page under docs/protocol/.

The petition, in one request

A grant petition co-mints, in a single request: the grant, a consent observer, and the facets (view, pointer, text). The client allocates a new_id for each.

The facets are born inert. They are real objects from the moment the request is sent, and they confer nothing until the grant resolves. That shape is why there is no window in which a client holds a live handle to an unapproved capability — the alternative, minting facets after approval, would need a second round trip and a state machine on both sides.

client                                        core
  │  request_grant(new_id grant,                │
  │                new_id consent,              │
  │                new_id view, ptr, text)      │
  ├────────────────────────────────────────────>│
  │                                             │  petition registered,
  │              consent.state(shown)           │  prompt raised
  │<────────────────────────────────────────────┤
  │                                             │
  │              grant.resolved(outcome, verbs, │  human decides
  │                             expiry_ms, ...) │
  │<────────────────────────────────────────────┤
  │                                             │
  │  view.capture_frame()                       │
  ├────────────────────────────────────────────>│
  │              view.frame_ready(+ memfd)      │
  │<────────────────────────────────────────────┤

resolved carries the effective verbs and expiry, which may be narrower than requested. A client that assumes it got what it asked for is wrong.

The error razor

The single most important thing to get right.

Fatal — you violated the protocol. The core closes the connection. No retry helps, because the bug is in your client.

invalid_object · invalid_opcode · invalid_argument · oversized · fd_violation · pre_handshake · version_unsupported · auth_failed · internal_error · resource_exhausted

Recoverable — your request failed. The connection is healthy and you may try something else.

not_granted · grant_expired · revoked · rate_limited · preempted · consent_held · no_surface · operation_failed

The line is: did the client send something incoherent, or did a coherent request get refused? Setting an out-of-range verb bit is incoherent — fatal. Asking for a verb the core does not serve is coherent — refused, unsupported. This is exactly why the Python SDK carries the three defined-but-unserved verbs: an SDK that omitted them would turn a recoverable refusal into a dead socket.

Ordering

Requests on one connection are processed in order, and events for one object arrive in order. sync is a barrier: it returns once all prior requests are processed and their events delivered — petition resolution excepted, since that waits on a human.

Versioning

Wayland-style. Interfaces grow by appending messages and appending enum values; existing opcodes never change meaning. The negotiated version comes out of the handshake, and each side serves only what that version defines.

Version 0 is frozen for Phase 1 — not forever. Phase 2 brings semantic trees and epoch/CAS action semantics. Expect to move.

Validating your understanding

The IDL is machine-checked and so is the code generated from it:

xmllint --noout --relaxng protocol/vitrin-v0.rng protocol/vitrin-v0.xml
cargo xtask codegen --check     # generated Rust + C header match the IDL

crates/vitrin-golden holds golden frame vectors, and the Python SDK’s tests/test_golden_vectors.py checks its encoder against the same bytes. Those vectors are the cheapest way to validate a new client’s codec.

Next: Build your own client or shim.