P3: the books' format moves only with the owner's go-ahead (D-39)

Books an update could write a newer format for open on their old format
and are asked at every open; anything needing a new feature is refused
until the owner agrees with the passphrase, after a verified backup that
includes the keys. New books start on the current format. Each projection
schema has its own database file, so an older version still opens the
books until the owner agrees.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
swaitsandClaude Opus 5.5 committed 2026-10-21 14:04:11 -06:00
1 parent b0a9fd8ad4
commit 3a9aa92315
62 files changed
+5309 -628

No files matched your search

+10 -1
View File
@@ -212,7 +212,7 @@ No scope unwraps the vault. Unwrap happens only inside an `/h/` request that has
run/admin.sock
legacy/<id>-<date>/ old copies moved here at adoption if the owner agrees (Q-3); the server never deletes them
books/<id>/ == today's data dir layout, unchanged (Store/Snapshot/verify_and_heal untouched)
journal.jsonl blobs/aa/bb/<sha> wanderbooks.db(+wal/shm) wanderbooks.lock
journal.jsonl blobs/aa/bb/<sha> wanderbooks.v<N>.db(+wal/shm, one per schema; P3) wanderbooks.lock
vault/{tin,w9}-<party>.bin HPKE-sealed (v2); moved from <books>/vault (S:511)
keys/keyring.json {pubkey, slots:[passphrase|recovery|passkey_prf|member_passkey]}, all wrapped
keys/preview.key HMAC key for preview_token (0600)
@@ -343,6 +343,15 @@ The books are open while something holds them; there is no background service. T
**Format changes are the server's gate, not the updater's.** A server that would write a newer journal format than a book has opens that book read-only, with guard `format_upgrade_pending`, until the owner consents in the app (or on a `/h/` page). Before it asks, it makes a verified local backup that includes the keys (`legacy.key` until P7). Consent comes only from the owner (the passphrase, through the viewer's `consent_format_upgrade` or a `/h/` page), never from an agent. Declining leaves the book as it was, and before consent the way back to a version that can write it is always there: a self-updating install keeps the previous verified app and offers "Go back to version X" (`wanderbooks rollback`); a package-managed one downgrades through its package manager (our apt and dnf repositories keep every release, and the AUR keeps old PKGBUILDs). This holds however the new version arrived: our updater, a package manager, or a hand install. **After consent** the previous version can no longer open that book, so "Go back to version X" refuses for it; the only way back is restoring the pre-upgrade backup, which would drop every entry since, so that restore says how many entries would be lost, arrives as its own book beside the current one (never replacing it), and needs the passphrase.
**As built (P3, the format upgrade, D-39), and where it departs:**
- **Waiting is a fact of the journal.** This version's format is the newest it reads with every feature it knows (`ledger_store::format::current`); a book declaring less waits (`JournalFormat::upgrade`), format 1 included, so the gate covers the step to format 2 as much as P3's features. New books declare the format in their second line (`Store::create`) and never wait; an adoption's copy is new books of this version and moves to it as it is made (the original keeps its own). The lazy declarations (`ensure_format` declared format 2 at the first setting) are gone: `ensure_format` and `ensure_feature` (the other P3 branches' call, same signature) now only refuse.
- **Served on the old format, not read-only.** The book opens and is served as before; only a write that needs the newer format or a feature it hasn't declared is refused, before anything is journaled (`StoreError::FormatUpgradePending`, guard `format_upgrade_pending`; the guards envelope is P5's, so the guard leads the error text and `books.start`'s `format_upgrade`). "Read-only" above is narrower than D-39's "declining never stops the books opening on the old format", which this follows.
- **The consent path.** The viewer's dialog and `wanderbooks books upgrade ID` (the owner passphrase typed at a terminal) send the lease intent `consent_format_upgrade`; the host, which reads no books, hands it to the server's admin socket (`upgrade_format`, the passphrase redacted from Debug), which checks the passphrase (Argon2id, `passphrase::verify`) before anything else. No MCP tool reaches either socket. No `/h/` page (P5).
- **Asked at every open.** The viewer asks once each time it opens waiting books, with a standing notice above every page and a Books-menu item; the owner's commands that open the books (`serve`, `books`, `token`, `connect`) ask once each run where the question is seen and answered (input and output a terminal, in the foreground: a command whose output goes to a file or a pipe would wait unseen, and mix the question into its output), and elsewhere print a note; `books list` and `doctor` show it; `books.start` puts it first.
- **The backup first.** Made at the go-ahead, not before asking (as above): a backup made when the owner is asked could be behind the books by the time they agree, and this one is the books exactly as they are moved. Under the books' lock throughout: the books are committed to their own `archive.git` (whatever targets the settings name, or none), the archive cloned and proven as a restore proves a backup, its head the books', its journal theirs byte for byte, its vault and keys file for file (`backup::prove_archive`); only then the one `journal.format_set`. Both are records of `ops/backup.jsonl` (`format_upgrade`, with the backup's commit and head); the targets take the pre-upgrade commit, as history, at the next backup after the write.
- **Not built:** "Go back to version X" (`wanderbooks rollback` doesn't exist yet: P11's updater); restoring the pre-upgrade backup as its own book that says how many entries it lacks (the backup is a commit in the archive's history; `books restore` brings back a repository's latest, so this needs a restore of a named commit, its own book id, the passphrase and the count: not small, left for P11 with the rollback).
- **Each projection schema in a file of its own** (the lead's decision, 2026-10-21), so going back to an older version stays possible until the owner consents: an older version refuses a database of a newer schema (`SchemaTooNew`), so a version that migrated the one file in place would have kept P2 from opening books that declined the upgrade. The projection's file is `wanderbooks.v{N}.db` for schema N (`ledger_store::db_file_for`, the name depending on the number alone, so the P3 branches' v13s merge to one name). A version opens or creates only its own; its own missing, it builds it from the journal (and catches it up at open, as ever); P2's `wanderbooks.db` is used only while it already holds this version's schema (no rebuild for nothing); any other schema's file is never migrated, written, refused or deleted. `books.verify` (`database`, `other_databases`) and `wanderbooks doctor` (`database: …`, with the others left there) say which; stale files are listed, never deleted. Old books dirs kept in git (before P2) ignore `wanderbooks.db`, not the new name: served by `dev-stdio` only.
**As built (P2 slice 8), and where it departs (D-32):**
- **No confinement, no helper.** No mode confines itself (no Landlock, no seccomp), so the helper is gone (`wanderbooks helper`, `ledger-host` helper.rs): the bridge, like every holder, starts the host itself with the clean start (start.rs). The host still refuses to run when it finds itself confined (started from a sandboxed app). Isolation is said as `same_user (advisory)` in `books.start` (`isolation`) and `wanderbooks doctor`.
- **The lease socket's intents are two:** `close` and `consent_format_upgrade`. Backing up, restoring and new books were intents only so a confined viewer could hand them over; the viewer now does them as the command line does, through the server's admin socket (`back_up_now`, `restore`, `create`), holding a lease while it asks.