Changes touching this path

  • a forge backup is operator recovery, and a restore replays the revocation journal (#586) #586 asked whether a backup defeats a burn. It does, but not where the ticket looked, and the grilling moved the question in both directions at once. NARROWER: the #502 amendment means the forge files a content key only where `publish::published_rows` authorizes one, so an operator backup reconstitutes plaintext ONLY for content that was Published -- i.e. world-readable anyway. And `published_key` is not "the one table with no other copy": the rows come from the client's own bundle at ingest, so a re-push re-supplies them. Painful, loud, not unrecoverable. WIDER, and this is the find: burn is not the only thing a restore inverts. Three columns are monotone capability-removals that no client can push back -- `burn_tombstone`, `publication.retracted_at`, `grant_log.revoked_at`. A restore is the single operation that runs time backwards over all three. Stated once, because it is the whole ADR: A RESTORE RE-GRANTS A CAPABILITY SOMEONE DELIBERATELY TOOK AWAY. Burn is just the one with an ADR attached. So the cure is not a shorter window alone. Snapshots stay immutable at rest and the RESTORE PATH is scrubbed by construction: a key-free, content-free revocation journal, exported continuously and replayed before the forge binds a port. The window (7 days, enforced by an R2 lifecycle rule rather than by a prune script that can fail silently) then bounds media at rest only, never a running system. The dump is `age`-encrypted to a key the VPS does not hold, because `tree_entry.path` and `path_touch.path` are PLAINTEXT for every tier -- #610's barrier views protect the live database and protect a dump not at all. The journal is deliberately NOT encrypted: putting the one unrecoverable artifact behind a losable key buys nothing. R2 gets no mirror. The realistic threat to blobs is #493's own unbuilt reaper, which that ticket says of itself deletes live data unrecoverably when it drifts -- so the reaper reaps into a `reaped/` prefix under the same 7 days, and burn keeps deleting outright. Two paths remove blobs; only one may be recoverable. Weekly automated restore-verification is rejected as SELF-DEFEATING rather than expensive: automating decryption puts the private key back on the VPS. The one scoping correction: #586 said the work lands beside `setup-forge.js`. That holds for the machinery. The honesty half -- burn's third tier and the retention window advertised on `/info` -- lands in loot proper, so this is two build tickets in two repos. Decision only; nothing here is built. ADR 0046, three CONTEXT.md entries, a third tier marked into ADR 0038 in place, and forge-datastores.md's closing "still unspecified" line replaced with a plan that says out loud it is a plan until the drill's negative has been run. 79fa0586 · dbf3dbe6…
  • a forge advertises its retention window on /info, and burn prints one honest bound per host it disclosed to (#814) 652f4023 · dbf3dbe6…diff
  • the backup machinery gets its record: ADR 0046 is amended by what building it found, and the drill is run as far as a key can take it (#813) The build lands in the `scripts` repo (6d31542). This is loot's half: the decision doc corrected where it was wrong, and the evidence file told what was actually run. Three amendments, each because building the thing falsified the text. Section 5's lifecycle rule is PREFIX-SCOPED. Sections 4 and 5 together ask one bucket for two opposite retentions, and a bucket-wide rule deletes the artifact section 6 calls genuinely unrecoverable -- silently, a week later. Section 3's burn record carries the burner and the timestamp, not the oid alone. burn_tombstone declares two more NOT NULL columns, so an oid on its own cannot be replayed at all. signed_tombstone stays out because it carries the burned PATH into a file that is unencrypted and kept forever, and the cost of that -- a replayed tombstone cannot be the audit record -- is now written down rather than discovered. Section 2's five replay steps are three today. The blob delete and the CDN purge belong to a durable job purge.rs says does not exist, and the blob tier is never restored from backup anyway. The evidence file gains sections 8.1 through 8.4, run 2026-08-16. The recipient keypair is minted -- rage rather than the reference age, because age ships only sigsum proofs nothing here can verify, and the act that mints the key protecting every backup took the checksummed path. Custody is proved rather than asserted: a probe round-tripped through the offline key and a freshly minted stranger identity was refused. Then a runner-shaped artifact -- dump, TOC, row manifest, both credentials files -- was encrypted, its plaintext deleted, decrypted with the offline key alone, restored and replayed. The negative first, because it is what makes the positive mean anything: without the replay all four assertions came back true, with it all four false, and a second replay moved nothing. What is still not proved is said plainly. Section 8.5 wants the superuser password coming back out of a REAL artifact and actually authenticating against the SCRAM verifier, and the artifact above carried a stand-in because nothing has deployed. The age-to-rage interop is assumed on the spec and gets proved by the first real run. Until setup-forge.js has run once, this forge has no backup and /root/.postgres-credentials has no second copy. c26f154c · dbf3dbe6…diff
  • the forge has backups: deployed, and the drill run whole against a real artifact (#813) `npm run setup:forge` has run. /root/.postgres-credentials has a second copy for the first time in its life, and the first journal export took 12 records off the live forge -- 8 burns and 4 revocations that a restore would have put straight back. This was never hypothetical. ADR 0046 section 8, all five steps, against an artifact pulled off R2: The artifact decrypts with the offline key and nothing else. That also settles the interop question I had left open: the VPS encrypts with Ubuntu's Go age 1.1.1 and the key was minted by rage, so one spec and two implementations, proved rather than assumed. The negative is the half that matters and it fires -- restored without the replay, all four capabilities came back; with it, none did. And the recovered 48-character superuser password matches pg_authid's SCRAM verifier, while a wrong one does not. Section 5's real claim is exercised too: the forge's own R2 credential is REFUSED on the backup bucket. An account-wide token would have satisfied every other check in the deploy and silently meant a compromised forge reads seven days of history. The open question in the consequence list is answered. R2 has no versioning endpoint, confirmed against the live API, and the deploy re-asks every run rather than trusting a sentence in a document. Three amendments, each because deploying it falsified something. The artifact now carries pg_dumpall --globals-only. pg_dump -d forge dumps one DATABASE and roles are cluster-level, so a restore had no forge, forge_read or forge_identity -- every GRANT in the archive an error -- and no pg_authid, which is the only thing section 8.5 can check a password against. That step was uncheckable until this landed. Section 8.3 and 8.4 used real rows for the burn and the revocation and fixtures for the publication, and had to: the row-count manifest in that very artifact records publication 0 and published_key 0. The live forge has published nothing yet. That is the same fact that leaves section 7's named trigger unfired. Two of section 2's replay steps still have nothing to mirror, unchanged from the build: the blob delete belongs to a durable job purge.rs says does not exist. What is not proved is said plainly. The timer has never fired on its own schedule -- every run so far was systemctl start from the deploy -- and the age private key has no second copy, which the ADR's own consequence list warns makes every backup landfill. That is now literally true rather than prospective. a2d9f41b · dbf3dbe6…diff
  • the forge stops saying it does not know its own window: ADR 0046 section 9 last step, taken (#813) The media existed and the bucket enforced the window, while /info still answered backup_retention_days null -- so `loot burn` went on printing an unknown-retention line against a forge that does in fact keep a copy for seven days. Section 9 assigns that key to this ticket in as many words, and it was the one step nobody had taken. https://forge.millerbyte.com/info now answers 7. Written only when the backup half is on, because section 9 rule cuts both ways and a window nobody keeps is the worse lie. In the env half rather than the mounted credentials file: it is not a secret, and the drift check greps it out of docker inspect, so moving it would blind that check while everything kept appearing to work. And the part that makes it a fact rather than a claim. A container reads its environment at CREATE, so one older than the backup half advertises null however often the script rewrites .env.forge. That is drift now, and it fired on the real deploy before recreating; probeForge then asserts /info returns the number, which is the only place the answer belongs to the running process rather than to a file this script wrote. Section 9 says the advertised number and the rule enforcing it are tied by one key name written once at deploy. They are tied a little better now -- one RETENTION_DAYS constant feeds both -- and the bucket is still the enforcer. 630c9069 · 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.