libtracer protocol — version 1 (DRAFT)

Status: DRAFT. The wire format is not yet stable. Pin to a specific commit if you depend on this.

This document is normative. It uses the keywords MUST, MUST NOT, SHOULD, SHOULD NOT, MAY as defined in RFC 2119.

1. Scope

This specification defines the libtracer protocol: the wire format (frame header, options bits, fixed-width lengths, trailer timestamp/CRC), the core TLV type registry and each type’s payload layout, addressing (canonical PATH form and constraints), the error model (the tr::<concept>::<error> registry), the data API semantics an implementation must expose (read / write / await plus the :-field control surface — there are no connect/disconnect/subscribe wire operations; subscription is a SUBSCRIBER write into :subscribers[]), and the conformance procedure.

It deliberately does not specify: the module/host ABI (implementation- defined, ADR-0013); a transport’s native framing below the TLV layer (each transport documents its own binding); application payload semantics (a VALUE is opaque to the protocol); or discovery (a separate, layered concern).

2. Terminology

The canonical project vocabulary — vertex, edge, path, view, segment, rope, TLV, address-shift slicing, transport vertex, and the rest — is defined in the root glossary, CONTEXT.md, which this specification incorporates by reference for terminology. Where this document and the glossary disagree, this document wins.

For the purposes of section 3.1, the following additional terms are defined:

  • Path handle. An opaque, stable identifier for a vertex address that points (directly or indirectly) at a pre-encoded PATH TLV in the implementation’s read-only data segment. A path handle MUST resolve to the same byte sequence for the lifetime of the node.

  • Pre-encoded PATH TLV. A PATH TLV whose wire bytes are emitted at build time (e.g., as static const data in .rodata) or computed exactly once at node initialization, and never re-encoded thereafter.

  • Hot path. Any code path executed per-message during steady-state operation (typically inside a publisher’s sample loop, an ISR, or a router dispatch). The hot path is contrasted with the init path, which executes once at node startup.

3. Wire format

The wire format is specified normatively by incorporation (ADR-0007, RFC-0001 §A.2):

  • docs/reference/01-data-format.md — the frame layout: 4/6-byte header, the opt bits (R|PL|TS|CR|LL|CW|TF|R, reserved bits MUST be zero), fixed-width u16/u32 length per opt.LL, the optional trailer (timestamp per opt.TS/opt.TF, CRC-32C or CRC-16-CCITT per opt.CR/opt.CW), little-endian multi-byte fields, and the iterative- parsing requirement (nesting depth receiver-resource-bounded, RFC-0006).

  • docs/reference/05-protocol-tlvs.md — the type-code registry (0x00 sentinel; 0x010x1F core; 0x05 reserved with no assigned meaning; 0x0D reserved codepoint with no mechanism; 0x06 PATH, the canonical address form, whose body is not a child sequence — opt.PL MUST be 0 and the body is zero or more segment records, each [u8 len][len bytes of UTF-8], in order, walked as p += 1 + body[p], with len in 1..64; len == 0 is the escape record 00 <u8 kind> <u8 len> <len bytes>, which is admissible in a frame path (a hop that does not implement kind MUST step over it by its declared length rather than reject the frame it is relaying) and MUST be rejected wherever a path is used as a canonical key, kind = 0x16 being reserved for the label element of RFC-0027 (RFC-0018 §5 and §5.4 amendment 1; this reverses RFC-0004 §134’s “children MUST be NAME” invariant, NAME (0x02) surviving for SETTINGS keys and :schema labels); 0x14 PATH_REF, the bound-path address form, whose body is a bare array of fixed 8-byte (u32 index, u32 generation) elements — opt.PL and opt.LL MUST both be 0, length MUST be a multiple of 8, and the element count MUST be ≤ 255 (length ≤ 2040), RFC-0024 §4; and 0x15 PATH_REF_REVERSE, the reverse-direction list a mint-flagged request accumulates, which carries that body grammar identically and differs only in role, RFC-0024 §7.1 amendment 2) together with its routing semantics0x14 §routing semantics, RFC-0024 §5–§7): the bounds-check / generation-compare / per-operation-authorize order, the rule that a failed validation MUST drop and MUST NOT repair or forward, the MUST NOT wrap a generation, the requirement that every bound-form operation re-check access at the dereferenced vertex, and the FWD op byte’s op & 0x3F masking rule with bit 7 as the mint request; and, with the forwarding car, the hop rules: a host reading a residual longer than one element consumes element 0, egresses through the connection vertex that element dereferences to (never a bus mount and never a bus peer), shrinks the dst by exactly that element while src accumulates canonically, a hop forwarding a mint-flagged request MUST either prepend its own reverse-direction element to the request’s trailing PATH_REF_REVERSE child or strip that child entirely — that child being identified by its type code and never by its position (RFC-0024 §7.1 amendments 1 and 2; the origin’s own frame carries no such child), and a hop forwarding a reply that carries a mint answer MUST either prepend its own element or strip the answer — never, in either direction, relay a list that skips a hop — the rule that the hop consuming the final element of a bound dst re-heads its egress dst as a canonical empty PATH rather than a zero-element PATH_REF, so a client that never speaks the bound form is never answered in it (RFC-0024 §7.1 erratum 3), and the scope of the refusal echo — a failed validation always drops, and an addressed echo of the refused PATH_REF is emitted on the one-element delivery arm only, a multi-element forward refusal dropping silently (RFC-0024 §5.3 erratum 4) — and every core type’s byte-precise payload layout, including the ERROR model (RFC-0002, accepted) and the remote-operation FWD/FIELD frames (RFC-0004). This document is incorporated in full, not only for its layouts: it is also where §1’s promised data-API semantics land — the read/write/await and subscribe/delivery clauses and the :-field control surface — together with the ACL model (access-mask bits, inheritance, evaluation order, and the denial codes) that §4’s acl vector category tests. Incorporated in full likewise: §0x06 §path label element — the path label (RFC-0027 §§4–8), which is not a type code but the escape record’s kind = 0x16 carrying a u32 LE (u16 slot index, u16 generation): it aliases one hop’s whole local part of a canonical address in place, is meaningful only on the host that minted it (a host MUST NOT interpret a label it did not mint), and is an address, never a capability — every labelled operation MUST evaluate the access check at the dereferenced vertex for its own right, exactly as the string spelling does; together with its routing semantics: minting is passive and rides a reply the hop was relaying anyway, adds no frame, flag or setup exchange and MUST NOT happen before the operation’s own authentication and ACL gates have passed; a host that cannot validate a presented label MUST NOT forward it, MUST NOT apply the operation and MUST NOT attempt any repair, and MUST answer tr::path::not_found, the sender falling back to the canonical original it still holds; a slot’s generation MUST saturate, never wrap, and a saturated slot is retired permanently; and there is no withdraw frame, no unbind, no lease and no TTL. Mixed name/label bodies are legal in a frame path and a labelled PATH is never admissible as a canonical key (§3 0x06); minting is optional at both ends, and a host that implements none of it steps over the escape record by its declared length.

  • docs/reference/03-addressing.md §path syntax — canonical PATH constraints (segment limit 255 (RFC-0023), NAME limit 64 bytes, total ≤ 1024 bytes, reserved characters, UTF-8).

