Skip to main content

Module wire

Module wire 

Source
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 can match on the variant once and dispatch.
Response
Reply shape for every Request variant. 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 separate Notification type in Phase 2).
WireError
Serializable mirror of ControlError. The trait’s Internal variant carries anyhow::Error which doesn’t serialize; we lose the typed downcast across the wire but keep the message — which is fine because clients can only ever treat Internal as 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 — ListSessions reply 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=/foo and 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_BYTES so a malformed prefix can’t trigger an unbounded allocation.
write_msg
Write a length-prefixed CBOR-encoded message.