Expand description
Wire-format types for the tear-daemon ↔ tear-client RPC.
One Request variant per [MultiplexerControl] method; the
daemon dispatches on the variant and replies with a Response
whose shape matches the trait’s return type. The framing is
4-byte big-endian length-prefixed CBOR (RFC 8949) via ciborium.
CBOR was chosen over bincode because LayoutNode uses an
internally-tagged enum representation (#[serde(tag = "kind")])
that bincode rejects — CBOR handles every serde tagging style.
The size + speed difference is negligible at IPC scale (single
Request/Response per call, not a streaming hot path).
§Why this lives in tear-types
Both tear-daemon (server) and tear-client (client) need to
agree on the on-wire shape. Putting it here means there’s one
source of truth — no risk of the two crates drifting because each
re-declared the Request enum. Pure types only; the framing
helpers (read_msg / write_msg) take any Read/Write so
transports beyond UDS (stdio pipes for embedded use, TCP for
future remote modes) compose trivially.
§Versioning
The wire is CBOR + serde (see the framing note above — an earlier revision of this paragraph said “bincode”, which was stale: bincode was evaluated and rejected for the tagging reason stated above, and never shipped). Adding a new variant to either enum at the end is backwards-compatible (older clients ignore variants they don’t understand because they never emit them). Removing or reordering variants is a breaking wire change — bump the workspace minor version when that happens.
Field-level compatibility. Request is externally tagged and
its struct variants are encoded as field-name-keyed CBOR maps.
No type here sets deny_unknown_fields, so a #[serde(default)]
field is compatible in BOTH directions: an old daemon decoding a
new client’s frame ignores the key it doesn’t know, and a new
daemon decoding an old client’s frame fills the missing key from
Default. That is what makes a new field safe, and equally
what makes it silent: an old daemon drops the key and does
the old thing. For args that reads as “the program spawned
without its arguments”, with no error anywhere.
§Capability negotiation
Request::Hello / Response::Hello close that hole. A
client probes once at connect time and gets back a
crate::capability::DaemonHello naming every field/behaviour
the daemon implements; a call site that needs one refuses with
crate::ControlError::Unsupported instead of sending a frame
that will be half-ignored. Read
crate::capability for why this is a capability set and
not a protocol version integer.
An unknown variant does not have to end the connection. A
frame whose length prefix was honoured and whose bytes were all
consumed leaves the stream aligned at the next frame boundary
even when the payload names a variant the peer has never heard
of — measured, not assumed. read_frame surfaces that as
Framed::Undecodable so a server can answer
Response::Err(Rejected(..)) and keep serving, which is what
tear-daemon now does. Before that, serve_connection_full’s
read loop did Err(e) => return Err(e), so the first client
to send a variant the daemon didn’t know got a bare connection
close. A daemon built before this change still behaves that way,
which is why the client treats a lost connection during the
probe as “protocol 0 / no capabilities” and re-dials rather than
failing.
Enums§
- Framed
- Outcome of reading one frame off the wire.
- Request
- Every [
MultiplexerControl] operation, encoded as a single tagged enum so the daemon canmatchon the variant once and dispatch. - Response
- Reply shape for every
Requestvariant. The daemon always emits exactly one Response per Request — there is no streaming or multi-frame reply at this layer (subscription / event streams will land in a separateNotificationtype in Phase 2). - Wire
Error - Serializable mirror of
ControlError. The trait’sInternalvariant carriesanyhow::Errorwhich doesn’t serialize; we lose the typed downcast across the wire but keep the message — which is fine because clients can only ever treatInternalas opaque-and-fatal anyway.
Constants§
- MAX_
FRAME_ BYTES - Maximum frame size we’ll deserialize. Caps allocation on a
malformed length-prefix (16 MiB is far above any real Request
or Response —
ListSessionsreply with thousands of sessions is still well under a megabyte).
Functions§
- default_
socket_ path - Default UDS socket path. Resolves at call time so a daemon
started with
XDG_RUNTIME_DIR=/fooand a client started later without the var both look in the same place (the XDG fallback). - read_
frame - Read one length-prefixed CBOR frame, distinguishing an undecodable payload from an unreadable stream.
- read_
msg - Read a length-prefixed CBOR-encoded message. Caps the frame at
MAX_FRAME_BYTESso a malformed prefix can’t trigger an unbounded allocation. - write_
msg - Write a length-prefixed CBOR-encoded message.