Changes touching this path
- day 0: loot hosts loot
f4c30e75 · dbf3dbe6… - evidence: crew minted and verified (#86)
1fada823 · dbf3dbe6…diff - hard embargo engine/wire lands (#14, format v5)
7784bcac · dbf3dbe6…diff - normalize working tree to LF: byte-stable co-located bridge (.gitattributes -text)
e58fdda6 · dbf3dbe6…diff - Workspace accepts its clock; lane flag-gate goes pure so cmd_* tests never touch a real .loot (#322)
20ac82e1 · dbf3dbe6…diff - Sweep the rust-1.96 clippy debt; document the land-holds-the-binary hazard (#667, #681)
26cfbaa9 · dbf3dbe6…diff - the grant lane is fixed-width and acked, so a deposit frames no length and a read destroys nothing (#818)
Closes #818 and #816, deliberately together. Both are hard wire breaks on the
same two endpoints, and #818 said so itself: landed separately it buys a second
relay-before-clients redeploy for a shape improvement. Landed as one, the grant
lane breaks once and comes out uniform.
#818 — the deposit body is `[recipient pubkey 32][grantor envelope...]`.
`split_deposit` is still the only thing that runs before authorization, and it
is now a constant-size slice: no length field, no UTF-8, no hex decode. This was
never a vulnerability — the old length was attacker-controlled but correctly
bounds-checked, and #621 already moved `Frame::decode` below the gate. What it
was is a shape every future reader had to re-verify, and a "the grantee is a
key" rule enforced by a validator that a later well-meaning change could relax.
A fixed-width field cannot be relaxed. The mailbox index stays hex-keyed on
disk, so the relay now mints that hex instead of parsing the caller's spelling.
#816 — `/pull-grants` returns the due blobs and keeps them; a new
`POST /grants/ack` names the content addresses the caller durably applied and
drops exactly those. Handing bytes to a socket was never proof of receipt: the
connection can drop, or `apply_sealed_grant` can fail after they arrive, and the
grant was then gone from both sides. `loot pull-grants` acks only what it
applied or quarantined — anything that hit a `skipping` branch stays pending, or
this would be drain-on-read wearing an extra round trip. An ack is scoped to the
acker's own mailbox, and since blobs are content-addressed a shared file is
unlinked only when no index entry anywhere still refers to it.
This also shrinks #621's accepted replay cost: a replayed read envelope now
re-reads bytes the holder already had instead of destroying them.
Not a format major, in either half — store and bundle formats are untouched and
the on-disk mailbox index is unchanged, so a relay carrying pending grants keeps
them across the redeploy.
Recorded as the second amendment to ADR 0015 (2026-08-07), with CONTEXT.md's
grant-allowlist, grant-discovery and forge entries corrected to match; the forge
lost one of its four named differences from a relay, because the relay adopted
it here.
a5e2ffcf · dbf3dbe6…diff - a relay is measured, not capped, and the only bytes it may forget are torn writes (#817)
f52d53e4 · dbf3dbe6…diff - a push deposits this identity's own keys: a standing self-grant carries Restricted custody to the second machine, so a same-identity clone opens what it authored (#980)
684f37cb · dbf3dbe6…diff - the forge serves the peek it always served the fetch for: /grants/peek mounted on both mounts, and a peek now says how much of its count is standing self-custody instead of inviting the ADR 0057 alarm (#1114)
cc003626 · dbf3dbe6…diff - the peek stops telling a fresh second machine there is nothing to receive, the glossary catches up to #1114, one predicate decides standing self-custody, and the shared warn gets its provenance back (#1120)
e9544471 · dbf3dbe6…diff - the relay store goes behind a RelayStorage seam, the SigV4 driver moves to its own crate, and an object-store relay is a second deployable the CLI never ships (#38)
330df915 · dbf3dbe6…diff - the relay contract is asserted once and both backends run it, the mailbox choreography moves behind a blanket impl a backend cannot override, and the fs and bucket acks stop disagreeing about which of them was right (#1164)
eb659a91 · dbf3dbe6…diff - a peek costs one batched blob read instead of one per pending grant, the shared contract counts the round trips so neither backend can regress it, and the standing split is why the reads cannot leave (#1165)
204263a6 · dbf3dbe6…diff - a bucket-backed relay reaps, because the anchor is when the bytes last landed in the store and the store is what answers it, and the reap itself moves behind the seam so the standing exemption is one rule both backends run (#1166)
39e1e1eb · dbf3dbe6…diff - loot-relayd finds its verb wherever the operator put it and refuses rather than guessing past a flag it does not know, and the reap checks the answers it is about to zip instead of trusting an order nothing enforced (#1222)
76efc87f · dbf3dbe6…diff - the anchor refresh and the deposit race become shared contract rules every relay backend must answer, the reap report has one renderer instead of a drifted hand-copy, and a rule that is written but never asked is now a test failure (#1223)
d31bf81e · dbf3dbe6…diff - the custody sweep residuals are answered on the record: the sealed-path demo is re-run so the evidence page shows the binary it names, the disk scan learns base64 and UTF-16, the pull entry points drop their bool, and the mailbox keeps failing loud by decision (#1232)
333e9517 · dbf3dbe6…diff - the reap window becomes a struct a transposition cannot compile against, the reap-cost rule says at most three like the ADR it pins, the hand-rolled base64 gets its reason, and ADR 0070 admits its refinement in its Status block (#1263)
ff6d4344 · dbf3dbe6…diff - the pull declaration becomes an enum a subset cannot alias through, which also stops a fresh clone re-downloading the full delta every batch, the wire timing line gets named fields, and the reap window scalar stops sharing its name with the pair type (#1284)
3b657494 · dbf3dbe6…diff - the two forge backends stop each keeping their own copy of when an embargo opens, because ADR 0027's gate and ADR 0057's standing split are lifted out of the adapters into one choreography both servers call, and a live-Postgres case now reads the same mailbox at five clocks and compares the driver's answers to the reference's (#1412)
882f9837 · dbf3dbe6…diff - the relay mailbox takes the door the relay already writes objects through, and the keypair archive reports a held file rather than waiting it out: #2026 routed the object store loose rename through store::rename_retrying and left two sites its census named, and the question here was where that helper should live. it does not move. the mailbox needed no export at all, because store::atomic_write is already public and loot-net has called it for every loose object since storage::write_loose_object was written, so save_index and write_blob now stage through that door and both ends of their renames are waited out, the reader holding the index it replaces and the scanner holding the staging file, and the fixed index.tmp that two concurrent writers shared is gone with it. the wait is spent on the host too, where rename has no sharing window and the refusals that predicate answers to are permanent, and that is accepted rather than gated on cfg(windows): a second spelling of one predicate is the shape #565 is the record of, and the relay has paid this on every loose object all along. loot-identity cannot reach the helper and the helper cannot come to it, since that crate depends on no workspace crate but loot-codec, which is the no-fs wasm core a filesystem retry loop is defined not to live in, so the choice was a second copy or a report. it reports, and on its own terms: a store staging rename waits out a scanner passing over a file microseconds old, where what is held here is a long-lived keypair file whose holder no budget outlasts, and a rotation is one operator-driven act where a refusal costs a re-run. what makes that safe is the order, so id.pub now moves before id and a refusal leaves the private key in the active slot, where archiving id first left a repo whose key had moved and whose re-run met NoKeypair; the refusal names both paths. the census loses its two mailbox rows and its blind-instrument guard stops reading a file that renames today, asserting instead that every crate directory the tree declares is in the walk. red under mutation, counts read each time: the index save put back to a bare rename (loot-net mailbox 20 passed and 1 failed), the blob write put back (20 passed and 1 failed, refused with os error 32 on the source), the archive order restored (loot-identity 3 passed and 1 failed, the private key gone), the paths dropped from the refusal (3 passed and 1 failed), a retry loop given to the archive (3 passed and 1 failed, the rotation waiting the hold out and returning Ok), a bare rename put back where the census cannot see it (store_rename_census 3 passed and 1 failed) and the walk cut to three crates (2 passed and 2 failed). ADR 0016 records the archive decision and store.rs records where the helper stays. no migration, no wire or format byte moves and no schema moves, but the relay binary changes on its own write path, so this owes a relay redeploy. the workspace suite is green (4083 passed over 130 binaries, 8 ignored) (#2112)
8b194a68 · dbf3dbe6…diff
Renames are not followed. loot's tree maps a path to an address, so a rename is a delete and an add. This list is the history of the name, not of the bytes.