Those documents’ MUST/SHOULD/MAY clauses are normative clauses of this specification. Behavioral notes marked informative in them are not. Where this section’s incorporated documents and any other document disagree, the incorporated text wins. There is no per-frame protocol-version field: version negotiation is a discovery-layer concern (ADR-0002); an incompatible peer is reported as tr::version::mismatch.

3.1 Static path-handle conformance

This subsection is normative. It exists to make libtracer usable on MCU-class devices (Cortex-M0/M3/M4, ESP32, RISC-V microcontrollers) where the hot path MUST NOT allocate, format strings, or walk a parser tree to address a vertex.

3.1.1 Path-handle invariant

A conforming implementation MUST provide a way for an application to obtain a path handle for any path it intends to address more than once. The handle:

  1. MUST resolve, deterministically and without allocation, to a byte sequence equal to the canonical PATH TLV (type=0x06, opt.PL=0, a body of packed [u8 len][utf8] segment records per docs/reference/05-protocol-tlvs.md §0x06 PATH, RFC-0018 §5) for the named vertex. A path handle is a canonical key, so its bytes MUST NOT contain an escape record (§3 0x06).

  2. MUST remain valid for the lifetime of the node, or until the application explicitly releases it.

  3. MUST be cheap to copy (the implementation defines whether this is a pointer, a small integer index, or a struct-by-value).

  4. MUST NOT require string parsing, snprintf, or malloc on the hot path.

The byte-equivalence requirement (1) is the load-bearing one: a write through a path handle MUST be indistinguishable on the wire from a write through the equivalent string-form path, after both are canonicalized. (Informative: under the packed body a segment record carries no per-segment option byte, so an address has exactly one spelling and this equivalence is structural rather than conventional — RFC-0018 §5.1.)

3.1.2 Static encoding at build time

A conforming implementation MUST permit pre-encoded PATH TLVs to be placed in read-only memory (.rodata, flash, or equivalent) and used directly as the source of bytes for a path handle. No copy from .rodata to RAM is required.

The PATH TLV byte layout is fully specified by docs/reference/01-data-format.md and docs/reference/05-protocol-tlvs.md §0x06. A build-time encoder (macro, code generator, or hand-written byte literal) producing those bytes MUST satisfy:

  • type byte = 0x06.

  • opt.PL = 0 — the body is a packed record run, not a child sequence (RFC-0018 §5).

  • length field width selected per opt.LL; total payload size MUST match the sum of the segment records’ sizes (1 + len each).

  • Each segment is one record [u8 len][len bytes of UTF-8], len in 1..64, payload the segment’s UTF-8 bytes (no NUL terminator). A pre-encoded PATH TLV is a canonical key, so len == 0 (the escape record, §3 0x06) MUST NOT appear in it.

  • All multi-byte fields in little-endian per docs/reference/01-data-format.md §frame layout.

  • Path constraints from docs/reference/03-addressing.md §path syntax (segment limit 255, name limit 64 bytes, total ≤ 1024 bytes measured as the PATH TLV’s length field) MUST be checked at encode time. A pre-encoded PATH TLV that violates these limits is non-conforming. (Informative: under the packed body a one-byte segment costs 2 bytes, so the 1024-byte budget admits up to 512 records and the 255-segment count binds first for short segments; the byte limit still binds first once the mean segment exceeds 3 bytes. Under the retired NAME-TLV body each segment cost 4 + len and the byte limit bound first at 204 segments, so the count clause could never fire — RFC-0023 §4, crossover per RFC-0018 §5.)

