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_*, vitrin_launcher, vitrin_layout_*, vitrin_egress
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 (realm-0 is the well-known realm and is always served; a deployment may configure more)
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)
vitrin_launcherRealm-launch facet (since wire version 2) — fork a new realm instance from an operator-written template, under a core-minted id; launch carries no arguments, so the command never crosses the wire
vitrin_layout_focusFocus facet (since wire version 2) — bind the output to the granted realm and send the human’s own input there, one act
vitrin_layout_arrangeArrangement facet (since wire version 2) — fill the output, or compose at the app’s own size; place, resize, raise and stacking are absent rather than refused
vitrin_powerboxDesignation facet (since wire version 2) — ask the human to pick one file or one directory subtree and have the descriptor delivered to the realm; no path crosses the wire in either direction. Vocabulary only so far: no deployment serves the verb, so vitrind mints the facet (issue #322) and refuses every ask not_granted until the picker lands
vitrin_egressEgress facet (since wire version 2) — one outbound connection to the single host:port the grant names, handed back as a socket fd. No deployment serves the egress verb: vitrind mints the facet and refuses every request_connect not_granted (issue #322), because the out-of-core mediating proxy does not exist

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 · resource_exhausted

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

not_granted · expired · revoked · rate_limited · preempted · consent_held · no_surface · internal · capacity

Those are the IDL’s own spellings, which is what a client in another language has to match. The Python SDK’s exception names are deliberately not all identical to them — expired is GrantExpired, internal (7, recoverable) is OperationFailed, capacity is AtCapacity, and the fatal internal (8) is InternalError — so transcribe from the IDL, never from an SDK.

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 every defined verb bit, served or not, and every defined outcome and refusal code: one it did not know would turn a recoverable answer 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. The wire integer is now 2, and it appends:

  • the realm_launch verb, and vitrin_principal’s attention event;
  • five structural mints on vitrin_grantget_launcher, get_layout_focus, get_layout_arrange, get_powerbox, get_egress;
  • the five interfaces they mint — vitrin_launcher (launch/launched), vitrin_layout_focus (focus), vitrin_layout_arrange (set_fullscreen), vitrin_powerbox (request_file, request_dir, designated, refused), vitrin_egress (request_connect, connected, connect_failed) — and the vitrin_layout_arrange.mode, vitrin_powerbox.mode, .kind and .refusal, and vitrin_egress.failure enums;
  • the designate_file verb, the file:/dir: resource prefixes, and vitrin_shim_session.designation — the powerbox vocabulary, refused unsupported by every deployment until the core-drawn picker and its consent copy exist. This core dispatches the messages as of issue #322 — get_powerbox mints, and both requests on the facet it mints refuse not_granted recoverably. Until then it had no arm for any of the three, so sending one was fatal invalid_opcode rather than a mint or a refusal;
  • the egress verb (128, at P2.7.2) and the net:HOST:PORT value its authority is named with. That half landed as a verb bit and a resource grammar, and no message at all; its facet followed separately. Every deployment still refuses the verb unsupported, because the out-of-core proxy a connection would be made through does not exist — a facet is a request to ask through, not a mechanism to answer with. get_egress and vitrin_egress.request_connect were unhandled on the same terms, and issue #322 closed both halves at once: the five requests this core did not dispatch were the two mints and the three facet requests behind them — not the two mints alone, which is what this list said while the two facets were landing on parallel branches, and the count is why one fix had to cover all five;
  • the capacity refusal code and the layout_held outcome;
  • on vitrin_shim_session, the cross-realm clipboard, the pointer constraints and the idle inhibit (request_selection, offer_selection, pointer_constraint_state, selection, pointer_constraint, idle_inhibit), and on vitrin_shim_seat, relative_motion and the four gesture events — plus the enums their arguments carry.

It changes nothing else: every version-1 signature is byte-identical at version 2. The complete, normative enumeration — every message, and every enum with a count to check the list against — is 00-conventions.md §7.3, and this list restates it. 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.