Reference 05 — Protocol-defined TLVs

Status: normative, v1 (incorporated by docs/spec/v1.md §3 per RFC-0001 §A.2). Per-TLV byte-precise specification for every type code in the core-reserved range. The header layout, options bits, fixed-width length, and trailer (TS + CRC) are in 01-data-format.md; this document specifies what each type code’s payload looks like.


Type code allocation summary

Range

Use

0x00

Reserved sentinel; never a valid TLV

0x01 – 0x1F

Core protocol types (this document)

0x20 – 0x7F

Reserved for future core extensions

0x80 – 0xFF

User-defined application payload types (0x80 = BATCH, the one assigned code — §User range)

Assigned in the first block: 0x01–0x04, 0x06–0x0C, 0x0E (12 types). 0x05 is a reserved code with no assigned meaning in v1 (see §0x05); 0x0D ROUTER is a reserved, decodable codepoint with no implemented mechanism (see §0x0D). 0x0E is SPEC (vertex-creation spec, ADR-0017). The 0x0F–0x1F fast-track range holds the remote-operation, route-handle and bound-path frames (0x0F–0x15 assigned, §reserved range 0x0F – 0x1F); 0x20 – 0x7F is the long-term registry.

The names below are the canonical type-code names; an implementation’s own enumeration matches them.

Structured TLVs

Several core type codes are structured — they carry opt.PL=1 and their payload is a concatenation of child TLVs. In the first block the structured types are: 0x04 SUBSCRIBER, 0x07 POINT, 0x09 STATUS (when non-empty), 0x0A ACL, 0x0B SETTINGS, 0x0E SPEC. In the fast-track range 0x0F FWD, 0x10 FIELD and the 0x11–0x13 route-handle frames are structured as well (§the fast-track range); 0x14 PATH_REF and 0x15 PATH_REF_REVERSE are the two codes in that range that are not. Each entry below specifies its own children layout.

There is no generic container type: every structured container declares its purpose via its type code. User-range type codes (0x80–0xFF) MAY also be structured (set opt.PL=1) for application-defined records.

No address form is structured. 0x06 PATH carries a packed run of self-delimiting records — [u8 len][utf8] segment records and escape records (RFC-0018) — and 0x15 PATH_REF_REVERSE carries the same body (RFC-0029 §7.1); both set opt.PL = 0 (§0x06, §0x15). 0x14 is retired as an address form (§0x14). A PATH was a NAME-child container before RFC-0018.


0x01 — VALUE

Opaque application payload. No protocol-imposed structure; the bytes are whatever the publisher and subscriber agreed on out-of-band.

Payload layout

[ payload bytes — application-defined ]

The payload is a contiguous, untouched user region. Wire-time TS and CRC live in the optional trailer per 01-data-format.md, not interleaved with the payload.

Defaults

  • opt.PL = 0 (payload is opaque, not nested).

  • opt.CR recommended 1 for any non-loopback transport.

  • opt.TS recommended 1 when wire-time-stamping matters (latency telemetry, dedup tie-breaking).

  • For application-domain timestamps, embed a sibling TIME TLV inside a wrapping structured TLV (a user-range type code with opt.PL=1) instead.

Where it appears

  • Body of normal tracer_write / tracer_read.

  • Inside SUBSCRIBER records as the configuration scalar.

  • Inside SETTINGS as field values.

  • Inside STATUS as error-detail bytes.

Validation

  • No application-level validation by the core.

  • The receiver MUST validate length against the available buffer before reading payload bytes.

Hex example

5-byte payload AA BB CC DD EE, CRC-32 enabled, no wire-time (default LL=0 u16 length):

01 10 05 00 AA BB CC DD EE [crc:4]
^  ^  ^^^^^ ^^^^^^^^^^^^^^  ^^^^^
|  |  len=5  payload         trailer_crc (CRC-32C over payload)
|  opt = 0x10 (CR=1)
type = 0x01 VALUE

4 (header) + 5 (payload) + 4 (trailer_crc) = 13 bytes.

Same payload with absolute wire-time-stamp + CRC-32:

01 30 05 00 AA BB CC DD EE [ts:8] [crc:4]
^  ^  ^^^^^ ^^^^^^^^^^^^^^  ^^^^^  ^^^^^
|  |  len=5  payload         ts     CRC over payload+ts
|  opt = 0x30 (TS=1, CR=1)
type = 0x01 VALUE

4 + 5 + 8 + 4 = 21 bytes.


0x02 — NAME

A single name segment. UTF-8 bytes, no NUL terminator on the wire.

Payload layout

[ N bytes UTF-8 ]

Constraints

  • Length: 1..64 bytes (per 03-addressing.md §path syntax).

  • MUST NOT contain reserved characters (/ : . [ ] * ?).

  • MUST be valid UTF-8. Invalid byte sequences MUST be rejected with ERROR{tr::path::invalid}.

⚠️ Conformance gap — the reference implementation accepts [ and ] in a NAME. Its segment predicate rejects only / : . * ?, by its own account a deliberate temporary subset held until address-index parsing lands (03-addressing.md §reserved characters, core/include/libtracer/path.hpp:valid_segment). The rule above is unchanged.

Where it appears

  • Inside SETTINGS as field-name keys.

  • Inside :schema responses as field labels.

  • Inside :children[] reads, as the member name beside each POINT.

  • Wherever a “label” is needed inside a structured TLV.

Not inside a PATH. Until RFC-0018 a PATH (0x06) body was one NAME child per segment. It is now a packed run of [u8 len][utf8] records with no per-segment TLV header (§0x06 PATH). NAME is unchanged and un-retired — it keeps its type code, its fast-track range slot and every use above.

Hex example

NAME “sensor” (6 bytes), no trailer (typical when nested inside a structured TLV whose outer trailer covers everything):

02 00 06 00 73 65 6E 73 6F 72
^  ^  ^^^^^ ^^^^^^^^^^^^^^^^^
|  |  len=6  "sensor"
|  opt = 0 (no PL, no TS, no CR)
type = 0x02 NAME

4 (header) + 6 (payload) = 10 bytes.


0x03 — DESCRIPTION

Free-form UTF-8 human-readable description of a vertex or field. Optional in every context; tooling shows it to operators.

Payload layout

[ N bytes UTF-8 ]

Constraints

  • Length: 0..1024 bytes recommended; no hard upper limit beyond length field range.

  • MUST be valid UTF-8.