3.1.3 Init-time registration

When path handles cannot be statically derived (e.g., interpolated peer-id mounts under /peer/{peer_id}), a conforming implementation MUST provide an init-time registration call that:

  1. Accepts a string-form path.

  2. Validates per the rules of docs/reference/03-addressing.md.

  3. Allocates a single, reusable PATH TLV (in a long-lived segment).

  4. Returns a path handle equivalent to the build-time form.

The registration call MAY perform allocation. The path handle it returns MUST satisfy 3.1.1 thereafter — including the no-allocation requirement on subsequent uses.

3.1.4 Hot-path API requirement

The implementation MUST expose a write entry point that takes a path handle (not a string) and a value TLV. Calling this entry point MUST NOT:

  • Allocate memory (other than what the dispatch / fanout itself requires for subscriber views).

  • Format a string from numeric components.

  • Parse the path handle’s bytes (the handle is already a validated, canonicalized PATH TLV).

  • Invoke per-segment hashing or comparison if the implementation uses a path-handle-keyed dispatch table.

The corresponding read and await entry points MUST satisfy the same constraints.

A string-form convenience entry point (e.g., tracer_write_str("/sensor/temp", tlv)) MAY be provided for ergonomics on hosts where the cost of string parsing is acceptable. It is NOT required, and a minimum-feature implementation (P0) MAY omit it entirely.

3.1.5 Inline-segments vs reference-segments (informative)

Two implementation strategies satisfy 3.1.1–3.1.4. Both are conformant.

  • Inline-segments: the pre-encoded PATH TLV holds its segment records inline as a concatenated record run in .rodata. The handle is { const uint8_t *bytes; size_t len; }. Suited to bare-metal / Cortex-M targets.

  • Reference-segments: the pre-encoded PATH TLV is assembled from offsets into an interned-segment table. The handle is a small integer index. Suited to hosts where the same segment recurs across many paths and table reuse pays off.

Senders MAY choose either strategy. The wire bytes a path handle resolves to MUST be identical regardless of strategy.

4. Conformance

An implementation is libtracer v1 compatible if and only if it:

  1. Honors every MUST clause in this document.

  2. Passes every test vector under tests/conformance/vectors/v1/.

Vectors are organized vectors/v1/<category>/<case>/ (framing, crc, tlv-types, path, fwd, field, errors, acl, …), each case a self-describing input.bin with a human-readable expected.json; a conforming codec MUST round-trip each input.bin byte-exactly (encode(decode(x)) == x). New categories are added alongside spec additions; adding a vector is not a spec change, changing an existing vector’s bytes is.

There is no other compatibility test.

5. Versioning

Spec versions are integers. v1 is immutable once finalized — corrections that change conformance behavior require v2.

Changelog

  • unreleased — initial draft.

  • unreleased — added §3.1 static path-handle conformance (MCU-friendly addressing without runtime string formatting).

  • unreleased — §1 scope, §2 terminology (glossary by reference), §3 wire format normative-by-incorporation of reference/01 + 05 + 03 §path-syntax, §4 vector layout (RFC-0001 §A.2/§B/§E; RFC-0002 error model).

  • unreleased — §3 and §3.1.2: the canonical PATH segment cap is repriced from 32 to 255, and the encode-time limits sentence gains the informative note that the byte budget binds first (RFC-0023).

  • unreleased — §3, §3.1.1, §3.1.2 and §3.1.5: a PATH (0x06) body stops being a sequence of child NAME TLVs and becomes opt.PL=0 plus a self-delimiting run of [u8 len][utf8] segment records, with len == 0 reserved as the escape record — admissible in a frame path, rejected in canonical/key context (RFC-0018 §5 and §5.4 amendment 1, accepted 2026-08-06). This reverses RFC-0004 §134’s “children MUST be NAME” invariant; the §3.1.2 segment-cap note is re-derived for the packed body, where the 255-segment count binds first for short segments.

  • unreleased — §3: the path label is incorporated (RFC-0027 §§4–8, accepted 2026-08-15, amendments 4–7 ratified) — the escape record’s kind = 0x16 element, its node scope, its passive mint-on-reply distribution, the tr::path::not_found-and-fall-back-to-strings rule, saturate-and-retire generations, and the no-withdraw/no-aging rule. This adds no type code and no frame: 0x160x1F stay unassigned in the type registry, every existing conformance vector is byte-unchanged, and a PATH with no label element is byte-identical to before. Minting is optional at both ends and off unless an implementation is given a label table.