Changes touching this path

  • ADR 0041 + spec: GitHub for loot — public multi-tenant forge (map #461 terminal) 81c89386 · dbf3dbe6…
  • 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
  • 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
  • loot-forge: the anonymous read surface is 11 barrier views and a forge_read role confined to them (#610) bb193149 · dbf3dbe6…diff
  • the change ledger becomes the history surface, banded by what the server can read (#760) 8ad80be0 · dbf3dbe6…diff
  • the browser mints the seed and the account binds the key: Clerk in the site's own routes, variant-B onboarding, and the account tier's one producer (#753) Migration 0007 adds account_seed (the passphrase-wrapped envelope, opaque ciphertext the server cannot open) and forge_identity, a NOLOGIN role scoped to exactly the identity tier -- SELECT/INSERT on tenant, SELECT/INSERT plus a column-scoped UPDATE of retired_at on account_key, the full row on account_seed, and nothing else (pinned live by forge_identity_is_scoped_to_the_identity_tier). The producer lives in site/src/server/identity/ behind a verified Clerk session (verifyToken in the site's own route handlers, reusing the millerbyte Clerk instance and pattern, never the gateway service). Registration is one transaction: upsert tenant on the verified clerk_user_id, add the key, store the envelope, retire what rotation names -- and never zero live keys. The Rust service still writes none of the tier; tests/account_tier.rs now guards that boundary instead of a vacancy. Onboarding is #487's variant B, the two-column contract: the app owns the 32 bytes (crypto.getRandomValues -> 24-word BIP39 over the ENTROPY, never the 64-byte derivation -> ed25519 pubkey via @noble/curves, held to the engine's from_seed by the pinned RFC 8032 vector), a WebCrypto PBKDF2+AES-GCM envelope, a .txt download that excludes the passphrase, and two consent checkboxes that no download waives. Clerk mounts only under /account, which joins /k/ on the prerender deny-list; every static surface stays static. Follow-ons filed rather than folded: #926 (CLI raw-seed import, so BIP39 recovery reaches the CLI) and #927 (account-aware may_push, burn entitlement via account, and the #506 historical-key 301s the anonymous read tier deliberately cannot serve today). 278e1886 · 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
  • the browser opens what the forge cannot: own-private read ships an owner read tier, a once-per-device unlock, and a grant mailbox that never acks (#1043) c8dd4da1 · dbf3dbe6…diff
  • the private shell learns to diff: two addresses, two keys and two openEntry legs meet diffLines in the page, and an unreadable side is named rather than folded into the pair's verdict (#1057) 94014155 · 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
  • the forge learns what a proposal IS, with no verb and no route: migration 0011 adds one table keyed by the tip's durable change id and scoped per repo, with no server-minted id, no base column since the declared base is CHECKED rather than stored in generation_expected's own shape, no extent table because a stack is derived and cannot be declared, and no index because both reads are answered by the primary key. The extent is the walk from the tip along parents to the landed frontier, and landed means the closure of ref_head over parents rather than everything the repo holds - a /stow having written repo_change is deliberately NOT landed, which is the difference between the two admission refusals doing their job and failing open. Refusal one is that the walk must terminate at landed changes, and it is an anti-abuse boundary rather than tidiness: an unrooted stack lets any party donate arbitrary unrelated history into someone else's namespace, unbounded, on a host whose quota producer still has none. Refusal two is that every change in the STACK and not merely the tip carries change_id, author and signature, all three of which are NULLable today because a legacy or unauthored change has none - no durable id is unidentifiable, no author is unattributable, no signature is not a contribution under ADR 0041. Each refusal was proved by deleting it and watching the donated or unsigned row be ADMITTED, and a third breakage is worth recording because it does not fail open: swapping the repo-scoped graph lookup for the global one still refuses eventually, but only after the walk has read another tenant's change_node row and it then blames the wrong thing. The ref_head pin is the decision itself and is asserted at three levels - a before-and-after on the declared head set with controls proving the proposal really is on file and its tip really could have been a head, a check that the migration text never names ref_head guarded by a non-vacuity read of its own prose, and a database-level count over a cluster only the shipped code ever wrote, where proposal tips that are a head of their own repo is zero. The reaper was CHECKED rather than assumed and the answer is the ticket's most important output: #493 is decided and not built, and its live set as specified roots on ref_head plus live publications plus un-acked grant blobs - so an OPEN proposal, which is outside ref_head by construction because that is the decision, is reachable from none of them and would be swept, deleting a contributor's offered work while the maintainer was reading it. ADR 0075's terminal sentence is right and its open half needs proposal.tip as a root, argued once in an amendment and filed as #1720. Visibility is a pure predicate and a concealed proposal reads as ABSENT rather than forbidden; no view was added at all, so the invariant that no forge_read view names grant_log or repo_member survives untouched and the reader holds nothing on the base table. The third viewer, the presenting proposer, is a deliberate widening over ADR 0075's letter of two and is recorded as an amendment rather than a comment: the presenter supplied the whole stack so existence discloses nothing they do not hold, and excluding them makes ADR 0075's own named feature - carrying a colleague's signed change - produce a row its creator cannot see (#1649) bc73a1dd · dbf3dbe6…diff
  • may_propose is born beside may_push and provably does not widen it: the write rule stays exactly as narrow as it was, and the pin is not a sentence but #927's whole matrix - five signers across an accounted and an accountless namespace - run through require_pusher with the contribution door SHUT and again with it OPEN, demanding identical rendered answers including the message text. That covers the rule's entire reachable input space with respect to this change, because require_pusher reads signer, owner_pubkey and account_of and nothing else, and the break that proves it is the conflation a careless implementation would actually write: consulting the door at the top of require_pusher collapses the whole open column to ok, which IS a stranger moving the owner's ref, and it is quoted in the test's failure rather than described. The door is two halves in order, the repo's own accepts_proposals and then a LIVE account_key row, returning Result rather than bool so the refusal sentence is the product; retirement bites for #927's reason, since a rotation retires a key so a leaked one stops writing and a proposal is a write. The account tier is READ and never written - account_tier.rs survives completely untouched, and rather than assert that, its scan was proved to reach the new files by planting an UPDATE of tenant_blob in the migration and watching it name the file. The setting is migration 0012's boolean defaulting to false with no backfill and no wire caller, so it ships CLOSED on every repo, and that is recorded in the ADR amendment so nobody later reads a shut door as a bug. It cannot be derived, proved four ways rather than argued: a behavioural case over both stores that makes the four tempting facts true - the repo has a tip, holds changes, is metadata-public, publishes a path, each asserted by its own control first - and then re-reads the flag and finds it still false; a DDL check that 0012 contains no UPDATE, GENERATED, TRIGGER, SELECT or CASE WHEN and names none of the tables an inference would read, guarded by a vacuity check and the inverse read that its prose still names them; the DEFAULT plus absent backfill; and the upgrade shape run against a REAL database, where a repo that is both metadata-public and carries a live publication comes out of 0012 shut. Two things are recorded because they were caught rather than avoided. An existence-oracle guard initially passed WITH the oracle planted, because the fixture repo had never been ingested to and so was the same state as an absent namespace to every trait method - it needed a real tip plus a control asserting the two states differ before the break went red, and the reason is written beside the helper. And account_of has THREE consumers rather than the two a pre-existing header credited it with, having missed purge::same_live_account, untrue since #927 - so all three sites now NAME the consumers instead of counting them. The Postgres run earned its keep independently: a conformance case omitting put_object before publish is tolerated by the in-memory tables and rejected by a real foreign key, so memory alone would have shipped it (#1650) 8403defb · dbf3dbe6…diff
  • the collaborator can read someone else's namespace: migration 0015 adds the fourth role forge_member and eleven forge_member views, one per relation 0009 serves, so the shared prefix-parameterised TreeReader reads either family unchanged - and the gate is strictly stronger than 0009's, because the app names only the account the Clerk session resolved to while the membership join sits INSIDE the barrier view, so the application cannot name a repo it is not a member of even by mistake. metadata_public is absent as both column and predicate, and repo_member is named exactly once, by the gate. Two premises did not survive, and the first is a defect in the decision itself: ADR 0077's and the ticket's literal current_setting of loot.reader_tenant cast to bigint DOES NOT fail closed. It fails closed for an UNSET parameter and not for a CLEARED one, because set_config with NULL leaves the empty string behind and casting that to bigint RAISES - which 0009 never met because decoding an empty string as hex does not. Proved by removing the fold from the shipped migration and reading invalid input syntax for type bigint, so the gate ships as nullif and then the cast, malformed values still raise deliberately, and withMember validates a positive decimal with the pin on both sides. The second is a defect in #1727, which landed earlier tonight: its exact-set spelling, that the views naming repo_member equal the forge_member family, is satisfiable ONLY by a family of one, because the ten chained views name the GATE view and never the base table - so a second member view gaining the roster, which is precisely the disclosure ADR 0077 forbids, read as CORRECTNESS under the old spelling. It is re-spelled to exactly forge_member_repo whenever the family exists, its vacuity guard extended to require the member family non-empty, and its positive control now plants four breaks where it planted two. The row-level-security interaction is proved rather than assumed, as three readings on one database in one moment: the seven enabled tables this family reads really carry relrowsecurity, read as a set; the role handed a direct SELECT on repo reads ZERO rows, which is 0014's own named accident and shows the policy system live for that role; and the same role on the same connection at the same moment reads its repo through the gate view and gets one. The mechanism is then named from the catalogue and red-proved by setting security_invoker, which takes the read to insufficient_privilege, and the failure text names the non-superuser deployment case so a future deploy diagnoses itself. The isolation break is ADR 0077's own named careless shape rather than an invented one - the gate keeps its repo_member join and loses only the correlation to repo, which passes every text guard in migrate.rs - and seven of eight tests fire on it. Rows are planted from tests and from the site, outside account_tier.rs's scan of src, the way rls.rs already recorded, so the tripwire is untouched and green. forge_member ships NOLOGIN and provisioning is a different repo, so the class answers 404 rather than 500 and is not an existence oracle (#1726) e76ca863 · dbf3dbe6…diff
  • the owner side of a proposal is decided, and only one of ADR 0075 three terminal states had a producer: close_proposal accepts withdrawn, declined and landed and both stores implement it, but its sole non-test caller is propose::withdraw and every other call site is a test or a conformance case, while propose::list had no caller outside its own tests and propose::read only handle_propose_withdraw, so an owner could not see, decline or land anything and the half of ADR 0075 naming their acts was unreachable rather than undecided. landing stays LOCAL and the forge only observes, foreclosed three independent ways rather than by preference: the forge would have to mint a HeadDeclaration carrying generation_expected, which is the one artifact this design has a client sign; ADR 0091 decides the forge runs nothing because it holds ciphertext and cannot run a step over a sealed path, and a land is a converge plus a gate; and a runner cannot stand in because ADR 0091 puts it under no account, so both write doors refuse it by absence. ingest closes the row pre-computed OUTSIDE its transaction, because IngestTxn is assembled and validated outside so the transaction is pure writes after the CAS, and that same CAS is what makes the pre-computation sound rather than racy, generation_expected pinning the very head set it assumed. the decline binds the repo, the change id AND the tip, the tip because re-proposal after a decline is permitted and a captured envelope must not end a revised one. the read surface is two transports sharing no path, the CLI over HTTP and the site over Postgres barrier views, so both are owed and a conformance pin demands that visible_to and the views answer with identical row sets, or the web discloses what the CLI conceals and nothing fails. the presenting key is world-visible and the propose-time disclosure now says so rather than naming existence alone. and one consequence of this ADR was WRONG: landing a proposal does not take policy::approval off its SelfAuthoredFastPath arm, because that function compares a GitHub PR author login with the GitHub account running the land and reads the loot change author nowhere, so an owner opening the PR keeps the fast path and a stranger code lands on the weakest approval signal with nothing in the gate knowing it was not theirs, which is why a distinct owner-signed review/approve role is required rather than the question handed to map #1014. ADR 0091 gains the landing policy home and the second human role, CONTEXT.md and spec section 7 gain the owner acts, graduated as #2174 through #2178 with #2180, #2181 and #2182 filed beside them. docs only: no code, no migration, no wire or format byte moves, so this owes no deploy (#2162) 31ecdf50 · dbf3dbe6…diff
  • the repo owner can decline a proposal: propose::decline beside withdraw, POST /propose/decline, and loot propose --decline <change-id>, the flag named as its siblings on the verb each spell their act. a declination is signed over the repo, the change id and the tip, and tagged Act::ProposalDecline so it can never be read as a bound withdrawal or a read; on the wire it is a bound withdrawal plus the tag byte, and neither format constant moves. the owner is the key require_pusher admits, the rule jobs already asks for an approval; a member reads the proposal and is refused in words naming the author withdrawal as the other route, and a key the read gate conceals the row from gets one sentence whether the row exists or not. may_propose is not asked, so a shut door does not stop a decline; a repeated decline is the same event and is not re-dated, a landed row refuses by name, and a withdrawn row refuses as the withdrawal refuses a declined one. a declination replayed after a revised re-proposal is refused by the tip binding, the same stack re-proposed is declined again, and one signed for another repo of the same owner is refused by the repo binding. the CLI signs the tip the forge reports at the moment of declining and prints it. uncontended, a decline is one close_proposal and no graph read, pinned on the store call log. it touches no job made for the declined tip, which ADR 0091 leaves undecided. ADR 0075 records the build and what a decline does to the bytes, with CONTEXT.md and the forge spec. red under mutation, counts read each time, each 0 passed and 1 failed and then 1 passed restored: the owner check skipped, the visibility gate skipped, the repo binding removed, the tip binding removed, a declined row refusing re-proposal, a landed row declinable, a withdrawn row declinable, the decline walking the stack, the route asking may_propose, the act tag dropped, the tip left out of the signed bytes, and the declination written under its own minor. no migration and no format byte move; the forge binary changes and an older forge answers 404 on /propose/decline, so this owes a forge deploy before the flag works against the live host. the workspace suite is green (4376 passed over 139 binaries, 12 ignored), and green under bash ci/local.sh against Postgres 18 (#2176) b45cbbf3 · dbf3dbe6…diff
  • a decline may carry a reason signed with it, kept on the proposal row and shown as the thread closing event: ProposalDeclination gains an optional reason as a trailer on its signed bytes, so a decline with none is the bytes #2176 signs and neither format constant moves, and a reason a carrier swaps or strips no longer verifies. migration 0024 adds proposal.decline_reason with a CHECK holding it to a declined row, never empty and at most COMMENT_BODY_MAX bytes, and replaces the three 0020 proposal views with the column appended, which a text guard holds equal to 0020 once the column is taken out. propose::decline refuses an empty or oversized reason whole and a repeated decline with other words, close_proposal writes the reason only with a decline, and a re-proposal clears it with closed_at. POST /propose/read carries the reason as a trailer after the thread continuation, absent from an older forge, which would drop a reason unread, so loot propose --decline <tip> --reason <text> asks the read first and refuses a reason such a forge would drop, then prints who can read it before signing; --show prints the decline under the thread with its reason caged through render sanitize. frozen before_2336 codecs pin old client against new forge and new client against old forge both ways, and tests/proposal_read.rs compares the column for each reader class with an executable red that blanks it in one view (1 class disagreeing). red with each piece undone, counts read each time, each restored green: the reason left out of the signed bytes (0 passed and 1 failed), the read trailer not decoded (0 and 1 on each of two pins), the route dropping the reason, the read serving none (0 and 1 each), no cap and an empty reason admitted (0 and 1 each), a repeated decline changing the reason (0 and 1), a re-proposal keeping it and a reason written with any terminal state in the memory store (1 and 1 each, the pg stamp of the case green without a database), the CLI signing for a forge that would drop it, the reason printed uncaged (0 and 1 each), --reason admitted beside no decline and no disclosure before signing (0 and 1 each), the migration cap moved and a view joined to another family gate (0 and 1 each). ADR 0095 amended, CONTEXT.md and the forge spec updated. bash ci/local.sh is green against Postgres 18 (4519 passed over 139 binaries, 13 ignored). migration 0024 rides the forge binary and the read and decline routes change, so this owes a forge deploy (#2336) c8d5a6ea · 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
  • the private shell stops dying a minute after /account: it authenticated with Clerk 60-second __session cookie while no Clerk JS ran on /private to refresh it, so every private call 401d and a reload wiped the vault. per the operator grill (ADR 0096, reversing #930 dec. 4), Clerk JS now runs on the private routes, loaded on demand behind PrivateGate so public pages carry none, under the strict nonce CSP widened only for Clerk (its frontend API host read out of the publishable key, img.clerk.com, the Turnstile and protect hosts, and worker-src blob: for its session timers) with the request nonce passed to ClerkProvider; every private call goes through one privateFetch that sends a Bearer from getToken and retries once with a fresh token, and /api/private/* no longer accepts the cookie; the vault is wiped only when Clerk reports signed out or a different user, or on Lock, and an HTTP status never wipes it; signed-out /private renders SignIn in place and returns to the requested URL, and the private bar gains Sign out beside Lock. the vault, private shell and CSP comments and CONTEXT.md now say what is true: the wrap key can decrypt, so any script on this origin can recover the seed, and the wrap protects it at rest; the boundary is this origin script set, loot code plus Clerk. tests for the CSP, bearer lane, retry and wipe triggers and the built server, each red under a named mutation and restored; site gate 804 passed, every surface lighter; a CSP-enforcing signed-out load renders SignIn with no violation and /why loads no Clerk; workspace suite 4549 passed. the operator live check closes the ticket (#2396) 25ab3fa8 · dbf3dbe6…diff
  • the private shell unlocks without waiting for its mailbox, and the mailbox pull becomes incremental: measured live, forge /pull-grants took 2.78 s of a 4 s /private load, because the whole mailbox of 1,640 standing self-grants was downloaded before the shell counted as unlocked. /pull-grants now takes an optional 17-byte cursor in its signed payload (a tag byte, then the inbox seq high-water mark and the forge clock at the answer, issued by the server and opaque to the browser, which may only send the all-zero one) and answers rows with seq past it or a reveal_at past its time, due on the forge clock, followed by the next cursor after the counted frames; the Postgres read takes a share lock on grant_inbox so a deposit that took a lower seq but commits later cannot fall behind every future cursor, and an empty payload answers byte-identically to before, so the CLI, the SDK and the relay client are unchanged and either deploy order works. the site keeps the sealed grant blobs and the cursor in IndexedDB beside the vault (version 2, the seed record kept), builds the keyring from them, shows the shell unlocked and pulls only the delta in the background; a file whose key is missing waits for an in-flight pull, the bars say keys are updating, and Lock clears every store with the vault. the gate shows Unlocking while a silent unlock runs rather than flashing the unlock card. forge tests on memory and Postgres and site tests, each red under a named mutation and restored; bash ci/local.sh green against Postgres 18 (4559 passed), site gate 819 passed. owes a forge deploy and a site deploy (#2403) 11cc1e3a · 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

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.