refactor!: one way to serve books: the stdio server is gone (D-44)
check / check (push) Canceled after 0s

The owner (2026-10-23): "We should probably just delete the stdio path
altogether. THere's no point in that. And it sitting there unused will
just lead to bitrot. Nuke it?"

Books are served only by the books server (`wanderbooks server`),
reached by agents through the bridge (`wanderbooks mcp --book ID`),
which stays.

Removed: `wanderbooks dev-stdio`, the `ledger-mcp` binary, and
`ledger_mcp::run` (stdio and `--http` over a bare data dir), with all
that only that mode had:
- files the books make coming back inline: `HandedFile.content_base64`
  is gone, and `download` and `expires_at` are always there;
- books with no access log or backups: `books.access_log`,
  `books.backup` and `books.back_up_now` are always offered, by scope;
- the settings file read and reloaded (`legacy_files`), the vault key
  named by a file or `--vault-key-file`, and the books dir's
  key-inside-the-books and git checks (with `paths.rs`);
- `wire.rs` (the bridge frames an agent's lines itself);
- `books.settings`' file fields and `books.start`'s `settings_source`.

`BooksServer` is built one way, from `Served` (a book as the books
server serves it), in production and in the unit tests. An unknown tool
is now a protocol error (mcp-tools.md). Inline file input is unchanged.

Then the owner's three decisions (2026-10-23, the question tool):
(a) `books.import_settings` is removed entirely (118 tools): old books
come in whole by `wanderbooks books adopt`, which reads their settings
file once; ledger-config keeps its settings-file parsing only as far as
adoption uses it (`load` and the path-reading `effective_receipt_policy`
are gone), and `books.chart_template` gives its settings as
`books.set_setting` changes. (b) The viewer's loose-books-folder mode is
removed (`--data-dir`, `--books-toml`, `--vault-key-file`, the settings
file and config.json followed): it opens only books of the data root,
reading settings from the journal and the vault from the book's own
`vault/` and `keys/`. (c) `wanderbooks dev-stdio` has no stub: it is an
unknown command like any other word. Also: an upload's expiry past the
calendar is an error, never an empty string.

Tests: every suite that drove the stdio server moved to
crates/ledger-cli/tests/ and drives a real `wanderbooks server` on a
data root of its own (tests/common), handed files by their download;
mcp_stdio.rs is mcp_journeys.rs. Tests of what is gone are deleted and
listed in docs/testing.md. Docs: plan D-44 (D-18 superseded), STATUS,
testing, rules, mcp-interface, the handbook, README; justfile recipes.

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-23 19:45:18 -06:00
1 parent 0df7f52ac3
commit 7741e96eb3
87 files changed
+3477 -6476

No files matched your search

+25 -25
View File
@@ -58,7 +58,7 @@ Paths are relative to `/home/swaits/Code/wanderbooks/crates/` unless they are ab
| # | Invariant | Enforced by |
|---|---|---|
| I-1 | **The MCP is the only agent interface to the books.** Locally this is **advisory** (owner decision 2026-10-16): the server runs as the human, and the books are hidden, not locked. Mechanical enforcement is the self-host deployment on another machine, where agents have no access to the data root. | A data root outside every project directory (3.4), mode 0700, created with an explicit `set_permissions` call (not the umask). No tool, instruction, handbook text or result names a filesystem path (I-2, I-12). The stdio data-dir transport is removed from every `connect` path (`dev-stdio` refuses any data dir inside the data root). `books.start`, `wanderbooks doctor` and the docs report `isolation: "same_user (advisory)"`. Landlock and seccomp confine the server itself where available; they do not restrict the agent and are never described as if they did. Test `agent_uid_cannot_read_tenant`, run in CI against the two-user self-host deployment, not the desktop default. |
| I-1 | **The MCP is the only agent interface to the books.** Locally this is **advisory** (owner decision 2026-10-16): the server runs as the human, and the books are hidden, not locked. Mechanical enforcement is the self-host deployment on another machine, where agents have no access to the data root. | A data root outside every project directory (3.4), mode 0700, created with an explicit `set_permissions` call (not the umask). No tool, instruction, handbook text or result names a filesystem path (I-2, I-12). The stdio data-dir transport is gone (D-44): books are served only by the books server. `books.start`, `wanderbooks doctor` and the docs report `isolation: "same_user (advisory)"`. Landlock and seccomp confine the server itself where available; they do not restrict the agent and are never described as if they did. Test `agent_uid_cannot_read_tenant`, run in CI against the two-user self-host deployment, not the desktop default. |
| I-2 | **No tool takes or returns a server filesystem path.** | Schema lint `no_path_params`: the build fails on any parameter matching `*path*`, `save_to` or `*_file`. Landlock harness `no_tool_touches_outside_tenant`, run with fuzzed arguments. Static check: no `std::fs` in `ledger-mcp/src/tools/**` outside the ledger-store APIs. |
| I-3 | **The journal holds all books state.** Settings, conventions, decisions, draft states, evidence links, annotations, extraction records and attestations are journaled. Reports are pure functions of the journal. `wanderbooks.toml` is import/export text only. The server never reads a toml file. | `ledger-core::settings` replay. Property test `settings_replay_is_pure` (Store, Snapshot and the view are identical). Test `toml_round_trip_zero_events`. The daemon binary contains no toml file-read code path. |
| I-4 | **Money is exact Decimal, every entry balances, and posted entries are permanent.** Corrections are reversal drafts that need evidence. | Existing kernel, Kani and Lean. New `Step.draftReversal` and `Step.discard`, with preservation proofs. |
@@ -78,10 +78,10 @@ Paths are relative to `/home/swaits/Code/wanderbooks/crates/` unless they are ab
|---|---|---|
| Process shape | A: single process. B: gateway plus per-book worker. C: daemon, with a gateway when hosted. | **One `ledger-mcp serve` process** (A), both locally and on a self-host machine. Locally it runs as the human, only while something the human started holds it open: the app, an agent session, or a terminal. Nothing is installed to run in the background (owner decisions D-21 and D-23, 2026-10-18). Every local program is a mode of the one `wanderbooks` executable (D-22). It has no OAuth authorization server and only a minimal human-page surface. The multi-tenant front/worker split (B) belongs to the hosted service, which this plan does not build (section 11). |
| Isolation, and when it lands | A: P5. B: P6. C: P3. All three assumed a service user. | **Owner decision 2026-10-16: "you, hidden away."** The desktop gets no service user, sudo, system account or container. P2 moves the books into a server-owned data root (0700, outside every project directory) whose path no text ever names. This isolation is advisory and is labelled that way. Mechanical enforcement is the self-host deployment on another machine (P9). From P2 on, CI proves it with `agent_uid_cannot_read_tenant` against a two-user deployment. P2 needs journaled settings first (P1), because the server never reads toml. |
| Settings import | A: server reads legacy toml (`from_legacy:true`). B/C: agent passes text. | **`import_settings {toml_text, config_json_text?}` only.** A legacy fallback (today's `settings::Live`) survives only in `dev-stdio`, until a book is adopted. `books adopt` refuses a journal with no `settings.set` unless `--toml FILE` is given. The human's CLI reads that file and passes the text. |
| Before import | C: writes blocked by a `settings_not_imported` guard. | **Never block.** Legacy books keep `legacy_files` mode in `dev-stdio` (P1). Daemon books always have journaled settings (enforced by `adopt` and `create`). |
| Preview/post | A: `confirmed`. C: split tools plus `preview_token`. B: seven read-only twins. | **Keep `confirmed`, add `preview_token`.** Add one read-only twin, `books.preview_entry`, for the high-frequency post path, so claude.ai doesn't prompt on every preview. Close, reconcile, dispose, `run_*` and purge keep `confirmed` plus the token. `confirmed` without a token is accepted for one release with a deprecation warning. This limits churn in `mcp_stdio.rs`, `oracle_mcp.rs` and `spec_mcp.rs`. |
| `requiresUserInteraction` | B: on every post. | **Not used anywhere in this plan** (owner decision 2026-10-16: no added friction). Post, reverse, close, reconcile, dispose, `run_*`, `purge_tin`, `consolidate_duplicates` and `import_settings` carry `destructiveHint` plus `preview_token`, and the client's normal permission rules apply. Full TINs leave the agent's path through human pages: TIN-bearing outputs from P2, and all TIN handling in P7. |
| Settings import | A: server reads legacy toml (`from_legacy:true`). B/C: agent passes text. | **No import tool** (D-44: `import_settings` is gone, owner 2026-10-23). No settings file is read by any server or the viewer: old books bring theirs in, once, by `books adopt`, and `books.set_setting` changes settings after. `books adopt` refuses a journal with no `settings.set` unless `--toml FILE` is given. The human's CLI reads that file and passes the text. |
| Before import | C: writes blocked by a `settings_not_imported` guard. | **Never block.** Daemon books always have journaled settings (enforced by `adopt` and `create`); the `legacy_files` mode of `dev-stdio` is gone (D-44). |
| Preview/post | A: `confirmed`. C: split tools plus `preview_token`. B: seven read-only twins. | **Keep `confirmed`, add `preview_token`.** Add one read-only twin, `books.preview_entry`, for the high-frequency post path, so claude.ai doesn't prompt on every preview. Close, reconcile, dispose, `run_*` and purge keep `confirmed` plus the token. `confirmed` without a token is accepted for one release with a deprecation warning. This limits churn in `mcp_journeys.rs`, `oracle_mcp.rs` and `spec_mcp.rs`. |
| `requiresUserInteraction` | B: on every post. | **Not used anywhere in this plan** (owner decision 2026-10-16: no added friction). Post, reverse, close, reconcile, dispose, `run_*`, `purge_tin`, `consolidate_duplicates` and (until D-44 removed it) `import_settings` carry `destructiveHint` plus `preview_token`, and the client's normal permission rules apply. Full TINs leave the agent's path through human pages: TIN-bearing outputs from P2, and all TIN handling in P7. |
| Harness deny rules | C: P0 `.claude/settings.json` deny rules. | **Rejected.** That is plan A, which is dropped. P0 corrects only the sentence in the server text and the generated template that caused the `cat` (`/home/swaits/Documents/the first company-Books/AGENTS.md:68-69`, "the journal is the evidence — read it"). This is a content fix, not a guard. |
| Defect 12 rule | A: everything through `financing_accounts` is excluded. | **Classified by instrument (C).** Payment-card and third-party-network payments are excluded (1099-K) and listed. A member's personal cash or check through 2100 stays reportable. An unknown instrument is `undetermined`. |
| Odd cent (17) | A: fair ties. C: rotating tie-break. | **Cumulative apportionment (B)**, extending `kernel::apportion` (kernel.rs:309), its existing Kani proofs, and `Apportion.lean`. |
@@ -140,7 +140,6 @@ The data root is `$XDG_DATA_HOME/wanderbooks` on Linux (default `~/.local/share/
| Viewer | `wanderbooks gui <book>`, the viewer mode (the `ledger-gui` crate; local, direct, the default; what opening the app shows); `wanderbooks gui --books <url>` (remote replica, P9) | the human | local: yes, read-only, never writes. Remote: none |
| CLI | `wanderbooks` (ledger-cli) | the human | **by design never**. Talks to `run/admin.sock` (peer-credential check: same user ID as the server). The admin socket cannot read books data; it can create or adopt books and mint agent tokens, admin-scope ones included. The peer-credential check keeps other local accounts out; any process running as the human, agents included, can use it (advisory, like the rest of the desktop deployment). |
| Agent bridge | `wanderbooks mcp --book <id>` (stdio to the agent; `--url` for a remote book, P9) | the agent's user | never; it has no data-dir flag. It may start the host (D-23), whose server then reads the disk as itself |
| Test/demo | `wanderbooks dev-stdio --data-dir` (also the `ledger-mcp` binary until the scaffold goes) | any | refuses inside the data root; `wanderbooks connect` never configures it (D-18) |
### 3.2 Transport (one code path, local and remote)
@@ -196,7 +195,7 @@ No scope unwraps the vault. Unwrap happens only inside an `/h/` request that has
- **Kept in the OS keyring** (Secret Service on Linux, through keyring-core; macOS and Windows stores come with P11), under service `wanderbooks` and an account naming the root by a hash of its path, the book and the token's name. One account is one item (2026-10-22, after a bridge on the live books ran on a token the keyring no longer gave while `download` read a revoked one): keeping a token first removes every item for its account in every collection, then makes one and reads it back; reading finds the items in every collection and, finding several, refuses with how to keep one again (`token create --replace`) instead of giving one of them; a keyring that is locked or can't be reached is an error, never "no token". The bridge (`wanderbooks mcp --book ID [--token NAME]`) reads it before each new session (even with a token held: the keyring's is used, and a change is said on stderr; a slow keyring leaves the held one in use after 2 s) and each request until it has one, and again when the door refuses it, so a token made or replaced mid-session is used from the next call (with a new session: a session belongs to the token that opened it); without one, the server's refusal says how to make one, but when the keyring couldn't be read the agent is told that instead (locked or unreachable, the token itself fine; or several items), never to make a token. Tests keep tokens in files under the root's `run/` (the `file_keyring` knob): a folder per collection, which a test can lock, make unreachable, or plant a second item in.
- **The door** (`/b/{id}/mcp`): no token or an unknown one, 401 with `WWW-Authenticate: Bearer` (the scheme in any case); another book's, 403; a session is bound to its token when opened and used only with it (403), forgotten when it ends, is found gone (an idle one, when next used), or its token is revoked or replaced; an id the server never opened binds nothing; past 100,000 bindings an initialize is refused, never a binding dropped. Each refusal at the door is recorded in that book's access log (action `connect`).
- **Scopes** (`ledger-mcp/src/scopes.rs`): a table of every tool. Read-only tools need books:read; a tool with `confirmed` needs books:write to preview and books:post to act; tools that post at once, settings commits, voids, renumbering, corrections and role changes need books:post, and so do the controls on posting (setting an approval rule, retiring a convention, approving an entry) and sealing a TIN (`set_tin`, `set_w9`, which can replace one); `books.access_log` and the tools that can still put a full TIN out (`vendor_1099`, `export_cpa_package`, `get_w9`) need books:admin, and so do backing up now and choosing where backups go (a `backup.*` setting, by `set_setting` or an imported `[backup]` table; slice 7): a backup is the whole book. A token is offered only the tools it can call (the list is then cached per caller, `cacheScope: private`); a call beyond its scopes is refused, unrun, naming the scope. Reading a resource needs books:read. A local token gets all four unless told otherwise.
- **The access log** (`books/<id>/access.jsonl`, own genesis) records every tool call and resource read, refusals included: who (token id, name), the client as it named itself (cut to 64 characters, any full TIN masked), the request id, the tool or resource, a SHA-256 of the arguments' canonical JSON with what may be a TIN masked first (a value under a key naming one, any key or text holding one in a shape the TIN scanner knows, any number of eight digits or more: an unsalted digest of a TIN whose last four the books show is broken by 10^5 guesses; a file's base64 is hashed as it is, never spelling a TIN in the clear), what came of it (`ok`, `error`, or `refused` with a code), and the journal's head after the call's own last write, if it wrote, whatever it answered (taken under the store's lock as the call lets go). A call's record is written on its own blocking thread while it still counts as running (one that panics part way included, with what it wrote), so a close waits for it too, and before the answer goes; one that can't be written stops the book (no more requests until a restart) and the answer says the call ran (a call refused as not run, by a close or a stopped server, keeps its "not run" code). A tool no table names is refused unrun. Known gap: a crash of the process between a call's journal write and its record leaves the journal ahead of the log for that call. A damaged log, or one holding a record that isn't an access, keeps its book closed, with why. `books.access_log` (admin) reads it newest first, filtered by token, action or outcome, paged with `before`; over stdio (no log) the tool isn't offered.
- **The access log** (`books/<id>/access.jsonl`, own genesis) records every tool call and resource read, refusals included: who (token id, name), the client as it named itself (cut to 64 characters, any full TIN masked), the request id, the tool or resource, a SHA-256 of the arguments' canonical JSON with what may be a TIN masked first (a value under a key naming one, any key or text holding one in a shape the TIN scanner knows, any number of eight digits or more: an unsalted digest of a TIN whose last four the books show is broken by 10^5 guesses; a file's base64 is hashed as it is, never spelling a TIN in the clear), what came of it (`ok`, `error`, or `refused` with a code), and the journal's head after the call's own last write, if it wrote, whatever it answered (taken under the store's lock as the call lets go). A call's record is written on its own blocking thread while it still counts as running (one that panics part way included, with what it wrote), so a close waits for it too, and before the answer goes; one that can't be written stops the book (no more requests until a restart) and the answer says the call ran (a call refused as not run, by a close or a stopped server, keeps its "not run" code). A tool no table names is refused unrun. Known gap: a crash of the process between a call's journal write and its record leaves the journal ahead of the log for that call. A damaged log, or one holding a record that isn't an access, keeps its book closed, with why. `books.access_log` (admin) reads it newest first, filtered by token, action or outcome, paged with `before`.
- **The owner passphrase** is an Argon2id hash (m=64 MiB, t=3, p=1, its own salt) in `auth/owner.json`, replaced whole; 12 to 1024 characters, no other rule (NIST SP 800-63B); a check refuses a hash weaker than that (changed by hand). `setup` at a terminal asks for it (twice); elsewhere it says to run `wanderbooks passphrase`, which works only at a terminal, unechoed, and asks for the one in force before a change. (Advisory, like everything local: a process running as the human can drive a pseudo-terminal and set the first one.) A stored hash of another Argon2 version, with a short salt or output, or asking a check for more than 1 GiB, 16 passes or 8 lanes is refused as changed by hand. The command line wipes token texts and passphrases from memory once used (the server and the bridge, which hold tokens for their lifetime, don't). `doctor` says whether it is set and lists the tokens. Its uses (`/h/` pages, consents) come with the slices that build them.
- **Known, product-wide (for P7):** the TIN scanner (`has_full_tin`) takes hyphens and spaces as separators, not commas or parentheses, so "123,45,6789" passes it in party fields and the access-log digest alike.
- **Departures:** rate limits (tower-governor) wait for P9: on a socket only this account can reach, nothing could be limited that couldn't read the files instead. Principals are per book, not lists of books. `initialize` (the connect text, composed from the book's settings) is given to any valid token and isn't recorded. Changing the token record holds its lock across one fsync, briefly delaying token checks.
@@ -260,13 +259,13 @@ The server, the CLI and the GUI find the data root with one shared function (`le
- **Color schemes** (owner, 2026-10-19: "full blown colorscheme support … all the top neovim style colorschemes", "on the order of 20 families, each with a flyout of all its variants", a light/dark/all filter; Ayu included). The button in the sidebar's lower corner opens a menu: the filter, then, alphabetically and all on screen at once, the viewer's own family (Wanderbooks Light and Dark) and twenty families of editor schemes (Ayu, Catppuccin, Cyberdream, Dracula, Everforest, GitHub, Gruvbox, Kanagawa, Material, Melange, Monokai Pro, Night Owl, Nightfox, Nord, One Dark, Oxocarbon, Rosé Pine, Solarized, Sonokai, Tokyo Night), each a flyout of all its variants, alphabetical, each a row with a strip of its colors that chooses it; a row rested on for 50 ms is worn live until the pointer moves on (owner, 2026-10-19), and only a click keeps it. The choice and the filter are per-human eframe preferences like `open_at_start`; nothing is journaled and no figure changes (`schemes.rs` paints nine pages in all 80 schemes and checks every painted word and figure is the same, in the same place, and readable on what is under it). Every palette keeps every text color (figures, gains and losses, warnings, links, the faintest text) at WCAG AA contrast (4.5:1) on every background it is drawn on: a scheme's own color that falls short keeps its hue and only changes lightness, and gains, losses and text stay plainly different to the eye. The scheme pins egui's light or dark theme, so the system flipping changes nothing. The palettes come from each scheme's official source (`docs/color-schemes.md`).
**As built (P2 slice 6), and where it departs:**
- **Finding the books.** `wanderbooks gui [BOOK]` (`ledger-gui --root PATH --book ID`): the data root from `--root`, else the one `setup` recorded (`Root::find`), never the working directory or a toml; the book named, else the root's only one (or the first by id). `--data-dir` still opens an older books dir's `books-data` (tests, and the first company's books until the cutover), and `--books-toml` names its settings file, read only while its journal keeps none.
- **Settings come from the journal.** A book of the data root never reads `wanderbooks.toml` or `config.json`: books that keep no settings show the defaults. An older books dir whose journal keeps none is read as its server reads it, from its `wanderbooks.toml` (or `--books-toml`) and `config.json`, followed live, until its settings move in (`books.import_settings`). The demo company journals its settings.
- **The vault.** For a book in the data root (`inside_a_root`), its own `vault/` and `keys/legacy.key`, whatever a setting says; for an older books dir, as before (`--vault-key-file`, the setting, the default). No passphrase: TINs get no handling beyond the vault (D-29); a click copies one to the clipboard as before.
- **Finding the books.** `wanderbooks gui [BOOK]` (`ledger-gui --root PATH --book ID`): the data root from `--root`, else the one `setup` recorded (`Root::find`), never the working directory or a toml; the book named, else the root's only one (or the first by id). `--data-dir` still opened an older books dir's `books-data` (tests, and the first company's books until the cutover), and `--books-toml` its settings file, until D-44 removed both (owner, 2026-10-23): the viewer opens only books of the data root.
- **Settings come from the journal.** A book of the data root never reads `wanderbooks.toml` or `config.json`: books that keep no settings show the defaults. An older books dir whose journal kept none was read from its `wanderbooks.toml` and `config.json`, followed live, until D-44 removed that mode: the viewer reads every book's settings from its journal alone. The demo company journals its settings.
- **The vault.** For a book in the data root (`inside_a_root`), its own `vault/` and `keys/legacy.key`, whatever a setting says; for an older books dir, as before (`--vault-key-file`, the setting, the default), until D-44: now every book's vault is its own. No passphrase: TINs get no handling beyond the vault (D-29); a click copies one to the clipboard as before.
- **`books.show` and presence** (`ledger_store::ops`, which replaces `viewer.rs`): the server makes the book's `ops/` (0700) and `ops/viewer.lock` (0600) when it opens the books; a show request is a line of `ops/show.jsonl` (`{seq, at, target}`, at most 32, the file replaced whole); the viewer watches `ops/` once it exists and holds a shared `flock` on `ops/viewer.lock` through a read-only descriptor while it runs, let go of when it closes; `books.show` answers `viewer_open` and says so. `ops/` is not the books: the read-only tool check leaves it out.
- **Save a copy / Open** on every document, **Save the W-9** on a party whose W-9 is in the vault, and the **Export** menu (the CPA package as of today, the 1099 worksheet for this year or last, full TINs included): each a new file in the Downloads folder (`ledger_store::saving`: never over a file, written aside and linked in; the viewer's report PDFs now save the same way instead of overwriting), what came of it shown beside the button. The menu's files are built off the UI thread from a fresh read-only snapshot; a document's copy and a W-9 (one stored file each, hash-checked as read) are saved at the click. "Open" is a copy in Downloads opened with the system's app (the one folder the viewer may write once confined, slice 8).
- **One builder** (`ledger-package`): the CPA package, the 1099 worksheet and the report helpers the server's tools share moved out of `server.rs` into a crate both the server and the viewer use, over `Store` or `Snapshot` alike (`PackageBooks`), the TINs passed in. Proven unchanged: 91 tool responses, 19 packages and 366 entries byte-identical before and after (only the XLSX and zip timestamps, which differ between any two runs, aside). The package now names files of the same name `name (2).ext` instead of failing, its README names only what it holds, and a K-1 worksheet whose rows don't match the members is left out with the reason.
- **The agent's side (D-29):** see 4.7's slice 5 list; `export_cpa_package`, `vendor_1099` (`csv: true`) and `get_w9` hand their files over as exports (`GET /b/{book}/x/{export}`, `wanderbooks download ex_…`), inline over stdio; no tool takes a path.
- **The agent's side (D-29):** see 4.7's slice 5 list; `export_cpa_package`, `vendor_1099` (`csv: true`) and `get_w9` hand their files over as exports (`GET /b/{book}/x/{export}`, `wanderbooks download ex_…`), never inline (D-44); no tool takes a path.
- **Not in this slice:** the menu's owner intents ("Close the books", "Back up now", "Restore…", "New books…", "Connect Claude Code / Codex") need the viewer's helper and the lease socket, and come with backups (slice 7) and setup (slice 8); so does running the viewer under read-only Landlock and Open-with through its helper.
### 3.7 Backup and tamper evidence
@@ -292,7 +291,7 @@ Email or webhook head delivery and a witness log belong to the hosted service (s
**As built (P2 slice 7), and where it departs (D-30, D-31):**
- **No archive process, no encryption.** D-31 made a backup the book as it is, in git, so there is nothing to encrypt and nothing to segment: the server itself backs each book up (`ledger_store::backup`, `ledger-mcp backup.rs`). Each book keeps a bare `archive.git` beside its journal; a backup is one commit of its journal, `blobs/`, `vault/`, `keys/` and `access.jsonl` exactly as they are, made under the store's lock (so whole lines and a vault that matches), its message `seq N head H` with `book <id>` below, then pushed outside the lock, fast-forward only (a target that moved on is refused and said, never forced), to `backup.git_remote` and to `backup.folder/<book>.git` (made bare on first use; a folder that isn't there, a disk not connected, is said plainly). git runs with no system config, no hooks (in a repository on this computer it pushes to too, through `--receive-pack`), no prompts, signing, ignore or attributes files or file watchers, its own identity, only the file, ssh and https protocols (never `ext::` or `fd::`, whatever the owner's own configuration says), and a limit of five minutes without a word (pushes and clones report progress; a git stopped is stopped with all it started); the owner's own `~/.gitconfig` still applies otherwise, for credential helpers. The stored files are staged outside the store's lock (they never change; a failure there is only a lost head start) and the commit, under it, is checked to hold every file there is, by name; bytes are kept as they are because no attributes rule of the owner's, the system's or the archive's is read, and a `.gitattributes` in a book's folder refuses the backup. A repository on this computer (a folder target, or an address that is a path) is pushed to or read only when plain: a bare repository at that very path (never one git would find beside or inside it), nothing in it sending git to another repository (a `.git`, `commondir` or `gitdir` file, objects borrowed from elsewhere, a link where it or its own parts are, a `<path>.git` or `<path>.bundle` beside it), its configuration only what `git init --bare` writes (and, for a copy made with `git clone --bare`, where it came from), since a setting of its own could run a command on a push; repositories are made with the files ref format and SHA-1 whatever the owner's own defaults; one is read as a far one is (`--no-local`, so its copying reports progress). A folder target's repository is made only while the target never took a backup: one gone since is a disk not connected (a mount point with nothing on it), said, never made again where the disk should be. A lock a stopped git left in the archive is cleared once older than any git may run quiet. The host now passes the owner's `SSH_AUTH_SOCK` (only a socket the account owns, not a link to one) so a private repository over ssh is reachable. `backup.folder` must be absolute and `backup.git_remote` can't read as an option; setting either (or importing a `[backup]` table) needs books:admin, as backing up does (a backup is the whole book, vault and key included).
- **When.** When the books open; a while after they were last written (`--backup-quiet`, 30 s, the `backup_quiet` knob for tests); before they close (at most 60 s); and on `books.back_up_now`. A failed commit holds every run off for 15 minutes; a target whose push failed is left out of the backups after writes for 15 minutes and then tried again, written to or not, while the others carry on. `period.closed` needs no trigger of its own: a close is a write. No head handoff files (`ops/heads.jsonl`) yet: the backup's commits are the head record.
- **The record and the tools.** Every commit, push and drill is a record of `ops/backup.jsonl` (a chain log; a backup never moves the journal's head), each target's keyed by a digest of where it is (a new folder or address starts with no standing). Stale means behind, with the first try to back up what it lacks more than a day old (not a day since the last good push: quiet books aren't stale the moment they're written); a failed commit is every target's error until a push succeeds. The chain isn't verified at each run: the server verifies it when it opens the books, and drills prove what reached each target. Two tools, not `{status|run}`: `books.backup` (books:read) for the standing of each target (last good backup, through which seq, stale after a day behind, the last error, the last drill and why it failed) and `books.back_up_now` (books:admin, the one open-world tool). `books.start` adds a step when none is set up, or one is stale, failing or failed its drill. Over stdio neither is offered. No error names a path in the data root.
- **The record and the tools.** Every commit, push and drill is a record of `ops/backup.jsonl` (a chain log; a backup never moves the journal's head), each target's keyed by a digest of where it is (a new folder or address starts with no standing). Stale means behind, with the first try to back up what it lacks more than a day old (not a day since the last good push: quiet books aren't stale the moment they're written); a failed commit is every target's error until a push succeeds. The chain isn't verified at each run: the server verifies it when it opens the books, and drills prove what reached each target. Two tools, not `{status|run}`: `books.backup` (books:read) for the standing of each target (last good backup, through which seq, stale after a day behind, the last error, the last drill and why it failed) and `books.back_up_now` (books:admin, the one open-world tool). `books.start` adds a step when none is set up, or one is stale, failing or failed its drill. No error names a path in the data root.
- **Drills** run weekly per target that holds a backup, and at the next look after a failed one once 15 minutes have passed (looked for a minute after the books are first seen open, so also at the first start after one fell due, then hourly; `--drill-first`, the `drill_first` knob, for tests): clone, check the tree is plain files under the backed-up paths before any is written out (a link or submodule could otherwise point the checkout, or the store opened on it, at the live books), replay the chain, re-hash every stored file, check every TIN record the books name is there with its key, check the head against the commit's, and check the journal is a byte prefix of the live one (the reports then follow from the same journal, so no report comparison). Not after each close: weekly is the floor and a close changes nothing a drill would see.
- **Restore** is `wanderbooks books restore BACKUP [--id ID] [--replace] [--yes]` (without `--id`, the id of the books the backup holds, as its preview names them, else the repository's name), the admin socket's `restore`, previewed then confirmed against the previewed head. The server clones the backup into `books/.restore-*` (the address or folder crosses the owner-only admin socket as an adoption's path does; no staging through `uploads/`), proves it as a drill does (but for the live-journal check), one restore at a time, builds the book as an adoption builds (every stored file re-hashed, opened, verified, synced; the vault and its key come with it, D-31), gives it the backup's history as its own `archive.git` (so its next backups fast-forward), and renames it into place. Over existing books it is refused unless `--replace`: then the server lets go of the open books (their writer lock released; a session still holding them is refused, its client's next session sees the restored books), waits for their last backup run and starts no other until the restore is done (one in flight could otherwise push past the backup being restored), and moves them whole into `legacy/<id>-<time>/`; a restore that fails puts them back and opens them again. `books.purge_tin` now says backups made before keep their copies (D-29, D-31). Each backup commit names its books (`book <id>` in its message), so a restore knows whose books it brings back (and the command line, given no `--id`, restores them under that id, deciding after one preview, which only reports; a restore leaves `ops/restored-from` in the books, noted by the server when it opens them, so the note lands with the books): the same books count their targets' backups as their own (a backup folder gone since is a disk not connected, never made again); books restored beside the books they were, under another id, never push to those books' repository (it would stop their backups): it is paused for them, said by `books.backup`, until another is named. A damaged `ops/backup.jsonl` is set aside and begun again, never keeping the books shut, what it knew of whose backups the targets hold carried over; a backup folder that took the books' backups and holds none now says so (a disk not connected, or a new one: `books.back_up_now {new_disk: true}` begins it again, only when the human says so). One book's drill holds the others' backups until it ends; a backup at close waits at most a minute for it. No passphrase for a replacing restore (D-31: the old books are kept). Viewer and helper: "Restore…" and "Back up now" come with the menu intents (slice 8); the lease socket's `restore` intent now names the backup and the id, and both intents point to the terminal and the agent until then.
- **Tests:** `crates/ledger-cli/tests/backups.rs` (both targets after a write; a write not backed up before the quiet; close, and catching up at the next open; admin only and the head unmoved; a target that moved on; restore beside, on a clean install, of the previewed backup only, with its vault and key, refusing five kinds of damage and writing nothing, never overwriting and keeping the replaced books, a failed replace leaving the books open; a drill on open; a failed drill flagged), `open_while_held.rs` (`books restore` from the command line), `ledger-store` (backup unit tests: links and strangers refused before checkout, target hooks never run, ignore rules never leave a file out, stale locks, drills; `root.rs`: never moving books something holds), the knobs, the host's environment, the scopes, the settings' checks, and the loop's and the standing's unit tests. The suite passed nine of nine runs three at a time (the replace race above showed only under load).
@@ -350,7 +349,7 @@ The books are open while something holds them; there is no background service. T
- **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.
- **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: they come in by `books adopt`, which copies them into the data root.
**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`.
@@ -482,7 +481,7 @@ Report results also carry `as_of_status {drafts_open_on_or_before, unreconciled,
- **No path on the server's computer is read.** `books.import_local_file`, the import roots and `--import-root` are gone, and so is `set_w9`'s `w9_file_path`; a test (`no_tool_takes_a_path`) keeps any path-like parameter out, except the three outputs with full TINs (`export_cpa_package` and `get_w9` `save_to`, `vendor_1099` `save_csv_to`), which keep theirs, written only outside the books, until the viewer takes them over in slice 6 (D-28: the viewer reads and downloads; it never writes).
- **An upload, for books served from the root:** `books.begin_upload {sha256, size, mime_type, file_name, purpose: receipt|document|import|w9}` returns `{upload_id, command, expires_at, max_bytes}`. No `put_url`, `curl` or `human_url`: the server listens on a Unix socket, so the agent runs `command`, `wanderbooks upload --book ID UPLOAD FILE`, which reads its token from the keyring (`--token NAME` is in the command when the caller's token isn't the default `agent`) and PUTs the file to `/b/{book}/u/{upload}` (D-28: the agent does it all; no `/h/inbox` drop zone, no `inbox_id`; the hosted server of P9 adds a URL). The bytes stream to `uploads/<id>.part` and are kept only if exactly the size and SHA-256 declared, by the token that began it, once (409 after); 25 MiB at most; 15 minutes; at most 16 under way per token; lapsed ones swept when the books open, whenever a grant is made, and every 10 seconds (owner, 2026-10-19: a W-9 or a file with a TIN "shouldn't just linger there forever... EVENTUALLY cleaned up is fine"). One lock per book orders every change to its uploads (grants, sweeps, a send's start and end, a take, a give-back), and a grant is written aside and renamed. Refusals carry their code (`no_such_upload`, `not_yours`, `already_sent`, `in_use`, `not_as_declared`, `stopped`), and a refused body is read and dropped (up to 25 MiB) so the client gets the answer. Every PUT to books the server holds open is in the access log (action `upload`, with the upload's id, refusals at the door included; a request for books it doesn't hold has no log to go in), written in the same blocking task as the bytes, so a client hanging up never leaves bytes kept without their record, and bytes whose record can't be written are thrown away. An upload's id is `up_` and 32 letters `a`-`p`: with no digits, the access log's TIN masking never hides one.
- **Filed once, and only when filed.** A tool takes the upload by renaming its bytes aside (so two calls never both have it), hashes them again, and spends it only once they are in the books (an import: once its rehearsal passed and the writing begins); a call refused after taking it, or one that panics, gives it back, and a server stopped mid-call gives it back when the books next open. A tool takes only an upload of its own purpose (receipt or document: `attach_receipt`, `file_document`; import: the two CSV imports; w9: `set_w9`). `existing_attachment_id` is offered only for a receipt or document, and never names a withdrawn attachment.
- **Inline:** `content_base64` (≤ 256 KiB, its length checked before it is decoded) with a required `sha256`; never both, never neither. Over stdio only inline, and no larger file comes back: stdio is the development transport now (the owner's books run released code until the cutover, `just build` building `main`), so it gets no upload route. Names and types are checked when a file is filed, whichever way it came: a name of 1 to 255 bytes, no folder in it, no control or invisible characters; a MIME type of visible ASCII. `attach_email` and `inbox_id` aren't built. `set_w9` takes `w9_upload_id` or `w9_content_base64` + `w9_sha256`, judged a PDF, PNG or JPEG by its first bytes, with an optional `w9_file_name`. A request on `/b/{book}/mcp` is at most 1 MiB now that files have their own route (it was 64 MiB).
- **Inline:** `content_base64` (≤ 256 KiB, its length checked before it is decoded) with a required `sha256`; never both, never neither. (Until D-44 the stdio development transport took files inline only; it is gone.) Names and types are checked when a file is filed, whichever way it came: a name of 1 to 255 bytes, no folder in it, no control or invisible characters; a MIME type of visible ASCII. `attach_email` and `inbox_id` aren't built. `set_w9` takes `w9_upload_id` or `w9_content_base64` + `w9_sha256`, judged a PDF, PNG or JPEG by its first bytes, with an optional `w9_file_name`. A request on `/b/{book}/mcp` is at most 1 MiB now that files have their own route (it was 64 MiB).
- **Out:** `books.get_receipt {attachment_id, view: meta (default) | bytes}`: bytes inline for a file ≤ 256 KiB; a larger one answers with `download`, the command `wanderbooks download --book ID ATTACHMENT --to FOLDER`, which GETs `/b/{book}/a/{attachment}` with the agent's token (books:read; `Content-Disposition` with the name in ASCII and RFC 8187 UTF-8, `nosniff`, and the SHA-256 as an RFC 9530 `Repr-Digest`, which the command checks before it saves), into a new file beside any of that name (`name (1).pdf`), never over one: written to a hidden file and linked in whole, so the name never holds part of it. The name it saves under is the server's made plain (no folder, no leading dot, nothing invisible, at most 200 bytes), else `attachment-N`. The read and its record are one blocking task, as for uploads. The `books://receipts/{id}` resource is meta only. Not built yet: `text`, `page_image` and `link` views, `books.download_link`, and links from `export`, `asset_labels` and `export_cpa_package` (slice 6, with the viewer's downloads).
### 4.8 Drafts waiting on evidence
@@ -499,7 +498,7 @@ Report results also carry `as_of_status {drafts_open_on_or_before, unreconciled,
| Area | Tools |
|---|---|
| session | `books.start`, `books.handbook`, `books.job`, `books.journal_history {after_seq?, types?, subject?, limit}` (the only way to "read the journal"), `books.viewer_status` |
| settings / conventions | `books.settings` (from the journal, with provenance and schema), `books.set_setting`, `books.import_settings {toml_text, config_json_text?}`, `books.export_settings`, `books.conventions`, `books.set_convention`, `books.retire_convention`, `books.record_decision`, `books.decisions`, `books.open_items`, `books.raise_item`, `books.resolve_item`, `books.draft_from_template {template_id, ...}` |
| settings / conventions | `books.settings` (from the journal, with provenance and schema), `books.set_setting`, `books.export_settings`, `books.conventions`, `books.set_convention`, `books.retire_convention`, `books.record_decision`, `books.decisions`, `books.open_items`, `books.raise_item`, `books.resolve_item`, `books.draft_from_template {template_id, ...}` |
| drafts | `books.preview_entry` (read-only), `books.list_drafts {as_of?, state?}`, `books.set_draft_state`, `books.discard_draft {reason}`, `books.inherit_evidence {entry_id, from_entry, reason}` |
| chart | `books.rename_account` |
| evidence | `books.begin_upload`, `books.inbox`, `books.discard_inbox_item`, `books.attach_email`, `books.find_attachment {sha256\|md5\|receipt_number\|text}`, `books.annotate_attachment`, `books.consolidate_duplicates`, `books.add_instrument`, `books.instruments`, `books.set_entry_instrument`, `books.download_link` |
@@ -615,7 +614,7 @@ The `WANDERBOOKS_TOML` template (`ledger-cli/src/main.rs:237-347`) becomes the e
### 5.5 Migrating existing books (generic)
- `wanderbooks books adopt <data-dir> --id X [--toml FILE] [--config-json FILE]` runs as the human, with no sudo, through the admin socket. The server, which also runs as the human, does the copy:
1. Takes the source flock, so it refuses while a stdio server holds it.
1. Takes the source flock, so it refuses while another program holds it.
2. Verifies the chain and records head H.
3. Copies `journal.jsonl` and `blobs/` into a staging folder (a cut-off last line or a link among the blobs is refused), opens the copy (rebuilding the projection), checks that the head equals H, verifies it end to end and re-hashes every blob; only then is it renamed into `books/X/`, and on any failure the staging folder is removed and nothing appears. A source inside the root, or holding it, is refused.
4. Imports settings from the given texts (preview printed, confirmation at the TTY) unless the journal already has `settings.set` events.
@@ -623,7 +622,7 @@ The `WANDERBOOKS_TOML` template (`ledger-cli/src/main.rs:237-347`) becomes the e
6. Seeds `archive.git` from the source's git history, filtered to the journal and blob paths, and records the old repository's last commit hash. The push destination is the owner's answer to Q-1.
7. If the owner agrees (Q-3), moves the source data dir, its vault and the project folder's git history into `<root>/legacy/X-<date>/`. Nothing is deleted. If the owner says no, the source stays where it is, read-only, and Needs attention lists it as an exposed old copy.
- `ledger-cli` `init`/`new` stop scaffolding and call `books create`.
- `dev-stdio` keeps `legacy_files` mode for unadopted data dirs for one minor release; `books.start` returns `next_steps: adopt`.
- (`dev-stdio` kept `legacy_files` mode for unadopted data dirs until D-44 removed it; old books come in by `books adopt`.)
- Legacy semantics kept:
- `entry.reversed` stays a posted reversal. During the re-audit the original's receipts are linked with `inherit_evidence` (D-12, decided); any not yet linked are reported as `legacy_reversal_exempt`.
- Old close variants keep their rounding.
@@ -683,7 +682,7 @@ This section covers the local (T1) and self-host (T2) deployments. Threats speci
Each record is fsynced, and a failed write fails the request. Readable through `books.access_log` (admin) and the viewer's Access page. The CPA package carries a summary. Its head is part of the head handoff. `replica.connect` occurs only for remote viewers (P9). The local viewer's direct reads, including its in-process TIN unlock, are not logged, because the GUI never writes.
**Security test suite** (`ledger-mcp/tests/security_*.rs`):
**Security test suite** (`ledger-cli/tests/security_*.rs`, driving a real books server as `tests/common` does, D-44):
- auth on every route; wrong audience → 401; bad Origin → 403; bad Host → 400; body over limit → 413
- expired or replayed grant → 410; upload sha mismatch refused
- `scope_step_up_403`; `handles_bound_to_sub`; `tin_entry_requires_same_sub`
@@ -808,7 +807,7 @@ The first company keeps working between phases.
- `company.timezone` drives `today()`.
- New crate `ledger-view`: `attention::find` moved from `ledger-gui/src/attention.rs:68-571` together with its helpers (`books.rs:643-650` `missing_receipt`, `depreciation_due`, `w9_status`, `reconciliation_due`, `party_infos`), over a `BooksView` trait.
- GUI reads settings from the Snapshot; journal-page `describe` arms (`pages/journal.rs:825`); Company page shows provenance; copy fixes `company.rs:59`, `cash.rs:51`, `attention.rs:562`; the "due to" heuristic uses `financing_accounts` (`attention.rs:349`).
- `dev-stdio` keeps `legacy_files` mode.
- (`dev-stdio` kept `legacy_files` mode, until D-44.)
- The first company P1 steps.
**Defects:** none directly. P1 enables 8 and 16.
@@ -833,10 +832,10 @@ The first company keeps working between phases.
- `ledger-view` (`attention::find` moved out of the viewer) moves to P5, where its first user (`books.start`'s needs_attention, `next_steps`) lands; P1 has no server-side consumer, and moving 700 lines of the viewer for nothing would be risk without use. `attention_server_equals_gui` goes with it.
- The "due to" heuristic keeps reading account names: `financing_accounts` also holds bank loans, which aren't "paid personally".
- Approval rules are checked by the engine (`Ledger::approvals_missing`) and enforced by the store: `Store::post_entry` refuses a covered draft until approved, and the operations that post at once (invoices, bills, payments, payroll, remittances, disposals) are refused when a rule covers the entry they would post (`approval_gate`: draft it instead). Reversals, closes, depreciation and amortization are exempt. An approval records the draft's fingerprint (date, description, lines, classes); any change voids it. A rule names accounts that take postings, and an account a rule names can't be renumbered until the rule is revised.
- `books.import_settings` takes only the file the server reads, whole (compared key by key with the server's own reading), never turns approval rules on, and on books that keep their own settings changes nothing (review round 1: an agent-transcribed text could drop the EIN).
- `books.import_settings` took only the file the server read, whole (compared key by key with the server's own reading), never turned approval rules on, and on books that keep their own settings changed nothing (review round 1: an agent-transcribed text could drop the EIN). D-44 removed it.
- `books.start` v1 leaves `book.id`, `book.url`, `head.last_handoff`, the `agent` input and `alwaysLoad` for P5 (they need the served book identity and tool search).
- Posting templates take slots (`{expense}`): an account each draft names, so "an expense a member paid: Dr <expense> / Cr 2100" is one template.
- A vault key file is never a books setting: `--vault-key-file` on the server and the viewer, and an import is refused unless the key in force stays the same.
- A vault key file is never a books setting: `--vault-key-file` on the server and the viewer (both gone with D-44: a book keeps its own key), and an adoption is refused unless the key it names is the one given.
- The connect text's lead names `books.start` first, then the handbook.
### P2: One executable, open while held, hidden away, and the the first company cutover
@@ -859,7 +858,7 @@ The first company keeps working between phases.
- `wanderbooks mcp`, the agent bridge (D-23, 3.9): stdio to the agent, MCP over `run/mcp.sock` to the local server (or HTTPS with `--url`, P9), the token from the keyring. It opens the books if they are closed and holds a lease for the session. Across a server restart (an update) it reconnects and re-initializes upstream without the agent noticing, sending `notifications/tools/list_changed` only if the tool list changed.
- CLI subcommands and modes: `setup`, `serve`, `close`, `mcp`, `host`, `helper`, `rollback`, `books create|adopt|list|export-legacy`, `connect`, `token`, `gui`, `doctor`, `verify-archive`, `fetch`, `update`. `books export-legacy <id>` (rollback only) writes a P1-format data dir outside the data root, verifies its head equals the server's, and stops the server's writes to that book.
- **Scaffold deleted:** `JUSTFILE`, `AGENTS_MD`, `CLAUDE_SETTINGS`, `CODEX_HOOKS`, `GITIGNORE` (`ledger-cli/src/main.rs:43-521`); `install.sh` rewritten, with no sudo.
- The test harness drives HTTP (`mcp_stdio.rs` keeps the bridge).
- The test harness drives HTTP (`mcp_journeys.rs` keeps the bridge).
- **Two-user CI deployment:** a container that runs the server as a dedicated user and the agent as another, in the configuration P9 turns into the self-host recipe. It is the target of `agent_uid_cannot_read_tenant`.
- The first company cutover, preceded by the two open cutover questions (10.3).
@@ -1647,7 +1646,7 @@ Until vendored, they go on the Open list in `docs/spec/README.md`.
- **Correcting a record's source.** A convention's citation (or who decided it, or when) can't be corrected today unless its words change too: `set_convention` with only a new source is refused as "already says exactly that" (found at the the first company P1 cutover, 2026-10-17, correcting an unchecked citation). A source-only revision, journaled with its reason; the same for decisions and open items.
- Tool descriptions ≤ 700 characters; handbook complete; URL-mode elicitation where the client declares it.
- `docs/mcp-interface.md` rewrite (24-95, 580-648, 723-747, 909-931).
- **Agent-eval harness** `ledger-mcp/tests/agent_evals/`: scripted, with no LLM in CI. A zero-context transcript that follows only `books.start` and `next_steps` must complete onboarding, a receipt-backed post, a draft awaiting a receipt, a reversal draft with inherited evidence, a reconciliation and a month-end close.
- **Agent-eval harness** `ledger-cli/tests/agent_evals/` (a real books server, D-44): scripted, with no LLM in CI. A zero-context transcript that follows only `books.start` and `next_steps` must complete onboarding, a receipt-backed post, a draft awaiting a receipt, a reversal draft with inherited evidence, a reconciliation and a month-end close.
**Tests:**
- Every mutating result has a head and a non-empty `next_steps`.
@@ -1954,7 +1953,7 @@ The owner decisions of 2026-10-16 (at the top of this document) replace the form
| D-13 | The first company timezone | (a) `company.timezone = America/Denver`. |
| D-15 | OCR for image-only receipts | (a) None in core for now: page images plus agent attestation. |
| D-16 | CPA access to TINs | (a) To start, the owner prepares and sends a single-use human link each time. |
| D-18 | `dev-stdio` | (a) Kept permanently for tests and demo. It refuses inside the data root and is never offered by `connect`. |
| D-18 | `dev-stdio` | (a) Kept permanently for tests and demo. It refuses inside the data root and is never offered by `connect`. **Superseded by D-44** (2026-10-23): `dev-stdio` is gone; tests and demos use a books server on a data root of their own. |
| D-19 | One app for Windows, Mac and Linux (owner, 2026-10-17) | Planned: backup and restore in P2, the app itself in P11. One installer and auto-updater per system. The server, the viewer and the CLI ship and update together; since D-22 they are one executable that runs each as its own process: the agent needs the server while the viewer is closed, and the viewer stays read-only as a separate process. No separate launcher app, which would be a third program and a second writer (the helper of 3.9 is a mode of the one executable, not an app). The viewer is the app's face and gains menu items: "Export for CPA" (read-only; the viewer does it) and "Back up now" (handed to the archive process, 3.9), "Restore…" and "New books…" (they change books, so the viewer hands them to the server), and the CPA package with TINs through the human path (P2 `/h/`, P7 vault). The books live in each system's usual app-data folder (the data root of 3.4): hidden, not locked; a little more protected on macOS. **Backups must restore**, into an existing install or a new one, proven before use and never destructive (3.7). |
| D-20 | Company records (owner, 2026-10-17: "It's just too damn convenient to keep it all together in one place.") | Every company document goes into the books, not only what the CPA needs: formation papers, registrations, plans, notices, drafts, history. Done for the first company 2026-10-17 with the existing categories; the filing scan passes the company's own EIN printed 2-7 (rules.md B-26). P6 adds what makes it pleasant (below). |
| D-21 | How the local server runs (owner, 2026-10-18: "Not sure I want a background service at all. Running in the foreground for now works for me.") | Started by hand in a terminal, not a background service: `wanderbooks serve` opens the books (the server and its archive process) and runs until stopped; agents connect over HTTP while it runs, several at once; Ctrl-C or closing the terminal closes the books cleanly. Nothing is installed to run in the background: no systemd unit file, no LaunchAgent, no socket activation, no login autostart. The viewer works with the server stopped (it reads the data root). **Amended by D-23** (same day): the books also open when the app or an agent starts, and close when the last of them ends. |
@@ -1965,7 +1964,7 @@ The owner decisions of 2026-10-16 (at the top of this document) replace the form
| D-26 | Where releases are hosted (owner, 2026-10-19: "I'm assuming the GH releases page will do the hosting?") | **GitHub Releases** on the public mirror (`swaits/wanderbooks`): each release carries the `.dmg`, the setup `.exe`, the AppImage, the `.deb` and `.rpm`, and the signed release manifest; the self-updaters read the manifest at the fixed `releases/latest/download/` address, so no server of ours is needed. CI builds and code-signs on release tags on GitHub's macOS, Windows and Linux runners and holds no release key (P11). The apt and dnf repositories' signed indexes are served from GitHub Pages on the same repository; the AUR package points at the Release assets. |
| D-27 | The agent dock and books homes (owner, 2026-10-19: bundle the agents "then it'd be a one stop shop … a chat window maybe full span across the bottom"; "Yeah I like the idea of the embedded alacritty launching claude/codex … maybe MVP defaults to claude"; "you'd want like a 'books home' dir somewhere that makes sense so it has a place to work"; "add it to the plan!") | The app docks a terminal (alacritty_terminal; ConPTY on Windows) along the bottom of its window and runs Claude Code (the default) or Codex in it, found or installed through the vendors' installers, never bundled; started by the viewer's helper, in the book's **books home**: a visible folder per book holding only the agent's pointer and configuration for that book, never the data root. Trust in that folder is the person's to give. **Never required** (owner, 2026-10-19: "we wouldn't want to prevent people from using their OWN AI agent. They should never HAVE to use our built in tty+claude session! Ideally they could even use the claude.ai browser version to connect to it. And of course, eventually we'd host the entire MCP (and gui!) remotely ... use any agent they prefer to remote connect into that.. and they'd view the GUI in a browser."): the books stay fully usable from any agent the person runs themselves, locally or (P9) remotely, claude.ai in a browser included; the dock's one extra is pasting the app's suggested instructions straight into its session. A hosted MCP and a browser viewer are the later service (section 11). P11 (the agent dock and books homes). |
| D-28 | Who does what, locally (owner, 2026-10-19: "Just have the agent ask for them and the user replies. The interface is through claude/codex and that's that. Don't fucking stray from it. TIN, W-9 is NOT critical or super secret PII." and "for downloading the CPA package or other assets, I don't mind if that can happen through the GUI app ... the GUI is for READING the data. The claude/codex is for MUTATAING (and reading) the data!!!") | **Two interfaces, no third.** Every change to the books goes through the agent (Claude Code or Codex) over the MCP; the agent asks the human for what it needs, TINs and W-9s included, and seals them in the vault (`books.set_tin`, `books.set_w9`): no human web pages, terminal commands or open ports for that locally. Reading and downloading (the CPA package and other exports, receipts, documents) happen in the viewer, which still never writes the books. This supersedes, for local books, the `/h/` pages of 3.3, 4.7 and P7's TIN entry: I-7's "the agent never holds a full TIN" becomes "a TIN passes through the agent only on its way into the vault; no result shows one". `/h/` pages remain for self-hosted books reached remotely (P9). |
| D-29 | The full-TIN outputs, from both doors (owner, 2026-10-19, asked whether the agent's CPA export should go away once the viewer has one: "Both WITH TINs. Stop acting like TINS MATTER SO MUCH THEY DONT") | **The agent and the viewer both make them, TINs included.** `books.export_cpa_package` (TINs by default), `books.vendor_1099` with `include_tins` and `books.get_w9` keep working for the agent; their files are made by the server and handed over by a command, `wanderbooks download`, like a receipt too large for a call (no path parameter, nothing in the agent's context). The viewer's menu makes the same files into Downloads. Over stdio (the development transport) the file comes back inline. TINs get no handling beyond the vault: no passphrase, no extra prompts, no special cleanup. |
| D-29 | The full-TIN outputs, from both doors (owner, 2026-10-19, asked whether the agent's CPA export should go away once the viewer has one: "Both WITH TINs. Stop acting like TINS MATTER SO MUCH THEY DONT") | **The agent and the viewer both make them, TINs included.** `books.export_cpa_package` (TINs by default), `books.vendor_1099` with `include_tins` and `books.get_w9` keep working for the agent; their files are made by the server and handed over by a command, `wanderbooks download`, like a receipt too large for a call (no path parameter, nothing in the agent's context). The viewer's menu makes the same files into Downloads. (Over stdio, the development transport, the file came back inline until D-44 removed it.) TINs get no handling beyond the vault: no passphrase, no extra prompts, no special cleanup. |
| D-30 | Q-1 answered: where the backups go (owner, 2026-10-19, asked "Where should the books' automatic backups go?" with a private git repo, a folder or other disk, or both: chose "Both") | **Two targets.** The archive pushes to a private git repository the owner names and also copies to a folder the owner names (an external disk or a synced folder); both hold the archive (not encrypted since D-31). The repository's address and the folder are asked for when backups are first set up (slice 7, or at the cutover for the first company), and kept as the book's backup settings. |
| D-31 | Backups are not encrypted (owner, 2026-10-19, asked where to keep the backup's recovery key: "Why encrypt them? How about we let the backup system handle the security???? You're over complicating everything.") | **A backup is the book as it is, in git.** No age encryption, no archive key, no recovery key: the archive is a plain git repository of the book's journal, receipts, vault (as the vault keeps it) and keys, one commit per backup, pushed to the owner's private git repository and to a git repository in the owner's folder (D-30). The destinations are trusted to keep it private. This replaces 3.7's and 3.4's age encryption on push and 3.5's archive key. |
| D-32 | No self-confinement (owner, 2026-10-20, asked how much of the planned Landlock/seccomp sandboxing of the server and the viewer, with the viewer's helper process, to build now that the server runs git for its backups: chose "None") | **No Landlock, no seccomp, no helper process for confinement.** The viewer stays read-only by design and by test (it never writes the books; its menu hands work to the server, through its admin socket as the command line does); agents still reach the books only through the MCP. Isolation is reported everywhere as `same_user (advisory)`. This replaces the self-confinement of P2's scope, 3.9's confined modes and their helper, and the archive-vs-Landlock decision row. |
@@ -1980,6 +1979,7 @@ The owner decisions of 2026-10-16 (at the top of this document) replace the form
| D-41 | The contributed devices' tax basis (P3, defect 13; owner, 2026-10-20) | **Depreciable basis is the lower of fair market value and the member's adjusted basis at conversion** (Treas. Reg. §1.168(i)-4(b); Pub. 946, property changed from personal use): for the four contributed devices, the CPA to confirm at year end. **§704(c) method: traditional**, recorded now and confirmed with the CPA before the first Form 1065 (for members filing jointly every method gives the household the same tax). **The four devices' values on 2026-10-15 are the books' amounts**, citing the entries that recorded them (figures in the company's private change log). **New P3 item: the K-1 worksheet reports tax-basis capital** (Form 1065 instructions, Item L, since 2020), the contributed property at its §723 carryover basis, beside the book figures. |
| D-42 | A member paying for the company, on the cash flow statement (P3, defect 1; owner, 2026-10-20, revising D-35's first point after the review found ASC 230 silent on a payment a third party makes on the entity's behalf) | **Constructive receipt and disbursement:** when a member (or a lender) pays a company expense or purchase directly, the statement shows a financing inflow (borrowed from the member) and the operating or investing outflow, on that date; paying the member back is a financing outflow. Source: ASC 230 does not address it; practice fills the gap by substance (Deloitte DART Roadmap 7.2 with AICPA TQA 1300.16 and the SEC staff's view; PwC, BDO and EY guides alike), the way a CPA would present it; KPMG accepts noncash only for equipment a lender pays for. Property a member contributes that it already owned stays noncash (ASC 230-10-50-4: no cash moved). Total cash is the same either way; the first company's figures are in its private P3 change log. |
| D-43 | Invoices and bills take a project, line by line (owner, 2026-10-22) | **Each line of an invoice or bill takes one project or none.** A document that spans N projects is N lines, each with its own amount; no percentage split, so every figure is exact. A cost truly shared among projects names none on the document and reaches projects only as allocation entries of their own under the company's monthly allocation convention, when the books have one (otherwise it stays untagged and the agent asks the human). Reasoning: job costing charges a job its traceable (direct) costs whole and reaches shared costs only by allocation on a stated basis. ASC 340-40-25-7 counts as directly related both the costs traceable to a contract (25-7(a), (b), (d), (e)) and allocations of costs that relate directly to it (25-7(c)); 25-8 expenses general and administrative costs unless chargeable to the customer. The books carry a traceable cost whole on the document's line and an allocated cost as entries under the allocation convention on its basis; neither is a percentage on the document. Behind its own journal feature, `document_line_projects`, so books that declared `projects` wait for the owner's next upgrade (rules.md B-37 item 8, B-27). |
| D-44 | One way to serve books: the stdio server is gone (owner, 2026-10-23: "We should probably just delete the stdio path altogether. THere's no point in that. And it sitting there unused will just lead to bitrot. Nuke it?") | **Books are served only by the books server** (`wanderbooks server`: every book of the data root on its local sockets, `/b/{book}/mcp`, uploads and downloads), reached by agents through the bridge (`wanderbooks mcp --book ID`: stdio to the agent, the server's socket upstream), which stays. Removed: `wanderbooks dev-stdio`, the `ledger-mcp` binary and `ledger_mcp::run`'s stdio and `--http` serving of a bare data dir, with everything only that mode had: files the books make coming back inline (`content_base64` on a handed file; every one is now a download), books with no access log and no backups (`books.access_log`, `books.backup` and `books.back_up_now` are always offered, by scope), a settings file read and reloaded (`legacy_files`; old books bring theirs in by `books adopt`), the vault key named by a settings file or `--vault-key-file`, and the key-inside-the-books and books-kept-in-git checks of the books dir (a book in the data root keeps its own key). `books.settings` loses `source`, `books_toml`, `books_toml_exists`, `config_json_present`, `reload_error` and `loads`, and `books.start` its `settings_source` (always the books now). **Owner decisions, 2026-10-23 (asked with the question tool):** (a) `books.import_settings` is removed entirely (the tool, its types, handbook, docs and tests; 118 tools): old books still come in whole through `wanderbooks books adopt`, which reads their settings file once, and ledger-config keeps its settings-file parsing only as far as adoption uses it (`load` and the path-reading `effective_receipt_policy` are gone); `books.chart_template` gives its settings as `books.set_setting` changes. (b) The viewer's loose-books-folder mode is removed (`wanderbooks gui --data-dir DIR --books-toml FILE --vault-key-file FILE`, and following a settings file and `config.json`): the viewer opens only books of the data root (`wanderbooks gui ID`), reads their settings from the journal and their vault from their own `vault/` and `keys/`; tests open throwaway books through the same `Source` and loader the live viewer uses. (c) `wanderbooks dev-stdio` is deleted with no stub: it is an unknown command like any other word. An unknown tool is now a protocol error, as mcp-tools.md asks. Inline file input (`content_base64` with its `sha256`, at most 256 KiB) is unchanged: a separate decision. Every test that drove the removed mode drives a real books server on a data root of its own, reached as an agent reaches it (ledger-cli `tests/common`), and checks a handed file by its download. Supersedes D-18. |
| D-45 | Receivables and payables, first class (P4b; owner, 2026-10-23: "we kinda need stronger invoice support. Like make it a first class citizen") | **A document's figures come from its tagged ledger lines that still stand**: every line on 1200 or 2000 carries its document; paid, outstanding and status are derived, so a reversal, void or write-off is right by construction and the control accounts always equal their documents (B-23). **Hand entries to 1200 and 2000 are refused.** Invoices: numbered `INV-0001` running, previewed then confirmed, terms and due dates, the issued PDF filed at issue, any payment methods printed (ACH, wire, links, QR codes, check, P2P handles, crypto, other). Bills: the vendor's number, terms, every line on its own account, paid on terms with early-payment discounts by the gross method. **The books never send, receive or charge** (as for payroll and taxes): the owner or a delegate (an agent) does, and the books record the send (who, channel, recipients, reference) and the payment (processor, reference, fee: gross to the receivable, fee to expense, net deposited). Bad debts by direct write-off (IRC §166). The slices and the owner's answers are in P4b. |
### 10.2 Deferred