Changes touching this path

  • ADR 0041 + spec: GitHub for loot — public multi-tenant forge (map #461 terminal) 81c89386 · dbf3dbe6…
  • ADR 0041: record the local-first, forge-optional invariant (map #479) de036b99 · dbf3dbe6…diff
  • ADR 0041 + spec: change metadata is a second, repo-level visibility axis (#494) 8de4b27e · dbf3dbe6…diff
  • Correct the Internal-tier and Manifest facts where truth lives (#497, #498) (#505) * Correct two false facts where truth lives: Internal is sealed, the Manifest is local (#497, #498) Both tickets are editorial: prose that was trusted instead of checked against the engine. Every claim below was re-verified against the source. #497 — spec §1 called Internal a "plaintext object, not sealed". False: `loot_codec::sealed::seal` encrypts unconditionally with AES-256-GCM for every visibility; the tier selects only zstd (`compressed = matches!(vis, Visibility::Internal)`). There is no plaintext-object path. What separates the tiers is where the content key travels, so repo membership *is* the Internal boundary — a gate over bytes the server cannot read, not one bolted over plaintext it could. Corrected in the spec tier table (+ a new paragraph), CONTEXT.md's Visibility glossary, and ADR 0041, where the same wording had leaked into the tier list, the Vocabulary rationale, and a rejected alternative. #498 — CONTEXT.md's Manifest entry claimed it "travels in bundles ... so every peer has a complete audit trail". False: `BundleBody` is `{ changes, objs, keys, attestations }`, with no manifest lane; the Manifest is written by `apply_sealed_grant` and persisted by `save_to`, local-only. The trail is per-peer and partial. The error originates in ADR 0008, whose title asserts it and whose last consequence promised a wire section that `git log -G manifest -- bundle_codec.rs` shows was never written. Amended in place rather than rewritten — the decision is history; the amendment records that it was not built, and that this is why the forge's `grant_log` has no wire producer (#484 supplies one). Also fixed the same false claim still live in `manifest.rs`'s own doc comment, and dropped the clause's "escrow entries" half: the plaintext escrow bundle section was deliberately removed in format v5 (ADR 0027) because it shipped plaintext keys. Docs + one doc comment; no behavior change. cargo check --workspace clean; loot-core + loot-codec 357 tests pass. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * Fold in the #494 review findings the ticket widened #497 to cover (#497) Eleven recording-hygiene and accuracy defects a two-axis review of d20d2b2c found in the same three files, folded into this landing per the ticket comments. None change a decision. Hard: 1. ADR 0041 was amended in place with no marker and no #494 citation, so the metadata subsection read as decided at acceptance. Added the repo's established `> **Amended <date> (#nnn):**` form; #494 is now cited in all three files. 2. The ADR had taken on DDL (`repo.metadata_public boolean NOT NULL DEFAULT false`) it explicitly delegates to the spec. It now states the axis; the column and type live only in the spec. 3. `vis_tag` was undefined in-repo and collided with a same-named wasm display helper of a different type. Now "visibility tag" in prose, as the ADR had it. Accuracy: 4. Dropped the ADR 0038 overclaim. That gate fires on a secret-shaped name resolving world-readable by fallthrough, per path, at a signing verb; a derived-metadata leak has neither path nor name shape, so it could never fire. Kept as an analogy of accident *class*, explicitly not of mechanism. 5. The flag is consulted only for anonymous readers (`access_class <> 'anon' OR r.metadata_public`, verified against #483), so it widens access rather than narrowing it. All three files had implied it and the access class were coequal inputs. 6. Recorded the invariant that makes the listTree corollary safe to build from: tree rows store no Restricted id-set, and grant_log is the sole answer to "who". Without it a builder could render the grantee set and leak who has access on a metadata-public repo. 7. Replaced the undefined "barrier-view predicate" ("barrier" already means ADR 0031 operation barriers) with a definition at its one buildable home. Judgement: 8. Thinned the spec to normative statements; the ADR keeps the rationale, so renaming the column at build time is one edit rather than three. 9. Gave the rejected repo-level Public/Private toggle its reciprocal note — Alternatives sections get read standalone. 10. Moved Metadata visibility below Identity in CONTEXT.md, restoring the antecedent its 17-line insertion had severed, and back-linked it from Visibility. Citation now uses the dominant *(decided <date>, …)* form. Also, from #484 via the same ticket: recorded the missing-producer pattern as a standing ADR 0041 invariant beside local-first — loot's sync assumed a trusted peer that already holds the whole store, so a forge must ask of every fact whether the wire carries it. Five tables have hit this already; the sixth should be anticipated. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com> f8b890b5diff
  • ADR 0041: #501 is resolved, not a blocker (#497 follow-up) (#507) The missing-producer invariant landed in be26664 listed `burn_tombstone.signed_tombstone` as "(#501, a blocker)". #501 has since closed on a second pass — its first entitlement rule ("an author of a change referencing the oid") was withdrawn, because `ChangeNode.tree` is a full manifest and `loot duplicate` copies trees address-for-address, so the predicate meant nothing. The build moved to #503. Factual staleness only; the invariant's substance is untouched. Note for #504: the surrounding paragraph frames the pattern as a question about the sync wire ("does the wire carry this?"), and #504 is a sixth instance that framing cannot catch — its missing producers (`repo`, `tenant`, `directory.username`, `default_position`) are account-layer state owned by the web app, where the wire answer is "no, and it never should be". Widening the invariant to "what produces this row?" is deliberately left to #504's resolution rather than guessed at here. Git-Author: Connor Miller <53197564+Connor-Miller@users.noreply.github.com> 0296965cdiff
  • 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
  • a publication survives a merge, an ingest and a resolve, so a catch-up stops silently reverting one (#783) The reported symptom was consent fatigue: every capture in the repo refused with "refusing to publish README.md", including changes touching nothing published. The cause was the opposite of the symptom -- README was not published any more, and the gate was correctly reporting a new @world transition on a publication that had been reverted. The ticket's hypothesis (an adopt/ferry fold re-sealing README from git-side content) was not the mechanism. Reading the real graph, #750's own publication change never published README at all, while it did publish both licenses. The difference is that the licenses were NEW paths, so the snapshot seal minted them @world, while README already existed and went through the merge -- where converge picked between two addresses holding identical plaintext under identical Visibility and its tie-break took the unpublished one. @world is address-neutral, outside every signature and ignored by open, key-travel and compression. That inertness is what made publication cheap; it is also why nothing carries the marker structurally. It survives only where the code writing a tree entry chooses to carry it, and four writers did not: converge, the git bridge's ingest, resolve (so a bounce quietly unpublished the conflicted path) and absorb's span rebuild. The snapshot seal learned this at #481 and nothing generalized it, so Repo::put_sealed now is the generalization. converge gains an is_published question on KeyOracle and compares publish-status as part of a path's content, which makes the existing 3-way answer publish flips correctly in both directions: a publication since the fork is an edit and wins over a stale untouched side, and a deliberate unpublish is equally an edit and still lands. On identical bytes with no base to judge from, the published side wins -- widening was consented to once, and reverting it on no evidence is the only outcome that loses something silently. The other three carry, and carry ONLY. A re-seal never originates a publication: ingest takes its policy from a git commit, which reaches loot without passing a consent seam, so a break-glass commit adding a `published` rule seals plain and leaves the transition for the capture gate to ask about in the usual place. Sealing more tightly than the rule says is always safe; more loosely is what this subsystem exists to prevent. Ask 2 -- should an unpublish need consent? No. A prompt would make consent MORE frequent, which is the failure the ticket is actually about: a gate that fires routinely teaches operators and agents to pass publication flags reflexively. The fix is fewer ways to unpublish, not another prompt, so dropping the rule is now the only one. For the same reason the gate still asks the anchor OBJECT's marker and not the anchor's .lootattributes: policy and marker disagree both when a marker was lost and when a rule has just begun publishing a path, nothing there can tell them apart, and trusting policy would turn the standing rule into a way to publish unasked. A future marker-losing path would make the gate nag again, and that is the failure worth having. Ask 4 -- the forge's published index cannot disagree with main: this repo pushes to relay.millerbyte.com and has never pushed to the forge, so the index holds no README row. Had the push happened inside the window, it would have received the unpublished object. ADR 0041 gains a 2026-08-04 amendment; CONTEXT.md's Published entry records the carry/originate split. ec50222e · dbf3dbe6…diff
  • a forge push deposits the keys the forge itself drops: standing self-grants widen to Internal and embargoed-to-self at forge remotes, closing #502's second-machine cost (#1042) f8edab5d · dbf3dbe6…diff
  • a relay's deposit stops standing in for the forge's: the push-time dedupe moves off the Manifest onto a per-remote deposit ledger, so relay-first Restricted custody reaches the forge inbox the browser reads (#1052) 499739af · dbf3dbe6…diff
  • the forge door stops being a quota stopgap: the push allowlist is recorded as a custody list, and the six places promising open multi-tenant push behind metering now name what actually shuts it (#1045) a790ec8e · dbf3dbe6…diff
  • the docs gate themselves: a land refuses a link that resolves to nothing, five rotted ADR cross-links are fixed, every amended ADR warns in its status, and the artifact sweep retires with its producer 586e3260 · dbf3dbe6…diff
  • the forge stops holding lanes and holds proposals instead: #504 decision 3 recorded two objections and this ticket carried only the privacy one, but a hosted lane tip would also enter ref_head and reintroduce the multi-head ambiguity v1 exists without, so a hosted lane was the wrong noun rather than a deferred feature and both objections dissolve at once rather than trading off. ADR 0075 records the eight decisions - a proposal and not a lane, its own verb over its own route with bytes riding stow so ingest stays the one ref-moving endpoint, a stack named by its tip's durable change id with extent derived to the landed frontier and the base declared as a checked assertion, existence riding metadata_public paired with a propose-time disclosure because the owner's flag cannot consent for the contributor, three declared terminal states because deriving liveness from silence is exactly what #483 forbade, may_propose beside may_push rather than a widening of it, and provenance as two records that never merge - and the forge spec's contribution section, its swimlanes paragraph, ADR 0041 and CONTEXT.md are amended to match, with the approval flip that arrives when the author is not the lander handed to map #1014 rather than answered here (#1638) 6968491d · dbf3dbe6…diff
  • ADR 0099 records the move of loot from millerbyte.com to loot.build (map #2412): the site and one-liner at the apex with www redirecting, artifacts at dl.loot.build, the forge at forge.loot.build, security@loot.build through Cloudflare Email Routing with no catch-all, the relay not recreated, and the production Clerk instance on loot.build; and a hard cutover, the operator decision, in which the new names serve beside the old, a release carries them, every clone re-points its forge remote, and the old names are removed with no alias or redirect kept. it states what that breaks for anything made before it, read from the code: an opted-in old binary POSTs telemetry to the old name silently, so the old names must end NXDOMAIN rather than answered by the millerbyte.com wildcard, which would hand those reports to Vercel; old --help links; installers and manifests pinned to dl.millerbyte.com; the crates.io placeholders published with the old homepage, which only a new publish by the operator fixes; and it records the measured costs of the move (a 19.2 s first unlock on the new origin, no forge change because a signed request does not bind the host). ADRs 0037, 0041, 0045, 0049, 0061, 0064, 0066, 0067, 0074 and 0097, whose decisions name a loot host, each gain an amendment pointing here, and a banner where they had none; their bodies keep the names they were written with (#2414) bf2242c5 · 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.