Changes touching this path

  • 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…
  • the site stops understating what the forge enforces, and the host path it documents becomes runnable: --allow learns the only pubkey form the CLI actually prints (#1086) c83e98f5 · dbf3dbe6…diff
  • the relay half of the honest-limits pass stops overstating itself: rotation no longer claims burn authority it never took, and the push allowlist is named as the one gate it is (#1086) 23c8d4b2 · dbf3dbe6…diff
  • the site chrome becomes an instrument: a slim mono Console bar carries the six links, the release tag it now imports rather than retypes and the account key chip on every page, the footer band collapses into a one-line end-mark that still carries the disclosure route, docs trades its in-page contents list for a prop-driven side navigation any later surface can mount, and the account page is re-presented as a dashboard of key rows and a four-step ceremony with every Clerk call, every byte of crypto and every load-bearing sentence untouched - the #1323 prototype verdict, raising every surface ceiling on the record for the shared stylesheet it costs 786e5a21 · dbf3dbe6…diff
  • a retired key no longer burns what it introduced on the forge: purge::honor_burn now asks account_of about the burner and applies the require_pusher rule with the introducer as owner, so a key with no account row burns by the byte compare as before, a live key burns as the introducer or as a key of the account that holds the introducer, and a retired key is refused, as the new RetiredKey verdict where the live rule would admit it and as NotEntitled otherwise. same_live_account becomes same_account and no longer asks liveness. the maroon arm still asks no retirement, since it is the only way to revoke a forge grant and has no account lane, and the relay and peer policies read no account tier, so none of them moves. a new server.rs test asserts that the burn verdict agrees with require_pusher over the #927 cast plus a retired stranger, and member_class.rs runs the rule against real account_key rows. before the change the in-memory reproduction went red (11 passed, 1 failed) and member_class against a throwaway cluster went red (3 passed, 1 failed). over the purge tests and the server.rs agreement tests the pins went red with the retirement check removed (13 passed, 4 failed), with the introducer compare answering first (14 passed, 3 failed), with an accountless burner refused (15 passed, 2 failed), with retirement asked before entitlement (15 passed, 2 failed) and with the tenant equality dropped (14 passed, 3 failed), and member_class went red under the first three (3 passed, 1 failed each). ADR 0038 and ADR 0077 are amended, CONTEXT.md, store.rs and server.rs name honor_burn, and the Rotate card on the account page no longer says burning is not covered and names the maroon limit instead. the site gate is green, the workspace suite is green, and the loot-forge suite is green against a throwaway cluster with the CI env block and LOOT_FORGE_TEST_REQUIRE_DATABASE (#2004) e6641d95 · dbf3dbe6…diff
  • loot apply-patch no longer keeps the old name of a rename onto a hard link: #2007 renamed the old path onto the new one whenever the two were one file, which rename(2) does nothing for between two hard links, so the old name stayed and the run exited 0. the writer now renames only when the two paths are one directory entry, which on windows is two paths with one canonical path and on unix two names of one inode that differ only in case and are not both listed in their directories, and otherwise writes the new name and removes the old one by name, and an old path is kept only when a row writes that directory entry, so a rename from a hard link of a path another stanza writes removes the old name too. a stanza whose path or rename old path is or passes through a symbolic link is now refused, checked component by component with symlink_metadata, since a write through a link lands at its target and the file identities follow links. PathKeys and PathKey replace Names and Alias, refuse_shared_paths asserts one row per stanza, the removed-twice refusal words are one const, honor_burn says it is pub(crate) only for the server.rs agreement test, and the purge.rs, ingest.rs and ADR 0082 rewrap leftovers are fixed. the Rotate card on the account page now says a leaked key stops pushing and proposing rather than writing, that the retired-key refusal covers burns, and that this forge does not check retirement on every signed request, naming withdrawing an authored proposal beside revoking a grant as examples, and its JSX comment states the forge-before-site deploy order ADR 0038 gives. the new hard-link rename tests pass on windows, where the old code already passed them, and their unix red-first has not run here; with the unix entry rule compiled on windows the rename tests stay green (20 passed) and go red with the new name read as listed (18 passed, 2 failed), the windows rule answering two entries goes red (18 passed, 2 failed), and the symbolic-link test, which uses a junction here since a file link needs a privilege, goes red with the refusal disabled (19 passed, 1 failed) and with only the last component checked (19 passed, 1 failed). the unix code type-checks for x86_64-unknown-linux-gnu in a scratch crate, ADR 0082 section 4 and CONTEXT.md state the entry rule and the link refusal, the site gate is green and the workspace suite is green (#2014) ab00ecd2 · dbf3dbe6…diff
  • cherry-pick, revert and a surface that settles a stale-disk debt no longer destroy a case-only rename where the filesystem folds case, and the first no longer records the loss: #2011 routed two tree writers through loot_core::disk_entry and wrote that every writer which removes a path it may just have written asks that module, while two that did not were sitting in the tree as the sentence was written. Workspace::apply_change_delta, the one core cherry-pick and revert share, walked the merged actions as a BTreeMap and so wrote A.txt and then removed a.txt by name, which where case folds is the file it had just written, and it snapshots, so loot revert over a recorded case-only rename reported both paths converged over an empty directory and minted a change recording 1 deletion. Workspace::stale_debt_the_surface_left, the one scoped deletion a surface is allowed, compared the written set by name, and a case-only rename preserves the bytes, so the digest guard that exists to protect the operator matched the file the surface had just written: loot surface printed the path it had surfaced, left the directory empty, and the next loot status read it as deleted, which is the reproduction #2011 declined on the false reading that a surface prunes nothing. Each was reproduced by hand through the spawned binary first and then as a new spawned pin, and the pair went red over an empty listing before the change (4 passed, 2 failed) and is green after (6 passed); with the delta writer put back to removing by name they go red again (5 passed, 1 failed) and so does the new census (0 passed, 1 failed), and the stale-debt prune put back reddens the same two with the same counts. Both writers prune through prune_unwritten now. Which working-tree removals are weighed stops being a sentence: workspace.rs carries a census that reads every removal joined onto a working-tree root out of loot-cli/src and loot-core/src, names what each one removes and what the walk cannot see, and it named the offending function under each of those two mutations, went red at its own classifier control with ROOTS emptied and went red with a name dropped from its expected set (0 passed, 1 failed each). one_entry refuses a path that is or passes through a symbolic link now, since file_id follows links and on Windows two paths with one id were otherwise one entry, so fs::rename would have moved a link over the file it points at; the check is the symlink_on apply-patch already made, moved into the module, pinned with a junction here and red without it (5 passed, 1 failed). The hard-link pin names its links A.txt and a.txt so the fold map reaches the entry rule at all, where old.txt and new.txt never called it, and it skips a filesystem that folds case with a note; it stays unix-only and its red-first has not run here, though the body type-checks and runs on windows with the cfg lifted (7 passed). The four case probes become one loot_core::disk_entry::folds_case, and with it answering no the entry pin and two apply-patch case pins go red (5 passed, 1 failed; 18 passed, 2 failed). enclosing_fn moves into source_walk beside the #1929 census that asked it first and strips any visibility, after pub(super) made the new census name the wrong function. ADR 0022 carries the correction and its prunes-nothing claim is gone, ADR 0082 section 4 states where the probe and the link refusal live, CONTEXT.md names both writers and the census, the surface.rs and materialize_target notes stop claiming the two prunes answer the same set of failures, and the rewrap leftovers in ADR 0022, CONTEXT.md and the account page are fixed. The perf gate dry run reads every counter the same before and after and says no move both times, the site gate is green, and the workspace suite is green (3824 passed) (#2018) baec4112 · dbf3dbe6…diff
  • review sweep 15 fix-up: the evolog dash is on an empty FIRST LINE, the subject rule now has one home, and the surface prune says plainly that it discards. loot evolog --porcelain dashes its last column when the message first line is empty, which an empty message and a message whose body starts under a blank one both are, and three places said an empty message: the doc above the encoder, the ADR 0023 amendment of 2026-09-17 and CONTEXT.md. The wording moved rather than the bytes, because dashing on an empty message instead is a porcelain byte change for a message that opens with a newline, and a new spawned pin, a_message_whose_first_line_is_empty_is_the_dash_too, puts a run under the sentence: the file reads 5 passed, and with the whole message put back in the column it is 2 passed, 3 failed, the single-line control among the two that stay green. render::subject_line was billed as the one home while six call sites derived that first line inline, the four the ticket named plus the shortlog author rollup and the missing-object listing, so log --oneline, heads, show, range-diff and those two all ask it now, and the near-cousins that each do something more, the note ellipsis, the revert subject and the loot-first PR title, are named beside it rather than routed. The control is the spawned binary: 28 outputs over two repos, one carrying a multi-line message and one a blank first line, are byte-identical before and after, and with subject_line returning the whole message 10 of 11 of them move and the loot-cli lib suite reads 1219 passed, 4 failed, naming the shortlog rollup and the oneline row among them. The surface stale-debt prune keeps the removal error it discards, which #2018 declined and its closing note then reported as fixed, and the doc above it now says the let _ is deliberate, that apply_change_delta propagates where this one does not, and why: the delta writer snapshots, so a refused removal would be recorded as a deletion the disk never took. The removal census in workspace.rs states what its matcher is blind to, a type-annotated binding, a call split over lines and a path derived from a root-joined one, and names disk_entry::drop_empty_parents, whose remove_dir climbs from a dest its caller joined and which the fold cannot reach, because remove_dir takes an empty directory only and a directory holding a file the writer just wrote is not empty; widening the matcher would not have reached that call, so the blindness is stated instead. CONTEXT.md gives that census its crate and spelling scope. migration_serial records that hold blocks and is not re-entrant, and that it serialises migration runs rather than pg_authid, naming the fixed-name CREATE ROLE in tests/rls.rs that writes the shared catalog outside it; Held loses its Option and its Drop impl, since dropping the field is what the compiler already does. The migration census compares a BTreeSet the way the removal census does: with the const reordered it stays green where the Vec compare it replaced goes red (0 passed, 1 failed), and a name dropped from the const reddens it (0 passed, 1 failed). pg/tests.rs cross-references its own source walk and fn finder to source_walk in loot-cli and states where the pair differs. The folds_case helpers in two test files become folds_case_under, so neither shadows the disk_entry rule it asks. The rewrap leftover in account.tsx that #2018 moved rather than fixed is rewrapped with its neighbours, and so is a second one mid-paragraph in CONTEXT.md that the same rewrap reached. The forge suite is green against a throwaway 17.0 cluster (469 passed over nine binaries, no skip note) and its lock case goes red with the session dropped rather than held (0 passed, 1 failed), the site gate is green (643 passed, every surface under its ceiling), and the workspace suite is green (3833 passed) (#2023) c38e3299 · dbf3dbe6…diff
  • key management on the site: a key can be retired by itself, named, and chosen at unlock. per the operator grill on #2398 (ADR 0100): POST /api/identity/retire retires one key of the signed-in account, refusing the last live key under the account-row lock registration takes and never re-dating a retirement, with no route to un-retire and no deletion, since #506 keeps every key resolvable; /account lists live keys and folds retired ones under a collapsed section, and the retire confirmation names the repos the key owns. names live in a new owner-only account_key_label table (migration 0027) that only forge_identity reaches, trimmed, at most 40 characters, with control, line-separator and direction characters refused and a CHECK repeating the cap; they reach only /account, the unlock picker and the private bar, always beside the fingerprint. the unlock picker lists every live key, defaulting to the key that owns the URL namespace, then the key this device last unlocked, then the only live key, and a key with no stored passphrase asks for its 24 words and can save one. both routes are Bearer only and refuse a key of another account; /api/identity/me stops returning the internal tenant id. rebased by hand over migration 0026 and ADR 0099. pg tests pin the role reach, route and picker tests pin the rules, each red under a named mutation; bash ci/local.sh green against Postgres 18 (4568 passed), site gate 842 passed. owes the setup-forge tripwire update, a forge deploy, then a site deploy (#2398) a90c85fb · 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.