Where it appears

  • ⚠️ <vertex>:description field — unimplemented; a read or write answers tr::schema::not_found (#586).

  • Inside :schema responses annotating fields — reachable only through the RFC-0010 owner part, whose descriptor bytes are served verbatim; the synthesized protocol part emits no TEXT.

  • Inside ERROR TLVs as the human-readable detail.


0x04 — SUBSCRIBER

Subscription record. The presence of a SUBSCRIBER TLV at <vertex>:subscribers[N] causes the router to fan out future writes to that vertex to the subscriber’s target path.

Payload layout

Always structured (opt.PL=1). Children, in order:

SUBSCRIBER (PL=1) {
  PATH        target_path     ; required — where to dispatch matched writes
  SETTINGS    qos_settings    ; optional — QoS overrides for this subscription
  ACL         capability      ; optional — capability token if enforced
  NAME        subscriber_id   ; optional — opaque ID for self-identification
}

The qos_settings SETTINGS carries per-subscriber encoding hints and this subscription’s delivery policy (byte-agnostic; numeric filtering such as deadband is an application filter vertex, never a field — the sibling decision of ADR-0019). It carries no value-based delivery filter and no throttle: there is no delivery_mode == ON_CHANGE byte-diff, no min_interval_ns and no keepalive_ns (RFC-0008) — the runtime inspects neither values nor times to decide delivery. Delivery selection is structural and per-vertex; see delivery_mode below.

qos_settings = SETTINGS {
  NAME "delivery_scope"    VALUE <u8: DELTA=0, SNAPSHOT=1>   ; reserved (RFC-0005: as-written is the delivery; SNAPSHOT re-aggregation is a BRANCH WRITE)
  NAME "delivery_compact"  VALUE <u8: 0=off, 1=on>  ; opt into route-handle compaction (§route-handle)
  NAME "delivery_policy"   VALUE <u16>              ; this subscription's DELIVERY policy (RFC-0022 §3.A)
  NAME "batch_count"       VALUE <u32>              ; RETIRED (RFC-0025 §4.1.3) — carried verbatim, read by nothing
  NAME "batch_window_ns"   VALUE <u64>              ; RETIRED (RFC-0025 §4.1.3) — carried verbatim, read by nothing
}

delivery_scope = SNAPSHOT is no longer deferred. It was parked in RFC-0005 §E as “producer-side re-aggregation”; RFC-0025 §4.1.2 (Amendment 3, 2026-08-21) un-defers it by reframing it as a branch write: propagate gains a FOLD emission mode that emits one branch-write frame per sweep — the RFC-0016 POINT-tree grammar, rooted per §0x07’s branch-write shape, with payload TIME (§0x0C) children carrying stream-list times — instead of one FWD{WRITE} per selected vertex. The terminus is unchanged (§0x07 decomposition slices the one frame per covered subscriber), and it is not a wire batch container: one frame per subtree, several subtrees stay N self-contained frames in one send(iov) (the retired-LIST rule, §0x05). The delivery_scope key itself is still reserved — no implementation reads it today.

The mode is live host-side as graph_t::propagate(v, emission_mode_t::FOLD) — see 02-graph-model.md §”Assign, propagate, and the coalescing sweep” for the refusal rules — and it stays a producer-side choice per call: nothing about it is negotiated, readable or writable by a peer, and the default emission is unchanged.

The batch_count / batch_window_ns magnitudes are RETIRED (RFC-0025 §4.1.3, Amendment 4, 2026-08-23): batching is user-orchestrated, so when to swap and push is an application decision that never crosses into the graph — the app already holds the sample count it composed and owns the clock it stamped with. Neither key is honoured, and none will be. They keep the qos_settings verbatim-carry rule, so a subscription that spells one is not made non-conforming; nothing reads it. The producer owns cadence (RFC-0005 §E), there is no graph-plane timer, rate cap or scheduler, and explicit flush is the host-side graph_t::propagate.

delivery_policy (RFC-0022 §3.A) is one packed 16-bit value carried in this same SETTINGS child, so the per-subscription policy introduced no new wire structure:

bits

field

values

0–1

reliability

0 = best-effort, 1 = reliable; 2–3 reserved

2–4

priority

0–7, 0 = default

5

durability_request

1 = deliver the producer’s latched last value on join

6–7

delivery_class

0 = conflate (default), 1 = immediate, 2 = batch, 3 = stream (RFC-0025 §4.1) — assigned, not yet honoured; see below

8–15

reserved

MUST be written 0, MUST be ignored on read

Absent ⇒ all-zero ⇒ the default behaviour, byte-identically — a sender that predates the key is a conforming sender (subscriber/policy-absent). Reserved bits MUST be ignored, never rejected, and are carried verbatim so they read back unchanged from :subscribers[] (subscriber/policy-reserved-bits). Only durability_request is honoured today (§the latch above, subscriber/policy-durability); priority is stored and read back, awaiting the transport work that honours it. reliability is stored and read back too, but it awaits nothing — the §4.4 pressure arm is the receiving vertex’s own owner-side declaration, so bits 0–1 are carried verbatim and read by nothing (RFC-0025 §Erratum 2026-08-24).

These three were per-vertex :settings knobs until RFC-0022: a single reliability or priority has no coherent meaning across a heterogeneous fan-out (one vertex to a CAN peer and a WebSocket peer at once), which is why nothing ever consumed them. No magnitude may be packed here — a deadline or queue bound is a magnitude, and a bit-width on a magnitude is a synthetic limit this project forbids; one would arrive as a full-width field in the subscription’s cold half, never in these bits.

Bits 6–7 are DECODED, not yet honoured. RFC-0025 §4.1 assigns them delivery_class; the default 0 (conflate) is today’s behaviour byte-identically, which is why the assignment costs no wire byte and breaks no sender — a subscriber that predates the class wrote 0 there when the bits were reserved and is conflate-class by construction. Every core now reads the field (C++ delivery_policy_t::delivery_class, Rust DeliveryPolicy::delivery_class, TS deliveryClassOf), and the subscriber/policy-reserved-bits vector was repaired with it — in place, same bytes: its 0xFFC1 word is unchanged (it always carried delivery_class = 3), and only its description and its three-language gates narrowed to “bits 8–15 reserved” (RFC-0025 §4.1.2, Amendment 3, clause 7). Reading the class is not honouring it: every class beyond conflate needs the fan-out-edge mechanics and the receiving vertex’s ring, which land with #1204 phase 3’s remaining work. Until then a producer delivers as it always did, whatever a subscriber’s word says.

Class semantics (RFC-0025 §4.1, as amended):

class

delivery

0 conflate

last-wins; delivery MAY coalesce to the newest value. The LKV contract, and the default.

1 immediate

every write delivered as its own event; order-preserving, never conflated.

2 batch

the wire encoding of the RFC-0008 assign/propagate flush. Accumulation is the source vertex’s own state — a plain value coalesces (LKV overwrite, §B.2), a STREAM vertex keeps its bounded since-last-flush list (§E) — never a per-subscriber buffer at the fan-out edge. A flush emits the snapshot on a plain vertex and the full list on a STREAM. Batching is user-orchestrated (RFC-0025 §4.1.3, Amendment 4): the application ropes its sample values into ONE batch value, swaps it in through the ordinary atomic LKV publish, and calls propagate/push. The graph holds no counter, no window and no buffer, derives no time, and never interprets the record — batch_count / batch_window_ns are retired, and a full bounded list is still discharged rather than trimmed (no loss, no gap signal, no counter). The batch has one layout and two spellings, chosen by carriage: a standalone flush is one BATCH record (§User range, 0x80); a flush folded into a propagate(v, FOLD) branch write is seated in the swept node’s single structured VALUE (opt.PL=1) — the one value RFC-0005 §B admits — byte-identical apart from the header type byte (RFC-0025 §4.1.2 clause 6, erratum 2026-08-23).

3 stream

append-preserving: every write delivered in order, none conflated, with the RFC-0025 §4.4 pressure contract at the receiving vertex’s ring.

Per-vertex delivery_mode (RFC-0008). Whether a vertex rides an ancestor’s propagate sweep is a value-agnostic property of the vertex (not the subscriber): UNCONDITIONAL (always swept), IF_NEWER (default — swept only if it was assigned since the last covering sweep, i.e. it holds a pending mark), EXPLICIT (never swept by an ancestor; deliverable only by a direct propagate on the vertex). assign and a direct propagate on the vertex are never gated by it. It is host state defaulting to IF_NEWER. Wire configuration reuses the vertex’s own :settings (a delivery_mode NAME/VALUE under the vertex SETTINGS) and is deferred, so the host call is the only way to set it.

delivery_compact (RFC-0004 §E.1, ADR-0035 slice 4) is the consumer’s opt-in to label-compacted deliveries: on a full-TLV transport (ws/UDP, default full-route deliveries) a producer MAY, for a subscriber that set it, advertise a per-link label aliasing that subscriber’s return route and thereafter stream lean COMPACT frames instead of full-route FWD{WRITE} (see §route-handle). It is optional and NAME-tagged: a parser that does not know it, or a producer that does not honor it, keeps the full-route delivery path, so it perturbs no conformance vector. Header-elided transports (CAN) always label and ignore the hint.

The per-vertex delivery_mode is enforced producer-side (during the propagate sweep, before fan-out), and it applies to bubbled deliveries (below) exactly as to direct ones. The capability child carries the subscriber’s subject-token (ADR-0018); subscribe-authorization is gated by the source’s :acl.

Subtree subscription — vertical bubbling

Every subscription is a subtree subscription (RFC-0005): a SUBSCRIBER at <vertex>:subscribers[N] observes writes to that vertex and to any descendant of it (a leaf subscription is the trivial case), so no wildcard target_path is needed — subscribing to the composite vertex is the subtree subscription. A write at vertex W MUST deliver, once per subscriber, to the subscribers of W and of each ancestor of W (“vertical bubbling”). The delivered payload is the written TLV as-is — the exact frame the producer wrote, at the granularity it chose (a leaf VALUE, or a whole branch POINT per §0x07); there is no re-encoding and no delivery-metadata envelope. Local subscribers receive the usual zero-copy view clone; remote subscribers receive the frame over the existing return-route FWD{WRITE} delivery path unchanged. Any provenance a consumer needs beyond the frame itself travels in the data (CONTEXT.md §SUBSCRIBER direction); wire-level concrete-path tagging of remote deliveries is the separate, draft RFC-0003 proposal.

The write path MUST stay near-free when nobody listens: an implementation maintains per-vertex listener bookkeeping (updated at subscribe/unsubscribe, at control-plane frequency) so a write performs the ancestor fan-out walk only when a subscriber exists at or above it — an idle write takes no vertex lock and costs two atomic loads (RFC-0005 §A cost model).

        flowchart BT
    C["/a/b/c ← write(VALUE)"] -- "fan_out: own subscribers" --> C
    C -- "bubble: written TLV as-is" --> B["/a/b (subscriber S2)"]
    B -- "bubble: written TLV as-is" --> A["/a (subscriber S1, remote via FWD)"]
    A -.-> R["no subscriber above /a ⇒ walk ends;<br/>with no S1/S2 the write never walks at all"]
    

Producer fan-out to remote subscribers

When a SUBSCRIBER is written into <vertex>:subscribers[N] over a transport (an inbound FWD{WRITE} to :subscribers[], RFC-0004 §D), the slot retains the request’s accumulated return route (the FWD src) and the inbound link. The route bytes are copied once, at subscribe time, into a refcounted segment; the slot holds a view over it. Thereafter a write to that vertex fans out a delivery back to the consumer along that return route — a FWD{WRITE, dst=<return route>, payload=<VALUE>} (delivery-is-a-write), or, when the subscriber set delivery_compact, an auto-promoted COMPACT (advertised once per flow, then streamed; re-advertised after a reconnect — §route-handle). Each full-route delivery refcount-clones the stored route and scatter-gathers the frame from stack-built heads + the roped route + the roped value — no route or payload bytes are copied per delivery, and an in-flight rope keeps the route segment alive across a concurrent unsubscribe. This is the producer half of consumer-initiated subscription; it composes the existing field-writes and adds no wire verb (RFC-0004 / ADR-0035 slice 4).

A subscriber that sets durability_request (bit 5 of its delivery_policy, below) additionally receives a latch: the subscribe itself emits one immediate delivery of the producer’s last-known value, so a late joiner paints the current state without waiting for the next write. A subscriber that does not ask (the default) receives only writes that happen after its subscribe — and the two may sit on the same producer at the same time, which is the point of RFC-0022 §3.A: this was a producer-side :settings.durability flag that replayed to every subscriber, including the ones that never asked. The latch reuses the same delivery path (full-route or COMPACT) and carries no new wire bytes of its own, so it is observable only as delivery timing; the vectors pin the REQUEST (subscriber/policy-durability, subscriber/policy-absent).

The producer fan-out, end to end — the subscribe binds a remote subscriber, its durability_request fires the latch immediately, and later writes stream out (auto-promoted to lean COMPACT when the subscriber opted in):

        sequenceDiagram
    autonumber
    participant Cons as Consumer
    participant Prod as Producer node
    participant V as Vertex (producer)
    Cons->>Prod: FWD{WRITE, dst=/V, :subscribers[], src=/cons,<br/>SUBSCRIBER{ delivery_compact=1 }}
    Prod->>V: subscribe_wire — the ADR-0049 admission door<br/>(retain return_route=/cons + inbound link)
    Note over Prod,V: durability_request ⇒ latch the current LKV
    Prod-->>Cons: FWD{WRITE, dst=/cons, VALUE}  (delivery #1 — the latch)
    Prod-->>Cons: FWD{REPLY} (subscribe ack)
    Note over V,Prod: a later local write fans out via the injected sink
    V->>Prod: write(new VALUE) → fan_out → remote sink
    Prod-->>Cons: ADVERTISE{ label, route=/cons }  (once per flow)
    Prod-->>Cons: COMPACT{ label, VALUE }          (then lean frames…)
    Prod-->>Cons: COMPACT{ label, VALUE }
    

Where it appears

  • <vertex>:subscribers[N] slot, one per subscription.

  • Inside <vertex>:subscribers[] reads (returned as a sequence of SUBSCRIBER TLVs nested in the response).

Validation

  • target_path MUST be a syntactically valid path (per 03-addressing.md).

  • A SUBSCRIBER with no target_path is treated as “clear this slot” (unsubscribe sentinel).

Future extensions

The optional fields after target_path may grow. New optional sub-fields MUST appear after the existing ones and MUST be NAME-tagged so older parsers can skip them.


0x05 — RESERVED

Type code 0x05 is a reserved code with no assigned meaning in v1. Structured payloads are expressed by opt.PL, not by a dedicated container type: every structured TLV in the registry has a specific purpose declared by its type code.

  • Senders MUST NOT emit type=0x05.

  • Receivers MUST treat type=0x05 as a reserved-but-unassigned code per 01-data-format.md §handling unknown type codes (skip safely, do not crash).

  • The code is not available for reuse; collision-prevention keeps it unassigned.

The structural concept lives in the options bits: any TLV with opt.PL=1 is a structured container holding concatenated child TLVs. The protocol’s structured types are SUBSCRIBER (0x04), POINT (0x07), STATUS (0x09), ACL (0x0A), SETTINGS (0x0B), SPEC (0x0E) in the first block, plus FWD (0x0F), FIELD (0x10) and the route-handle frames ADVERTISE (0x11) / COMPACT (0x12) / HANDLE_NACK (0x13) in the fast-track range; PATH_REF (0x14) and PATH_REF_REVERSE (0x15) are the two codes in that range that are not. PATH (0x06) is not structured either — since RFC-0018 its body is a packed record run with opt.PL=0, so the three address forms (0x06, 0x14, 0x15) are all opaque-bodied. User-defined structured records use user-range type codes (0x80–0xFF) with PL=1.

Why no generic container

A generic list would have no semantic meaning of its own — an un-named default whose role is always “structured stuff goes here.” Real uses always have a specific purpose. Forcing every container to declare its purpose via type code is what makes the type byte a proper L3 concern.


0x06 — PATH

A hierarchical address. An opaque TLV (opt.PL=0) whose body is a self-delimiting run of records (RFC-0018 §5). Distinct from an ordinary opaque TLV in that the constraints below are enforced, not merely conventional — see enforcement.

Two layers, one sentence. A PATH is a list of path elements; an element’s kind is NAME or PAIR; a NAME element is encoded as a segment record, a PAIR element as an escape record. “Path element” is the model-layer word — what an address is made of — and the two record words are the encoding-layer ones, naming the differently-framed byte patterns that spell the two kinds. Both layers are canonical and neither replaces the other (#1347, ruled 2026-08-16): an escape is precisely not a segment, so the grammar below needs the record words, and an address is not a list of byte patterns, so the model needs the element word. An element self-describes by its kind, never by its position (RFC-0029 §4, carrying RFC-0027 §5.1). A PATH whose elements are all NAME — the canonical form — is byte-identical to the pre-RFC-0027 encoding.

Amended. Until RFC-0018 the body was a child sequence (opt.PL=1) of NAME TLVs, one per segment, and “each child MUST be a NAME” was the invariant. NAME (0x02) is not retired — it still spells SETTINGS keys and :schema labels; RFC-0018 removed it from PATH bodies only.

Payload layout

PATH (PL=0) {
  [u8 len_1][len_1 bytes UTF-8]        ← element 1, kind NAME — a segment record
  [u8 len_2][len_2 bytes UTF-8]        ← element 2, kind NAME — a segment record
  ...
  [u8 len_K][len_K bytes UTF-8]        ← element K, kind NAME — a segment record
}

The walk is p += 1 + body[p] — one byte load and one add, with no option decode and no header construction. An empty body is valid: it is the graph root (/), zero elements.

A PAIR element occupies the same list and is encoded as an escape record — 00 <u8 kind> <u8 len> <len bytes>, kind = 0x16, len = 8 — so the walk over it is p += 3 + body[p+2] instead. That is the next bullet’s rule, and the reason the two encodings need two names: the records are framed differently, while the elements they spell sit in one list.

Header settings

  • opt.PL MUST be 0. The body is not a child sequence.

Constraints

  • Each record’s len MUST be in 1..64 (the per-segment limit of 03-addressing.md §path syntax; the u8 length field caps it at 255 forever).

  • len == 0 is the escape record — 00 <u8 kind> <u8 len> <len bytes>, total 3 + len bytes (RFC-0018 §8, made normative by its §5.4 amendment 1). It is admissible in a frame path and rejected in canonical / key context — see enforcement. kind = 0x16 is the PAIR element of RFC-0029 — the escape record is that element’s encoding, not a third kind of thing; no other kind is assigned, and a host that does not own the kind it meets steps over the record by its declared length rather than reading it.

  • The records MUST tile the body exactly: a record whose declared length runs past the body’s end is ragged framing, not a short read.

  • Total path length ≤ 1024 bytes, measured as the encoded PATH body — the concatenated segment records, i.e. exactly this TLV’s own length field — not the sum of segment bytes plus separators, a unit that excludes the per-segment length byte and so admits paths path_t::parse rejects (see §path syntax).

  • Segment count ≤ 255 (RFC-0023). Under this body encoding a record costs 1 + len, so 1024 bytes admit up to 512 one-byte segments and the count cap binds first for segments averaging ≤ 3 bytes; past that the byte cap binds. Under the retired NAME-child body each segment cost 4 + len, the byte cap bound first at 204 segments, and the count clause could never fire — the crossover is pinned by the path/path-deep-255-packed vector.

Enforcement of the PATH constraints

Enforcement sits at the resolver, not at the codec. Three rules, in the order a frame meets them:

  • The codec does not enforce these constraints, and is not expected to. A PATH’s body is decoded as opaque bytes; the bytes above round-trip byte-identically. That is deliberate — the constraint is about what an address means, not about what the octets are, and it keeps codec-only cores (TypeScript, Rust) free of resolver semantics. The packed body makes this tier cheaper: there is no child-type rule left for the codec to not-enforce.

  • The resolver enforces the record grammar, when it turns a PATH into a vertex lookup key. Ragged framing, an over-long segment, or an escape record in canonical / key context makes the address unspellable, so the op answers ERROR{tr::path::invalid} (0x0021) — not tr::path::not_found, which would wrongly assert that the address was well-formed but absent. Enforcement precedes write-create: a WRITE to an illegally-spelled path creates nothing.

  • The length and segment-count limits are bounded where the address is constructed or admitted, not at decode.

The two contexts, and why they differ. In a frame path — a FWD dst/src a hop is relaying — a forwarder that does not implement an escape record’s kind MUST step over it by its declared length rather than drop a frame it is only relaying; that is the whole reason the escape is self-delimiting. In canonical / key context — a vertex-map lookup key, an ADVERTISE route, a pre-encoded path handle — an escape record is rejected, because a PAIR is not canonical bytes and the key must stay pure-string for the byte-prefix-implies-ancestor property 02-graph-model.md depends on.

Conformance vectors: path/path-escape-in-key-context carries an escape that a frame admits and a key refuses; path/path-record-overruns-body carries a record whose declared length runs past the body. Both are input.bin cases, not reject.bin ones: decode must succeed, because decode is not where the rule lives. They replace path/path-value-children-illegal, retired with RFC-0018 — a packed record has no type byte, so a mistyped child is unrepresentable.

Path element PAIR (escape kind = 0x16) — routing semantics

Status. This section states the form of RFC-0029 (accepted 2026-09-30, ships as v0.18.0), which replaces RFC-0027’s 16/16 path label and RFC-0024’s bare PATH_REF address with one element. The reference implementation reaches it slice by slice (RFC-0029 §13.2): until S1–S3 land it still emits and honours the RFC-0027 len = 4 label, the 0x14 PATH_REF address and the op bit 7 mint request, and the conformance vectors path-label/*, path-ref/ref-*, fwd/fwd-label-*, fwd/fwd-bound-* and fwd/fwd-reverse-mint still describe that form. They retire, and the RFC-0029 §13.4 vectors replace them, in S1–S3.

The layout above is what a PAIR element is; this is what a host does with one (RFC-0029 §§4–10). A PATH carrying PAIR elements is still a 0x06: a chain is a frame path, the canonical string of the same address is the durable truth, and the chain is a learned cache of it — re-learned after a reboot, a departure, a retirement or a refusal.

There is no separate type code. The element is the escape record of §Constraints above — 00 <u8 kind = 0x16> <u8 len = 8> <u32 LE index> <u32 LE generation>, 11 bytes — and 0x16–0x1F stay unassigned in the type-code registry (§reserved range). The escape kind space and the TLV type space are different namespaces that happen to share a number. A kind = 0x16 record of any other length (including RFC-0027’s retired 7-byte label) is malformed and refuses the address (tr::path::invalid), never the frame.

The value. The vertex’s owner-issued (u32 index, u32 generation): the index is a slot in the owner’s dense, append-only, pointer-stable vertex index, handed out at registration and bounds-checkable; the generation is the vertex’s retirement stamp. It is minted, never hashed — two vertices cannot share an index, so a false match is not constructible.

Node-scoped, and an address rather than a capability. A PAIR means something only on the node whose index it names. Element k of a chain is read by the k-th node of the route and by no other; a hop consumes exactly its own head element and forwards the tail, the same monotone shrink the canonical dst performs.

One element per node’s whole local part; mixed paths are legal and expected. A node’s local part is its entire mount run (net/<module>/<name>, however many segments) and one PAIR stands for all of it. A PATH MAY carry any mixture of NAME and PAIR elements, in any order: a node that cannot issue a PAIR for its part — a shared (bus) mount, a saturated slot, a connection vertex that does not exist yet — leaves that part as NAMEs and every other node’s part still compacts. Because each element self-describes by its kind and is read by exactly one node, skipping is not expressible.

The per-hop algorithm

A host receiving FWD{op, dst, src, …} reads the head element of dst:

  1. NAME head — resolve as for any canonical path (the mount descent, else the local tree walk).

  2. PAIR head — bounds-check the index, compare the generation (a saturated element never matches), check the vertex is registered. Any refusal is tr::path::not_found, answered to the request’s src when it is non-empty; the host MUST NOT forward, MUST NOT apply the operation and MUST NOT attempt any repair — no re-resolution, no nearest match, no retry.

  3. What the vertex is decides what happens next, and nothing else does:

    • a point-to-point connection vertex with a non-empty residual — egress over that link, dst shrunk by exactly the consumed element, src grown canonically by the inbound mount run; the same vertex as the last element is a local terminus that addresses its own :-facets;

    • a shared-mount (bus) vertex — refused, tr::path::not_found: a bus link’s send() broadcasts, so no element names an egress through it until RFC-0029 §10’s session-anchor egress (slice S8) lands. Across a bus the chain stays NAMEs;

    • any other vertex, as the last element — the terminus: apply the operation exactly as the NAME spelling does; a PAIR terminus never write-creates;

    • any other vertex not last — tr::path::invalid: a descent below a non-mount vertex names nothing.

  4. Authorization, at the dereferenced vertex, before any egress or operation (below).

A REPLY is routed by the same steps against its dst. The forwarding hop is zero-heap and holds nothing across frames. The hop that consumes the final element and still has a frame to put on the wire re-heads its egress dst as a canonical empty PATH, so a client that never speaks a PAIR is never answered in one (RFC-0024 §7.1 erratum 3, kept).

Authorization is spelling-independent. RFC-0004 §F’s two gates — the forward right at each intermediate connection vertex, the operation’s right at the target — are evaluated at the dereferenced vertex, for the caller’s subject and the operation’s right, and the verdict is a function of those three alone: a host MUST reach the same verdict whether a hop’s element arrived as a NAME or a PAIR, and MUST evaluate the check on every hop of every spelling. A generation match authorizes nothing; a peer may present any pair exactly as it may spell any string. Conformance carries the paired acl/label-vs-string-* and acl/bound-vs-canonical-* vectors, which RFC-0029 §13.4 re-spells in PAIRs.

Learning — passive, on the reply src, never load-bearing

There is no advertise, no request flag, no setup exchange and no control frame. The FWD op byte keeps its op & 0x3F masking rule with bits 7–6 reserved, MUST be zero; RFC-0024’s bit-7 mint request is retired.

  • A forwarding hop grows the request’s src by the NAME run of its inbound link, never by a PAIR: the holder of a return route holds no other original, so a return route must stay canonical.

  • A host relaying or issuing a REPLY prepends to that reply’s src its own part of the forward route — as a PAIR when it can issue one, as the canonical NAME run when it cannot, never nothing. A forwarding hop contributes the PAIR of the connection vertex the reply arrived over (the one it egressed the request through, read off the frame’s own arrival, never from a table); the terminus seeds the reply’s src with the PAIR of the vertex it applied the operation to. RFC-0004 §B’s “a reply accumulates no return route” is amended accordingly: a reply’s src is the responder’s address from the origin’s vantage.

  • The origin receives the complete forward route from its first link onward, prepends its own element for hop 0, and caches the chain beside the canonical bytes, which it never discards. Subsequent requests spell the cached chain as dst.

Learning is post-auth by construction: the reply it rides exists only after the terminus’s gates passed. A PAIR is never load-bearing: a host MUST behave correctly when no PAIR is ever issued anywhere on a route, and a hop that issues none is simply a NAME run in the chain.

Refusal and fallback. On tr::path::not_found for a chain-spelled request the origin clears its cached chain and re-sends the canonical string it still holds; the reply to that request re-teaches the chain. One failed operation is the entire cost. There is no withdraw frame, no unbind, no lease and no TTL, and a multi-element forward refusal answers NOT_FOUND like every other arm rather than dropping silently.

Invalidation — the generation, and nothing else

A PAIR stops validating when its vertex’s generation moves. Generations only move forward, so a stale PAIR never becomes valid by waiting. A generation MUST saturate, never wrap: a saturated vertex is permanently unbindable and every element for it stays a NAME. The generation moves on:

  • retirement of the vertex;

  • a point-to-point child’s tenancy change — a same-named re-add is a new identity, and a pair issued against the previous tenancy MUST NOT resolve to it, because the elements behind it were issued by the node that used to be there;

  • the child’s link going down while the tenancy is kept — the far node may have rebooted behind the same socket and re-issue the same small pairs;

  • on a transport that cannot report a session boundary (UDP, CAN), a changed per-boot epoch, which such a link carries once per session or advertise, never per frame, and which counts as a session boundary. The epoch is not part of the pair.

The tenancy and link bumps advance the generation only — they do not empty the vertex’s :acl, :settings or app fields the way retirement does.

Statelessness. A forwarding hop holds no hard state — nothing whose loss changes an answer — and no per-request state, ever. The origin’s cached chain and a subscription edge’s learned chain are soft: a miss falls through to the canonical string with the same result. There is no per-hop table, no ceiling and no refuse-on-full. The single named exception is RFC-0004 §E.1’s COMPACT route handle (§route-handle frames): streams only, recoverable through HANDLE_NACK and re-advertise, and never a wrong delivery.

Optionality. Issuing and honouring PAIR elements are both optional. A host that implements neither still relays a frame carrying one, by stepping over the escape record by its declared length (§Enforcement above).

On the wire, in a capture. The Wireshark dissector renders an escape element in place inside the address; its PAIR rendering follows slice S1.

Where it appears

  • Inside SUBSCRIBER as target_path.

  • As the PATH form of tracer_read/write/await arguments when the path is constructed programmatically (the C API also accepts string form for ergonomics).

  • As the dst/src routes of a FWD frame (§reserved range) and the route of an ADVERTISE (§route-handle frames).

Note on string form vs PATH-TLV form

A path may be expressed two ways:

  • String form: "/sensor/temp" — a UTF-8 byte string with / separators. Used at the API surface for ergonomics. Stored as a single VALUE TLV when transported as data.

  • PATH-TLV form: a PATH TLV (opaque body, packed records — one per path element). Used inside structured TLVs (SUBSCRIBER, FWD) where elements must be addressable individually rather than re-split from a byte string.

Both forms canonicalize to the same internal representation. Implementations MUST accept either form where a path is expected.

Static / pre-encoded PATH TLV (init-time form)

Normative reference: ../spec/v1.md §3.1. See also: 03-addressing.md §static path handles for the addressing-level rationale, and 04-communication-flows.md §the static-path write flow for hot-path semantics.

For MCU-class deployments the PATH TLV is intended to be encoded once — at build time as a .rodata byte literal, or at node init as a single allocation — and reused for the lifetime of the node. The hot path treats the pre-encoded bytes as the address of a vertex; no parser walk, no string formatting, no allocation occurs per write.

Build-time-encodable byte layout

Every conforming PATH TLV is byte-equivalent to the following structure. A correct build-time encoder produces exactly these bytes:

        flowchart LR
  subgraph Outer["PATH TLV outer"]
    direction LR
    T["type<br/>= 0x06"]
    O["opt<br/>PL=0"]
    L["length<br/>u16 LE"]
  end
  subgraph Records["payload (packed segment records)"]
    direction LR
    N1["segment_1<br/>SS bytes..."]
    N2["segment_2<br/>SS bytes..."]
    NK["segment_K<br/>SS bytes..."]
  end
  Outer --> Records
  N1 --> N2 --> NK
    

The encoder’s invariants:

  • Outer header (4 bytes, default LL=0): 06 00 LL_lo LL_hi. 0x00 = no PL, no TS, no CR, LL=0. (Note the distinction: 0x10 = CR only per 01-data-format.md §options bitfield; the pre-RFC-0018 0x40 set PL=1 and is no longer a legal PATH option byte.)

  • length = sum of the segment records’ total sizes; each record costs 1 + len(segment_bytes).

  • Each segment record: SS <segment_bytes>, where SS is the segment’s UTF-8 byte length (1..64) as a single u8.

  • No inner headers and no inner trailers. A record is length byte plus text — there is no per-segment type byte, no option byte, and nothing inside a PATH that can carry a TS or a CRC; the outer (when in transit) covers everything. This is what gives an address exactly one spelling.

  • Reserved characters (/ : . [ ] * ?) MUST NOT appear inside any segment_bytes.

⚠️ Conformance gap — the reference encoder does not enforce the bracket half of that rule (core/include/libtracer/path.hpp:valid_segment rejects only / : . * ?; §0x02 NAME §constraints, 03-addressing.md §reserved characters). The invariant above is unchanged.

A path that resolves to more than 255 segments (RFC-0023), has a single segment longer than 64 bytes, or whose encoded PATH body exceeds the addressing-level cap MUST fail to encode.

Byte literal — /sensor/temp

06 00 0C 00     ← outer: type=PATH(0x06), opt=0x00 (PL=0), length=12 (u16 LE)
   06 73 65 6E 73 6F 72                 ← record: len 6, "sensor" (7 bytes)
   04 74 65 6D 70                       ← record: len 4, "temp"   (5 bytes)

16 bytes total when stored as graph data (no outer trailer) — 6 fewer than the 22 the retired NAME-child body cost. When transmitted with CRC-32, the outer trailer adds 4 bytes; the records are unchanged.

Byte literal — /camera/frame

06 00 0D 00     ← outer: length=13 (opt=0x00, PL=0)
   06 63 61 6D 65 72 61                 ← record: len 6, "camera" (7 bytes)
   05 66 72 61 6D 65                    ← record: len 5, "frame"  (6 bytes)

17 bytes total. A C macro emitting this literal is straightforward; a code generator emitting one per registered path is even simpler.

Conformance for the static form

A pre-encoded PATH TLV intended for use as a path handle (../spec/v1.md §3.1.1) MUST:

  1. Be byte-identical to the canonical encoding above.

  2. Pass the segment-validity rules of 03-addressing.md at encode time.

  3. Be stored in memory whose lifetime spans every read / write / await that uses it.

A conforming receiver MUST treat a PATH TLV the same regardless of whether it arrives over the wire, was assembled from heap segments, or points into the sender’s .rodata. The wire bytes are the contract; the segment they live in is implementation choice.

Why no allocation on the hot path

The motivation for this section is twofold:

  • MCU deployments (Cortex-M, ESP32) cannot afford snprintf+malloc per write. Code size and ISR-safety both forbid it.

  • The TLV-as-bytes invariant (02-graph-model.md §the same-substrate insight) extends naturally: if a TLV in memory IS the wire bytes IS the graph node, then a TLV in .rodata is the same — just at a different address. Routers and dispatchers read it identically.

Implementations on hosted platforms (Linux, Windows) MAY accept string-form paths at the API surface for ergonomics, but the dispatch underneath SHOULD canonicalize to a PATH TLV byte-blob exactly once and key its routing tables on those bytes.


0x07 — POINT

Endpoint definition: a vertex’s full descriptor as a structured TLV. Used for vertex enumeration and replication snapshots.

Payload layout

POINT is structured (opt.PL=1). Children, in order:

POINT (PL=1) {
  NAME           vertex_name        ; required — the leaf segment (first child)
  VALUE          value              ; optional — the vertex's OWN value (RFC-0005)
  DESCRIPTION    description        ; optional
  SETTINGS       default_settings   ; optional
  SUBSCRIBER     sub_0              ; zero or more, in slot order
  SUBSCRIBER     sub_1
  ...
  POINT          child_0            ; zero or more, recursive
  POINT          child_1
  ...
}

Subscribers and children appear as direct children of POINT, identified by their type code. There is no intermediate “subscribers list” or “children list” wrapper — the type byte of each child tells its role. The optional VALUE child (RFC-0005) carries the vertex’s own value, making a POINT tree a value-bearing view of a subtree — the read-side dual of the branch write below. That dual is a served operation (RFC-0016): a plain read of a vertex with ≥ 1 registered child returns exactly this shape — the composed branch read, a view composed over the live last-known values (per-node stored TLVs verbatim, READ-denied subtrees pruned, a names-only topology tree when the branch is value-free).

Where it appears

  • Returned by read("/some/parent") — the composed branch read of the parent’s registered subtree (RFC-0016); names-only member enumeration is read(<parent>:children[]).

  • Written to a vertex as a branch write (below) — the write-side dual of the composed branch read.

  • Returned by read(<vertex>:schema) — the two-part schema read (below).

  • Snapshots of vertex state taken by a recorder/replay module.

  • Announcements of exported vertex trees by discovery modules.

The :schema read — two parts, defined precedence

read <vertex>:schema (RFC-0010 §B.2) serves one POINT holding the synthesized protocol part and — iff the owner installed a field descriptor table — the owner part, appended verbatim:

POINT (PL=1) {
  NAME      <vertex name>
  SETTINGS  <protocol part>          ; synthesized by the runtime — authoritative for
                                     ; protocol fields; the owner part is never consulted
  NAME "app"  SETTINGS (PL=1) {      ; owner part — present iff a table is installed
    NAME <field-name>  SETTINGS (PL=1) {
      NAME "access" VALUE <"ro"|"rw"|"wo">  ; runtime-projected from the table — the one
                                            ; member the runtime owns (it cannot be lied about)
      <owner descriptor bytes, verbatim>    ; SHOULD-level vocabulary: dtype/unit/min/max/label…
    }
    ...
  }
}

Precedence is by position, with zero merge logic: the two parts describe disjoint namespaces (flat protocol knobs vs the reserved app subtree, §0x0B), so a name collision cannot occur by construction. A vertex without a table serves the protocol part alone, byte-for-byte the record a runtime without owner fields serves.

:schema addresses the whole vertex or nothing. It describes that vertex’s own structure and never its children’s: a child’s schema is read from the child (<parent>/<child>:schema). There is no schema below a field, and :children[] lists members while :schema describes the vertex.

Branch write — decomposition

A write whose payload TLV is a POINT is a branch write (RFC-0005): the written tree is rooted at the target vertex — the root POINT’s NAME MUST equal the target’s leaf segment (mismatch ⇒ ERROR{tr::path::invalid}) — and it decomposes:

  • Each value-carrying node (a POINT with a VALUE child) is stored at the corresponding descendant vertex — the target’s path extended by the chain of NAMEs — as a refcount-bumped subview of the written frame (zero copy, never re-encoded). Values are the truth at the vertices where they land; a branch is a view.

  • A landing vertex that does not exist is created, mkdir -p style, gated by the CREATE access bit on the nearest existing ancestor’s effective ACL (§0x0A) — this is the same write-creates rule that applies to any data write to a nonexistent path.

  • Each covered subscription point is notified once with the smallest subview covering every value landed at-or-below it: the VALUE slice at a leaf landing site, the node’s whole POINT subtree at an interior node, and the written TLV as-is at the root (and, via §0x04 bubbling, above it).

  • Strict shape in a branch write: a node’s children are exactly the leading NAME, at most one VALUE, and zero or more POINT sub-branches; anything else — or any trailer-carrying node in the tree — is rejected with ERROR{tr::schema::type_mismatch} and nothing lands (stored values are trailer-less at rest, ADR-0041 §4). A branch with no VALUE anywhere is a valid no-op.

  • One store per vertex; no branch transaction. A read of any vertex returns its latest stored value — never behind what a subscriber saw, legitimately newer. Admission (shape + ACL + creation gating) is all-or-nothing, but application is per-leaf: cross-leaf atomicity is explicitly not promised; snapshot coherence is the coherent-sampling (origin, ts) group (ADR-0019).

        sequenceDiagram
    autonumber
    participant P as Producer
    participant S as /s
    participant T as /s/t
    participant U as /s/u (does not exist yet)
    P->>S: write POINT{NAME s, POINT{NAME t, VALUE a}, POINT{NAME u, VALUE b}}
    Note over S: decompose — admit (shape, CREATE/WRITE gates), then land subviews
    S->>T: store VALUE-a slice (refcount subview, zero copy)
    S->>U: write-creates /s/u, store VALUE-b slice
    T-->>P: /s/t subscribers notified with the VALUE-a slice
    S-->>P: /s subscribers (and ancestors, bubbled) notified with the written POINT as-is
    

Constraints

  • opt.PL MUST be 1.

  • The vertex_name MUST be the first child; the optional VALUE (the vertex’s own value) immediately follows it when present.

  • Children that represent recursive vertex structure MUST themselves be POINT TLVs (type 0x07).


0x08 — ERROR

A single error condition. Used inside STATUS TLVs (which may carry zero or more ERRORs) and as the response payload for failed read/write/await calls.

Payload layout

ERROR (RFC-0002, accepted) is a structured TLV (opt.PL=1) in all cases — never special-cased; a generic PL=1 walker handles it. Its first child is the identity, selected by the child’s type alone:

First child

Identity form

Payload

VALUE (0x01)

registered code

u16 LE code from the registry below

NAME (0x02)

string

UTF-8 tr::… path (no NUL) — third-party extensions

Subsequent children are optional detail (DESCRIPTION 0x03 human text, VALUE 0x01 binary detail, or concept-specific TLVs).

Worked bytes — tr::path::not_found (code 0x0020), code-only:

08 40 06 00   01 00 02 00 20 00     ERROR: type=08 opt=40(PL=1) len=6
└ERROR hdr┘   └── VALUE child ──┘   VALUE payload = 20 00 (u16 LE = 0x0020)
= 10 bytes    (14 wrapped in STATUS: 09 40 0A 00 + the 10 above)

Error registry (tr::<concept>::<error>)

Identity is a path in a hierarchy keyed by stable protocol concept (never an implementation module): frame · tlv · path · schema · flow · access · transport · version. severity ∈ warn|error|critical; disposition ∈ transient (retry) · permanent (don’t retry this request) · fatal (tear down the peer) — both live in the registry, never on the wire.

Code

Path

Severity

Disposition

0x0001

tr::frame::truncated

error

transient

0x0002

tr::frame::invalid

error

permanent

0x0003

tr::frame::crc_fail

error

transient

0x0010

tr::tlv::nesting_too_deep

error

permanent

0x0020

tr::path::not_found

warn

permanent

0x0021

tr::path::invalid

warn

permanent

0x0022

tr::path::in_use

warn

permanent

0x0030

tr::schema::type_mismatch

error

permanent

0x0031

tr::schema::not_found

warn

permanent

0x0040

tr::flow::backpressure

warn

transient

0x0041

tr::flow::timeout

warn

transient

0x0042

tr::flow::address_shift_gap

error

permanent

0x0050

tr::access::denied

error

permanent

0x0060

tr::transport::down

error

transient

0x0070

tr::version::mismatch

critical

fatal

There is no OK code (an empty STATUS means OK) and no user/application error range (ADR-0010): an application failure is ordinary data described by the application’s schema. A module outside the frozen registry uses the string form (tr::<vendor>::…) — no registry entry, no RFC. tr::frame::invalid covers reserved-bit-set / type=0x00 / oversize length; tr::version::mismatch is a discovery/link-level outcome, not a frame-parse result. The namespace is prefix-filterable (tr::flow::*). Additions to the registered set are RFC-gated.

tr::flow::address_shift_gap (0x0042) is the single ordered-flow discontinuity code: it is raised both for a missing interior slice of a slicing group and for a best-effort ring-overflow shed, so a receiver has one gap-handling path. The “address-shift” in its name is historical.

Where it appears

  • Inside STATUS TLVs (zero or more ERRORs per STATUS).

  • As inline reply payload in implementations that opt to skip the STATUS wrapper.


0x09 — STATUS

Communication status / response signal. An empty STATUS means OK; a non-empty STATUS contains one or more ERROR TLVs and optional DESCRIPTION text.

Payload layout

When empty: length = 0. (Smallest valid STATUS is the 4-byte empty-OK form.)

When non-empty: structured (opt.PL=1) with children:

STATUS (PL=1) {
  ERROR        first_error
  ERROR        second_error      ; optional, multiple permitted
  DESCRIPTION  human_message     ; optional
  ...
}

Header settings

  • Empty STATUS: opt.PL = 0, length = 0.

  • Non-empty STATUS: opt.PL = 1.

Where it appears

  • Synchronous return from read / write / await on failure.

  • Sentinel TLV used to clear subscriber slots (write empty STATUS to :subscribers[N]).

STATUS is a reply and a sentinel, not a field. There is no asynchronous status surface: <vertex>:status is not a selector, and a field read or write to it answers ERROR{tr::schema::not_found} (0x0031), the declared reply for a field a vertex does not expose. The field namespace a dispatcher recognises is {subscribers, acl, children, settings, schema, identity, stats}, and status is not in it. A recognised name can still answer NOT_FOUND when the facet is empty, or SCHEMA_NOT_FOUND for a spelling it does not accept (bare :subscribers requires [N]) or for a facet deliberately absent (:identity with no keypair, §0x0B) — the set is a namespace, not a list of things that read. Whether an asynchronous status surface should exist is open (#584).

Hex example

Empty STATUS=OK (the smallest valid libtracer TLV — used as the unsubscribe sentinel and the implicit OK reply):

09 00 00 00
^  ^  ^^^^^
|  |  length = 0 (u16 LE)
|  opt = 0  (no flags; LL=0 default u16)
type = 0x09 STATUS

4 bytes total. No trailer.


0x0A — ACL

Access control list — a collection of capabilities granting permissions on a vertex. Stored at <vertex>:acl.

Payload layout

ACL is structured (opt.PL=1). Its children are themselves ACL TLVs, each one an ACE (access control entry, NFSv4-style — ADR-0020). (The recursion is deliberate: the outer ACL is the ACE collection; each inner ACL is one ACE with NAME-tagged fields.)

ACL (PL=1) {                                ; outer = ACE collection
  ACL (PL=1) {                              ; inner = one ACE
    NAME "type"         VALUE <u8: ALLOW=0, DENY=1>
    NAME "flags"        VALUE <u8: INHERIT=0x1 INHERIT_ONLY=0x2 NO_PROPAGATE=0x4 GROUP=0x8> ; optional, default 0
    NAME "subject"      <subject-token (ADR-0018); the one special subject is "EVERYONE@">
    NAME "access_mask"  VALUE <u32 bitfield, below; canonical width per RFC-0026>
    NAME "expires_ns"   VALUE <u64>          ; optional
  }
  ACL (PL=1) { ... }                         ; next ACE
}

access_mask bits: READ=0x01 WRITE=0x02 SUBSCRIBE=0x04 CREATE=0x08 DELETE=0x10 READ_ACL=0x20 WRITE_ACL=0x40 WRITE_OWNER=0x80 (0x100+ reserved). The admin right is WRITE_ACL (modify the ACL / delegate); CREATE gates vertex creation — both the RFC-0005 write-creates path and the creator endpoint of §0x0E (ADR-0059, which supersedes the :children[] creation-field spelling of ADR-0017).

Inheritance: an ACE with INHERIT on a composite vertex applies to its whole subtree; a vertex’s effective ACL is its own ACEs + inherited ancestor ACEs (ADR-0020). Evaluation: ALLOW/DENY, ordered, first-match-per-bit. The wire layout is the full NFSv4 model; the required-modules MCU profile enforces a subset (ALLOW-only, single INHERIT flag); full DENY/ordered evaluation is the security_acl host module.

Enforcement (core subset). A core implementing the MCU subset enforces ALLOW-only — an :acl write carrying a DENY ACE, or any flag bit beyond the single INHERIT, is rejected with TYPE_MISMATCH, so stored ACEs never carry semantics the subset evaluator would silently weaken — and is open by default twice over: enforcement is off until a subject resolver is installed (the pluggable-subject-token seam of ADR-0018: caller context → subject token; the FWD terminus passes the inbound link as the caller context, local API calls are trusted by default), and a vertex whose effective ACL is empty stays unrestricted. With a resolver set, an operation is allowed iff some non-expired ACE with a matching subject (byte-equal, or the special EVERYONE@) grants the operation’s bit: data/field read and await need READ, data/field writes need WRITE, the :subscribers[] append needs SUBSCRIBE on the producer and delivery needs WRITE on the target (the two-ACL gate of ADR-0026), creation needs CREATE, and :acl read/write need READ_ACL/WRITE_ACL. The one named exemption is :identity, which resolves above the READ gate (§0x0B). Denial is ERROR{tr::access::denied} (0x0050). The effective ACL is computed at check time by walking the ancestor keys — control-plane frequency; the data hot path pays one null check when no resolver is installed.

EVERYONE@ is reserved in the subject-token space, and the reservation is enforced on the RESOLVER’s output. The wire carries one spelling for a subject token — the acl/acl-aces vector sends peer-a and EVERYONE@ as the same opaque VALUE, and a resolver returns opaque bytes — so a deployment that passes a caller-supplied identity through (a username, a certificate CN, a peer name) could otherwise mint a principal indistinguishable from the wildcard, and an ACE meant for that one principal would grant everyone. This core therefore refuses a resolved subject equal to EVERYONE@ at every gate, on a vertex with an effective ACL and on one without, the same fail-closed arm the resolver’s error return takes — rather than leaving each integrator to blacklist the string. Nothing about the wire changes: an ACE still names the wildcard exactly as the vector spells it. (#908.)

EVERYONE@ is the only special subject. OWNER@ is not one, and this document used to say it was. Earlier revisions of this section, CONTEXT.md and ADR-0020 named OWNER@ alongside EVERYONE@, but no evaluator ever special-cased it: ace_applies branches on exactly one string, and every other subject — OWNER@ included — is compared byte-equal against the resolver’s token. Publishing it as special was therefore harmful in two opposite directions. An operator following the old text would write {subject: "OWNER@", access_mask: WRITE_ACL} believing the vertex owner keeps admin; the ACE matches nobody, and because any present ACE closes an otherwise-open vertex, that write locks the vertex instead of delegating it. In the other direction, a deployment whose resolver passes a caller-supplied identity through could mint a principal literally named OWNER@ and match such an ACE exactly. OWNER@ is now an ordinary opaque token with no meaning to this core — a deployment may still use those bytes as a principal name, and must not expect the core to attach any owner semantics to them. Implementing real owner semantics needs a per-vertex owner identity the graph does not hold and would change how a stored ACE evaluates, so it is an amendment, not an erratum, and is out of scope here. (#1033.)

Non-canonical ACE shapes are rejected at write time. An ACE’s fields are positional (NAME key, value) pairs, and a reference core reads them as pairs rather than scanning every offset — so a NAME-typed value (the EVERYONE@ subject spelling) is never re-read as the following key. It answers TYPE_MISMATCH for: a numeric field whose VALUE payload is empty or wider than the width that field is parsed at — type and flags u8, access_mask u32, expires_ns u64 (a big-endian u16 type of 0x0001 would otherwise read as its low byte, ALLOW — leniency here inverts a refusal into a grant); a known key carrying the wrong value TLV type (a skipped expires_ns would make a time-limited grant permanent); an unknown key; a repeated key; and a body whose pairing does not hold (a non-NAME where a key belongs, or a trailing key with no value). A payload narrower than the parsed width is accepted and zero-extends — little-endian narrowing is exact, so a two-byte access_mask (the vector’s pre-RFC-0026 spelling) and the canonical four-byte one name the same rights. The canonical access_mask width is u32 (RFC-0026, #993): the layout above, the acl/acl-aces vector and both cores’ typed builders all spell four bytes, and the parsed widths bound acceptance so older narrow spellings stay readable. Unknown-key tolerance is deliberately not granted here, unlike SETTINGS (§0x0B): an ACL is a security document, and an attribute dropped in silence widens access.

The OUTER container’s shape is checked too, and a read serves a re-encode. The collection must be structured: a primitive ACL (opt.PL=0) is rejected with TYPE_MISMATCH, because its bytes are opaque payload rather than children, so it would parse as no ACEs at all — clearing enforcement, the most permissive outcome the field has, on a write that looks like it installs a policy. An empty container (opt.PL=1, zero children) is the sanctioned way to clear: it stores an ACL that grants nothing, which under the open-by-default rule restricts nothing. A read of :acl is answered by re-encoding the stored ACEs, not by echoing the bytes that were written, so what an auditor reads back is a projection of the very list the gates evaluate and the two cannot drift apart; the canonical spelling above is therefore what comes back, whichever accepted spelling went in. NOT_FOUND means no :acl was ever written, which stays distinct from an empty container. (#907.)

Header settings

  • opt.PL = 1.

Where it appears

  • <vertex>:acl field. The field is served whole: an ACE is not separately addressable, so :acl[N] names nothing and answers SCHEMA_NOT_FOUND.

  • Core-subset enforcement (ALLOW-only, single INHERIT, resolver-gated — the paragraph above) lives in the core; the full DENY/ordered/audit model is the security_acl module (post-MVP per 10-module-catalog.md).

Constraints

  • A vertex without an :acl field defaults to “no restrictions” (when security_acl is not loaded) or “deny by default” (when security_acl is loaded with strict mode).

The protocol does authorization, not authentication: identity authenticity is the transport’s or a security module’s job. The subject token is pluggable. v1 uses the transport-authenticated origin_peer_id, which is advisory on an unauthenticated bus; a security module may supply a stronger token (raw-key ed25519 with trust-on-first-use, the public key being the identity; CA-based X.509 is rejected) without changing the ACL model. ACL lists and capabilities are therefore one subject → rights authorization over a weaker or stronger token.


0x0B — SETTINGS

QoS and configuration block. Structured (opt.PL=1); children are NAME-keyed value pairs describing writable fields under :settings.

Payload layout

SETTINGS (PL=1) {
  ; the vertex core namespace is EMPTY (RFC-0022 §3.B) — no flat knob is minted today
  ; module-namespaced fields use a nested SETTINGS:
  NAME "transport_tcp"     SETTINGS (PL=1) { NAME "send_buf_kb" VALUE <u32> ... }
  ; the application's own fields use the same shape under the RESERVED key `app`:
  NAME "app"               SETTINGS (PL=1) { NAME <owner-defined> <owner-defined TLV> ... }
  ...
}

Nested SETTINGS for module namespacing (instead of an unnamed structured wrapper) keeps the type byte semantically meaningful at every level.

The app key is reserved (RFC-0010 §A.1): the protocol MUST never mint a QoS or machinery knob named app, and implementations MUST NOT accept settings.app as a protocol knob. Everything below settings.app. is owner-defined — names, nesting (the module-namespacing shape above), and value bytes are the application’s, opaque to the runtime. Fields there are writable only where the owner’s field descriptor table declares them (ro/rw/wo); undeclared names return ERROR{tr::schema::not_found} on read and write — for the owner and for callers the vertex ACL admits the operation’s right. A caller the ACL denies receives ERROR{tr::access::denied} before any name under settings.app. is resolved, uniformly over declared, undeclared, ro and wo spellings, so a protected vertex never discloses owner-field existence through the error channel (RFC-0010 §Erratum 2026-08-12 — owner names are a per-node secret, unlike the protocol’s published field namespace, which resolves name-validity above the gate on both doors). One reservation, collision-proof both ways: the protocol keeps minting flat knob names forever; applications only ever mint below .app..

Header settings

  • opt.PL = 1.

Where it appears

  • <vertex>:settings for atomic multi-field reads; a bare :settings read serves the container — the nested app record when a descriptor table is installed, and an empty SETTINGS{} when none is (RFC-0010 §A.4 as amended by RFC-0022 §4) — and :settings.app serves the app record alone. Writes are per-field under settings.app.; the flat core namespace takes none, and there is no atomic multi-field settings write (a bare :settings write resolves nothing and returns SCHEMA_NOT_FOUND).

  • Inside SUBSCRIBER as the qos_settings sub-field for per-subscription overrides.

  • Inside the :schema POINT (§0x07): the synthesized protocol part, and one SETTINGS per declared app field inside the owner part.

  • As the whole reply to read <vertex>:identity — the node-identity record below.

Validation

  • Unknown NAMEs MUST be either (a) ignored if module-namespaced and the module is not loaded, or (b) rejected with ERROR{tr::schema::not_found} if in the core namespace.

  • Type mismatches (e.g., a u32 where u8 expected) MUST return ERROR{tr::schema::type_mismatch}.

⚠️ Conformance gap — arm (a) is unimplemented in the reference core. It does not distinguish a module-namespaced NAME from a core one: every second step below settings other than the reserved app is rejected with tr::schema::not_found, not ignored, and there is no module registry keyed on a settings sub-name. The rule above is the requirement and is unchanged; the reference implementation currently meets only arm (b). See 02-graph-model.md §module field namespacing.

The core knob namespace is empty

(RFC-0022 §3.B/§4 — full rationale in 04-communication-flows.md §Storage policy is declared owner-side.)

All seven names the vertex SETTINGS core namespace ever held are gone. A read or a write of any of them answers ERROR{tr::schema::not_found} — the honest answer, and the one an unsupported field already gives — caller-independently, since an unknown core-namespace NAME resolves before any ACL gate:

removed name

where it went

reliability, priority, durability

the subscription’s packed delivery policy (§0x04 SUBSCRIBER) — they describe a producer→subscriber relationship, not a vertex

deadline_ns, queue_max_bytes

deleted: inert, and with no coherent per-vertex meaning

history_keep_last

owner-side vertex state (vertex_policy_t::retention) — an application retention intent, with no wire surface

store_ref_min_bytes

owner-side vertex state — a deployment copy/pin trade (ADR-0042 §3), with no wire surface; the host call is vertex_policy_t::share_threshold_bytes — an absolute copy-or-share threshold in bytes (RFC-0028 §5.3, which replaced the RFC-0022 §3.D ratio K). What that declaration buys and what it costs — a shared value borrows its inbound RX segment for its whole lifetime, which on a pooled RX backend is a pool slot of receive capacity, and the application owns that budget because no per-value knob bounds the number of retained values — is in 02 §”The pin is a BORROW, and the application owns the budget”. The host default is 4,096 B; NARROW targets copy always (the ESP-IDF component binds SIZE_MAX)

No deprecation window: the protocol is DRAFT, and of the seven only three ever drove behaviour — none of them as remotely writable QoS. Conformance vectors: settings/removed-knob, stream/history-depth-host-only.

The :schema and bare-:settings reads therefore enumerate nothing in the core namespace (settings/schema-enumerates-nothing, settings/read-container-shape), so the read surface and the write gate cannot disagree. settings.app.* (RFC-0010 §A) is untouched, and the reservation of the app key means a future protocol knob can still be minted flat without colliding with it.

The node-identity record — :identity

read <vertex>:identity (RFC-0011) serves one SETTINGS TLV carrying the node’s public key. It reuses the NAME-keyed record shape of the : plane rather than a bare key blob, so a second identity kind can be added without a surface break.

SETTINGS (PL=1) {
  NAME "kind"  VALUE <u8>          ; required, FIRST — identity-kind registry below
  NAME "key"   VALUE <key bytes>   ; required, SECOND — the raw public key
  ...                              ; optional future members; readers MUST ignore unknown NAMEs
}

The two required members appear in that fixed order.

Identity-kind registry (additions RFC-gated, like the error registry):

kind

Meaning

key length

0x00

reserved — invalid

—

0x01

ed25519 raw public key (RFC 8032 encoding)

exactly 32 bytes

Worked bytes — the complete ed25519 record, 60 bytes:

0B 40 38 00                                SETTINGS: type=0x0B opt=0x40(PL=1) len=56
  02 00 04 00 6B 69 6E 64                  NAME "kind"                    (8 bytes)
  01 00 01 00 01                           VALUE u8 = 0x01 (ed25519)      (5 bytes)
  02 00 03 00 6B 65 79                     NAME "key"                     (7 bytes)
  01 00 20 00 <32 raw pubkey bytes>        VALUE = ed25519 public key    (36 bytes)

4 (header) + 56 (payload) = 60 bytes; a trailer rides per the serving link’s egress policy, as for any reply.

Normative rules:

  • Malformed records never reach the wire. A kind outside the registry, a missing or misordered required member, or a key length that contradicts the kind (kind 0x01 ⇒ exactly 32 bytes) is rejected with ERROR{tr::schema::type_mismatch} — at install time on the serving side, and on decode at the reader.

  • Absent keypair ⇒ ERROR{tr::schema::not_found} (0x0031). A node with no keypair answers byte-for-byte as it answers any field it does not expose. Absence is absence; an empty record would fabricate an “identity exists but is vacant” state no consumer can act on.

  • The whole identity namespace resolves. The record is served whole and has no member or indexed addressing, so :identity.key, :identity[0] and every other shape answer SCHEMA_NOT_FOUND — caller-independently, the same answer for an authorized and an unauthorized caller alike.

  • Node-scoped and pre-serialized. A node holding a keypair MUST serve the record at every vertex of its graph, and all of them MUST return byte-identical bytes; a multi-homed node MUST present the same record on every transport. That identity-per-node invariant is what makes the record a valid cross-path dedup key — the surface a client-side topology walk uses to tell “same node reached two ways” from “two nodes”.

  • Pre-auth readable — exempt from the READ gate. The :identity read MUST be served regardless of the caller’s subject and the vertex’s effective ACL, including to an anonymous caller: identity resolves above the ACL check. The public key is precisely what an unauthenticated peer must obtain in order to TOFU-pin, and the default ACL ships closed, so an implementation that gates :identity behind READ deadlocks first contact. The exemption is narrow and named: it applies to this field alone, and it discloses nothing an authenticating handshake would not present as its static key anyway.

The seam census record — :stats

read <any-vertex>:stats.<seam-class>.<seam-name> (RFC-0010 §Amendment 1) serves one SETTINGS TLV carrying one seam’s whole counter block. Like :identity it is node-scoped — it takes no vertex, and every vertex answers identically — and unlike :identity it is READ-gated.

SETTINGS (PL=1) {
  NAME "<noun>"  VALUE <u64>       ; one pair per counter, fixed-width u64 little-endian
  ...                              ; readers MUST ignore unknown NAMEs
}

The seams a node’s graph answers for:

spelling

the seam

nouns

:stats.mem.control

the injected failable block source every peer-provokable allocation draws from

capacity, in_use, peak, refused, largest_refused

:stats.mem.ring

the graph-level default receiver-ring source

the same five

:stats.graph.delivery

the graph’s delivery-drop door (the net plane counts through it too)

no_target, denied, out_of_memory, fan_out_truncated

And the NET-PLANE seams (RFC-0010 §Amendment 2), which a node publishes when it has a router — the router registers a sampler UP into the graph, so L4 still reaches nothing below itself:

spelling

the seam

nouns

:stats.router.drops

the node’s forwarder’s counted cold-path drops

flatten_dropped, forward_iov_dropped, arena_dropped, assemble_dropped, reply_iov_dropped, delivery_iov_dropped, malformed_rx

:stats.labels.table

the RFC-0027 label plane — mint refusals beside dereference tallies (deleted with the table by RFC-0029 slice S3, as is labels_used below)

labels_exhausted, refused_bindings, label_not_found, label_resolves

:stats.link.<child>

ONE registered transport child, by the NAME its /net/<module>/<name> connection vertex carries

dropped_rx, malformed_rx, dropped_tx, and labels_used where the node mints labels

A node that constructed no router publishes none of the three, and a link sub-key naming no registered child — or a removed one — is not a seam: all of these answer ERROR{tr::schema::not_found} (0x0031), which a monitor reads as “not published here”. A link’s rx_capacity / tx_capacity ceilings are deliberately absent: they are per-kind and in per-kind units (buffer bytes on a WebSocket link, TX-pool slots on CAN), so there is no unit-safe ceiling to publish at the interface.

Worked bytes — the graph-door block on a fresh node, 113 bytes (the settings/stats-seam-block vector):

0B 40 6D 00                                SETTINGS: type=0x0B opt=0x40(PL=1) len=109
  02 00 09 00 6E 6F 5F 74 61 72 67 65 74   NAME "no_target"
  01 00 08 00 00 00 00 00 00 00 00 00      VALUE u64 = 0
  02 00 06 00 64 65 6E 69 65 64            NAME "denied"
  01 00 08 00 00 00 00 00 00 00 00 00      VALUE u64 = 0
  02 00 0D 00 6F 75 74 5F 6F 66 5F 6D 65 6D 6F 72 79    NAME "out_of_memory"
  01 00 08 00 00 00 00 00 00 00 00 00      VALUE u64 = 0
  02 00 11 00 66 61 6E 5F 6F 75 74 5F 74 72 75 6E 63 61 74 65 64    NAME "fan_out_truncated"
  01 00 08 00 00 00 00 00 00 00 00 00      VALUE u64 = 0

Normative rules:

  • One READ is one SEAM is one BLOCK, sampled in ONE call. The snapshot-coherence clause (core/STYLE.md §Introspection: monotonic since construction, sampled unsynchronized, read as the difference between two snapshots) is unachievable across separate field reads, so per-counter fields are not this surface.

  • NAME validity resolves ABOVE the READ gate. Bare :stats, a bare :stats.<class>, an unknown class or seam, any [N] / [] / [*] selector, and any deeper tail all answer ERROR{tr::schema::not_found} (0x0031) caller-independently. Aggregating seams into a container is deliberately unspellable.

  • The VALUE resolves BELOW it — gated on READ. A denied caller receives ERROR{tr::access::denied} (0x0050), which discloses that the seam exists; that is intended, because the seam namespace is published spec, identical on every node.

  • Read-only, and never awaitable. A write of any :stats spelling answers ERROR{tr::schema::not_found} caller-independently. Counters do not advance the vertex’s write sequence, so no await could ever fire on them — and every field-tailed await already answers ERROR{tr::schema::not_found} regardless (§0x0F FWD).

  • A reader MUST ignore unknown NAMEs and MUST NOT depend on the member count: a seam may grow a noun. A node that does not publish a seam answers SCHEMA_NOT_FOUND, which a monitor MUST read as “not published here”, never as an error.


0x0C — TIME

64-bit absolute timestamp, nanoseconds since Unix epoch (1970-01-01 00:00:00 UTC).

Payload layout

[ u64 timestamp_ns_le ]   ; 8 bytes, little-endian

Where it appears

  • Inside structured TLVs (typically a user-range record type with opt.PL=1) as a sibling of VALUE when application-domain timestamps matter (sample-acquisition time, sensor exposure window, control deadline). Multiple TIME TLVs in one structured TLV is permitted; semantics are application-defined (typically discriminated by a sibling NAME).

  • The wire-trailer opt.TS=1 (see 01-data-format.md) is transport-time — it tells you when the sender put the TLV on the wire. That is a different concern from application-domain time and the two SHOULD NOT be conflated.

Constraints

  • u64 wraparound: year 2554 (584 years from 1970). Acceptable.

  • Negative (pre-epoch) values: not representable; reject with ERROR{tr::path::invalid} (no dedicated INVALID_TIME code).

Hex example

A BATCH record TLV (type=0x80, opt.PL=1) containing a TIME and a VALUE, with outer CRC-32. 0x80 is the user-range code formally assigned to BATCH by RFC-0025 §4.1.2 (Amendment 3, clause 6) — see §User range; this example predates the assignment and reads the same either way, since the graph never interprets the record:

80 50 14 00 [inner 20 bytes] [crc:4]
^  ^  ^^^^^
|  |  length = 20
|  opt = 0x50 (PL=1, CR=1)
type = 0x80 (BATCH — the assigned user-range record; §User range)

  Children (20 bytes):
  0C 00 08 00 00 00 00 00 00 00 00 00 00         ← TIME, 12 bytes
  ^  ^  ^^^^^ ^^^^^^^^^^^^^^^^^^^^^^^
  |  |  len=8  u64 = 0 (epoch)
  |  opt = 0
  type = 0x0C TIME

  01 00 04 00 DE AD BE EF                         ← VALUE u32 = 0xDEADBEEF, 8 bytes

4 (outer header) + 20 (children) + 4 (outer CRC) = 28 bytes total.

(The application uses a specific user-range type code to declare what the wrapper means — there is no generic container type; see §0x05.)


0x0D — ROUTER (reserved)

0x0D is a reserved, decodable codepoint with no implemented mechanism (ADR-0040). The frame codec parses a 0x0D TLV generically (a structured container when opt.PL=1, per the rules of 01-data-format.md); no protocol mechanism emits or interprets it.

The remote-operation plane is the source-routed FWD (0x0F, below): every remote endpoint is addressed by an explicit source route, and dst shrinks monotonically per hop, so a delivery travels exactly as far as its explicit route and no further — loop-free by construction, not by a revisit check (there is no visited-set and no revisit ERROR; a dst that spells out a physical cycle simply routes around it as many times as the route names, then stops). So FWD source-routing needs no duplicate suppression. Parallel links to one peer are different explicit addresses (deliberate redundancy), not auto-multipath, so no “same value arrived two ways” case exists to dedup.

The codepoint is held in reserve for a possible future flooding profile (an auto-multipath deployment class outside the current topology scope). Until such a profile assigns it a payload layout:

  • Senders MUST NOT emit type=0x0D.

  • Receivers MUST handle type=0x0D per the unknown-code rules of 01-data-format.md (decode structurally, skip safely, do not crash).


0x0E — SPEC

Vertex-creation spec. Writing a SPEC requests that the device instantiate a child vertex of a device-known type (ADR-0017). Creation is an ordinary write — no new wire verb — and it is one optional, standard control surface (the vertex-ioctl model of ADR-0021); a device that does not support dynamic creation exposes no creation surface and answers SCHEMA_NOT_FOUND.

Payload layout

Structured (opt.PL=1):

SPEC (0x0E, PL=1) {
  NAME "type"    NAME <catalog selector — required only where the catalog is global>
  NAME "name"    NAME <the new child's path component>
  NAME "config"  SETTINGS { … }   ; optional — instantiation params
}

The body is a run of positional pairs: a NAME key followed by its value child. Both halves are typed, and the value’s type is part of the grammar, not a stylistic choice — type and name are carried by a NAME (0x02) child, config by a SETTINGS (0x0B). A receiver matches each pair on the value’s type and skips any other, so a VALUE (0x01) in a type/name slot is not a lenient spelling of the same thing: the field is dropped, the catalog selector comes up empty, and the create is refused (INVALID_PATH). The distinction is invisible to a round-trip — such a SPEC decodes and re-encodes to itself perfectly — so it is pinned by the spec/ conformance vectors instead.

The same typing rule governs config’s own key/value pairs: an integer or flag is a VALUE (little-endian), a string is a NAME. A string-valued key is found only as a NAME child. Note that such a string is not an address segment and need not satisfy the addressing grammar — a addr dotted quad contains ., which an address segment may not — it is simply the wire’s string node.

The walk is pair-consuming: an unrecognised key is skipped together with its value, so a value child is never re-read as the next position’s key. That is what lets forward-compat tolerance coexist with positional pairing.

Where it appears

  • Written to the owner-designated creator endpoint (ADR-0059 §Decision 1): a creation field on the parent is superseded, because :schema is vertex-only and a field-hosted catalog has nowhere to live. The accepted surface (RFC-0014 §1) is a per-module creator-endpoint vertex, conventionally /net/<module>/conn, where a module is one (transport, role) pair mounted flat under the net root — conventionally /net, a recommendation (the constructor default), not a library rule — under an application-declared name (register_module, declared-only per ADR-0073 §4; ws-client, ws-server, can, … are the built-ins’ suggested spellings). There, both transport and role are positional — the path already says them — so the SPEC carries { name, config } with no type and no role member, and read /net/<module>/conn:schema is the catalog of that module’s accepted config. write SPEC{name, config} creates /net/<module>/<name> atomically; write NAME{<name>} to the same endpoint retires it. The endpoint is implemented (RFC-0014 S2b) — including the reserved conn name in both directions and the no-op success for a NAME naming nothing — and it is the only connection-creation door: the superseded SPEC{type = "client"|"listener", name, config} write to /net:children[] was retired at S7, so it now answers SCHEMA_NOT_FOUND. (:children[] as an enumeration is a read and is unaffected, and :children[] creation still serves stored_value and any application-registered type.) The per-module :schema-as-catalog the endpoint needs is not implemented (S3), so a read of conn:schema does not yet serve the module’s config catalog.

  • Gating (RFC-0014 §5, RFC-0009 §A.1.1): a SPEC (create) write needs CREATE (0x08) on the endpoint; the NAME (remove) write needs WRITE (0x02), not DELETE. The two rights are independent, so a peer can hold create-but-not-remove, or the reverse.

  • The device validates type/config against its catalog; an unknown or malformed selector returns ERROR{tr::schema::not_found}. A create naming an existing child returns ERROR{tr::path::in_use}.

  • Reading a parent’s :children[] returns the subtree members, not SPECs (write-spec / read-members asymmetry).

  • The created controller exposes its own port vertices; wiring them is a separate binding step (SUBSCRIBER edges).

Validation

  • type MUST name a type in the device’s catalog, else SCHEMA_NOT_FOUND.

  • name MUST be a valid single path component (per 03-addressing.md) and MUST NOT collide with a protocol-owned name on the endpoint.


Reserved range (0x0F – 0x1F)

Allocated on a fast-track basis during v1. Assigned so far:

  • 0x0F FWD and 0x10 FIELD — the remote-operation frames (RFC-0004 §B/§C, ADR-0035).

    • A FWD’s dst and src are PATHs; a dst MAY spell any node’s part as a PAIR element (§0x06 §path element PAIR), and a src on the request leg stays canonical NAMEs. A dst presented as 0x14 is refused (RFC-0029 §5.3).

    • The op byte’s opcode is op & 0x3F; bits 7–6 are reserved, MUST be zero — RFC-0024’s bit-7 mint request is retired (RFC-0029 §5.3). A forwarder MUST mask rather than switch on the raw byte (RFC-0024 §9.3).

    • A request whose src is a zero-length PATH is unacknowledged: it carries no return route, and the terminus emits no frame for it (RFC-0004 Amendment 2). A WRITE is applied and answered with silence — success, refusal and ACL denial alike, the same drop a denied COMPACT delivery takes (§route-handle below). A READ, an AWAIT or a :subscribers[] subscribe with an empty src is malformed and dropped at the terminus (no route can carry a NACK). The child is empty, never omitted — the child run is positional and a WRITE payload may itself be PATH-typed, so omission is ambiguous; the grammar is unchanged. Forwarders still accumulate into src unconditionally, so the marker reaches a terminus only from a directly attached origin or on the delivery leg, where the producer emits src=<empty PATH> itself. The standing plane — a SUBSCRIBER plus delivery_compact (§0x04, §route-handle) — remains the first answer for streaming wherever the topology admits a consumer-initiated subscription; the unacknowledged write is for push-ingest, where the producer is the client and subscription would invert who initiates.

    • A forwarded subscribe request MAY additionally carry a trailing PATH_REF_REVERSE (§0x15) child after RFC-0004 §B’s closed child list — the reverse-direction chain, contributed by forwarding hops only, never by the origin (RFC-0029 §7.1). It is identified by its type code, never by its position; it is nonetheless last, so a positional reader of an ordinary request is untouched.

    • A REPLY’s src accumulates the forward route head-first on the way back, each host prepending its part as a PAIR or as its NAME run, never nothing (RFC-0029 §6.2, amending RFC-0004 §B).

  • 0x11–0x13 — the route-handle transport-plane control frames (below).

  • 0x14 — retired (was PATH_REF, RFC-0024’s bound path; RFC-0029 §5.3, below). Not reassigned.

  • 0x15 PATH_REF_REVERSE — the reverse-direction chain a subscribe request accumulates (RFC-0029 §7.1, below). A PATH body; a different role.

Unassigned: 0x16–0x1F. 0x16 is unassigned as a type code and taken as an escape kind — RFC-0029’s PAIR element is kind = 0x16 inside a packed PATH body (§0x06 §path element PAIR), which is a different namespace that happens to share the number. Assigning TLV 0x16 would not collide, but a reader meeting 16 in a PATH body is meeting the escape kind, not this registry. Candidate uses: CAPABILITY (opaque token, lighter than full ACL), HEARTBEAT (an explicit liveness ping; the intended alternative is writes to the :liveness.last_seen_ns field, 04-communication-flows.md — ⚠️ which is itself unimplemented, so neither spelling exists today, #586). Receivers MUST handle unknown codes in this range per the forward-compatibility rules of 01-data-format.md §forward / backward compatibility.

A single-hop FWD request → reply round-trip (the consumer reaches a terminus node directly). The reply’s dst is the request’s src; a failure comes back as kind=ERROR carrying STATUS{ ERROR }:

        sequenceDiagram
    autonumber
    participant C as Consumer (client)
    participant N as Node (resolver)
    participant V as Vertex
    C->>N: FWD{ op=READ, dst=/sensor/temp, src=/client }
    N->>V: resolve dst → local vertex, read LKV
    V-->>N: zero-copy refcount clone of the stored value
    N-->>C: FWD{ op=REPLY, dst=/client, kind=RESULT, VALUE }
    Note over C,N: WRITE/AWAIT/subscribe ride the same shape<br/>an error returns kind=ERROR + STATUS{ERROR}
    

At the terminus — the one place a node reads the whole FWD tree — the frame is decoded into a flat arena rather than an owning tree (ADR-0041): the decoder parses it into pre-order span-nodes ({type, opt, wire — trailer-excluded, body, end}), every span pointing into the inbound frame, and the resolver runs over that arena. The nodes are drawn from an injected nothrow block source — a host that supplies a bounded source over its own slab gets a terminus that allocates nothing from the global heap, and one whose exhaustion is a tr::tlv::nesting_too_deep reject rather than an allocation failure (ADR-0065).

        flowchart LR
    F["inbound FWD frame<br/>(bytes)"] --> D["decode into a span arena"]
    D --> A["flat pre-order span-nodes"]
    A --> R["resolve"]
    R --> K["vertex lookup:<br/>span-aliased path key<br/>(canonical PATH body IS the map key)"]
    R --> S["WRITE store:<br/>one trailer-sliced copy<br/>(header+body, trailer-less at rest)"]
    R --> E["FWD{REPLY} head direct-emitted<br/>into ONE exactly-sized segment;<br/>payload refcount-roped, zero-copy"]
    

Three properties of the arena resolve:

  • Span-aliased vertex lookup — a canonical PATH body is byte-identical to the graph’s vertex-map key, so dispatch uses the frame’s own bytes as the key with zero materialization. Since RFC-0018 that alias is unconditional: a packed body has exactly one spelling per address, so there is no non-canonical form to re-emit and no re-emit fallback to fall into. A body that does not tile into literal records is refused outright (§PATH enforcement).

  • Trailer-sliced stores — a stored WRITE value copies the node’s header+body span exactly once (below the target vertex’s copy-or-share threshold, into the value’s own block — RFC-0028 §5.1 — or, at or above it when the frame arrived as an owning view, is referenced as a zero-copy subview of the refcounted frame — ADR-0042); the trailer never lands at rest either way (a trailer-carrying payload always falls back to the sliced copy). The referenced form is a borrow of the whole inbound RX segment for the stored value’s lifetime, so on a pooled RX backend it costs a slot of receive capacity until the value is displaced — an application-owned budget: 4,096 B and up on the host by default, never on a NARROW target (02 §”The pin is a BORROW”).

  • Direct-emitted reply — every reply-head length is known from the node spans, so the FWD{REPLY} head (including the route bytes, copied once) is emitted straight into one exactly-sized segment, and a READ’s reply payload rides as a zero-copy refcounted rope.

Across hops, FWD is source-routed and stateless: each forwarder strips the whole leading dst mount run (the next link — net/<module>/<name>[/<peer>], RFC-0014 S2a) and prepends its own mount run for the inbound link to src (a zero-copy rope head-prepend), so dst shrinks toward the target while src grows into the return route. A REPLY retraces that accumulated src and does not itself accumulate:

        sequenceDiagram
    autonumber
    participant C as Consumer
    participant A as Forwarder A
    participant B as Producer B
    C->>A: FWD{ dst=A·B·temp, src=C }
    Note over A: strip leading dst (A), prepend inbound link to src
    A->>B: FWD{ dst=B·temp, src=A·C }
    Note over B: first dst segment is local ⇒ terminus
    B-->>A: FWD{ op=REPLY, dst=A·C }
    A-->>C: FWD{ op=REPLY, dst=C, VALUE }
    

Route-handle frames — 0x11 ADVERTISE, 0x12 COMPACT, 0x13 HANDLE_NACK

The route-handle (RFC-0004 §E.1, ADR-0035 slice 4) is ws/UDP delivery-compaction — the full-TLV counterpart of the CAN transport’s identity↔path map (14-can-transport.md). These three frames are transport-plane control, not core conformance TLVs: they ride a link alongside FWD, are emitted only for a delivery_compact-flagged flow, and a peer that ignores them simply keeps the full-route delivery path — so they carry no conformance vectors and do not perturb the cross-core machine. All are structured (opt.PL=1); the label is a per-link u16 (VALUE, little-endian — 65 535 usable labels per link, since 0 is reserved to mean “none”), allocated monotonically per link and swapped each hop (MPLS-style).

ADVERTISE (0x11, PL=1) {            ; bind a label to a delivery route, in-band
  VALUE  label   ; u16, FIRST child — the (per downstream-link) label being bound
  PATH   route   ; the dst route the label aliases; each hop strips its leading
                 ; segment, allocates its OWN out-label, and re-advertises downstream
}
COMPACT (0x12, PL=1) {             ; a label-compacted delivery (no route rides)
  VALUE  label   ; u16, FIRST child — names the established route on THIS link
  <payload TLV>  ; the delivered value (a VALUE), expanded to a write at the terminus
}
HANDLE_NACK (0x13, PL=1) {         ; "I have no binding for this label" — prompts re-advertise
  VALUE  label   ; u16 — the stale/unknown label seen; sent back over the inbound link
}

A COMPACT whose label has no binding on its inbound link is dropped and a HANDLE_NACK is returned (never a crash); re-advertising on (re)connect — the same producer-holds reconnect trigger — rebinds the flow (ADR-0030 self-heal).

A COMPACT terminus is ACL-gated exactly as the FWD{WRITE} it compacts is. RFC-0004 §E.1 makes the terminus expand the label to the bound route and apply the write, and §F gates the target vertex’s :acl at the final hop — so compaction is a framing optimisation and never an authorization one. The caller context is the inbound link’s name, the same string the full-route form presents, and it is evaluated per frame: a label binding caches the address, never the authorization (ADR-0062), so an :acl written after a flow was advertised applies to that flow’s very next COMPACT and a label never becomes a capability that outlives its grant. A denied delivery is dropped like any other unwritable one — no new frame, no wire surface change. This is descriptive of the reference core: the two paths used to disagree, because the compacted one wrote with no caller at all and inherited the local-trusted context (#974).

A node clearing a link’s label state on (re)connect MUST also treat as stale every ingress binding it holds — on any link — whose downstream half crossed that link, and drop those bindings too. A forwarding binding is keyed by the link the label arrives on while it names the link the swapped label leaves by, so clearing only the reconnected link’s own tables leaves a mid-chain node still holding a binding aimed at an out-label that no longer exists. The upstream never observed the reconnect and so never re-advertises: it keeps streaming COMPACTs, the mid-chain node keeps swapping onto the dead out-label, the downstream keeps returning HANDLE_NACKs the mid-chain node can no longer answer, and the flow is silently dead in both directions. Dropping the crossing bindings makes the upstream’s next COMPACT miss, which draws the ordinary stale-label HANDLE_NACK and prompts the upstream’s own re-advertise — so recovery uses only the frames above, in the situations already specified for them, and no wire surface changes. A terminus binding has no downstream half and is never dropped by this rule.

That drop covers the bindings a node already holds; it says nothing about the ones still being made. An ADVERTISE is processed on its inbound link’s receive thread while a reconnect of the downstream link is processed on another, so a hop can mint its out-label and retain its egress route against the pre-clear downstream tables and bind the swap only afterwards — after the drop above has already scanned the inbound link. The binding then lands aimed at exactly the out-label the drop existed to invalidate. So the rule is a pair: a node clearing a link’s label state MUST also refuse any forwarding binding whose downstream half was resolved against that link’s pre-clear state, and the refusal and the drop MUST NOT interleave. A refused binding takes the same path an unbindable label already takes — the peer’s next COMPACT misses, draws the stale-label HANDLE_NACK, and re-advertises against the new state — so, again, no wire surface changes.

A node holds label state only for the compact flows crossing it (bounded by the number of such subscriptions); one-shot / cold / non-compact traffic allocates none, which preserves the stateless-forwarder property. Per-hop multiplexing of a reply to a specific request remains the transport’s concern (RFC-0004 §D), so no end-to-end handle exists.

0x14 — retired (was PATH_REF)

Retired as an address form by RFC-0029 §5.3 (accepted 2026-09-30). RFC-0024’s bound path — a bare array of 8-byte (u32 index, u32 generation) elements — survives as the PAIR element inside PATH (§0x06 §path element PAIR), which spells the same pair with per-element mixing and one grammar for every address. The type code carries no meaning: a host MUST refuse a frame that presents it as a dst (tr::path::invalid). With it go the FWD op bit 7 mint request, the reply’s trailing PATH_REF mint answer and RFC-0024 §7.1 erratum 1’s strip-whole-list rule. The code is not reassigned.

Implementation status: the reference implementation still emits and honours 0x14 until RFC-0029 slices S1–S2 land; the path-ref/ref-*, fwd/fwd-bound-* and bit-7 mint vectors retire with them.

Reverse path — 0x15 PATH_REF_REVERSE

The reverse-direction chain a subscribe request accumulates on its way to the responder (RFC-0029 §7.1; the type code is RFC-0024 §7.1 amendment 2’s). A subscription edge delivers with no reply leg, so it learns its chain on the request leg instead.

Payload layout

The body is a PATH body (§0x06): opt.PL MUST be 0, and the body is a self-delimiting run of NAME segment records and PAIR escape records, head-first in delivery order (responder-first), under every §0x06 constraint. RFC-0024’s bare-array grammar for this code is retired with the last positional list.

Hop behaviour

  • A forwarding hop relaying a subscribe request (a SUBSCRIBER write) prepends its reverse-direction element to the request’s trailing 0x15 child, creating the child if the request carries none: its PAIR for the identity the request arrived on — the inbound connection vertex point-to-point, the accepted session’s anchor for a bus arrival — or, when it cannot issue one, its inbound mount run as NAMEs. Never nothing, so the chain is never stripped and never skips a hop. No flag gates this.

  • The responder completes the chain with element 0 — its own element for the egress toward the writer — and stores it beside the canonical return route. Each delivery spells dst as that chain: element 0 is consumed locally, the rest ride the wire, src empty.

  • A refusal at any hop of a delivery is a drop (there is no src to answer); the edge falls through to its canonical return route on the next delivery and re-learns.

  • The child is identified by its type code, never by its position, and appears only on a forwarded subscribe request — never on a reply and never on the origin’s own frame.

The price is +3 B per element over RFC-0024’s bare array, on the subscribe request leg only: the steady-state delivery frame spells the learned chain in dst and does not grow.

Edges with no request leg. A mount-routed subscription (whose reverse chain spells the way back to the writer, not to the third-party consumer) and a host-local subscribe_toward edge both start with no chain. Such an edge learns on its first fire: it sends that delivery with a non-empty src — its node’s one learn endpoint, the edge identified in the tail — so the terminus answers a REPLY whose src carries the forward chain (§0x06 §learning). There is one endpoint per node, never one per edge. One reply per learn is the whole cost; later deliveries are src-empty.

Implementation status: until RFC-0029 slice S5 lands, the reference implementation still spells 0x15 as RFC-0024’s bare 8-byte array, accumulates it only on an op bit 7 request and strips it when a hop cannot contribute; path-ref/reverse-len-not-multiple-of-8 and fwd/fwd-reverse-mint describe that form.

Optionality. Optional to emit, optional to accept. A peer that does not accept it treats the child per the forward-compatibility rules of 01-data-format.md §handling unknown type codes, and a responder that never receives one keeps a canonical return route.


Reserved range (0x20 – 0x7F)

Long-term registry for future core extensions, post-v1. Allocation procedure: PR against this document with rationale + byte spec; implementer review; assignment.


User range (0x80 – 0xFF)

128 type codes the protocol does not opine on. Senders and receivers agree out-of-band. Recommended convention: register a project-specific “magic” prefix (e.g., 4-byte UUID-derived bytes at the start of the payload) so multiple unrelated user types can coexist on the same wire without collision.

One code is assigned: 0x80 = BATCH. RFC-0025 §4.1.2 (Amendment 3, 2026-08-21, clause 6) promotes 0x80 from a worked example to the formal record type of the batch convention — a structured (opt.PL=1) written value whose children are the sample frames, carrying one payload TIME (§0x0C) child as the batch base and, for a non-uniform stream, a packed i32 offset array (RFC-0025 §4.2.1, Amendment 1). Three properties of the range survive the assignment intact:

  • No new grammar. A BATCH is an ordinary structured TLV every conforming decoder already decodes. No core-range type code is minted, no opt bit is added, and the graph still never interprets the record (claim 5) — the §4.3 stream descriptor tells consumers how to read it.

  • The protocol still does not opine on the range. A deployment already using 0x80 for its own record is not made non-conforming; the register-a-prefix advice above still applies, and this range remains per-deployment. What changed is that libtracer’s own convention now has a number.

  • Nothing on the wire changes. No conformance vector’s bytes move, and a receiver that has never heard of BATCH treats 0x80 exactly as it did before.

The assignment is scoped to a STANDALONE flush (RFC-0025 §4.1.2 clause 6, erratum 2026-08-23). When the flush is folded into a propagate(v, FOLD) branch write, the batch is seated in the swept node’s single structured VALUE (opt.PL=1) instead — because a branch-write node’s grammar (RFC-0005 §B: leading NAME, at most one VALUE, recursive POINT children) admits no 0x80 child, and the folded frame’s node shape is held byte-for-byte at that grammar. One layout, two spellings by carriage: the TIME base, the sample-frame children and the non-uniform offset array are identical in both; only the header type byte differs (0x80 → VALUE). A folded batch therefore never presents a user-range byte to the graph at all, which is why the graph-never-interprets rule survives the fold untouched.

Who composes a batch: the application, always (RFC-0025 §4.1.3, Amendment 4, 2026-08-23). The app ropes its sample values into one batch value — a small owned header segment carrying the record header and the TIME{u64 base} child, with the app’s existing sample bytes appended as refcounted links, never copies — swaps it in through the ordinary atomic LKV publish, and calls propagate/push. The graph composes nothing, derives no time, and holds no flush counter or window. The TIME base and any uniform-rate fact are payload bytes the app chose, which is why claim 5 holds trivially here: the graph does not know the value is a batch. Both carriages come out of one layout — the reference helper is tr::wire::compose_batch (core/include/libtracer/batch.hpp).


Pitfalls

Each entry states the rule, then the failure mode it produces when an implementation gets it the other way round.

Validating PATH in the codec

The rule: PATH’s grammar constraint is a resolver rule, not a codec rule. The codec decodes a PATH’s body as opaque bytes and round-trips them byte-identically; the resolver refuses to address a vertex through a body that does not tile into literal segment records, answering ERROR{tr::path::invalid} (0x0021).

The failure mode: a codec that “validates” PATH pushes the check to the wrong layer, and the resolver — the layer that still has to build a lookup key — normalizes whatever it is handed. Two byte-different PATHs then address one vertex, and every peer, cache and router keyed on PATH bytes has two spellings for one address. Vectors: path/path-escape-in-key-context, path/path-record-overruns-body.

How RFC-0018 changed the shape of this pitfall. Under the retired NAME-child body the classic instance was #436: a resolver re-materialized a non-canonical PATH by emitting every child body as a NAME regardless of the child’s actual type, silently rewriting PATH{VALUE "sensor"} into PATH{NAME "sensor"}’s key. That whole class is now structurally unrepresentable — a packed record has no type byte and no option byte, so there is nothing to mistype and no re-emit fallback to get wrong; the body is the key. What survives is the second half of the rule: an illegally-spelled address (ragged framing, or an escape record in key context) answers tr::path::invalid rather than resolving to something.

The related failure mode: answering tr::path::not_found instead of tr::path::invalid asserts that the address was well-formed but absent, which sends a peer retrying an address it can never spell. And because enforcement precedes write-create, an implementation that checks the child type after the mkdir-p branch materializes vertices for addresses the spec says cannot be spelled.

Treating the field namespace as a list of readable facets

The rule: {subscribers, acl, children, settings, schema, identity, stats} is the namespace a dispatcher recognises. A recognised name can still answer NOT_FOUND (the facet is empty), or SCHEMA_NOT_FOUND (a spelling it does not accept, such as bare :subscribers where [N] is required, or a facet deliberately absent, such as :identity with no keypair).

The failure mode: an implementation that maps “recognised” to “always returns bytes” invents surfaces. <vertex>:status in particular is not a selector — it answers ERROR{tr::schema::not_found} — and a client that polls it as an asynchronous status channel polls something that never existed.

Gating :identity behind READ

The rule: identity resolves above the ACL check, deliberately, and the whole namespace resolves there — any member or indexed spelling answers SCHEMA_NOT_FOUND caller-independently.

The failure mode: an implementation that routes :identity through the ordinary “data/field read needs READ” gate deadlocks first contact, because the default ACL ships closed and the public key is exactly what an unauthenticated peer needs in order to TOFU-pin and authenticate. A second, subtler failure: resolving the bare spelling above the gate but letting :identity.key fall through to it makes the answer caller-dependent — a denied caller gets PERMISSION_DENIED where an allowed caller gets SCHEMA_NOT_FOUND — which leaks the caller’s authorization state through an error code.

Gating :stats like :identity

The rule: :stats is node-scoped in the :identity mould but its gating is inverted — only NAME validity resolves above the READ gate; the counter block itself is READ-gated, and a denied caller gets ERROR{tr::access::denied}.

The failure mode: an implementation that extends the pre-auth exemption to :stats because the two fields look alike publishes its memory census — slab sizes, occupancy, refusal counts, drop counts — to any unauthenticated peer that can open a link. Nothing deadlocks by gating it: a memory census is not first-contact material. The exemption is narrow and names :identity alone. The mirror-image error is gating the name too: resolving an unrecognised :stats spelling below the gate makes one spelling answer SCHEMA_NOT_FOUND to an allowed caller and PERMISSION_DENIED to a denied one, which is the caller-dependent disclosure §Gating :identity names.

Serving a per-vertex identity record

The rule: the record is node-scoped and pre-serialized; every vertex of one node, on every transport, serves byte-identical bytes.

The failure mode: a node that derives or re-encodes the record per vertex (different member order, an added member, a trailer on one link) breaks the one property consumers depend on — byte-equality as a dedup key. A topology walk that reached the node two ways then counts it as two nodes, and orbits the cycle.

Reading a delivery filter into qos_settings

The rule: qos_settings carries encoding hints only. There is no value-diff delivery filter and no throttle; delivery selection is the per-vertex, value-agnostic delivery_mode.

The failure mode: a producer that suppresses a delivery because the bytes did not change makes a value-inspecting decision the protocol forbids, and a consumer that assumes suppression mis-reads a stream of identical samples as a stalled producer. Deadbands and rate limits are application filter vertices, not fields.