docs(spec): the custody chapter, with its receipt prefix reserved - #488
Merged
Merged
Conversation
bahdotsh
force-pushed
the
docs/headless-custody-chapter
branch
2 times, most recently
from
September 30, 2026 16:08
e78a713 to
e51a2bd
Compare
A forwarder holds a stranger's frame for five seconds; the device's own messages get seven days on disk. The custody chapter specifies how a device closes that gap for one class of traffic, and it is written before the code so the shape is agreed where the mechanism is invariant-laden. The chapter states seven invariants first: custody is replication, never transfer; a receipt settles nothing; the hold is strictly shorter than the outbox lifetime, because a custodian holds ciphertext it cannot re-seal; a custodian never blanks the route it is holding; Class A only, by the depositor's word, since a custodian cannot classify a sealed frame; a deposit comes from the depositor itself over the link that proved it, so the frame's sender must be the proven arrival peer and every forwarder strips the unsigned deposit key from any third-party frame it transmits; and custody is opt-in with its own erase. Then the deposit (the depositor's own frame with a reserved hop-local metadata key, accepted only where forwarding has already failed and the forwarding identifier is already released), the receipt (a signed control frame under the ordinary gate, sent once over the arrival link and gated on a capability entry so a peer that does not reserve the prefix never receives one), redelivery through a dedicated governor intake that skips the suppression check and the hop accounting and returns a frame that reaches no link to the store, a hold judged in wall time from the acceptance timestamp so a lowered hold applies to records already held and a custodian that was off for a week delivers nothing stale, the quotas with a stranger tier of zero, and the erase. The chapter states the condition on "cannot falsely settle": it holds with crypto recovery enabled, the default; with it disabled a stale copy is a terminal decrypt failure that is acknowledged, and for Class A the next version-offer exchange re-offers the change. Reservations, each in the registry that owns it: `__CUSTODY_RECEIPT__` in the control-message chapter and the engine's prefix macro, so the two-way registry test binds them and application text can never occupy the name; `__custody` in the wire-format chapter's reserved metadata keys; `data_versions` entry 7 in the capability chapter. No handler consumes the prefix yet and no peer advertises the entry, so nothing changes on the wire until the implementation lands. The threat model gains R19 (custody-borne re-key pressure, its condition, and what an integrator should read from the signal), R20 (a custodian retains third-party routing metadata for hours) and R21 (deposit spam, bounded by one proven address for depositor, tier, receipt and capability). The acknowledgement side-channel section says custody changes no acknowledgement decision. The replication state machine records the redelivery trigger, what each redelivered kind does at the recipient, and that the bottom rung of the catch-up ladder becomes reachable more often, which is one of the two reasons the hold is hours and not days. The conformance chapter lists custody among the chapters that are not yet surfaces: the receipt vector lands with the codec.
bahdotsh
force-pushed
the
docs/headless-custody-chapter
branch
from
September 30, 2026 19:35
e51a2bd to
b88a3c6
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A forwarder holds a stranger's frame for five seconds in memory; this device's own messages get seven days sealed on disk.
docs/spec/custody.mdspecifies how a device closes that gap for one class of traffic, and it lands before the code so the shape is agreed where the mechanism touches the most invariant-laden path in the engine (the mesh offer and the overdue drop point). Nothing changes on the wire in this PR: the prefix, the metadata key and the capability entry are reserved, no handler consumes the prefix, and no peer advertises the entry until the implementation does.The chapter is built on the custody design record, re-verified against
mainat 69e13b1 first: the receiver dedup is 2000 ids over 24 hours and persisted (the record said 1000 over one hour), there are seven sync frame kinds (chunkjoins the never-carry class), and the stale-epoch path moved. The record itself is local and is not in this PR.Invariants the chapter fixes
chunkanswers are not idempotent in cost__MLS_ENC__frames carry none), so it survives any forwarder that does not strip it. Acceptance requires the frame'ssenderto be the proven arrival peer, and every forwarder strips the key from a third-party frame; otherwise a two-hop network deposits at a custodian that keys quotas on the forwarder and answers a sender it never saw, and a zone holds every frame everywhereWhat is reserved, and where
__CUSTODY_RECEIPT__docs/spec/control-messages.mdand the engine'sdefine_internal_prefixes!DATA_PLANE_PREFIXES__custodymetadata keydocs/spec/wire-format.md, reserved metadata keysdata_versionsentry 7docs/spec/capability-negotiation.mdThe deposit itself has no prefix: it is the depositor's own
__MLS_ENC__frame offered to neighbours as today, carrying the key. That keeps the offer and the settle arm one change, as the outbox state machine requires of any path that hands a parked message to another carrier, and keys the quotas on the proven arrival peer, which the acceptance table requires to be the frame'ssender.Decisions PR17 inherits
take_due, whereseen.forget()already ran. A frame that transmitted is never held. Acceptance requiressender == proven arrival peer(not_depositorotherwise); depositor, tier key, capability lookup and receipt recipient are then one address.__custodyfrom a third-party frame at hop adjustment, custody-enabled or not.admit: it skips the suppression check and the hop accounting (hop fields transmitted as stored), takes queue capacity, the forward send budget (never the own-traffic reserve) and the per-neighbour rate toward the target, obeysallow_relayand the battery floors, never targets the depositor, and marks the queued forward by its held identifier so the drop point returns it to the store (not a deposit, not a refusal, not counted) when it reaches no link. At most once per distinct neighbour per hold.Instant. Expiry is judged at restore and every sweep asnow - accepted_at > hold_msagainst the configuration in force, the way control-frame freshness is judged, so loweringhold_msexpires held records and a custodian that was off for a week delivers nothing stale. Restore keeps survivors as stored and sends nothing.enabledis the off switch and validation refuses on-but-holds-nothing;hold_msstrictly underretry.outbox_max_lifetime_ms. Reference defaults 6 hours, 64 entries and 2 MiB per depositor, 512 and 16 MiB global.disabled,no_request,unknown_class,not_sealed,unproven_peer,not_depositor,duplicate,stranger_refused,depositor_full,store_full,battery.erase_custodyis its own verb, the data-layer wipe calls it, and a start with custody disabled erases at restore.Threat model and state machine
crypto_recovery_enabled(the default). With it disabled a stale copy is a terminal decrypt failure that is dropped and acknowledged, settling the depositor's entry for a frame never read; custody is what makes that path reachable, and for Class A the next version-offer exchange re-offers the change. A deployment that disables crypto recovery anywhere should leave custody off.vvis answered from live state,delta/snapabsorbed as already applied,blob_gonea floor report), and records that the bottom rung of the catch-up ladder becomes reachable more often, one of the two reasons the hold is hours and not days.Validation
cargo fmt --all -- --check,cargo clippy --workspace -- -D warnings,RUSTDOCFLAGS="-D warnings" cargo doc -p offline-protocol --no-deps: pass.cargo clippy -p offline-protocol-sealed --no-default-features --target thumbv8m.main-none-eabihf -- -D warnings: pass.the_chapter_publishes_every_prefix_this_build_reservesandthis_build_reserves_every_prefix_the_chapter_publishes, which are what bind the new prefix to the chapter.control-messages.mdandcapability-negotiation.md. Core: all 132 tests pass, including the wire chapter guards.Not in this PR
CustodyConfig, the deposit on the offer path, the forwarder strip, acceptance at the drop point, the held-frame governor intake, the receipt handler, redelivery,erase_custody,custody_stats, and the FFI section. That is PR17.conformance.mdlists custody among the chapters that are not yet surfaces.docs/message-delivery.mdanddocs/mesh.md, which follow the code.Notes for reviewers
INTERNAL_PREFIXESmeans the public send APIs now refuse application text beginning with__CUSTODY_RECEIPT__. That is the intent: the name is occupied before any frame uses it, exactly as__DATA_V1__was.hold_msis relative, not absolute, for the reason the key package lifetime is.