1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
//! # khive wire protocol
//!
//! This crate is the normative specification of khive's frame protocol
//! (ADR-137: "Tailnet Wire Transport for the khive Frame Protocol"). It
//! defines framing, the handshake sequence, every frame kind and its
//! fields, the closed wire error taxonomy, and protocol versioning. A
//! non-Rust client can implement this protocol from this documentation
//! alone — that is the ADR's stated bar for this crate, and the sections
//! below are written to meet it.
//!
//! This crate does no I/O and depends on no async runtime. It is types, a
//! byte-buffer codec, and a handshake state machine; a transport crate
//! (Unix-domain socket, tailnet TCP, or any other carrier of the same
//! framing) drives it.
//!
//! ## Framing
//!
//! One wire frame is:
//!
//! ```text
//! +----------------------------+----------------------------------+
//! | length: u32, big-endian | payload: `length` bytes of JSON |
//! | (4 bytes) | |
//! +----------------------------+----------------------------------+
//! ```
//!
//! `length` is the byte length of the JSON payload only — it does not
//! include itself. This is the same framing the existing Unix-domain-socket
//! transport uses (`crates/khive-runtime/src/daemon.rs`, `read_frame` /
//! `write_frame`): a 4-byte big-endian `u32` length prefix followed by JSON
//! bytes. This crate defines what that JSON is; it does not change the
//! outer framing.
//!
//! The default maximum frame size is **8 MiB**
//! ([`codec::DEFAULT_MAX_FRAME_BYTES`]), matching the existing transport's
//! configured limit. A deployment may configure a different maximum via
//! [`codec::FrameCodec::new`]. A frame whose declared length exceeds the
//! configured maximum is rejected at decode with
//! [`codec::CodecError::FrameTooLarge`], which a server maps to the wire
//! error [`error::WireErrorCode::FrameTooLarge`] and closes the connection
//! — the frame is never partially buffered.
//!
//! [`codec::decode_frame`] and [`codec::encode_frame`] (or the
//! [`codec::FrameCodec`] wrapper) operate on one complete frame's bytes at a
//! time. They do not support partial/streaming reads: a transport reading
//! from a socket is responsible for buffering until it has the 4-byte
//! prefix and then the declared number of payload bytes before calling
//! into this crate.
//!
//! **Decode errors are connection-terminal.** A decode error carries NO
//! consumed byte count: once a frame fails to decode, the stream position
//! is unrecoverable and the transport cannot find the next frame's start.
//! A transport must map the error through [`codec::CodecError::wire_code`],
//! send the corresponding wire error, and close the connection — never
//! attempt to resynchronize and keep reading.
//!
//! ## The JSON payload: frame kind and fields
//!
//! Every payload is a JSON object with a `"kind"` string field naming one
//! of the eleven closed frame kinds, plus that kind's own fields flattened
//! into the same object (an internally tagged encoding). For example, a
//! `cancel` frame referencing operation id `"op-42"`:
//!
//! ```json
//! {"kind":"cancel","id":"op-42"}
//! ```
//!
//! The eleven frame kinds ([`frame::FRAME_KINDS`]), grouped by role:
//!
//! | Kind | Direction | Fields | Role |
//! | ------------------ | --------------- | -------------------------------------------------------------- | ---- |
//! | `handshake` | client → server | `version: u32` | First frame on every connection; names the client's protocol version. |
//! | `handshake_ack` | server → client | `version: u32` | Accepts a `handshake`; names the version the connection now speaks. |
//! | `request` | client → server | `id: string`, `ops: string`, `deadline_ms?: u64`, `namespace?: string`, `actor_id?: string`, `visible_namespaces?: string[]` | One DSL batch/chain (ADR-016) to execute. The three optional identity-override fields exist for transports that accept caller-supplied context; a mapped transport rejects a request carrying any of them with `context_rejected`. |
//! | `response` | server → client | `id: string`, `result: json` | Successful terminal frame for a `request`; `result` is the verb-dispatch result, opaque to this crate. |
//! | `error` | server → client | `id?: string`, `code: string`, `message: string` | Wire-level failure. `id` is present for a request-scoped error, absent for a connection-terminal error. |
//! | `cancel` | client → server | `id: string` | Asks the server to terminate the named `request`. No-op on an unknown/subscribe/unsubscribe/already-terminal id. |
//! | `subscribe` | client → server | `id: string`, `topic: string`, `resume_cursor?: u64` | Opens delivery for one topic. |
//! | `subscribe_ack` | server → client | `id: string`, `topic: string`, `start_cursor: u64` | Confirms a `subscribe`; names the cursor delivery begins after. |
//! | `unsubscribe` | client → server | `id: string`, `topic: string` | Ends delivery for one topic. Idempotent no-op if not subscribed. |
//! | `unsubscribe_ack` | server → client | `id: string`, `topic: string` | Confirms an `unsubscribe`. |
//! | `event` | server → client | `topic: string`, `cursor: u64`, `occurred_at: string`, `payload: json` | One state-change delivery. Carries no operation id — correlated by topic and ordered by `cursor` instead. `occurred_at` is RFC 3339. `payload`'s field-by-field shape is owned by the per-topic catalog (ADR-137, "Implementation-phase deliverables"), not by this crate. |
//!
//! The set is closed within one protocol version. A decoder that sees a
//! `"kind"` value outside this table rejects the frame
//! ([`codec::CodecError::UnknownFrameKind`]) rather than skipping it.
//!
//! ## Strict field rejection (closed grammar)
//!
//! The grammar is closed in both dimensions — kinds AND fields. Every
//! payload is parsed by its kind's payload struct
//! ([`frame::HandshakePayload`], [`frame::RequestPayload`], ...), each of
//! which carries `#[serde(deny_unknown_fields)]`: a payload carrying any
//! field its kind does not declare is rejected with
//! [`codec::CodecError::InvalidFields`], never silently ignored. Unknown
//! fields are rejected within a protocol version; forward compatibility is
//! carried by the version handshake ([`version::ProtocolVersion`]), not by
//! field tolerance — new fields arrive by bumping the protocol version and
//! teaching the new version's grammar about them. This is the fail-closed
//! posture ADR-137 requires: a decoder never guesses that an unrecognized
//! field is ignorable.
//!
//! The strictness covers the fields each frame KIND declares. The two
//! opaque JSON values — `response.result` and `event.payload` — are data,
//! not grammar: keys inside them are preserved, and their field-by-field
//! shape is owned by the verb result surface (ADR-016) and the per-topic
//! event catalog respectively, not by this crate.
//!
//! Three deliberate boundaries of the strictness, stated so nobody
//! re-derives them:
//!
//! - **Explicit `null` on an optional field is equivalent to absence.**
//! Optional payload fields are `Option<T>`; a member present with value
//! `null` decodes as absent and re-encodes with the member omitted. No
//! frame distinguishes present-null from absent.
//! - **Duplicate members are last-wins, not rejected.** Payloads pass
//! through `serde_json`'s object model before field checking, so a
//! duplicated member name silently keeps the last occurrence —
//! `deny_unknown_fields` cannot see the earlier one. Rejecting
//! duplicates would require validating the raw document; the grammar
//! takes the documented last-wins stance instead.
//! - **`topic` syntax is not validated here.** The codec accepts any JSON
//! string (including empty) for `subscribe`/`unsubscribe`/`event`
//! topics; the `<domain>.<event>` shape is enforced by the server
//! against its topic catalog, where the catalog lives.
//!
//! ## Opaque payload fidelity
//!
//! `response.result` and `event.payload` are preserved as JSON VALUES —
//! semantic equality, not byte-for-byte. Decoding and re-encoding yields a
//! payload semantically equal to the original under the JSON data model,
//! but the wire bytes may differ in two documented ways, both consequences
//! of this workspace's `serde_json` feature set (default features only —
//! no `preserve_order`, no `arbitrary_precision`):
//!
//! - **Object key order is not preserved.** Decoded objects use
//! `serde_json`'s `BTreeMap`-backed map and re-encode with keys in
//! sorted order. Consumers must treat key order as insignificant.
//! - **Integers outside the u64/i64 range lose precision.** Integers
//! within u64/i64 range — including values above 2^53 — parse and
//! re-encode exactly; anything outside that range parses as `f64` and
//! may lose precision.
//!
//! The codec's `opaque_payloads_are_preserved_semantically_not_byte_for_byte`
//! test pins exactly this behavior.
//!
//! ## Server-produced fields
//!
//! `event.occurred_at` (RFC 3339) and `event.topic` are SERVER-PRODUCED
//! fields: the server validates them when it produces an event, and the
//! codec accepts them as plain strings without parsing. This crate
//! deliberately takes no timestamp-parsing dependency for decode-side
//! validation of server-produced data.
//!
//! ## Handshake sequence
//!
//! 1. The client opens the transport connection (Unix-domain socket or
//! tailnet TCP) and sends `handshake` as its first frame, naming the
//! highest protocol version it supports.
//! 2. The server checks that version against its own supported range
//! ([`version::SupportedVersions`]):
//! - If supported, it replies `handshake_ack` naming the accepted
//! version. The connection now accepts `request`, `subscribe`,
//! `unsubscribe`, and `cancel` frames.
//! - If not supported, it replies `error` with code
//! `unsupported_version` (no `id` — connection-terminal) and closes
//! the connection. The client must surface this rejection; it must
//! not fall back to a different protocol or a local code path.
//! 3. No `request`, `subscribe`, `unsubscribe`, or `cancel` frame is valid
//! before step 2 completes successfully.
//!
//! [`handshake::HandshakeGate`] implements the server side of this sequence
//! as a type: it is fed every inbound frame and returns an admit/accept/
//! reject decision, so "no request frame before handshake completes" is a
//! property of the gate's API rather than a rule every call site has to
//! remember to check.
//!
//! ## Wire error taxonomy
//!
//! [`error::WireErrorCode`] is the closed set of wire-level error codes for
//! protocol version 1, matching ADR-137's "Wire error taxonomy" table
//! exactly (serialized as the `snake_case` names below). A wire error is
//! distinct from a DSL-level per-operation error (ADR-016's `{ok: false,
//! error}` result carried inside a *successful* `response` frame) — a wire
//! error is returned instead of, or before, DSL dispatch.
//!
//! | Code | Condition | Terminal |
//! | ---------------------------- | ----------------------------------------------------------------------------------------------- | -------- |
//! | `unsupported_version` | Handshake named no mutually supported protocol version. | Connection |
//! | `identity_rejected` | Whois lookup failed, no/invalid node mapping, or native-frame ingress by a class without access. | Connection |
//! | `malformed_frame` | A frame cannot be decoded or violates the frame grammar. | Connection |
//! | `frame_too_large` | A frame exceeds the configured maximum size. | Connection |
//! | `subscriber_overflow` | The connection's bounded outbound event queue reached its limit. | Connection |
//! | `subscription_revoked` | A live mapping/class change short of deletion removed authorization for an active subscription. | Connection |
//! | `context_rejected` | A frame-level attempt to supply `namespace`/`actor_id`/`visible_namespaces` on a mapped transport. | Request |
//! | `peer_class_denied` | A parsed operation names a verb outside the mapped class allowlist. | Request |
//! | `subscription_denied` | A `subscribe` names a topic outside the mapped class's allowed-topic set. | Request |
//! | `already_subscribed` | A `subscribe` names a topic with an already-active subscription on this connection. | Request |
//! | `cursor_expired` | A `subscribe` resume cursor is older than the topic's retention window. | Request |
//! | `in_flight_limit_exceeded` | A `request` arrives while the per-connection in-flight limit is reached. | Request |
//! | `deadline_exceeded` | The server abandoned the request at its deadline. | Request |
//! | `cancelled` | The request was terminated by a `cancel` frame before normal completion. | Request |
//! | `shutting_down` | The server is draining and refuses new work. | Request |
//! | `internal` | An unclassified server-side transport failure — also the fallback for an unrecognized code. | Request |
//!
//! A **connection-terminal** error is followed by connection close and
//! carries no operation id. A **request-terminal** error terminates only
//! the operation id it echoes; the connection stays usable. That pairing
//! is enforced at encode AND decode time: an `error` frame whose id
//! presence contradicts its code's terminal scope is rejected, never
//! represented and never emitted. The check lives in
//! [`frame::Frame`]'s serde visitor, so EVERY decode path agrees — the
//! codec's `decode_payload` path (which re-classifies the
//! rejection into the typed [`codec::CodecError::InconsistentErrorScope`])
//! AND a direct `serde_json::from_str::<Frame>`. The check covers the
//! same wire invariants in `Frame`'s serializer, so direct
//! `serde_json::to_vec::<Frame>` cannot bypass the encode-side validation.
//! On decode, the check covers the codes in the closed set
//! ([`error::WIRE_ERROR_CODES`]); an unrecognized
//! code falls back to `internal` and is processed per the fallback rule
//! above (its true scope is unknown to this version, so the pairing is
//! not enforced for it), but the raw code string is preserved in the
//! decoded frame's `unrecognized_code` diagnostic field rather than
//! discarded.
//! If that fallback has no operation id, a consumer has nothing safe to
//! correlate: it should surface the frame as a connection-level diagnostic
//! and must not guess which request to fail.
//!
//! **Fallback frames are not re-encodable by this relay.** A decoded frame
//! carrying `unrecognized_code` can be inspected locally, but the encode
//! path rejects it with [`codec::CodecError::FallbackFrameNotEncodable`]:
//! re-encoding would emit `internal` and silently discard the newer code
//! the peer sent, so this crate never corrupts it; the connection remains
//! healthy. A relay that must pass unknown codes through has to operate on
//! the raw frame bytes, not on a decoded-and-re-encoded frame. The one
//! decode-only guarantee the codec path holds over a direct serde decode is
//! the finer-grained
//! [`codec::CodecError::UnknownFrameKind`] classification for a `"kind"`
//! outside the closed set; every other decode-time rule — strict field
//! rejection, the id/scope pairing, and the unknown-code diagnostic — is
//! enforced identically on both paths.
//!
//! The set is
//! closed within a protocol version: adding a code requires a version bump,
//! and a client that decodes a code it does not recognize treats it as
//! `internal` (request-terminal) rather than inventing semantics for it —
//! [`error::WireErrorCode`] implements this with `#[serde(other)]`.
//!
//! ## Versioning and compatibility
//!
//! Protocol version numbers ([`version::ProtocolVersion`]) are monotonic
//! `u32`s. A server supports the current version and at least the
//! immediately prior version ([`version::SupportedVersions::current`]). A
//! breaking wire change — a new frame kind, a new wire error code, or a
//! non-backward-compatible change to an existing frame's fields — is never
//! introduced within a version number; it requires incrementing the
//! version and updating this documentation as the normative source.
//!
//! ## What this crate does not define
//!
//! - Transport (socket binding, TLS/WireGuard, `tailscale whois`, peer
//! mapping) — ADR-137's "Transport identity and actor mapping".
//! - The DSL carried in a `request` frame's `ops` string — ADR-016.
//! - The per-topic `event` payload catalog — ADR-137's
//! "Implementation-phase deliverables".
//! - Peer-class allowlists and dispatch enforcement — ADR-137's "Peer
//! classes and dispatch enforcement".
pub use ;
pub use ;
pub use ;
pub use ;
pub use ;