Changes touching this path

  • 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…
  • Forge publish ingest: file the published key the bundle already carries (#499) 417aaf83 · dbf3dbe6…diff
  • Give the forge an operator channel, with one redaction rule (#522) f9ca9f91 · dbf3dbe6…diff
  • the forge grows an operator door list, so its first public ingress refuses all but listed keys with a typed 403 (#690) 0541e431 · dbf3dbe6…diff
  • the forge's metadata pool caps every connection it opens, so a burst queues for one instead of being refused by Postgres (#611) f2d8ee80 · dbf3dbe6…diff
  • the two servers share one auth preamble, and a forge handler reaches the router only through the crossing (#865) a10e310f · dbf3dbe6…diff
  • a contributor can offer work to a repo they do not own: loot propose over a three-path family - posture, propose and withdraw - and none of them is the ref-moving endpoint, which stays single because ADR 0075 refused putting the contribution path one flag away from the path that advances someone's tip. The bytes ride /stow unchanged, since the forge's stow literally calls the same push, so the batches are byte-identical and there is no new transport. One premise had to be WIDENED and the reason is a real replay: the ticket says sign the claim exactly as the head declaration does, but a head declaration names no repo and does not have to, because require_pusher catches a replay at another namespace - while a proposal is BY DEFINITION a write by a non-owner, so every repo with an open door admits the same signer, and a tip-and-base-only envelope is replayable by any observer at every one of them, in the contributor's name. The claim therefore carries the repo path and the server compares it against the URL's namespace, proved by RUNNING the replay and watching the second namespace admit it rather than by arguing it. The disclosure is an ORDERING rule and the posture route exists only to make it one: a client that learned metadata_public from the act's own answer could only ever speak afterwards, so posture comes first, the disclosure is printed second, and only then the ref read, the stow and the claim. It is pinned twice - once by a transport that records every call into the SAME trace as the output lines, so the assertion compares positions rather than presence, and once by byte offsets in real CLI stdout against a real forge - and a shut door still discloses first and sends nothing at all. Withdrawal is the tip change's AUTHOR and only them, where visibility admits three readers, because seeing is not acting and the carrier's remedies are the author's withdrawal or the owner's decline; it does not consult the door, since a shut door must not strand what is already offered, and absent and concealed refuse in byte-identical sentences. The lane refusal keeps every one of its old assertions, including that nothing reaches the forge before it fires, and gains the sentence naming the alternative. Twelve breaks were each run and read before being restored byte-identical, and the ones worth naming are the replay above, deleting the author check so a presenter can withdraw work they did not write, adding the door to the withdraw path so a shut door strands an offer already disclosed, and declaring the tip as a head so the CAS counter moves. The ref-moving path is asserted untouched rather than described as untouched: a proposal leaves generation and head set alone, and the SAME fixture then lands a change the ordinary way and the generation does move, which is the control (#1651) 83c5afa5 · dbf3dbe6…diff
  • the forge refuses an /ingest that would drop a current head it neither descends from nor retires by name (ADR 0098 section 3, map #2422): ingest::heads_this_push_drops judges the declared heads against the heads read at the push generation, before the store transaction, so the CAS makes it sound and a push at a stale generation still loses the swap as before; the walk over the push own changes answers an ordinary push with one read, the head set, and only a head that walk does not reach costs the repo graph, through the landed closure propose already has. a refusal is a 412 whose text body names the count, the remedy (loot pull first) and each head, so a client from before this reads a sentence and this client prints the count it read with the same remedy. retirements ride the signed head declaration as a trailer after the kinds, written only when a push names one, so a push retiring nothing is the old bytes and a forge from before this reads past the names and replaces its heads wholesale as it always did. the client names each forge head its declaration does not reach and that this position has seen, holding the change or naming it in its abandoned record, so abandon --head and supersession are covered with no list of verbs, and never names a head it has not seen. four conformance cases run on the memory store and on Postgres through the assembled forge: a fast-forward carried or already held, a dropped unseen head refused and named with the ref unmoved, an explicit retire that lifts only the head it names, and a racing push that still loses the CAS; a runners test that replaced its root with a new root now retires it by name. red under ten named mutations, each 0 passed and 1 failed (2 failed for the trailer) and restored: no refusal, no generation guard, retirements ignored, no overlay fast path (an existing pin saw the graph read), the overlay alone, the client retiring nothing, the client retiring unseen heads, a view-only seen, the kinds left out ahead of the names, and the client not reading the 412. bash ci/local.sh is green against Postgres 18, 4599 passed over 143 binaries with 13 ignored and the site pg suite at 67 passed. no migration; the forge behaviour changes, so this owes a forge deploy (#2426) e1e5c828 · 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.