plan: the local server runs in the foreground, started by hand (D-21)
check / check (push) Canceled after 0s

The owner, 2026-10-18: no background service for now. wanderbooks serve
runs in a terminal (server plus archive process) until stopped; nothing
is installed to run in the background. P2's scope, cutover, tests,
limitations, user flows and P11 follow.

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-18 00:06:56 -06:00
1 parent b0b92235cc
commit 2aaba56b38
2 files changed
+38 -34

No files matched your search

+2 -2
View File
@@ -55,8 +55,8 @@ the MCP, no TINs), privately, by decision, never in this public repo
(plan 5.6). P1+ must reproduce them except where the P3 change log
adjudicates a difference (cash flow, defect 1).
**Then:** P2 (serve locally,
cutover; ask the owner the two open plain-English cutover questions
**Then:** P2 (serve locally, started by hand in a terminal with no
background service, D-21 (owner, 2026-10-18); cutover; ask the owner the two open plain-English cutover questions
in plan section 10.3 first; Q-2 is answered, D-20), P3-P11 as planned (P11, added 2026-10-17: one app for Windows, Mac and
Linux; P2 gained backup restore and the viewer's menu).
+36 -32
View File
@@ -4,7 +4,7 @@
Stephen Waits made these decisions on 2026-10-16. Where anything below conflicts with them, they win, and the plan has been revised to match.
1. **The books live hidden away, as you.** Your computer gets no separate system account, no sudo and no containers. The books server runs as you, as a background service that starts when it is needed. It keeps the books in a private folder that only your account can open, outside every project folder. Nothing the agent is told or shown ever names that folder. This means an agent cannot stumble into the books. An agent that went looking on purpose could still find them, and the books say so plainly ("same user, advisory"). Anyone who wants a hard wall runs the same server on another machine they control, such as a home server, a NAS or a rented server.
1. **The books live hidden away, as you.** Your computer gets no separate system account, no sudo and no containers. The books server runs as you, in a terminal you start it in, until you stop it (amended 2026-10-18, D-21: no background service). It keeps the books in a private folder that only your account can open, outside every project folder. Nothing the agent is told or shown ever names that folder. This means an agent cannot stumble into the books. An agent that went looking on purpose could still find them, and the books say so plainly ("same user, advisory"). Anyone who wants a hard wall runs the same server on another machine they control, such as a home server, a NAS or a rented server.
2. **The viewer reads the books directly, read-only, as it does today.** It never changes anything and never goes through the agent's connection. When the agent asks it to show you something, the request reaches the viewer through the books' own private folder, not through the agent's tools.
3. **No extra pop-ups.** Posting, closing and reconciling add no new permission prompts. The agent still shows you each entry before posting it, and a hidden check makes sure that what gets posted is exactly the preview the agent was given to show you.
4. **Your own server now, a paid hosted service later.** This plan builds the recipe for running the books on another machine you control, and claude.ai on the web can reach that machine. A hosted service for many customers is described separately at the end (section 11). This plan does not build it.
@@ -38,8 +38,8 @@ Paths are relative to `/home/swaits/Code/wanderbooks/crates/` unless they are ab
**Today.** Your books are a folder inside your project directory. That folder holds the journal, the receipts, the settings, a list of helper commands and the agent's instructions. The agent opens the books by starting a helper program itself, so it can also read every file in the folder directly. That is how an agent ended up reading the journal file instead of asking the books.
**After this plan.** The books move into a private folder that belongs to a small books server. The server runs quietly in the background as you. Agents are never told where the folder is, and they only talk to the server. In practice:
- You install once, with at most three commands and no administrator password. After that you just type `claude` in any folder. There is no project folder, there are no helper commands, and there is nothing to do at the start of a session.
**After this plan.** The books move into a private folder that belongs to a small books server. The server runs as you, in a terminal you start it in (`wanderbooks serve`), whenever you want the books open. Agents are never told where the folder is, and they only talk to the server. In practice:
- You install once, with at most three commands and no administrator password. After that you just type `claude` in any folder. There is no project folder and there are no helper commands; the one thing to do at the start is open the books, if they aren't open already.
- Claude Code and Codex reach the same books the same way. Later, claude.ai on the web does too.
- When an agent connects, the books tell it the rules, your company's conventions, what needs attention and what to do next. Step-by-step procedures also come from the books: setting up, the month-end close, handing off to the CPA, and the audit.
- Receipts reach the agent as readable text and pictures of the pages, not as giant blobs of data. Files go in and out through short-lived links.
@@ -76,7 +76,7 @@ Paths are relative to `/home/swaits/Code/wanderbooks/crates/` unless they are ab
| Question | Proposals | Resolution |
|---|---|---|
| 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 under a user-level service (a systemd `--user` unit on Linux, a LaunchAgent on macOS). 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). |
| 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, in the foreground: the human starts it in a terminal (`wanderbooks serve`) and it runs until stopped. Nothing is installed to run in the background (owner decision D-21, 2026-10-18). 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`). |
@@ -92,7 +92,7 @@ Paths are relative to `/home/swaits/Code/wanderbooks/crates/` unless they are ab
| Replica keyring route | A: `/replica/keyring` serves wrapped keyslots to `books:view`. | **Removed.** No route ever serves wrapped keyslots. Local GUI TIN click: the GUI reads the wrapped keyslot from the data root (read-only), unwraps in its own process after the human types the passphrase, copies the value to the clipboard only, and zeroizes. A remote replica (P9) carries no keyslot material; remote TINs are seen only on `/h/` pages. |
| TIN output scanner | A: match decrypted vault records. | **Pattern-based** (extend `has_full_tin`/`redact_full_tins` behind S:870 to every result, with the vault's last-four set as a hint). Fails closed. Documented as a pattern check. |
| Extraction | In-process (A/B). | **Sandboxed subprocess** `ledger-extract` (C): rlimits, seccomp, no network, Landlock read-only on the input blob. |
| Archive vs Landlock | A: server shells out to git, which contradicts its own Landlock rules. | **Separate user-level unit `wanderbooks-archive@<book>`.** It has read-only Landlock on `books/<id>/{journal.jsonl,blobs}`. Locally, Landlock ABI ≥ 4 limits its TCP connect to the target's port (443 or 22); it cannot filter by host, and `IPAddressAllow`/`IPAddressDeny` are not available to unprivileged `systemd --user` units, so host-level egress filtering exists only under a system unit on self-host (`IPAddressAllow`). The server: Landlock ABI ≥ 4 denies all TCP bind and connect (the listening socket arrives through socket activation on 127.0.0.1), so the local server has no egress. On self-host, a system unit adds `IPAddressDeny=any` (loopback allowed); if the built-in authorization server fetches CIMD documents (D-7 a), connect is allowed to port 443 only and `IPAddressDeny` narrows to private and link-local ranges (loopback still allowed), matching the CIMD SSRF guard. This confines the server only; it does not restrict the agent. |
| Archive vs Landlock | A: server shells out to git, which contradicts its own Landlock rules. | **A separate archive process, `ledger-mcp archive`,** that `wanderbooks serve` starts beside the server and stops with it (D-21); each of the two confines itself on its own. The archive process has read-only Landlock on `books/<id>/{journal.jsonl,blobs}`. Locally, Landlock ABI ≥ 4 limits its TCP connect to the target's port (443 or 22); it cannot filter by host, and an unprivileged process has no host-level egress filter, so that exists only under a system unit on self-host (`IPAddressAllow`). The server binds its listening socket on 127.0.0.1 first, then Landlock ABI ≥ 4 denies all further TCP bind and connect, so the local server has no egress. On self-host, a system unit adds `IPAddressDeny=any` (loopback allowed); if the built-in authorization server fetches CIMD documents (D-7 a), connect is allowed to port 443 only and `IPAddressDeny` narrows to private and link-local ranges (loopback still allowed), matching the CIMD SSRF guard. This confines the server only; it does not restrict the agent. |
| Attention engine location | A: `ledger-store/attention.rs`. B: wasm-clean `ledger-view`. | **New crate `ledger-view`** (depends only on `ledger-core`), holding `attention::find`, `next_steps`, the evidence gate view, and a `BooksView` trait implemented by Store and Snapshot. A pure-Rust evidence projection (dropping SQLite from the viewer) is deferred to the hosted service's web viewer (section 11). |
| Inline bytes | A: 256 KiB. B: 1 MiB without sha. C: 256 KiB plus sha. | **256 KiB with a mandatory matching sha256.** A mismatch is refused with guard `base64_corrupt`, whose remedy is `books.begin_upload`. `/mcp` body cap is 1 MiB. |
| Same-user mode | C: an announced tier. B: refuse to start without an acknowledgement flag. | **The local default, with no gate flag** (owner decision 2026-10-16). `books.start`, Needs attention, `wanderbooks doctor` and the docs report `isolation: "same_user (advisory)"`. |
@@ -113,7 +113,7 @@ Paths are relative to `/home/swaits/Code/wanderbooks/crates/` unless they are ab
| helper) | | OAuth or token | | passphrase |
v v v v v |
+---------------------------------------------------------------------------------+ |
| ledger-mcp serve local: user-level service, runs as the human | |
| ledger-mcp serve local: started in a terminal, as the human | |
| self-host (P9): same binary on another machine | |
| confines itself where available (Landlock, seccomp, no outbound TCP) | |
| /b/{book}/mcp /b/{book}/u/{id} PUT /b/{book}/d/{id} GET | |
@@ -123,7 +123,7 @@ Paths are relative to `/home/swaits/Code/wanderbooks/crates/` unless they are ab
v | | direct
<data root>/books/<id>/ journal.jsonl blobs/ ops/show.jsonl ... <-----------------------------+
^ |
| sandboxed subprocess (rlimits, seccomp, no net) +-- wanderbooks-archive@<id> --> git / s3 / dir / pull
| sandboxed subprocess (rlimits, seccomp, no net) +-- ledger-mcp archive --------> git / s3 / dir / pull
ledger-extract (pdf / xlsx / eml / page raster)
```
@@ -131,9 +131,9 @@ The data root is `$XDG_DATA_HOME/wanderbooks` on Linux (default `~/.local/share/
| Process | Binary | Runs as | Reads tenant disk |
|---|---|---|---|
| Books server (local) | `ledger-mcp serve --root <root>`, started by a systemd `--user` socket unit on 127.0.0.1:7411 (Linux) or a LaunchAgent (macOS) | the human | yes, sole writer, holds the flock (`ledger-store/src/store.rs:74-89`) |
| Books server (local) | `ledger-mcp serve --root <root>` on 127.0.0.1:7411, started in the foreground by `wanderbooks serve` in a terminal and running until stopped (D-21; `wanderbooks serve` itself reads nothing: it starts the server and the archive process and stops both) | the human | yes, sole writer, holds the flock (`ledger-store/src/store.rs:74-89`) |
| Books server (self-host, P9) | the same binary under a system unit on another machine | a dedicated account on that machine | yes, sole writer |
| Archive pusher | `ledger-mcp archive --book <id>` | the server's account, separate unit | read-only journal and blobs |
| Archive pusher | `ledger-mcp archive --book <id>` | the server's account, a separate process started beside the server | read-only journal and blobs |
| Extractor | `ledger-extract` (subprocess) | inherits, sandboxed | the single input blob, via fd |
| Viewer | `wanderbooks gui <book>` runs `ledger-gui --root <root> --book <id>` (local, direct, the default); `ledger-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). |
@@ -224,7 +224,7 @@ The server, the CLI and the GUI resolve the data root with one shared function (
| Vault private key | wrapped per keyslot; plaintext only inside one `/h/` request (server) or one GUI unlock (in the GUI process, local), then zeroized | the human holding the passphrase, passkey PRF, recovery key, or an optional CPA keyslot |
| `preview.key` | `keys/` | server |
| Agent tokens | sha256 in `auth/`; plaintext in the user's keyring, which any process running as that user can read (advisory locally) | the agent's user |
| Archive credentials | OS keyring entry read by the archive unit (local); systemd-creds for the archive unit (self-host) | archive pusher |
| Archive credentials | OS keyring entry read by the archive process (local); systemd-creds for the archive unit (self-host) | archive pusher |
| At-rest encryption | full-disk encryption (desktop); encrypted volume (self-host recipe) | protects against stolen disks |
### 3.6 GUI
@@ -248,7 +248,7 @@ The server, the CLI and the GUI resolve the data root with one shared function (
### 3.7 Backup and tamper evidence
The `wanderbooks-archive@<book>` unit (a user-level unit locally, a system unit on self-host) is triggered by a head-advance signal (debounced 30 s), by `period.closed`, and by a timer. Each run:
The archive process (`ledger-mcp archive`: locally started and stopped with the server by `wanderbooks serve`, D-21; a system unit on self-host) runs on a head-advance signal (debounced 30 s), on `period.closed`, on a timer while it runs, and once when it starts, for anything left unpushed. The books change only while the server runs, so a backup is never missed while they are closed. Each run:
1. Verifies the chain.
2. Commits the new journal lines and blobs to `archive.git` (one commit per push; the message is the seq range plus the head).
3. Pushes to the targets: git remote, S3, dir, or `pull` (the customer runs `wanderbooks fetch`).
@@ -264,14 +264,14 @@ Email or webhook head delivery and a witness log belong to the hosted service (s
1. **Proven before use.** The server replays the whole chain, re-hashes every blob and checks that the head equals the one the backup records. Any mismatch refuses the restore with a plain reason; nothing is half-restored.
2. **Never destructive.** On an existing install a restore arrives as its own book beside the current ones. Replacing a book moves the old one into `legacy/<id>-<date>/`, kept, never deleted, and only on the owner's explicit choice.
3. **The vault comes back only with its key.** The backup carries the vault sealed; the passphrase or the printed recovery key (P7) unlocks it on any machine. Until P7 the v1 key is a separate file: a backup counts as complete only when that key is saved too, and Needs attention says so when it isn't.
4. **Restore drills.** On a schedule (default weekly) and after each close, the archive unit restores the latest backup into a scratch folder, verifies it as in step 1, compares its reports with the live book's as of the backup head, and throws it away. A failed drill is a Needs-attention item; `books.backup status` reports the last drill.
4. **Restore drills.** On a schedule (default weekly, or at the next start if the books were closed when it fell due) and after each close, the archive process restores the latest backup into a scratch folder, verifies it as in step 1, compares its reports with the live book's as of the backup head, and throws it away. A failed drill is a Needs-attention item; `books.backup status` reports the last drill.
### 3.8 Local vs self-host
| | Local (T1, the default) | Self-host (T2, P9) |
|---|---|---|
| Where | the human's computer | another machine the owner controls: home server, NAS or VPS |
| Processes | one `serve` under a user-level service, plus a user-level archive unit | the same binaries under system units, plus TLS (Caddy) and, optionally, `wanderbooks tunnel` |
| Processes | `wanderbooks serve` in a terminal: the server and its archive process, until stopped (D-21) | the same binaries under system units, plus TLS (Caddy) and, optionally, `wanderbooks tunnel` |
| Runs as | the human | a dedicated account; agents have no account on that machine |
| Isolation | `same_user (advisory)`: hidden, not locked | `service_user (enforced)`: machine and user-ID boundary |
| Auth | static bearer in the keyring; `/h/` uses the owner passphrase (or a passkey) | static bearer over TLS for header-capable clients; OAuth 2.1 for claude.ai and ChatGPT (D-7); `/h/` uses a passkey or the passphrase |
@@ -554,7 +554,7 @@ The `WANDERBOOKS_TOML` template (`ledger-cli/src/main.rs:237-347`) becomes the e
| P0 | Install new binaries (still stdio). Capture **golden JSON** of TB, P&L, BS, CF, members' equity, GL, journal, vendor_1099, parties, assets, schedules and digest, as of 2026-10-11, every month-end, FY-to-date and today, through the old binary over MCP. By decision (2026-10-16) the first company's goldens are kept privately in its own books dir, not in this public repo: `export_cpa_package` zips plus a canonical JSON form made by that dir's `goldens/canon.py` (which normalizes the text each export varies, such as its timestamp), with the comparison protocol in its README. | Goldens stored |
| P1 | The agent reads `/home/swaits/Documents/Wanderland-Books/wanderbooks.toml` (and `books-data/config.json` only by asking Stephen to state its values, since it sits under `books-data/`) and calls `import_settings`. Expected diff:<br>• the 15 `[company]` keys; EIN as identity<br>• members 50/50 → 3000/3001 with `files_jointly_with`<br>• `accounting_basis=accrual`, `policy.evidence.threshold=0.00` (Stephen, 2026-10-12)<br>• `cash_accounts=["1000"]`, `financing_accounts=["2100"]`; the citation is corrected to ASC 230-10-45-14(b)<br>• `parties=require` (2026-10-15), `timezone=America/Denver` (D-13, decided)<br>• `policy.approval_rules` stays `off`<br><br>Conventions are proposed in one batch preview from `AGENTS.md:74-227`:<br>• 9 posting templates: joint contribution as two entries; expense from joint personal funds → 2100; expense from Bluevine; household reimbursement (`Dr 2100 / Cr 1000`) with a `note` that Stephen gives the go-ahead before money moves, recorded with its source and not an enforced gate; 50/50 distribution; revenue 4000; card 2050 expense and payment; cash entries dated by bank posting date<br>• policies: numbering, one file per receipt (PDF preferred), email as PDF, capitalization (`policy.capitalization` plus decisions 2026-10-11 and 2026-10-16)<br>• `close_step`: "ask Stephen whether a member paid anything personally"<br>• `calendar`: Utah annual report around Oct 4, renewal fee to taxes & licenses<br>• memory-only rules become conventions: post when ready; decisions to Stephen go one question at a time; TINs only in the vault<br><br>Decisions: one file per receipt (10-11), threshold (10-12), cutover with Sheet frozen as of 10-11 and reference attachment 37 (10-14), renumber 6100→6050 and card at 2050 (10-14), TINs in vault (10-15); light governance, meaning no consent paperwork, spending limits, signed memos or amendments for the two-member LLC, with decisions recorded with their source (10-16); local isolation "you, hidden away", the viewer reading the disk directly, no added permission prompts, and self-host now with hosting later (10-16).<br><br>Open items:<br>• IRS name control "WALN" discrepancy; letter = attachment 70, mailing pending<br>• deferred revenue to be raised with the CPA before the first subscription<br><br>Per-entry notes: entries 4 and 5 dated Oct 7 to match the Sheet. | Every golden report and export is byte-identical before and after the import. GUI shows Wanderland correctly with `wanderbooks.toml` renamed away. No `approval_rule` convention is created, and every daily-flow post still succeeds without an approval |
| P2 (pre-cutover) | Run `verify-sheet` and `drive-manifest verify` a final time and file their outputs as `workpaper` documents. Recapture goldens. | Both pass |
| P2 cutover (about 10 min) | 1. Ask Stephen the two open cutover questions (section 10.3: Q-1 backups, Q-3 old copies; Q-2 is answered), one at a time, in plain English.<br>2. Quit the agent sessions and the GUI.<br>3. `wanderbooks service install`, with no sudo: it writes the systemd `--user` socket and service units, creates the data root (0700), and asks for the owner passphrase.<br>4. `wanderbooks books adopt /home/swaits/Documents/Wanderland-Books/books-data --id wanderland` (archive target from Q-1; old copies moved per Q-3).<br>5. `wanderbooks connect claude-code --book wanderland` (removes the old stdio registration made by `just mcp-add`, adds the HTTP registration with `headersHelper`).<br>6. `wanderbooks connect codex --book wanderland`.<br>7. `wanderbooks gui wanderland` (offers login autostart).<br>8. Remove `justfile`, `.claude/settings.json` hook, `.codex/hooks.json`, `scripts/` (archived under the data root's `legacy/`); `AGENTS.md`/`CLAUDE.md` become a 3-line pointer ("connect the wanderbooks MCP; call books.start").<br>9. The `company/` records are already filed (Q-2, D-20); the local copies stay until P6. | `books.verify` over MCP equals H. Goldens byte-equal. `books.start` works from Claude Code started in `~` and in `/tmp/empty`, and reports `isolation: "same_user (advisory)"`. No connect text, tool text, handbook section or result in a recorded cutover session names the data root. No step asked for sudo. If Q-3 is "move": the project folder holds no journal, blobs, vault or git history containing them. Viewer updates within 1 s. **Rollback (nothing posted on the server):** stop the user service, move `legacy/wanderland-<date>/` back to the project folder (or use the untouched source if Q-3 was "no"), and re-register the P1 binary's stdio server as before cutover. **Rollback (entries posted):** `wanderbooks books export-legacy wanderland` writes a data dir with the same head check, then the same re-registration |
| P2 cutover (about 10 min) | 1. Ask Stephen the two open cutover questions (section 10.3: Q-1 backups, Q-3 old copies; Q-2 is answered), one at a time, in plain English.<br>2. Quit the agent sessions and the GUI.<br>3. `wanderbooks setup`, with no sudo: it creates the data root (0700) and asks for the owner passphrase. Nothing is installed to run in the background (D-21).<br>4. In a terminal of its own, `wanderbooks serve`, left open: the books are open while it runs.<br>5. `wanderbooks books adopt /home/swaits/Documents/Wanderland-Books/books-data --id wanderland` (archive target from Q-1; old copies moved per Q-3).<br>6. `wanderbooks connect claude-code --book wanderland` (removes the old stdio registration made by `just mcp-add`, adds the HTTP registration with `headersHelper`).<br>7. `wanderbooks connect codex --book wanderland`.<br>8. `wanderbooks gui wanderland`.<br>9. Remove `justfile`, `.claude/settings.json` hook, `.codex/hooks.json`, `scripts/` (archived under the data root's `legacy/`); `AGENTS.md`/`CLAUDE.md` become a 3-line pointer ("connect the wanderbooks MCP; call books.start").<br>10. The `company/` records are already filed (Q-2, D-20); the local copies stay until P6. | `books.verify` over MCP equals H. Goldens byte-equal. `books.start` works from Claude Code started in `~` and in `/tmp/empty`, and reports `isolation: "same_user (advisory)"`. No connect text, tool text, handbook section or result in a recorded cutover session names the data root. No step asked for sudo. If Q-3 is "move": the project folder holds no journal, blobs, vault or git history containing them. Viewer updates within 1 s. Ctrl-C in the `serve` terminal closes the books cleanly (`books.verify` ok with the same head after the next `serve`), and a Claude Code session opened while it was stopped reaches the books after `serve` and `/mcp` reconnect. **Rollback (nothing posted on the server):** stop `wanderbooks serve` (Ctrl-C), move `legacy/wanderland-<date>/` back to the project folder (or use the untouched source if Q-3 was "no"), and re-register the P1 binary's stdio server as before cutover. **Rollback (entries posted):** `wanderbooks books export-legacy wanderland` writes a data dir with the same head check, then the same re-registration |
| P3 | Defect fixes change reports (cash flow, 1099 sections, asset fields, digest). Each difference is hand-adjudicated in the P3 change log. 1099 classification and 704(c) fields are shown to the CPA. | Change log signed off |
| P4 | `list_drafts`. Leftover drafts are discarded or set to `awaiting_evidence`. | Zero unexplained drafts |
| P6 | • `consolidate_duplicates` resolves the 11 redundant records (preview first).<br>• Extraction is backfilled for all 66 attachments.<br>• Receipt numbers 1000–1029 are annotated from the "Receipt ID" in each description. Anomalies become annotations: 1009 unissued; 1011/1020 byte-identical pairs (merged); 43/45/47 have no ID. The next number is derived as 1030.<br>• Instruments: Bluevine, joint Amex, member cards, Chase Ink 2050.<br>• `verify-sheet.py`'s ALIASES/RECLASSIFIED/RENUMBERED tables (`scripts/verify-sheet.py:52, 69-108`) become a `mapping` convention, then `compare_reference` runs against attachment 37 as of 2026-10-11.<br>• `drive-manifest.json` text goes to `record_inventory`; the Drive connector listing goes to `check_inventory`. | `compare_reference` gives 0 differences. Inventory has 0 gaps. Drive deletion becomes possible (still Stephen's call) |
@@ -577,7 +577,7 @@ This section covers the local (T1) and self-host (T2) deployments. Threats speci
| History rewrite or fork (any deployment) | alter or fork the books unnoticed | head in every result, quiet-period handoffs, `verify-archive --expect-head` against an independent clone |
| Stolen disk or backup | read books | full-disk encryption on the desktop, an encrypted volume in the self-host recipe; archive encrypted with age to a customer recipient; the vault is always ciphertext |
| Untrusted files (PDF/XLSX/EML) | parser exploit inside the data-owning process | the `ledger-extract` subprocess sandbox; fuzz targets |
| A compromised server process | reach beyond the data root into the human's account | the server confines itself where available: Landlock (filesystem limited to the data root; on ABI ≥ 4, all TCP bind and connect denied, since the listening socket arrives through socket activation), seccomp, rlimits; extraction runs in a further sandbox. This limits the server; it does not restrict the agent |
| A compromised server process | reach beyond the data root into the human's account | the server confines itself where available: Landlock (filesystem limited to the data root; on ABI ≥ 4, all TCP bind and connect denied once its own listening socket on 127.0.0.1 is bound), seccomp, rlimits; extraction runs in a further sandbox. This limits the server; it does not restrict the agent |
| Other processes running as the human, on the desktop | read the books; drive the viewer | no mechanical control locally (same user). A same-user process can alter anything the GUI reads (journal, blobs, keyslots). Journal tampering is detected by the hash chain and head handoff, not prevented; a vault public-key swap fails `books.verify`; a forged show request can only navigate. The GUI never writes. Stated as the advisory residual in `books.start`, `doctor` and the docs; the self-host deployment is the enforced option |
**Immediate hole (P0).** `import_local_file` (S:4511) does an unrestricted `std::fs::read`, so it can file `~/.config/wanderbooks/vault.key` (`ledger-vault/src/lib.rs:216-222`), and `get_receipt` then returns the key as base64. P0 changes this:
@@ -608,7 +608,7 @@ Each record is fsynced, and a failed write fails the request. Readable through `
- `no_mcp_output_contains_full_tin`: canary TIN and key; session fuzz across responses, errors, server logs and access-log records
- `no_result_names_a_path`: the same session fuzz with the data root as the canary, across connect text, tool text, handbook, prompts, results and errors
- `access_log_one_record_per_call`, refusals included
- `isolation_label_is_advisory`: a user-level install reports `same_user (advisory)` in `books.start` and `doctor`
- `isolation_label_is_advisory`: a local install reports `same_user (advisory)` in `books.start` and `doctor`
- `agent_uid_cannot_read_tenant`, run against the two-user self-host deployment in CI (the desktop default is advisory and is not claimed to pass it); `no_tool_touches_outside_tenant` (Landlock harness)
New fuzz targets: `fuzz_http_mcp_request`, `fuzz_upload_put`, `fuzz_settings_replay`, `fuzz_extract_pdf`, `fuzz_extract_xlsx`, `fuzz_eml`, and `fuzz_replica_stream` (P9). New mutation recipes and scheduled CI for `ledger-vault`, `ledger-cli`, `ledger-view`, and `ledger-mcp/http`; none of these have a recipe today.
@@ -624,19 +624,19 @@ What you will see and do. The technical details are in sections 3 and 4.
### 7.1 Setting up on your computer
1. You run one install command. It installs the books programs (no developer tools needed), and then it:
- sets up the books server to run in the background as you, starting by itself whenever an agent connects. It does not ask for an administrator password;
- sets up the books server. It installs nothing that runs in the background, and it does not ask for an administrator password;
- asks you to choose a passphrase. You need it to type or see someone's Social Security or tax ID number, to download anything that contains them (such as the CPA package with them), and whenever the books open a private page for you, such as the page where you drop a big file;
- creates your company's books;
- connects Claude Code to the books, and Codex too if you have it;
- asks once whether the viewer should open when you log in.
2. You type `claude` in any folder and say "set up my books". The agent sees that the books are new and walks you through setup: company facts, accounts, members, opening balances, and a check that everything balances. Every answer is recorded in the books.
- shows the one command that opens the books: `wanderbooks serve`, run in a terminal and left open while you work. Ctrl-C, or closing that terminal, closes them.
2. With the books open, you type `claude` in any folder and say "set up my books". The agent sees that the books are new and walks you through setup: company facts, accounts, members, opening balances, and a check that everything balances. Every answer is recorded in the books.
### 7.2 Day to day
- You say: "The receipt is in Downloads, inv-883.pdf, the Figma annual plan." The agent sends the file to the books, reads it, drafts the entry, and shows you exactly what it will post. You say yes, it posts, and the viewer jumps to the new entry. Posting brings up no extra permission pop-up.
- You say: "Save it as a draft, I'll find the receipt." The draft stays on the to-do list until the receipt arrives. You see it in the viewer's Needs attention, and the agent sees it at the start of every session.
- You say: "Show me Q3 against last year." The viewer opens that report.
- Backups happen on their own. Every change the agent makes comes back with a fingerprint of the books, so any tampering would show.
- Backups happen on their own while the books are open. Every change the agent makes comes back with a fingerprint of the books, so any tampering would show.
**Compared with today:** no helper commands, no project folder, it works from any folder, you answer the same number of questions, and installing needs no administrator password.
@@ -755,24 +755,24 @@ Wanderland keeps working between phases.
- 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.
- The connect text's lead names `books.start` first, then the handbook.
### P2: Serve locally, hidden away, and the Wanderland cutover
### P2: Serve locally (started by hand), hidden away, and the Wanderland cutover
**Scope:**
- `ledger-mcp serve --root` with multiple books at `/b/{book}/mcp`; explicit transport config (3.2); bearer tokens and scopes; rate limits (tower-governor).
- **User-level service, no sudo.** `wanderbooks service install` writes a systemd `--user` socket and service unit on 127.0.0.1:7411 (Linux) or a LaunchAgent (macOS). It creates the data root (3.4) with an explicit `set_permissions(0o700)` and sets the owner passphrase. There is no system account, no container and no root step.
- **Foreground, no service, no sudo (D-21).** `wanderbooks setup` creates the data root (3.4) with an explicit `set_permissions(0o700)` and sets the owner passphrase. `wanderbooks serve` runs in the terminal it was started in: it starts `ledger-mcp serve` on 127.0.0.1:7411 and the archive process, prints which books are open, how agents connect and that an agent session opened earlier must reconnect (`/mcp` in Claude Code), and runs until stopped. Ctrl-C, SIGTERM or closing the terminal (SIGHUP) closes the books cleanly: no new requests, the one in progress finishes, the writer lock is released. A second `serve` on the same root is refused, saying one is already running. Nothing is installed to run in the background: no systemd unit, no LaunchAgent, no socket activation, no login autostart. There is no system account, no container and no root step.
- **Honest isolation label.** `books.start`, Needs attention, `wanderbooks doctor` and the docs report `isolation: "same_user (advisory)"`. There is no `--same-user` flag and no acknowledgement gate.
- **Self-confinement.** Where available: on the server, Landlock (filesystem limited to the data root; on ABI ≥ 4, all TCP bind and connect denied, since the listening socket arrives through socket activation on 127.0.0.1) and seccomp; on the archive unit, read-only Landlock on the journal and blobs, TCP connect limited to the target's port (443 or 22) on ABI ≥ 4, and seccomp. A user-level unit cannot filter egress by host. If they cannot be applied, `doctor` reports it; it is not fatal. The docs describe them as protecting the human's account from a compromised server, not the books from the agent.
- **Self-confinement.** Where available: on the server, Landlock (filesystem limited to the data root; on ABI ≥ 4, all TCP bind and connect denied once it has bound its own listening socket on 127.0.0.1) and seccomp; on the archive process, read-only Landlock on the journal and blobs, TCP connect limited to the target's port (443 or 22) on ABI ≥ 4, and seccomp. An unprivileged process cannot filter egress by host. If they cannot be applied, `doctor` reports it; it is not fatal. The docs describe them as protecting the human's account from a compromised server, not the books from the agent.
- Access log plus `books.access_log`.
- Owner passphrase for `/h/` (Argon2id).
- **Files transport:** `begin_upload`, the PUT route, the `/h/inbox` drop page plus `books.inbox` and the `process_inbox` prompt, `download_link`.
- **Every server-path parameter deleted:** `import_local_file`, `save_to`, `save_csv_to`, `w9_file_path`, get_w9 `save_to`. W-9 upload moves to `/h/w9/{party}` (sealed with the v1 key until P7). TIN-bearing outputs come only through `request_human_download` → `/h/`, behind the owner passphrase. This closes defect 19.
- `get_receipt view=link|bytes(≤256 KiB+sha)` (defect 3, transport part).
- **Viewer, direct and read-only (3.6):** data-root discovery through `ledger-store::paths::data_root()` or `--root` from `wanderbooks gui <book>`, never a project directory or `wanderbooks.toml`; `books.show` through `ops/show.jsonl` (delete `ledger-store/src/viewer.rs`); presence through the `ops/viewer.lock` shared flock; Save-a-copy and Open-with (defect 3, viewer part). Locally there is no replica, no socket and no peer-credential check.
- Archive unit (user-level) and module; `books.backup`; `ops/heads.jsonl`; `verify-archive`; `fetch`.
- Archive process and module; `books.backup`; `ops/heads.jsonl`; `verify-archive`; `fetch`.
- **Restore (3.7, D-19):** `books restore`, proven before use, never destructive, onto a new or an existing install; restore drills; the vault key's backup reported until P7.
- **Viewer menu (D-19):** "Back up now" and "Export for CPA" (read-only, done by the viewer); "Restore…" and "New books…" handed to the server; nothing in the viewer writes a book.
- `ledger-mcp proxy`.
- CLI subcommands: `service`, `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.
- CLI subcommands: `setup`, `serve`, `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 proxy).
- **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`.
@@ -782,7 +782,9 @@ Wanderland keeps working between phases.
**Tests:**
- `agent_uid_cannot_read_tenant` (the two-user CI deployment; not the desktop default).
- `service_install_needs_no_root` (install, adopt, connect and gui as an unprivileged user in a clean container).
- `setup_needs_no_root` (setup, serve, adopt, connect and gui as an unprivileged user in a clean container; nothing is left installed to run in the background).
- `serve_stops_cleanly` (Ctrl-C, SIGTERM and SIGHUP during a burst of posts: every request either completes or is refused, the head verifies at the next start, the lock is released); `serve_refuses_a_second` (a second `serve` on the same root exits, naming the first); `archive_catches_up_at_start` (a push that failed before a stop is made at the next start).
- `viewer_without_server` (the viewer opens and updates with the server stopped; "Restore…" and "New books…" say to start the books first).
- `isolation_label_is_advisory`.
- `no_path_params` schema lint; `no_result_names_a_path`.
- `no_tool_touches_outside_tenant`.
@@ -1061,8 +1063,8 @@ Run from clients that hold only a URL and a credential: Claude Code started in `
### P11: One app for Windows, Mac and Linux (D-19)
**Scope:**
- One installer per system: the server, the viewer and the CLI, versioned and updated together (the format gate of 5.1 then never meets mismatched parts). The server stays a separate background program: a LaunchAgent on macOS, a systemd `--user` unit on Linux (both from P2), and the per-user equivalent on Windows (a logon task, no admin rights). No step needs admin rights on any system.
- Opening the app opens the viewer and starts the server if it isn't running.
- One installer per system: the server, the viewer and the CLI, versioned and updated together (the format gate of 5.1 then never meets mismatched parts). The server stays a separate program. How it starts is the owner's call when P11 begins (D-21): until then it runs only when started by hand in a terminal; the app could start it while the app is open, or install it as a per-user background program. No step needs admin rights on any system.
- Opening the app opens the viewer (and starts the server, if the owner chooses that under D-21).
- Auto-update per system: download, verify the signature, replace all parts together, restart the server, reopen the viewer. A failed update rolls back to the previous version.
- Data root in each system's usual app-data folder (Application Support, AppData, `~/.local/share`), as in 3.4.
- Signing: Apple Developer ID signing and notarization (the company's membership); a Windows code-signing certificate; signed Linux packages.
@@ -1100,10 +1102,11 @@ Run from clients that hold only a URL and a credential: Claude Code started in `
- The path is never named, the root sits outside every project directory, and every place that reports isolation says "advisory".
- The server's own Landlock and seccomp are never presented as protecting the books from the agent.
- Enforcement is the self-host deployment (P9), tested in CI from P2.
7. **User-level services stop at logout.** A systemd `--user` service stops at logout unless lingering is on, and a LaunchAgent runs per login session.
- The server is socket-activated, so it starts on the first connection, and agents and the viewer only run while the human is logged in anyway.
- The backup timer is persistent, so a run missed while logged out happens at the next login.
- `doctor` offers lingering but never requires it.
7. **The books are open only while `wanderbooks serve` runs** (D-21: started by hand in a terminal; no background service).
- Agents reach the books only then. A session started while it was stopped has no books tools: start `serve`, then reconnect the session (`/mcp` in Claude Code); `serve`, `connect` and `doctor` say so.
- The viewer reads the data root directly, so it works while the server is stopped; "Restore…" and "New books…" say to start the books first.
- Backups run only while it runs. The books change only then, so nothing goes unbacked; the archive process pushes anything left over, and a drill that fell due, when it next starts.
- Closing the terminal stops it like Ctrl-C. A crash or power loss is what the append-only journal already survives.
8. **The OAuth authorization server is security-critical code written in-house** (rmcp has no server-side auth). It is needed only on the self-host remote path.
- Small surface: passkeys only, no password grant.
- Built on audited crates (webauthn-rs, oauth2 types); fuzzed; externally reviewed before the remote path is opened publicly.
@@ -1144,6 +1147,7 @@ The owner decisions of 2026-10-16 (at the top of this document) replace the form
| D-18 | `dev-stdio` | (a) Kept permanently for tests and demo. It refuses inside the data root and is never offered by `connect`. |
| 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 but stay separate programs inside the app: the agent needs the server while the viewer is closed, and the viewer stays read-only by being a separate program. No separate launcher, which would be a third program and a second writer. The viewer is the app's face and gains menu items: "Back up now" and "Export for CPA" (read-only; the viewer does them), "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 Wanderland 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, no LaunchAgent, no socket activation, no login autostart. The viewer works with the server stopped (it reads the data root). Revisited at P11, the app. |
### 10.2 Deferred