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 - Relay: explicit 64 MiB body limit; push batches byte-capped (#309)
0ac2fa23 · dbf3dbe6…diff - Workspace accepts its clock; lane flag-gate goes pure so cmd_* tests never touch a real .loot (#322)
20ac82e1 · dbf3dbe6…diff - loot-net: hint a relay redeploy when a format-version rejection means the relay is behind (#431)
7d2564ee · dbf3dbe6…diff - loot remote info + relay GET /info endpoint (#10)
0d05d373 · dbf3dbe6…diff - Authenticate the purge lane: a purge is a signed request (#503)
A wire purge carried only an oid and yielded an unauthored tombstone, and
stow honored it before storing objects -- so any authenticated pusher could
destroy any oid across every tenant, needing no key, only the address.
ADR 0038 already called a purge event a request asking cooperating relays
and peers to destroy their copy. Cooperating meant nothing: loot honored
anyone. The signature now says who is asking; a per-receiver policy decides
whether to honor. Peers honor registered senders and quarantine strangers;
relays honor their push allowlist. Local burn is untouched and needs no key
-- only propagation requires a signature, so a keyless repo burns locally
and refuses to propagate, explicitly.
No global entitlement predicate exists: ChangeNode.tree is a full manifest
and loot duplicate copies a tree address-for-address, so authored-a-change-
referencing-this-oid is satisfiable by any cloner. Both withdrawn rules
have regression tests.
Format 9 to 10. A v10 reader parses a legacy purge lane and drops it, so no
unsigned request is honored while the rest of a v9 bundle still decodes; an
older client hard-fails on v10.
Destruction is structurally unreachable without verification: HonoredBurn
has a private field and authorize_burn is its only constructor.
Both halves are signed, with domain-separated schemes -- without tags a
maroon signature re-encodes byte-for-byte as a burn tombstone at path
length 31. Maroon entitlement is an exact Manifest grantor join, and a
grant only records a grantor when it actually installed a key, so a crafted
grant cannot plant one for content you already hold.
Closes #503.
913c5cc6 · dbf3dbe6…diff - Forge service skeleton: /ingest, the sync surface, and CLI forge remotes (#502)
A new `loot-forge` crate and binary, sibling to the relay and never a mutation
of it: `loot-net` is a dependency of `loot-cli`, so a storage driver in the
relay would ship inside the released CLI binary (ADR 0041:53-57). The ban runs
one way -- the forge may use the other crates; none of them may gain a driver.
Every endpoint is envelope-authenticated, where a relay authenticates only
/stow. The one declared exception is GET /info: a client reads the capability
probe before it knows how to authenticate.
POST /ingest = envelope_sign(bundle || head_declaration), one signature over
both, so a genuine bundle can never be replayed under someone else's head
claim. CAS first; nothing is written before the generation compare. A lost CAS
answers with a 409 BODY carrying the current generation and head set -- behind,
not bounced -- so the client re-carries with no extra round trip.
/stow keeps relay parity and moves no ref, and consumes no generation either,
since a client reads the generation once and then stows N batches. It DEFERS a
change whose objects have not all arrived; /ingest REFUSES one, because a
declared head whose objects are missing is a repo no pull can complete. That
asymmetry is what keeping /stow is for.
Bundles are filtered PER OBJECT by the caller's access class, closing the leak
the ticket names: bundle_impl ships a key for every ANYONE-granted object, i.e.
every Internal object. Change metadata rides the second #494 axis and gates the
whole walk -- a ChangeNode carries the full tree, so shipping one while
withholding its bytes still discloses every path, address, message and author.
Validation before any write: author signatures, and a tree whose visibility
contradicts its objects is rejected. vis_tag is derived from the SealedObject
and never trusted from the tree, which is what makes content-addressed upserts
safe.
Forge purge policy: burn iff burner == object.introduced_by, recorded at first
arrival as introducing.author else the envelope pubkey; maroon read off
grant_log, global and exact. Both are judged before any object is stored and
outside the CAS transaction.
Repos are pubkey-addressed at /k/<pubkey>/<repo>, auto-created on first push,
and may_push holds iff the signer owns the namespace. resolve_remote is
untouched -- still no default host. CORS from day one.
Storage sits behind three narrow traits with in-process reference
implementations the whole suite runs against; the Postgres DDL they mirror is
checked in at docs/sql/forge-schema.sql and its driver is #516.
CLI: /info capability detection, the live head declaration off the Liveness
view, and a refusal to push a lane -- v1 ingests the primary position only.
verify_authored_change moves to loot-codec so the forge runs the identical gate
without linking the fs-hardwired engine.
Recorded in ADR 0041, the spec and CONTEXT.md: the forge stores no content key
but a published one, because filing the ANYONE key lane would let the server
read every Internal object, and the keystone is that only Published is
server-readable. The cost is stated rather than hidden -- a fresh clone of your
own Internal content from a forge is ciphertext you cannot open until #488
lands.
Closes #502.
300e06ff · dbf3dbe6…diff - Say what a relay can actually read (#520)
11055d72 · dbf3dbe6…diff - Gate loot push on the relay's format major; carry a failed relay push in the land verdict (#519)
6c5cb6e0 · dbf3dbe6…diff - Let the relay report the port it bound, so no test hardcodes one (#569)
2b7db5d3 · dbf3dbe6…diff - relay: count the bytes a push received and report them in the push response (#633)
2dc9f961 · dbf3dbe6…diff - forge: /stow answers the relay StowReport JSON so forge-routed pushes report wire bytes (#647)
431ade3f · dbf3dbe6…diff - push negotiates the change lane against the relay /haves so a small land ships its delta, not 7 MB of re-sent history (#728)
2df34147 · dbf3dbe6…diff - what a peer declares in a negotiation is a type now, so the head list that strands a pull cannot be passed (#799)
#40's fault-injection harness found a peer holding 3 of 6 files whose head had
advanced to the sender's tip, with the negotiation reporting up to date. The
ticket read that as "an interrupted pull cannot resume." It does resume:
`pull_via_interrupted_fetch_resumes` has been proving that since #217, and
`pull_via` declares `negotiation_have()` at all three of its call sites.
The harness resumed by declaring `bob.repo.heads()` instead. That is the
defect — not the sync path, but the fact that `have` accepted any `Vec<Oid>`
and the obvious thing to reach for was the wrong one. `Repo::heads`' own doc
said "what a peer passes as have", so the trait was teaching it. `loot clone`
was doing it too, correct only because a freshly inited repo has no heads.
What a peer may declare is now `Have`, and `negotiation_have()` is the only
thing that makes one — the #217 filter is the check the type certifies.
`Have::nothing()` is the one other constructor, for transports probing a relay
and clients with no repo to ask yet; declaring less can only over-send, and
over-declaring is the strandable direction. Threaded through the
receiver-declares half only: SyncTransport, loot_net::{offer,fetch,pull} and
the forge's signed twins.
The sender side keeps `&[Oid]` deliberately. `have` means one thing in both
directions — what the recipient of the bundle holds — so there is no second
concept to name; what differs is provenance, and a type cannot carry a
guarantee across a network. Wrapping it would advertise a check that does not
happen. That reasoning lives on `Have`'s doc, which is the only place it is
written.
Candidate 1 from the ticket — refuse to advance the head over an incomplete
closure — is not built, because ADR 0024 already priced and rejected it under
"cross-batch atomicity is deliberately not provided". A confirmation note says
so there, so the next reader does not re-derive it. The new
`tests/sync_resume.rs` asserts the head advance rather than treating it as the
failure, and covers the fault the suite genuinely missed: a bundle that is
short but valid, where every batch succeeds, the pull returns Ok, and the
operator is told it worked while holding half the files. The next pull heals
it, which is what is pinned.
Acceptance criterion 3 ("am I up to date" must answer no while any object is
absent) is deliberately not built: `closure_complete` is unsatisfiable on the
forge path, where a reader legitimately never receives ciphertext it holds no
grant for, so the warning would fire forever on every forge repo with
restricted content. That is #803.
`an_interrupted_transfer_resumes_to_convergence` runs with its ignore deleted.
271dd5cf · dbf3dbe6…diff - the relay's grant mailbox authorizes before it parses, and a read addresses only the caller's own (#621)
4b349b72 · 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 forge advertises its retention window on /info, and burn prints one honest bound per host it disclosed to (#814)
652f4023 · dbf3dbe6…diff - the two servers share one auth preamble, and a forge handler reaches the router only through the crossing (#865)
a10e310f · dbf3dbe6…diff - the reader names its own holes: the completeness filter and Have retire, and pull reports what never arrived (#803)
f15d576e · dbf3dbe6…diff - info names the build, not only the wire format: a deploy becomes verifiable from outside (#922)
ffe01788 · dbf3dbe6…diff - the review's findings: one shape for an unstated build, the glossary carries the field, and the ignore rule gets the test that was its point (#920, #922)
8784832a · dbf3dbe6…diff - the symptom stops leading: a transport failure names its request and says whether to retry (#966)
A first `loot clone` of the production relay failed with, in full, `loot: http:
error decoding response body`, and the identical command succeeded immediately
afterwards. That message names no request, no URL and no remedy — and it reads
as a CODEC defect, so the reasonable next move is to suspect a format skew and
go check /info. Dead end: the response had simply stopped. Same class as #914
and #921, a message that sends the reader where the bug is not.
Every `.send()` and `.bytes()` in loot-net mapped its error through a bare
`e.to_string()`, so this was not one bad message but the absence of one
everywhere. They now go through `transport_failed`, the sibling of the existing
`relay_rejected` (which is for a response that DID arrive and said no).
Three cases, because they have three remedies: unreachable (check the URL, and
do NOT retry — a wrong URL does not heal by repetition), timed out, and cut
short. The cut-short text names the interruption rather than the decode, says
re-running is the fix, says why that is safe (stow is idempotent and the
pull-side verbs negotiate from what the caller holds, so a retry resumes rather
than duplicates), and says that a repeat at the same point is the report-worthy
case — which turns a retry into evidence instead of superstition.
The underlying error survives verbatim at the end. Demoted, never hidden: it
just stops being read as the diagnosis.
⚠ The classification is split into a pure `transport_text` over a `Transport`
enum because a reqwest::Error cannot be constructed by hand, and a message whose
whole job is to send the reader to the right place is exactly the thing that
should not ship untested. Three tests, incl. one over the whole enum so a new
variant cannot quietly stop naming its request.
The eight remaining `NetError::Http(e.to_string())` sites are left alone on
purpose: they parse wire bytes already in hand, so they really are decode
errors.
Proven on the real path: a clone against a dead port now reports `pull failed
against http://127.0.0.1:59999/negotiate: could not reach it (...)` and points
at `loot remote info`.
086fb775 · dbf3dbe6…diff - the relay stops re-serving a dead line: an operator retires a stale head from the live view, and nothing is forgotten (#996)
fabfaddc · 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 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 - ADR 0070 cites the publish ban by its right number, ADR 0019 records its exemption class inline, a stow backend failure reads as server fault, and a working head stands every parent line down (#1168)
57354896 · dbf3dbe6…diff - the format-skew refusal tells the caller the host is behind and their repo is untouched, and scopes the redeploy to whoever operates the host (#1136)
74f719c6 · 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 - the allowlist parsers get one home in loot-net, which un-forks six copies and restores the OpenSSH form loot-relayd had silently narrowed back to hex a day after #1086 widened it (#1167)
a702c5dd · dbf3dbe6…diff - the format-skew rationale states the measured position: an allowlist of one refuses a stranger before any format check, so skew changes which refusal they read, and the land verdict's relay=FAILED was the detector that was always there (#1200)
8a623333 · dbf3dbe6…diff - a pull declares its ancestor closure instead of bare heads, so a server behind the client stops re-sending the whole history to say nothing new: the live forge no-op drops from 15.6s to 1.85s with no redeploy, and the wire gains an opt-in timing instrument (#1258)
c1971c5e · dbf3dbe6…diff - a forge that cannot be reached, or that says no, stops answering with a message naming no request and a relay that was not in it, because both clients now send over one Endpoint that carries transport_failed's four-way diagnosis and knows which kind of host refused, collapsing four hand-kept endpoint lists into one (#1415)
21b9625e · dbf3dbe6…diff - loot grant-relay stops sending the relay's /grant body at a forge remote, because the two body shapes are now the type of one client function's host argument instead of a branch every caller has to remember — the branch push had and grant-relay, one file over, never did (#1463)
e1dfdbb4 · dbf3dbe6…diff - the relay's type-to-status rule stops being retyped at eleven handlers, but not by the From impls this ticket prescribed: NetError is deliberately mapped both ways here, being what the four wire decoders return and what the stores return, so a single From must pick one and the prescribed Internal would have flipped five decode sites from 400 to 500 and left a footgun where a later ? at a decode site silently became a 500 - a Refuse trait with named crossings or_malformed and or_internal is the same-sized change and behaviour-preserving by construction, handle_stow keeps its inverted mapping explicit and is now pinned as an exception rather than merely commented, and the forge turns out to be a bad model for a reason worth recording, since its stores already speak ForgeError so NetError arrives from one direction only and its unambiguous mapping is to 400, the opposite of what this ticket asked for; and the safety net this ticket claimed does not exist, because loot-relay-contract drives the stores directly and asserts no status anywhere, leaving the whole relay one status assertion above the auth layer, so five Malformed sites and three Internal ones now have live-HTTP coverage proved non-vacuous by flipping two mappings red first (#1653)
381c624c · dbf3dbe6…diff - the argv door stops being per-binary and becomes one module in loot-net that every binary asks, and flag_value is deleted rather than widened - zero definitions remain workspace-wide, which turns the class is closed from a claim into a deletion. the recorded reasoning this ticket pointed at refused widening a shared reader, which is a different act: it argued a reader must not be taught a new spelling because that changes parsing everywhere at once, and its own third bullet diagnoses the hole as this binary having no gate, so it points at giving the binary the gate and leaving one reader. per-server spec tables were rejected as a second door, the thing this class exists to prevent, and a new crate was rejected because every binary already has loot-net in its graph and the module is pure std, so the odd thing about the home is its name and not its coupling. the destructive half is demonstrated against real binaries and seeded stores rather than argued: a before binary built by reverting only the readers reaps a real blob under reap-grants --older-than 30 --dir store --addr --apply, because --addr is never consulted in reap mode so nothing else in the line looks wrong, and it forges the window under --addr --older-than 1 --older-than 3650 --apply, reading one day where the operator wrote 3650 - after, the first refuses with nothing has been destroyed and the second keeps the grant. the forge enters migrate mode, the superuser DDL path, under --addr --migrate and now treats it as an address. this ticket's own headline argv is corrected rather than repeated: --dir --apply is destructive only under the s3 backend, since on fs both binaries stop at is_relay, and the fs-reachable variant is the one it did not name. loot-first's private reader is in scope and gone too, because land declares two valued flags and --allow-publish --pr 12 --pr 9 landed the PR nobody typed. an early forge pin stayed green through the revert, since the bug was never in the traversal but in what main asked, so the call site was extracted and re-proved red; and two further destructive readers are reported and not fixed, one of which takes a size operand as its scratch directory and recursively deletes under it (#1628)
089f8dda · dbf3dbe6…diff - the last two readers of the deleted argv shape ask the door instead, and the blocker this ticket named was not the one holding them out: the static lifetime was never in the way, because OPT_IN_HALVES is a const whose flag fields are already static, so the valued union is a const fn fold at an arity derived from the three class lists and a half without a class becomes a build failure rather than a runtime one - what actually kept the perf binaries from naming the door is that loot-net is a dev-dependencies row in their manifest, deliberately since #847, because a real edge would link axum, reqwest and tokio into the gate binary every land builds. so the door moves once more, to loot-core, which every binary-shipping crate already names directly and which is pure std, making this #1628's own criterion applied one crate further rather than a second door - loot-net keeps a re-export shim so no caller changed. the recursive delete is demonstrated rather than argued: --size 64 pinned-checkout scratch bound the scratch slot to 64 and a file planted under a temp tree at 64/counters-repo is gone after a run driven by the real production path, proved non-vacuous by aiming the same call elsewhere and watching the assertion fail; it now binds the checkout the operator typed. the gate's poisoning is worse than this ticket described and its example argv is corrected rather than repeated, since that one is already refused by the strict single-position parse - the argv that genuinely passes needs the forged operand followed by another flag, and before this it wrote a 925 byte measured record to a file named --reps in the working directory while the land-and-stash pairing guard passed, because the stash it checked had been forged out of the message operand. after, it exits 2 naming the pairing and never measures. both call sites were extracted from main first, following #1628's remedy, so a pin can reach what main asks rather than only the door's traversal - and every pin was reverted to prove it red except one that pins fresh rather than the binding, which is documented as such and proven non-vacuous separately. the exemption list is emptied and re-adding a name now costs deleting a working census, since the roll call asserts set equality (#1682)
d7e849b0 · dbf3dbe6…diff - the skew this ticket reported never happened, and the trap that produced the report is the finding: a land pushes with the loot.exe sitting beside the running loot-first.exe, which is the primary's last cargo build --release, not the tree being landed - the only build a land performs is the perf gate binary in the position. So the land that RAISES FORMAT_MAJOR pushes with the major before it, and reading the constant out of main's source is not reading it out of the pusher. Verified by timeline rather than argued: both pushes ran at 23:26 and 00:08 while the primary's binaries date from 01:28 the next morning, so the pusher predated all three lands and wrote major 11 to a relay serving 11. Two further claims in the ticket were false and are refuted by the tree rather than reasoned away. stow DOES version-check, at the frame decoder before a single object is stored - handle_stow to RelayStore::stow to DagRepo::stow_from, whose first statement is Frame::decode and whose UnsupportedFormat maps through NetError::Engine to a 400 - and it was already pinned by a green test when the ticket was filed, so the acceptance criterion asking for it was asking for something that shipped. Which means the observed 200 and its byte count are themselves proof the marker was readable, since StowReport is constructed only after stow returns Ok and put_version writes the compile-time constant unconditionally. What survives is worth landing on its own merits and not as this incident's cause. The fail-open branch stops being silent: relay_can_read_our_writes returns Checked or Skipped carrying the probe's own failure string, push returns that beside the report, and cmd_push prints a warning to stderr where the refusal would have stood - a value on the result rather than a log line, because the broken pairing was a promise asserted by the path that fires and inverted by the path that does not, and a caller may decline to say a returned thing but cannot fail to be told it. The new test is the first that watches the wire at all: every prior skew pin simulated one by moving the CLIENT to FORMAT_MAJOR plus one, a value production never has, and none of them observed a byte - this one stubs a relay that reports a chosen major, asserts the request log is exactly the probe with zero bytes sent, and carries its positive control in the same function so that no bytes recorded and this harness cannot record bytes stop being the same observation. Two mutations were each verified present in the file before being run red. And the doc comment on relay_push now names the trap rather than the code, because the code was correct every time (#1768)
25a5a122 · dbf3dbe6…diff - ahead and behind against a remote without pushing to find out, and the thesis constraint is met as a property of the REQUEST rather than as advice: the question carries an EMPTY PAYLOAD and every comparison is local. a relay is asked POST /haves with a zero length body, a forge POST /ref with a signed envelope over zero bytes, so a repo of one path emits BYTE IDENTICAL bytes to a repo of ten thousand and the question has no room to encode paths, object addresses, or even our own heads. the two endpoints that could are forbidden on this path and ADR 0021 now records why: /wants sends our object addresses, which are per-content identifiers, so a status in a loop hands the relay a per-path edit-frequency profile of ciphertext it cannot open - ADR 0083 refusal at a higher call rate - and /offer sends our head ids, which a push may do because a push is a CHOSEN act and status is not. the pin asserts the recorded path-and-body list SET EQUAL to exactly /info and /haves both empty, asserts the two recordings equal across two very different positions, and carries a positive control that a real push through the same stub records a NON-EMPTY body; mutated into the rejected design it goes red with /wants carrying a literal 32 byte address. offline is split POSITIVELY, the site gate SKIPPED-OFFLINE rule: only reqwest own is_connect and is_timeout may be called offline, and anything that ARRIVED - a refusal, a 404, a proxy page, a truncated body - is unusable, because those are different facts and collapsing them is how a guess gets reported as a measurement. the relay answered and holds nothing is a THIRD thing and reads as declared with a count. every unknown count renders dash or null and NEVER zero, so a machine that never reached the network cannot emit a number, and log --unpushed REFUSES rather than printing an empty listing, because a listing has no row meaning I could not ask and an empty one reads as everything is pushed. the asymmetry is stated rather than faked: unpushed is exact, being the same change lane a push would send through the same ancestor_closure, while unpulled is zero exactly when every declared head is held here and UNKNOWN otherwise, since a head declaration says what the tips are and not how deep they run. no FORMAT_MAJOR bump and the reason is recorded: ADR 0019 marker exists to prevent misparse of a durable or on-wire artifact, an unasked status emits byte identical porcelain, and the R row lives behind a flag that did not exist when the shape froze. no revset predicate either, because loot-revset is handed a GraphView and a KeyOracle and nothing else (#868), so a predicate answering over HTTP would put a network round trip inside revset::select and therefore inside grep and format-patch too (#1522)
2237a331 · dbf3dbe6…diff - the opt-in telemetry channel is built, and off means no call because a paired test says so rather than a sentence
loot#1658, building what ADR 0074 specified and loot#1626 shipped only the
guard for. loot telemetry on | off | status | show; an append-only accumulator
that writes one line per dispatched verb, the dispatch-table name, with burn and
maroon collapsed to other AT WRITE TIME so no local trace of an incident exists
either; a sender that runs in main after the verb has printed and decided its
exit code, at most once per 24 hours, behind a 3-second hard timeout, that can
never print, never change the code, and never delay a verb; a spool that IS the
outbox, so show reads the same entry the sender transmits; and a 16-byte id
minted on opt-in and deleted on opt-out, never machine-id.
The guarantee is the pairing. One local listener, the real binary, a sandboxed
config home: init, status, doctor and show with telemetry off make ZERO
connections; on, the same verbs make EXACTLY ONE, and the same verbs again in
the same period make no more; off again makes none. A lone assert-zero passes
when the listener is broken, the binary crashed early, or it pointed at the
wrong port - the exact shape of the logrotate -f check that could never fail.
The endpoint override is https only with the loopback LITERAL the sole
exception, enforced twice, and localhost is refused because what it resolves
to is the resolver business; the refusal happens BEFORE an id is minted, so a
broken override never leaves an identifier that can never be used.
Clause 5 of the field rule is a property of the wire bytes, not a promise:
a test parses every string out of the JSON and requires each to be a
compile-time constant, a member of a fixed array, a dispatch-table name, or
the hex id. The platform is a build-script constant rather than a runtime
assembly from std consts, which is what the rule forbids spelled from constants.
The verb emits machine output - show --json IS the report - so it joins the
emitting tier rather than the prose-only debt list the ceiling forbids growing.
Every count that moved was moved in the documents that state it, because the
censuses read them: 129 verb lines in three sites, 82 dispatched and 80 in the
table and 25 emitting in ADR 0066 with telemetry in the named list, all 82
verbs and the block in the README, the CLI page, and ADR 0074 eight-entry
allowlist - because the endpoint is now the binary ONE destination it contacts
rather than prints, and its ALLOWED row says under what condition.
The destination census flagged three lines that dial nothing: a printed hint
naming http://127.0.0.1 and the two scheme-prefix tests in check_endpoint. They
are respelled rather than exempted - the check splits on the separator and
compares the scheme WORD, same rule, no allowlist row - because a row is a
sentence someone has to be able to defend and these had nothing to defend.
The site half: POST /api/telemetry validates a report against the SAME closed
sets the binary declares, pinned cross-repo by reading the Rust source, and
refuses WHOLE on any field outside them. A valid report becomes source=cli
rows, one per verb invocation under /cli/<verb> plus two bucket rows, all
stamped with the arrival time so no ordering exists to reconstruct; the
telemetry_id rides the row and expires with the 30-day partitions. It sits
behind the same /api/ limit_req the beacon needed, which is why the ADR put it
there. doctor prints on/off, the EFFECTIVE endpoint, last send and the spool.
The notice gains the CLI tier, as ADR 0074 §11 requires: sends nothing unless
you run on, the id links reports and off deletes it, what is never in a report,
show prints it before you decide, and the erasure route - quote your id within
30 days, you hold the key. The test that asserted the page does NOT collect
telemetry is inverted, as it was for the visitor key.
1214 lib and 118 bin tests, 51 suites, 634 site tests, budget and font gates
green, rendered and read.
8a919b01 · dbf3dbe6…diff - a relay or forge answer that keeps arriving is read whole however long it takes, and one that stops fails on a named budget instead of hanging. reqwest blocking client defaults to a 30 s timeout that wire.rs believed it never set, and bytes() held it over the whole download, so loot clone failed with it did not answer in time while the answer was still arriving. transfers now carry a 30 s silence budget on the client and read the body read by read, so it bounds each wait for bytes and not the download; a probe keeps its deadline as a request timeout over the whole request. a timeout now names the limit that fired, no answer began within, no bytes for, or a probe deadline, claimed only when the failing wait lasted it, and no timeout says the relay is cutting the response. red first, the shipped binary clone through a local proxy throttled to 3 kB/s failed at 30 s with the ticket message; the lane binary cloned it (70 s body) and failed a proxy that stalls mid-body after 30 s naming the budget, and a read-only depth 1 clone of the live relay took its 53.7 MB answer in about 118 s. red under mutation: the body read through bytes() (2 passed, 1 failed), the transfer budget as a request timeout (2 passed, 1 failed), a client with no timeout (1 passed, 2 failed, both hung to the guard), a probe deadline on the client (0 passed, 1 failed), the budget claimed without the wait check (0 passed, 1 failed), and the timeout remedy put back on the cut sentence (0 passed, 1 failed). no wire or format byte moves and no forge or relay byte moves; the client binary change owes a release. the workspace suite is green (3942 passed over 123 binaries, 7 ignored) (#2064)
ef88503d · dbf3dbe6…diff - a lander can no longer read a pre-land finding as the refusal whose recovery is to change nothing, and the gate claims no verdict only when neither run reached one. the no-verdict refusal opens on a heading of its own, pre-land cargo test: NO VERDICT, and a finding keeps the old heading and never says in words of its own that the compiler did not answer, so the crash it quotes from the first run cannot be mistaken for the other refusal; land-change keys its two recoveries on those headings. a build failure and then a crash is now a finding, not no verdict: the first diagnostics are the only verdict either run reached and the crash neither confirmed nor cleared them, so telling the lander there is nothing to fix would claim more than the gate knows. a re-run that cannot start cargo now says a re-run happened, leaving the merged-tree remedy to #2066. afk-loop reads the verdict line through land-change instead of keeping a second list of values, the --skip-tests block stops stating the CI net as a fact and names ci/local.sh for when the job does not run, and the claims that the job is only cargo test are narrowed to what ci/test-main.sh runs. red first: a finding carrying the no-verdict words, a build failure and a crash judged as no verdict, and a re-run spawn failure that never said a re-run happened (0 passed, 3 failed). red under mutation: the finding rendered with the retry evidence (2 passed, 1 failed), the no-verdict guard dropped from the first run (1 passed, 2 failed), the re-run note dropped from a spawn failure (2 passed, 1 failed), and the deadline dropped from the new wire pin (0 passed, 1 failed). the ticket premise about a deadline ceiling is corrected: in reqwest blocking a request timeout replaces the client per-wait timeout, so a deadline is not undercut by the transfer budget, and a local server pins that. the smells are taken: the fake spawner stdio choice is an enum, wire post takes a budget as get does, Endpoint get sends in one place, the transport remedy sentence is written once, add_attestation is add_attestations with one record, and CargoTestFailure Other is named Finding. no migration, no wire or format byte moves and no forge or relay byte moves, so this owes no deploy. the workspace suite is green (3946 passed over 122 binaries, 7 ignored) (#2071)
bb8acc52 · dbf3dbe6…diff - a transfer waits for its answer on a budget read off the bytes it is sending, not on one constant: reqwest blocking holds a single wait over connecting, the whole upload and the peer working on it, so the number #2064 kept for the header wait was never a limit on silence but a floor under the uplink rate this client will tolerate, and below that rate a healthy push is refused while its bytes are still going out, which a push batch capped at an eighth of MAX_BODY_BYTES makes reachable rather than hypothetical; the silence half stays a constant, because a body read returns the moment any bytes arrive and there the number really does bound inactivity. wire::transfer_budget is the floor plus room for the upload at UPLINK_FLOOR, rounded up to a whole multiple of the floor because a budget is a client and a client is a connection pool, so an exact budget would mint a pool per body size, and the rounding costs a peer that says nothing at most one extra floor of waiting; bytes past what any loot host accepts buy none, so the wait stays bounded by the body. the budget now rides the response it produced, through Answered, so a body read is measured against the number reqwest held that request to rather than one the reader recomputes, which would otherwise repeat the #2064 error in the opposite direction. measured before choosing a number, with LOOT_NET_TIMING=1 on this desktop: the fault-injection pushes are hundreds of bytes and answer in tens of milliseconds, the eighty-change push at about 170 kB answers in under a second, and under two concurrent workspace builds, heavier than the load the refusing land ran under, the worst answer was 64 ms and thirty-three rounds of that binary went green; so 30 s was never marginal for a local relay, and the land #2079 escalated on was the process not running rather than the relay being slow, which is recorded beside TRANSFER_IDLE and leaves the ticket open for it. whether a wall-clock budget failure should be classified the way #1873 classified a compiler that did not answer is decided no, recorded beside Refusal in orchestrator.rs: that classifier reads stderr and a failing test panic is on stdout, widened to the whole run the text it would key on is minted by transport_text and asserted verbatim by the budget tests next door, so a genuine failure of one would read as not a finding, and the claim itself would be false because the compiler answered and the suite answered. red under mutation, counts read each time: the budget ignoring the bytes sent (9 passed and 3 failed), the body read recomputing the budget from the floor instead of carrying it (12 passed and 1 failed), the rounding dropped for exact seconds (12 passed and 1 failed), and the cap at MAX_BODY_BYTES removed (0 passed and 1 failed). no migration, no wire or format byte moves, and no host behaviour moves because the change is entirely on the client side of the transport, so this owes no deploy and rides the next release. the workspace suite is green (4011 passed over 125 binaries, 7 ignored) (#2079)
8e8a873d · dbf3dbe6…diff - a fetch gains a depth the host honours: the request carries a trailing depth after its wants, written only above zero so a request at zero is byte for byte what every client sent before, and an old host decoder returns after the wants and never sees it, which is what makes it safe to send to any host; the relay and the forge confine the change lane to the nodes within that many generations of their live heads, one being the heads and nothing older, intersected with the delta past have and, on the forge, after the entitlement gate, so a caller refused metadata in a full bundle is refused it at any depth and a node the caller holds is never re-sent, while a want is answered by address whatever the depth, the lane walking the cut changes as it walks the held ones; the seeds are live heads and not childless ids, the forge reading the ref declared set a client computed with its retire applied and the engine excluding any version a node names as a predecessor, since on a host that ingests amends a superseded sibling stays childless and would otherwise seed a generation of its own, 213 such tips against 31 real on this repo; /info advertises it as fetch_depth, false when absent, so a caller that needs the bound refuses a host without it and a caller that can afford the fallback proceeds. the receiver still cuts and that stays the rule, ADR 0089 amended rather than overturned: pull_metadata_via and the depth round of a bounded pull ask the host for the depth they will keep, FromTips as its n and a deepen as zero since the frontier is this position own, and applies IngestDepth to whatever arrives, so a host that predates the field sends the history and the position is the same one generation deep, which the test relay pins both ways by playing a host that honours the depth and one that does not. the WASM core encodes the same bytes, frozen in the parity suite against the native vector at depth one. decided by a grill of six questions on 2026-09-20 and recorded on the ticket and in the ADR: bounded per invocation regardless of persistence, a depth on the existing fetch rather than a listing endpoint, counted from the host heads, search kept on the receiving side over bodies fetched by address, the host cut an optimisation the client never depends on, and the SDK and the cache cap to follow. this is the wire half of the browser SDK stopping its 53 MB stateless read and of the serverless runner per-job pull; measured against the live relay in the landing comment once the hosts are deployed. pinned on the same shapes at each host so the two walks are held to one answer, a chain at depths one, two and zero and past a have, a fork whose two heads are both seeds, a superseded sibling that never is, and depth zero byte-identical to the one-pass walk this walk replaced, the forge with a reader the gate refuses getting nothing at any depth; in loot-net on the field decoded both ways with an old body as zero, an odd remainder refused, a pre-field info as false and one frozen vector both encoders pin; over the wire on a spawned relay and a spawned forge each answering one node of two at depth one and advertising the cut, with a want riding across the cut; and in the CLI cache refresh asking depth one. a clone at a depth asks the host too, so its declined count is 0 under a host that cut, since nothing behind the cut arrives to be declined, and the frontier width is what says the position is bounded, which the shallow suite now pins against a host that honours the depth and one that predates it; the count stays exact on a deepen, which asks the host for no cut because the frontier it deepens from is this position own. red under mutation, counts read each time: the engine ignoring the depth (0 passed and 1 failed), the engine cut counting one generation too many (0 passed and 1 failed), the depth never written (1 passed and 1 failed), the depth never read (1 passed and 1 failed), the relay not advertising the cut (1 passed and 1 failed), the relay handler dropping the depth (0 passed and 1 failed), the forge ignoring the depth (0 passed and 1 failed), the forge cutting before the gate (0 passed and 1 failed), the forge not advertising the cut (0 passed and 1 failed), the cache refresh asking no depth (0 passed and 1 failed), the receiver stopping its own cut when the host cuts (0 passed and 1 failed), the wasm framing never writing the depth (0 passed and 1 failed), the forge handler dropping the depth (0 passed and 1 failed), the engine seeding from a superseded version (0 passed and 1 failed), the forge seeding from every childless id declared or not (0 passed and 1 failed), an odd remainder read as no depth (1 passed and 1 failed), and the wants lane skipping the cut changes (0 passed and 1 failed). no migration and no format major move: a trailing field the old side never reads is a minor move, and /info default false is the whole compatibility story; the relay and the forge owe a deploy, which the release cut carries. the workspace suite is green (4017 passed over 126 binaries, 7 ignored) (#2123)
e3ddfdce · dbf3dbe6…diff - the forge learns runners and jobs (#2157, ADR 0091, map #2099, first of three slices of #2129): migration 0018 adds the runner and job tables, both repo-scoped, so both join 0014 row-security set and take 0017 forge_is_bound_to_one_repo policy verbatim in shape, and tests/rls.rs ENABLED_TABLES names them, which is the set equality that forces the decision rather than a list nobody maintains. A runner is its OWN keypair under NO account and that is the security property expressed as schema: require_pusher and the proposal door both resolve a signer through account_key, so a runner key is refused at push and propose BY ABSENCE, where the same key on the owner account would have been a full pusher since require_pusher admits any live key of the account; the row is written from an envelope the namespace owner signed, which is a proof needing no browser session, so tests/account_tier.rs is green UNAMENDED. A job identity is the triple version, trigger, kind, so creation is idempotent by the primary key; member and approved are computed in at creation so a later membership change cannot re-authorize queued work; a claim is claimed_by plus a lease the CLAIMANT names, exclusive by FOR UPDATE SKIP LOCKED in the driver and one lock across find-and-write in the reference store; an expired lease returns the job by a READ-TIME predicate, so there is no reaper to fall behind; a verdict is accepted whatever the lease says, because the runner did the work and a bookkeeping deadline must not discard it; a job is born unclaimed and unfinished and a creation carrying either is refused, since those are the two writes that have to be exclusive. Trigger and Kind move from loot-cli to loot_net::pipeline, the one crate the forge and the CLI both already depend on, re-exported so every path reads unchanged: the forge stores what the CLI writes, and two copies of one vocabulary would be a list to keep in step. Capabilities is exactly Kind as a set, and a bit this build cannot name is REFUSED rather than dropped, because reading a newer runner row as covering less would hand it jobs it cannot do. Five conformance cases join the roll call, so both stores answer one contract and neither can name a subset. Measured: bash ci/local.sh green end to end against Postgres 18, 129 test binaries, 0 failed and 0 SKIPPED, which is what proves the driver, the migration and both policies rather than only the reference store; the claim exclusivity and the lease expiry are proved there on real Postgres, and an EXPLAIN plan under contention is NOT measured here and is owed to #2158 where the poll load is. Three refusals found by the run rather than by thought: the destination census flagged a fixture wake-up URL, allowlisted with the reason that it is a value the store round-trips and never an address anything dials, which took the allowlist to ten and moved ADR 0074 spelled count; the shipped-predicate pin read migration 17 alone, so it now reads the migration that CREATED each table rather than recording which came from where; and the binding fixture seeds a runner and a job row per repo, without which its controls were zero and the refusal below them would have passed for the wrong reason. Five pins each broken once by a named mutation, red at 1 failed then green at 0 failed with the count above zero: the claimable predicate widened, the lease made to hold forever, retirement re-dated, the born-claim refusal disabled, and an unknown capability bit accepted. No routes, no wire and no verb here; those are #2158 and #2159. No FORMAT_MAJOR move (#2157)
ea4d34a5 · dbf3dbe6…diff - the forge gains the runner routes, part 1 of 2 of this ticket: the owner registers, retires and lists runner rows through /runners/add, /runners/retire and /runners/list, and a runner claims a job and answers it through /jobs/claim and /jobs/verdict, each with its client half in the new loot_net::runners. a runner key is refused at push and propose by absence and admitted at the claim by its row, and since the live operator door lists the owner alone a route a runner calls passes a delegated door, server::authenticate_runner, which admits a live row only while its registering key still passes the operator list, asked every request, so an owner the operator stops listing takes their runners with them. a verdict is accepted from the job claimant whatever the lease says and refused from anyone else, including a runner whose expired claim another has since taken, which narrows ADR 0091 section 7 and keeps its reason; the compare runs inside the store write, FOR UPDATE in the driver, and the signed pass is written in that same write into the attestation table, after the route checks it is present iff the job passed, verifies, and is this runner pass over this version under this job role. retirement is now terminal per key, because the 2157 upsert rewrote retired_at and a forge envelope carries no freshness, so a replayed add could bring a stopped key back; an account key, the owner own included, is refused as a runner; the claim filters by the runner trust floor through Trust::admits, one predicate for both ends; detail is capped at 16 KiB, a log tail at 256 KiB stored by its hash, a lease at 24 hours, a scope must reach .lootpipeline, and a wake-up URL must be https to a host that is not literally private, with the resolved-address check left to the sender in 2159. JobOutcome, the pass roles and the pipeline file name move to loot_net::pipeline and are re-exported, and the forge act tags become one repr u8 enum so a duplicated tag cannot compile. the job.log_oid cross-reference to 1720 is written in migration 0018 as a comment, so its checksum is unmoved. measured on a throwaway Postgres 18 on the desktop and not the VPS: the claim is an index scan of job_claimable at 0.14 to 0.17 ms over 50000 jobs, 4.0 ms at worst over 20000 inadmissible candidates, and one polling runner gets about 139 claims a second while 32 level off near 2150. red under mutation, counts read each time: fourteen named mutations through the routes and the reference store each went red at 0 passed and 1 failed or 1 passed and 1 failed with the pg stamp skipping, one of them only after a first attempt proved too weak to be a mutation, and two driver-only mutations on Postgres 18 went red at 0 passed and 1 failed, each restored green. bash ci/local.sh is green against Postgres 18 (4243 passed over 135 binaries, 10 ignored). no migration and no format byte moves, and the forge needs a deploy for the routes to exist (#2158)
8dc22d7d · dbf3dbe6…diff - a network budget refusal now says how long the wait lasted beside the limit, and a relay loot-net serves writes a received line and an answered line per request under LOOT_NET_TIMING=1 on the client clock, because the #2079 stall is unreproduced and nothing on record could say where its time went. the wait is timed around the whole send while the reqwest limit starts inside it, so time well past the limit was spent in this process rather than waiting for the host. the relay lines landed in the failing test captured output beside the client lines when a panic was put after a push and removed, and the land-shell use of the flag is written into workflow.md. no retry is built and no cause is claimed. a workspace run went red once on both format_skew_gate tests under load, one printing the wait lasted 5.5 s on its 5 s probe, and both passed alone: that file now prints what its stub took. the seek smoke test drops the flag from the loot it spawns, since it pins their stderr and failed with the flag set. red under mutation, counts read each time: the wait printed as the limit (1 passed and 1 failed), the wait stored as the limit (0 passed and 1 failed), the relay layer not attached (0 passed and 1 failed), no line on arrival (0 passed and 1 failed), each restored to 2, 1, 1 and 1 passed. no migration and no wire or format byte moves; a relay prints lines only with the flag set, so this owes no deploy. the workspace suite is green (4196 passed over 133 binaries, 9 ignored) (#2079)
ac8afc4b · dbf3dbe6…diff - Manifest::addresses reads the frames only after a step over them shows the stored keys strictly ascend in Path order, so a manifest whose frames are not its map, two keys naming one path or keys out of the map order, answers from the map, and a pull over a corrupt local graph file no longer asks for an address the map dropped. tier_counts keeps its backslash test, now documented as not a proof, and the a.txt and a/b ordering it cites is corrected to component order in both places. what the check costs, paired --missing readings, one loot-perf-gate --features count per arm, eight interleaved rounds, load idle at 2 to 10 percent: 0.875 to 0.924 ms without it against 2.09 to 2.18 ms with it at width 200, and 5.76 to 5.88 against 10.50 to 10.63 ms at width 800, where a decode arm read 18.75 to 19.13 and 100.0 to 100.8 ms, so about 9x against the decode where it was about 21x and 17x; the Path comparison is most of it. red under mutation, counts read each time: the check removed, compared by byte, not strict, byte equality only and path equality only (1 passed and 1 failed each), restored to 2 passed. riding along: store.rs and ADR 0075 say the ingest transaction still reads for checks of its own and that the landing decision is what reads nothing inside it, that a push with no proposal open pays 0.28 to 0.43 ms where #2177 asked for nothing measurable, and that the fallback decision rests on one repo size; finish_stage says why any refusal is answered by the address on disk; the dated test counts in format_skew_gate.rs and workflow.md say at the time; two rewraps; the calls.rs runner stubs use the file imports. no migration and no format byte moves, so this owes no deploy. the workspace suite is green (4274 passed over 135 binaries, 10 ignored) (#2273)
0a64d280 · dbf3dbe6…diff - the tutorial interrupt handler halts the writers the tutorial started before it sweeps the tree: a step loot subprocess is spawned, registered and reaped under a lock the handler takes and never releases, so the handler kills and reaps it and the walk blocks at the lock rather than fail its step and exit beside the handler, and the relay thread runs under loot_net::serve_until, a new stop that closes the listener and lets the requests in flight finish, which the handler asks for and waits on within 2 s. reproduced on Windows before the fix: with nothing injected, 7 of 15 runs interrupted as the clone started ended wrong (exit 1 with no handler line, a refusal, or the removal line over a tree holding config/loot/telemetry, which a loot process writes through create_dir_all), and 15 of 15 ended right after it; the relay thread held by temporary sleeps in its start-up or in a stow across a held handler put the relay store back under the removal line 5 of 5 each, right after; a subprocess in a process group of its own, standing in for kill -INT on unix, held the tree 5 of 5, right after. the halt adds under a millisecond at an idle prompt. serve_on, and so loot serve, keeps plain axum::serve. pinned by an interrupt mid-clone over three concurrent runs, a relay halt that leaves nothing serving, and a halt that ends a subprocess nothing signals. red under mutation, counts read each time: the handler without its claim and halt (6 of 6 runs red, 15 of 18 inner runs wrong), the relay started without a stop (0 passed and 1 failed), the halt skipping the relay (0 and 1), the halt without its kill (0 and 1), the holder name selecting nothing (0 and 1), each restored to green. no migration, no format byte and no published wording moves, so this owes no deploy. the workspace suite is green (4397 passed over 138 binaries, 13 ignored) (#2311)
f4a9172d · dbf3dbe6…diff - the browser SDK budget refusal prints how long the wait lasted beside the limit, as the native refusal has since #2079, and names it as read off the page clock: the wait runs from the moment its timer was armed, the whole request for the answer and the time since the last bytes for a silence mid-answer, so a frozen or throttled tab shows as a wait well past the limit. budgetRanOut and transport_text_from point at each other. a forge loot-forge serves writes the received and answered lines a relay writes under LOOT_NET_TIMING=1, opening loot-net: forge, through loot_net::timing_layer, which the relay now attaches through too; with the flag unset serve_until passes no sink and the forge serves the router app builds with no layer added. pinned by a stalling loopback peer across a blocked page loop and across a silence after a streamed answer, and by a forge with a timing sink. red first (7 passed and 2 failed). red under mutation, counts read each time, each restored to green: the wait printed as the limit (8 and 1), the wait timed from the first arm (8 and 1), the clock label dropped (7 and 2), the forge passing no sink (0 and 1), the forge named relay (0 and 1), the arrival line naming relay whatever the server (0 and 1), the layer ignoring its sink (0 and 1 on the forge, 2 and 1 on the relay). CONTEXT.md and workflow.md record it. no migration, no format byte and no wire byte moves; the SDK refusal text changes and the forge writes lines only under a flag the scripts repo never sets, so this owes no deploy. the workspace suite is green (4398 passed over 139 binaries, 13 ignored), the sdk suite 141 passed over 13 files, tsc clean (#2313)
1b9a9597 · dbf3dbe6…diff - the review-sweep fix-up over #2308, #2310, #2311, #2312 and #2313. the lock note beside a failed test is said only on a Windows land: loot-hygiene failed_tests now takes whether the run was on Windows, since os error 5 and 32 are an I/O error and a broken pipe on macOS and Linux, and loot-first passes cfg!(windows); both answers are pinned on this host, red under mutation with counts read each time (recognised everywhere 2 passed and 1 failed, nowhere 0 and 3, loot-first passing false 1 and 4). the plain pre-land test finding now prints the re-run-first recovery the merged pass prints, for each gate row that runs programs and rides the merged pass, derived from the row and pinned whole (no triage 3 and 2, ignoring the merged pass or the row judgement 4 and 1 each, the order swapped 4 and 1). the timing line noun is a ServerKind enum whose relay and forge lines are pinned byte for byte (a misspelled noun 7 and 1), and the loot-s3 put refusal calls the idle duration the idle budget and keeps floor for the rate (the old word 18 and 1). tutorial.rs qualifies the #2311 halt as run on Windows and reasoned elsewhere, and a reap failure no longer prints could not run; ADR 0075 names the graph read by MetadataStore::repo_change_graph and its callers rather than a list that missed propose::admit; sync.rs states the store property the scoped forge offer rests on and what would break it, and its round-zero comments speak of the remote; land-change SKILL.md, workflow.md and ADR 0055 carry the re-run-first order and the Windows-only note. the (idle, uplink_floor) data clump in loot-s3 is left as it is. no migration, format byte or wire byte moves; the loot-first change is live once the primary release binaries are rebuilt. the workspace suite is green (4406 passed over 139 binaries, 13 ignored) (#2348)
77d9c94c · dbf3dbe6…diff - a push now carries an attestation recorded over a change the remote already holds, so loot tag after loot push reaches a relay and a forge instead of being left behind under a success line: a local attestation ledger (.loot/attestation-ledger) records per remote what each push delivered and is read by a push and by no open or save, written by RepoStore::record_attestations_sent as a read-merge-write under the shared-store lock; the push sends the attestations over the held changes of the remote that the ledger has not recorded beside the send set and prints how many, the bundle builder keeps a late attestation only over a change inside the have closure of the recipient whoever handed it in, the forge /ingest keeps one over any change its repo holds through a new changes_held store read that costs no query when every attestation rides its change, and /info gains an additive late_attestations field so a forge that does not advertise it is sent none, has nothing recorded as sent, and the push warns how many it left behind. the land gate store_file_reads is 24 on its workload with no move, where a first cut that read the ledger on every open measured 26 and was refused; an open reads 20 store files, 21 with that cut. the #48 bound holds on the wire: a push carrying one late tag sent 256 B at a relay and 306 B at a forge over both 2 and 24 held tags. the ticket recipe, whose fresh clone lacked late-tag2 through the 0.4.24 binary, shows it through a lane build. red under mutation, counts read each time, each restored green: the open reading the ledger again (0 passed and 1 failed), the ledger write overwriting instead of merging (0 and 1), the ledger ignored (0 and 2), the late lane dropped (0 and 2), the privacy filter removed (1 and 1), the forge back to in-this-bundle (2 and 1, and end to end 1 and 1), the forge keeping any change (2 and 1), a relay push recording nothing (0 and 2), a push recording to a forge that does not keep them (1 and 1), the /info flag ignored (1 and 1). no format constant, codec byte or migration moves; the forge change is live once the forge is redeployed. the pull half is not built: a pull still carries an attestation only with a change it sends. the workspace suite is green under bash ci/local.sh against Postgres 18 (4425 passed over 139 binaries, 13 ignored; site live suites 7 files passed) (#2251)
414ba6b2 · dbf3dbe6…diff - the attestation ledger records what a remote is known to keep rather than what a push put on the wire, the review-sweep fix-up over #2251: a push records rows only for a host whose /info answered, as a relay or as a forge advertising late_attestations, and a host whose /info did not answer is still sent them but has nothing recorded; against a forge without the flag it records what rode with its change, so its left-behind warning counts the late ones and no longer grows; the ledger keeps per forge the tip generation the last recorded push committed at, and a push that reads a lower one sends every attestation over the changes that forge holds again and says it may have been restored, with deleting .loot/attestation-ledger documented as the recovery for a relay and for a restore hidden by later pushes; a ledger that will not read or write warns and never fails a push or skips its deposits, and a record over an unreadable one starts it afresh. the forge misread as a relay the sweep named fails at its unsigned /wants before sending, pinned; the rule stands without it. red first with each fix undone, counts read each time, each restored green: an unanswered /info read as keeping them (0 passed and 2 failed), a forge without the flag recording nothing (0 and 1, left behind 2 where 1), a restored forge unnoticed (0 and 1), an unreadable ledger fatal (0 and 1), an unwritable ledger fatal (0 and 1), the first-seal summary defaulting an unknown change to no rows (0 and 1). also stated: the privacy filter rests on the declared have, a forge holding a head without its ancestry drops a late attestation it is recorded as keeping, a ledger is one clone, late_attestations states the property rather than naming verbs, RemoteSync bound records nothing and its docs are current, the first-seal summary refuses an unknown change and names its tree clone, and the Route doc states where hosting is decided. the ledger stays push-only: store_file_reads is 24 on the gate workload with no move. no format constant, codec byte or migration moves. the workspace suite is green under bash ci/local.sh against Postgres 18 (4430 passed over 139 binaries, 13 ignored; site live suites 7 files passed) (#2355)
9511111e · dbf3dbe6…diff - a relay with a push allowlist now gates its reads on it too: every relay read route was unauthenticated while every bundle carries the key for every Internal object (ADR 0011), so an allowlisted relay.millerbyte.com served this private repo to a stranger key in plaintext (#2388). a route is a read when its answer comes from the store, and the store now sits behind Hold, whose only doors are a gated read and a gated write, with the router built from one match over Route that has no wildcard, so a later route cannot answer from the store ungated; the reads today are /negotiate, /offer, /fetch, /wants and /haves, /info stays the one open probe, and the grant lane is unchanged. with no list a relay stays open and unsigned, and loot serve and loot-relayd now warn at startup that anyone who can reach it can clone everything, Internal content included. /info advertises authenticated_reads and a client signs its reads iff it is advertised, so a new client reads an old relay unsigned and an old client gets a 401 naming the upgrade; loot seek records the posture and the SDK signs its reads the same way. no format constant moves. ADR 0011 and 0015 amendments, CONTEXT.md, the forge spec, sdk README and the site cli, guides and concepts pages say it. red under mutation, counts read: gate never on (the stranger clone succeeds), gate admitting any valid key, the 401 without the upgrade wording, /haves filed as open, /info not advertising, client never signs, client signing whenever a list exists, the startup warning dropped, the SDK never signing; each restored to green. workspace suite 4539 passed over 141 binaries, 13 ignored; SDK 145 of 145; site gate green (777). owes a release before loot serve users get it; the live relay stays stopped (#2389)
f6b5ecc7 · dbf3dbe6…diff - every compiled-in and current-tense loot host moves to loot.build (map #2412, ADR 0099): loot --help names security@loot.build and https://loot.build/trust, ALPHA_PROMISE_URL is https://loot.build/install and the telemetry DEFAULT_ENDPOINT https://loot.build/api/telemetry, so the next release carries them; the site install one-liners, R2_BASE and HOP_BASE (https://dl.loot.build, https://loot.build/dl), the metrics SITE_SCOPE and own-host referrer, the disclosure links on trust, terms, privacy and known-issues, and the install, quickstart and guides pages follow, with the privacy and terms markers moved to 25 September 2026 and re-pinned since readers see the new address; README, CONTEXT.md (the forge door now forge.loot.build), the release checklist, site CI and test fixtures follow, and the two specs carry a note instead of a partial rewrite. the destination census gains build in WEB_TLDS, without which security@loot.build would be invisible to its bare-host clause, and exempts the two shipped lines that tld makes read as hosts (state.build, r.build()) by their exact text; ADR 0074 section 9 says twelve-entry to match. 37 lines still name millerbyte.com outside ADR bodies, evidence, research and scratch, each kept on purpose: dated history, millerbyte.com the site and its @millerbyte/ui tokens, the two noted specs, and relay-era tooling with no successor host, filed as #2438. census red with build dropped from WEB_TLDS, restored; site gate 819 passed and within budget; workspace suite 4559 passed over 142 binaries, 13 ignored (#2418)
c5663115 · dbf3dbe6…diff - the forge half of #2130: the owner files key-only sealed grants for a runner of the repo through POST /runners/deposit, behind require_pusher, and a runner reads one version through POST /jobs/fetch, behind its runner row. a deposit is a SealedGrant with an empty body, due now and without an expiry, sealed to a live runner of this repo; grant::parse_runner_deposit refuses the rest by name, every blob of a request is judged before any is filed, and a request carries at most 1024, which the client splits by. that makes migration 0026's refused at the door true, through this route rather than the store method it names. the fetch takes one version id, any the repo holds, and answers a Frame::Sync bundle with the version's change node whole and the ciphertext of each object inside the runner's path scope, not burned, that the runner can open by a deposit or a live published key, and the runner's grants for exactly the objects whose bytes ride; a scope narrows content and not names, since a node cut to it would not verify. loot_net::runners::in_scope is the one scope rule, an entry naming a file or a directory and never a bare prefix, and the add route's pipeline check asks it too. the store gains runner_deposits_for, one runner's deposits by object, on both backends. act tags 12 and 13, and an old forge's 404 names #2130. ADR 0091 records the routes and the two operator decisions, and CONTEXT.md the key-only deposit. red under seventeen named mutations, each restored, counts read each time: no body refusal and no expiry refusal (0 passed and 1 failed over the parser test), no future-reveal refusal (0 and 1 there, 13 passed and 1 failed over the route tests), and 13 and 1 over the route tests each for no grantee row check, a retired grantee admitted, each blob filed as it parsed, no require_pusher on the deposit, no batch cap, no scope filter, bytes riding without a key, and grants not narrowed to what rides, which first survived at 14 passed until the test gained a keyed file whose bytes are gone; the memory read ignoring the runner or unsorted (1 passed and 1 failed over the two stamps), the Postgres read ignoring the runner or ordered by blob alone (0 passed and 1 failed on a throwaway Postgres 18), and a scope entry matching a bare prefix and the old-forge sentence naming #2158 (10 passed and 1 failed over the runner codec tests). bash ci/local.sh is green against Postgres 18, 4653 passed over 143 binaries with 13 ignored and the site live suites at 78. no migration; new routes, so this owes a forge deploy (#2130)
fffb0cca · 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.