plan: one app, open while held, installers and updaters (D-22, D-23, D-24)
check / check (push) Waiting to run

The owner, 2026-10-18: one app to simplify installing and updating.
One wanderbooks executable whose modes run as separate processes; the
books open while the app, an agent session (its stdio bridge), a serve
terminal or a CLI command holds them, and close after the last (still
no background service); a signed, notarized dmg and a per-user Windows
installer that update themselves, AUR, apt, dnf and a self-updating
AppImage on Linux. New section 3.9 (leases, the helper's clean host
start, local sockets instead of TCP, restarts, one version at a time,
the server's format-upgrade gate); P2, P11 and the decisions follow.
Five review rounds, the last clean.

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 10:45:44 -06:00
1 parent 2aaba56b38
commit c6fea172ec
2 files changed
+105 -60

No files matched your search

+6 -3
View File
@@ -55,10 +55,13 @@ 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, 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
**Then:** P2 (one `wanderbooks` executable, D-22; the books open while
the app, an agent session or a terminal holds them, no background
service, D-21/D-23 (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).
Linux, with installers and updaters per D-24: `.dmg`, a per-user Windows
installer, AUR, apt, dnf and AppImage; P2 gained backup restore and the
viewer's menu).
---
+99 -57
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, 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.
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, only while something you started is using it: the app, an agent session, or a terminal (amended 2026-10-18, D-21 and D-23: 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 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.
**After this plan.** The books move into a private folder that belongs to a small books server. The server runs as you, only while the app, an agent session or a terminal is using it. 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 (on Linux, the package manager asks as it always does). 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: starting the agent opens the books.
- 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.
@@ -68,7 +68,7 @@ Paths are relative to `/home/swaits/Code/wanderbooks/crates/` unless they are ab
| I-8 | **The GUI never mutates and never uses the MCP.** Locally it reads the data root directly, read-only, as today. It finds the root from the platform's well-known location or from `wanderbooks gui <book>`, never from a project directory or `wanderbooks.toml`. `books.show` reaches it through `ops/show.jsonl` in the book's own data root, never through the MCP. Remote books (self-host) are viewed through a read-only replica stream (P9). | `ledger-gui/tests/read_only.rs`. Tests `gui_works_under_read_only_landlock` and `gui_finds_root_without_project_dir`. Test `replica_snapshot_equals_store_snapshot` (P9). The GUI writes nothing under the data root and leaves no files under `$HOME` except eframe preferences. |
| I-9 | **Every access through the server is logged, and logging fails closed.** | A hash-chained `access.jsonl` (own genesis). Property: exactly one record per tool call, upload, download, human action, remote replica connection and backup, refusals included. The local viewer's direct reads are not logged, because the GUI never writes; the docs say so. |
| I-10 | **Tamper evidence.** Every mutating result carries `head {seq, hash}`. The head is handed off when a session goes quiet. The customer can clone the archive and run `wanderbooks verify-archive --expect-head`. Backup and access records stay out of the journal, so they never advance the head they report. | Test `backup_does_not_advance_head`. Test `clone_verify_expect_head`. |
| I-11 | **No loss of ease of use.** Local setup is at most 3 commands on a clean machine, with no sudo, no system account and no container. There is nothing to do per session, it works from any directory, the daily flow adds no client permission prompts (no `requiresUserInteraction`), and the viewer updates within 1 s of a post. | Acceptance criteria in P2 and P5. Tests `service_install_needs_no_root` and `annotations_complete`. The agent-eval harness. |
| I-11 | **No loss of ease of use.** Local setup is at most 3 commands on a clean machine, with no sudo, no system account and no container, beyond installing the app itself (D-24: on Linux the package manager asks for `sudo` as it does for every package; the dmg, the Windows installer and the AppImage ask for nothing). There is nothing to do per session, it works from any directory, the daily flow adds no client permission prompts (no `requiresUserInteraction`), and the viewer updates within 1 s of a post. | Acceptance criteria in P2 and P5. Tests `setup_needs_no_root` and `annotations_complete`. The agent-eval harness. |
| I-12 | **Guidance survives client truncation, and never names a path.** Connect text is at most 1,400 characters, and its first 512 stand alone and name `books.start`. Tool descriptions are at most 700 characters. No tool text mentions `just`, `wanderbooks.toml`, `~/`, git, GitHub PRs or any filesystem path, the data root included, and no result names one. | Tests `instructions_budget`, `tool_text_lint` and `no_result_names_a_path`. |
| I-13 | **Formal-methods bar.** Kani harnesses cover kernel arithmetic. Every new ledger command is a Lean `Step` with a preservation proof. A failing proof is a finding. All tools are pinned in `mise.toml`. Clippy reports zero warnings. Every mutation survivor is killed or documented as equivalent. | `just check` and `just prove` pass in every phase. |
@@ -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, 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). |
| 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`). |
@@ -92,12 +92,12 @@ 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. | **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. |
| Archive vs Landlock | A: server shells out to git, which contradicts its own Landlock rules. | **A separate archive process, `ledger-mcp archive`,** that the host starts beside the server, after the server holds every book's lock, and stops with it (3.9); each 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`). Locally the server listens on Unix sockets (3.9); if the owner turns TCP on, it binds 127.0.0.1 first. Either way Landlock ABI ≥ 4 then 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)"`. |
| Viewer channel | A: replica over a peer-credential socket. | **Direct read-only disk access locally.** This is the owner's rule: "the GUI has r/o access to ALL state on disk; it never mutates; it doesn't go through the MCP." `books.show` requests reach the GUI through `ops/show.jsonl` in the book's own data root, which the server writes and the GUI watches. Presence is a shared `flock` that the GUI holds on a read-only descriptor of `ops/viewer.lock`. Locally there is no socket, no peer-credential check and no executable check. The replica protocol exists only for viewing remote books (P9). |
| Local token | A: literal header in the Claude config. | **OS keyring plus a helper.** Claude Code `headersHelper` and Codex `http_headers_helper` run `wanderbooks token print --book X` (C). |
| Local token | A: literal header in the Claude config. | **OS keyring, read by the bridge** (D-23): Claude Code and Codex run `wanderbooks mcp --book X` over stdio, and the bridge reads the token itself, so no client config holds it. `headersHelper`/`http_headers_helper` running `wanderbooks token print` remain for clients that choose HTTP. |
---
@@ -105,15 +105,17 @@ Paths are relative to `/home/swaits/Code/wanderbooks/crates/` unless they are ab
### 3.1 Processes
From P2 every local program here is a mode of the one `wanderbooks` executable (D-22): `ledger-mcp serve` is its server mode, `ledger-gui` its viewer, `ledger-extract` its extractor, each run as its own process. The names below are the crates'. Claude Code and Codex reach a local book through the bridge, `wanderbooks mcp` (stdio, D-23), which speaks MCP to the server over a local socket (3.9).
```
agents (any directory, any client) human
Claude Code (any dir) Codex claude.ai / ChatGPT stdio-only browser ledger-gui
| | (P9: self-host or (ledger-mcp | (as the human)
| Bearer (keyring | Bearer tunnel) proxy URL) | owner |
| helper) | | OAuth or token | | passphrase |
| | (P9: self-host or (wanderbooks | (as the human)
| wanderbooks mcp | bridge tunnel) mcp --url) | owner |
| (stdio, lease) | | OAuth or token | | passphrase |
v v v v v |
+---------------------------------------------------------------------------------+ |
| ledger-mcp serve local: started in a terminal, as the human | |
| ledger-mcp serve local: open while held (D-23), 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 | |
@@ -131,13 +133,13 @@ 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>` 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 (local) | the server mode of `wanderbooks` (the `ledger-mcp` crate), listening on `run/mcp.sock` (and on 127.0.0.1:7411 only if the owner turns TCP on), started by the first opener (the app, an agent's bridge, or `wanderbooks serve`) through the host mode, and running while any opener holds it (D-21, D-23, 3.9; the host reads no books: it starts the server and the archive process, each confining itself, and stops both). TCP is off by default (3.9) | 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, 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 |
| 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). |
| stdio shim | `ledger-mcp proxy <url>` | the agent's user | never; it has no data-dir flag |
| 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 | `ledger-mcp dev-stdio --data-dir` | any | refuses inside the data root; `wanderbooks connect` never configures it (D-18) |
### 3.2 Transport (one code path, local and remote)
@@ -166,16 +168,16 @@ Both protocol eras are served: `initialize` and `server/discover` are both built
No scope unwraps the vault. Unwrap happens only inside an `/h/` request that has just presented a human factor, or inside the local GUI process after the human types the passphrase (3.6).
- **Local:** a static bearer `wb_` + 32 random bytes, stored only as a sha256, compared in constant time, kept in the OS keyring, minted by `wanderbooks connect`, and revocable with `wanderbooks token revoke`. Agent and server run as the same user, so the token identifies the client and keeps other local users and web pages out. It is not a wall against processes running as the human.
- Claude Code: `wanderbooks connect claude-code --book <book>` runs `claude mcp add --scope user --transport http wanderbooks-<book> http://127.0.0.1:7411/b/<book>/mcp`, sets `headersHelper` to `wanderbooks token print --book <book>`, and offers a `permissions.allow mcp__wanderbooks-<book>` line.
- Claude Code: `wanderbooks connect claude-code --book <book>` (also the app's "Connect Claude Code") runs `claude mcp add --scope user wanderbooks-<book> -- <absolute path of wanderbooks> mcp --book <book>`: stdio, through the bridge (D-23). The bridge reads the token from the keyring itself, so Claude's config holds no token and no helper. It offers a `permissions.allow mcp__wanderbooks-<book>` line.
- Codex: `wanderbooks connect codex --book <book>` writes:
```toml
[mcp_servers.wanderbooks-acme]
url = "http://127.0.0.1:7411/b/acme/mcp"
http_headers_helper = ["wanderbooks", "token", "print", "--book", "acme"]
command = "<absolute path of wanderbooks>"
args = ["mcp", "--book", "acme"]
```
No `default_tools_approval_mode` is written. Codex documents `writes` as prompting for every tool not marked read-only, which would prompt on every post, close and reconcile (against owner decision 3); leaving the key unset keeps Codex's normal rules.
The bridge answers `initialize` within Codex's startup timeout, opening the books first if they are closed. A client that needs HTTP can be pointed at `http://127.0.0.1:7411/b/<book>/mcp` with `wanderbooks token print` as its header helper once the owner turns TCP on (3.9); it holds no lease and needs the books already open. No `default_tools_approval_mode` is written. Codex documents `writes` as prompting for every tool not marked read-only, which would prompt on every post, close and reconcile (against owner decision 3); leaving the key unset keeps Codex's normal rules.
- **Self-host, remote (P9):** the same static bearer over TLS for clients that send headers (Claude Code, Codex). claude.ai and ChatGPT need OAuth, so the self-hosted server offers OAuth 2.1 on this remote path only (D-7):
- RFC 9728 PRM per book; 401 with `resource_metadata`.
- 403 `insufficient_scope` for step-up.
@@ -209,7 +211,7 @@ No scope unwraps the vault. Unwrap happens only inside an `/h/` request that has
ops/backup.jsonl ops/heads.jsonl not journaled
ops/show.jsonl books.show requests: the server appends (bounded, rotated), the GUI watches; not journaled
ops/viewer.lock created by the server; the GUI holds a shared flock on a read-only descriptor (presence)
uploads/<id>.part staging, swept after 15 min
uploads/<id>.part staging, swept after 15 min (a staged backup: kept until its restore ends, at most 24 h)
derived/<sha>/page-N.png disposable page-raster cache
archive.git/ bare; journal.jsonl + blobs/ + vault ciphertext; age-encrypted on push; destination Q-1
```
@@ -248,7 +250,7 @@ The server, the CLI and the GUI resolve the data root with one shared function (
### 3.7 Backup and tamper evidence
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:
The archive process (`ledger-mcp archive`: locally started by the host after the server, and stopped with it, 3.9; 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 they are open, so a backup is never missed while they are closed. "Back up now" in the viewer is the lease socket's `back_up_now` intent (3.9), opening the books first if 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`).
@@ -260,7 +262,7 @@ It writes `ops/backup.jsonl`. `books.backup {status|run}` reports it, and Needs
Email or webhook head delivery and a witness log belong to the hosted service (section 11).
**Restore (owner, 2026-10-17: "these backups need to be restorable, both in existing and new installations").** A backup is complete: the journal, every blob, the vault records (sealed), and the head `{seq, hash}` at backup time. `wanderbooks books restore <backup> [--id X]` (and the viewer's "Restore…", which hands it to the server) works the same on a clean install and on an existing one:
**Restore (owner, 2026-10-17: "these backups need to be restorable, both in existing and new installations").** A backup is complete: the journal, every blob, the vault records (sealed), and the head `{seq, hash}` at backup time. `wanderbooks books restore <backup> [--id X]` (and the viewer's "Restore…", which hands it to the server) works the same on a clean install and on an existing one. The server's Landlock keeps it inside the data root, so it never opens the backup by path: the CLI, or the viewer through its helper, stages the backup into the data root's `uploads/` (re-hashed on arrival), and the `restore` intent carries only that staged id.
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.
@@ -271,7 +273,7 @@ Email or webhook head delivery and a witness log belong to the hosted service (s
| | Local (T1, the default) | Self-host (T2, P9) |
|---|---|---|
| Where | the human's computer | another machine the owner controls: home server, NAS or VPS |
| 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` |
| Processes | one server and its archive process, open while the app, an agent session or a terminal holds them (D-23) | 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 |
@@ -283,6 +285,31 @@ A multi-tenant hosted service is out of scope for this plan; see section 11.
---
### 3.9 How the books open and close (D-23)
The books are open while something holds them; there is no background service. This section is the mechanism; P2 builds it, P11 adds updates.
**Who holds them.** The app (the viewer), an agent session's bridge (`wanderbooks mcp`), a terminal running `wanderbooks serve`, and any CLI command that needs the server. Each holds a **lease**.
**Three local channels, all in `run/` under the data root** (0700 on Linux and macOS; on Windows `%LOCALAPPDATA%\wanderbooks`, never the roaming profile, with an owner-only DACL; named pipes with an owner-only DACL, `FILE_FLAG_FIRST_PIPE_INSTANCE`, and a check of the client's SID):
- `run/lease.sock`: served by the **host**, leases only. Each lease records its kind (app, bridge, serve, cli) and PID; the app and `wanderbooks doctor` list them, so a stray holder keeping the books open can be seen and closed. Besides holding, the socket accepts exactly five **owner intents**, which is how the viewer hands work to the server: `close`, `back_up_now`, `new_books`, `restore` (a backup, as its own book beside the others), and `consent_format_upgrade` (a book). `consent_format_upgrade`, and a restore that replaces a book, carry the owner passphrase typed in the viewer, checked by the server (Argon2id, as on `/h/`); no MCP tool can give either, so an agent never consents. Nothing else is accepted.
- `run/mcp.sock`: served by the **server**, MCP for the bridge. Local agents never use TCP, so no other account on the machine can squat a port and catch the token or the traffic. Both ends check the peer's user ID (`SO_PEERCRED`/`getpeereid`, the pipe's SID). TCP is off by default. The owner can turn it on (`server.toml`, or `wanderbooks serve --http`) for a client that needs HTTP; it then binds 127.0.0.1:7411, a failed bind is reported by `doctor` and is not fatal, and such clients hold no lease, so the books must already be open.
- `run/admin.sock`: served by the server, admin only (create, adopt, tokens), as in 3.1. Neither the viewer nor the bridge opens it.
**Starting the host cleanly.** Every mode that confines itself (the viewer, the bridge) first starts a small unconfined **helper** (`wanderbooks helper`, spawned as its own process over an inherited socket pair before any thread, window or Cocoa start; never a fork of a running GUI), then confines itself. The helper accepts a fixed message set and nothing else: `start_host`; `open_with` an attachment id (the helper itself copies that blob, read-only, into a 0700 per-user directory, `$XDG_RUNTIME_DIR/wanderbooks-open/` or its per-system equivalent, swept when the helper exits, and hands it to the system's default app; it never accepts a path or a command line); `stage_backup`, whose file the helper asks the owner for in its own file dialog (never a path from the viewer), copying it into the data root's `uploads/` and returning a staged id for the `restore` intent; and `start_updater`, with no arguments. Only the helper starts the host, by a clean re-exec: environment scrubbed to a fixed allow-list (keeping `XDG_RUNTIME_DIR` and `DBUS_SESSION_BUS_ADDRESS`, which `systemd-run --user` needs, only after checking they point at the host's own user's runtime directory), stdin, stdout and stderr to null (a log file under `run/`), a new session (`setsid`), a fixed umask. On Linux under systemd (uwsm on Omarchy puts every app in its own scope, which would take the host down with the app) it re-execs through a transient `systemd-run --user --scope`, which installs nothing, persists nothing, and disappears with the host; elsewhere `setsid` alone. On Windows, `CREATE_BREAKAWAY_FROM_JOB` where the job allows it, and `DETACHED_PROCESS`. The host resolves the data root itself from the user's account (`getpwuid` home, or the known folder on Windows), never from the opener's environment, unless `wanderbooks setup` recorded another root. It refuses to run if it finds itself confined (seccomp in `/proc/self/status`, a Landlock write probe).
**One host, even in a race.** The host takes `run/host.lock` (an exclusive flock) before anything else; a second host exits and its opener attaches. The host starts the server, which takes every book's writer lock, and only then the archive process, so two archive pushers never meet on one `archive.git`. An opener that finds no live host and an unlocked `host.lock` starts one; one that finds a host shutting down (the lease socket refuses during shutdown) waits on `host.lock`, then starts a new one.
**Closing.** When the last lease ends, the host waits 10 s; a lease arriving in that time cancels the close. Then: no new requests, the one in progress finishes, every writer lock is released, the archive process finishes its push, and the host exits. "Close the books" (the app's menu, `wanderbooks close`) ends every lease at once; a bridge then tells its agent the owner closed the books and reopens them only in a new session. Before it ends leases the host says why on each one (`closed_by_owner`, `restarting`, `idle_close`). A holder that sees the connection end with no reason (a crash, a kill) starts a new host and re-takes its lease, with backoff (1, 2, 4 … 60 s, at most five tries); a host that keeps failing to start (a damaged journal, a panic) becomes a Needs-attention and `doctor` item, and a bridge answers its agent with a clear error instead of retrying forever. `host_respawns` and `respawn_backs_off` test it.
**Restarting** (an update, or the executable replaced by a package manager) is not a close. What lives between calls survives it: upload grants, download links, `/h/` sessions and finished job results are kept in `run/state/` (0600, the session key with them), and a running job delays the restart until it finishes. The host takes `run/update.lock`, closes the books as above but keeps the lease socket's holders informed, swaps (or, for a package-managed install, finds the new executable already in place), and restarts from the new executable. Openers that arrive meanwhile wait on `update.lock`. Bridges reconnect and re-initialize upstream without the agent noticing. A package-managed upgrade is noticed by the host (its executable's inode changed) and the restart happens the next time no request is in progress.
**One version at a time.** Children (server, archive, extractor) are started from the executable the host itself is running (`/proc/self/exe` or `fexecve` on Linux), never by path, so a swap mid-run can't mix versions. Where that isn't available (macOS, Windows) every child, bridge and lease handshake carries the version, and a mismatch triggers a restart. The lease, helper and bridge protocols carry protocol versions (not release versions, since a staged update can skip releases); each accepts its current version and the one before, and outside that range refuses with a plain message ("restart this agent session").
**The bridge and slow starts.** The bridge waits up to 5 s for the server and returns its real `initialize` (the connect text composed from the book, 4.1). If the server isn't ready by then (a cold start, a projection rebuild), it answers `initialize` itself, within a client's startup timeout (Codex allows about 10 s), from the tool list built into the same executable and a short generic text that points at `books.start`, which returns the book's rules. Calls are queued until the server is ready; a queued call fails after 60 s with a clear error that names `wanderbooks doctor`.
**Format changes are the server's gate, not the updater's.** A server that would write a newer journal format than a book has opens that book read-only, with guard `format_upgrade_pending`, until the owner consents in the app (or on a `/h/` page). Before it asks, it makes a verified local backup that includes the keys (`legacy.key` until P7). Consent comes only from the owner (the passphrase, through the viewer's `consent_format_upgrade` or a `/h/` page), never from an agent. Declining leaves the book as it was, and before consent the way back to a version that can write it is always there: a self-updating install keeps the previous verified app and offers "Go back to version X" (`wanderbooks rollback`); a package-managed one downgrades through its package manager (our apt and dnf repositories keep every release, and the AUR keeps old PKGBUILDs). This holds however the new version arrived: our updater, a package manager, or a hand install. **After consent** the previous version can no longer open that book, so "Go back to version X" refuses for it; the only way back is restoring the pre-upgrade backup, which would drop every entry since, so that restore says how many entries would be lost, arrives as its own book beside the current one (never replacing it), and needs the passphrase.
## 4. The MCP surface after the change
### 4.1 Connect instructions (composed in `get_info`; replaces `INSTRUCTIONS` S:57 and the string replace at S:8179)
@@ -469,7 +496,7 @@ The tool count goes from 93 to about 125. Only `books.start` and `books.handbook
- An unknown type is no longer `JournalCorrupt` (`store.rs:4419-4423`, `:4640`, Snapshot, GUI). It becomes `StoreError::NewerJournal {seq, type, need}`, shown as "update wanderbooks".
- A one-time `journal.format_set {format: 2, features: [...]}` is written before the first v2 event.
- Fixture journals from every phase are added to `books_written_under_older_rules_still_open`.
- `wanderbooks update` upgrades all binaries together. The local GUI reads the journal directly, so it hits the same `NewerJournal` gate; the replica protocol (P9) carries a minimum version header.
- There is one executable (D-22), so an update never mixes versions; the host starts its children from its own executable (3.9). `wanderbooks update` updates a self-updating install (dmg, Windows installer, AppImage) and refuses on a package-managed one, saying to use the package manager. The local GUI reads the journal directly, so it hits the same `NewerJournal` gate; the replica protocol (P9) carries a minimum version header.
### 5.2 New events (all additive; legacy events replay unchanged)
@@ -554,7 +581,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 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 |
| 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. Nothing to start: each command below opens the books while it runs, and afterwards the app or Claude opens them (D-23).<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 bridge's stdio registration, `wanderbooks mcp --book wanderland`).<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. With every holder closed the books close cleanly (`books.verify` ok with the same head at the next open), and a Claude Code session started with the books closed opens them by itself, with no reconnect. **Rollback (nothing posted on the server):** close the books (`wanderbooks close`), 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) |
@@ -623,13 +650,13 @@ 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:
1. You install Wanderbooks the usual way for your system: a `.dmg` on a Mac, an installer on Windows, your package manager on Linux (D-24). No developer tools, and no administrator password (on Linux your package manager asks for yours, as it does for everything). On first open it:
- 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;
- 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.
- tells you how the books open: opening Wanderbooks, or starting an agent, opens them; when the last of those closes, so do the books. Nothing runs in the background.
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.
### 7.2 Day to day
@@ -638,7 +665,7 @@ What you will see and do. The technical details are in sections 3 and 4.
- You say: "Show me Q3 against last year." The viewer opens that report.
- 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.
**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 (on Linux, the package manager asks as it always does).
### 7.3 How private your books are
@@ -670,7 +697,7 @@ The installer offers to connect Codex, or you can run `wanderbooks connect codex
### 7.7 What stays easy (acceptance)
- At most three commands from a new computer to the first posted entry, with no administrator password.
- At most three commands from a new computer to the first posted entry, with no administrator password beyond the Linux package manager's usual one.
- Nothing to do at the start of a session.
- Everything works from any folder.
- The viewer catches up within a second.
@@ -755,11 +782,12 @@ 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 (started by hand), hidden away, and the Wanderland cutover
### P2: One executable, open while held, 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).
- **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.
- **One executable (D-22).** `wanderbooks` is the only program: a thin dispatcher over the existing crates whose modes are the viewer (`gui`), the books server, the host, the agent bridge (`mcp`), the archive process, the extractor, and the CLI commands. Each mode that runs as a process confines itself at start, after forking its unconfined helper (3.9) and before it opens anything. The viewer, on Linux, confines itself read-only on the data root, with writes allowed only to its preferences folder and the Downloads folder (exports, Save a copy); Open-with and the updater go through its helper. A `gui` cargo feature, off in the self-host server build (P9), leaves the window libraries out. P11 then only packages it.
- **Open while held, no service, no sudo (D-21, D-23).** `wanderbooks setup` creates the data root (3.4) with an explicit `set_permissions(0o700)` (an owner-only DACL on Windows) and sets the owner passphrase. The books open when something holds them and close when nothing does, as section 3.9 describes: leases on the host's `run/lease.sock`, the helper's clean start of the host, `host.lock`, the 10 s close, "close the books", restarts, one version at a time, the bridge's own `initialize`, and the server's `format_upgrade_pending` gate. Nothing is installed or persistent: no systemd unit file, no LaunchAgent, no socket activation, no login autostart (the transient scope of 3.9 disappears with the host), and nothing runs after the last holder closes. 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 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`.
@@ -767,14 +795,14 @@ Wanderland keeps working between phases.
- **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.
- **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; the viewer's only channels are the lease socket's owner intents and its helper (3.9).
- 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: `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.
- **Viewer menu (D-19):** "Export for CPA" (done by the viewer, into Downloads); "Back up now", "Restore…", "New books…", "Close the books" and format-upgrade consent handed over as the lease socket's owner intents (3.9); "Connect Claude Code" and "Connect Codex". Nothing in the viewer writes a book.
- `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 proxy).
- The test harness drives HTTP (`mcp_stdio.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`.
- Wanderland cutover, preceded by the two open cutover questions (10.3).
@@ -783,8 +811,9 @@ Wanderland keeps working between phases.
**Tests:**
- `agent_uid_cannot_read_tenant` (the two-user CI deployment; not the desktop default).
- `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).
- `books_close_cleanly` (the last lease ending, "close the books", SIGTERM and SIGHUP of the host, all during a burst of posts: every request either completes or is refused, the head verifies at the next open, the lock is released); `openers_share_one_server` (the app, `serve`, a CLI command and two bridges started together: one host, one writer); `books_close_with_last_holder` (each kind of holder ending, `kill -9` included: closed 10 s after the last, not before); `bridge_opens_closed_books` and `bridge_survives_server_restart` (the agent's session carries on, no reconnect); `close_now_ends_every_lease`; `host_starts_clean` (started from a confined viewer or bridge with a hostile environment, `XDG_DATA_HOME` and `LD_PRELOAD` included, the host is unconfined, finds the real root, and holds no inherited descriptor); `host_respawns` (kill the host: a holder starts a new one); `host_survives_app_scope` (under a systemd app scope, as uwsm sets up, closing the app that started the host leaves it running for the other holders); `no_local_agent_tcp` (the bridge never connects to TCP; TCP is off by default; with it on, a squatter on 7411 makes `doctor` report a failed bind and sees no bridge traffic); `lease_socket_accepts_only_owner_intents` (exactly the five; consent and replace without the passphrase refused); `restart_is_not_close` (bridges carry on across a restart; openers wait on `update.lock`); `children_match_host_version` (replace the executable mid-run: no child of the new version joins the old host); `format_upgrade_waits_for_consent` (read-only until the owner's consent, a backup with the keys made first, no MCP tool can consent, declining leaves the book openable after `wanderbooks rollback`); `no_rollback_after_consent` ("Go back" refuses for an upgraded book; the pre-upgrade restore arrives beside it, states the entries it lacks, and needs the passphrase); `restore_reads_only_staged_backups` (a `restore` intent naming a path is refused); `bridge_initializes_during_cold_start`; `archive_catches_up_at_start` (a push that failed before a close is made at the next open).
- `one_executable` (every mode from one binary; the viewer mode under read-only Landlock opens and follows a book); `server_build_has_no_gui` (the self-host build links no window libraries).
- `viewer_without_server` (with the server stopped or failing to start, the viewer still opens and follows the books from disk; "Restore…" and "New books…" say why they can't run).
- `isolation_label_is_advisory`.
- `no_path_params` schema lint; `no_result_names_a_path`.
- `no_tool_touches_outside_tenant`.
@@ -1060,22 +1089,32 @@ Run from clients that hold only a URL and a credential: Claude Code started in `
- Agent context on receipts never exceeded 100K tokens.
- Stephen's sign-off is journaled as `decision.recorded`.
### P11: One app for Windows, Mac and Linux (D-19)
### P11: One app for Windows, Mac and Linux (D-19, D-22, D-24)
**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 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.
- Release builds and the full test suite on all three systems in CI.
- The viewer menu of P2 (back up, export, restore, new books) is the app's whole surface beyond the viewer; there is no separate launcher.
- **Choose the tooling first** (a spike, recorded in the change log): candidates Velopack (per-user Windows setup, macOS, self-updating AppImage, delta updates, a Rust SDK), cargo-packager (`.dmg`/`.app`, `.deb`, AppImage, pacman, NSIS and WiX `.msi`, with an updater crate; in public preview, no `.rpm`), and cargo-deb with cargo-generate-rpm for the distribution packages. Chosen against the rules below, not the other way round.
- **macOS:** a `.dmg` holding `Wanderbooks.app` (the one executable inside the bundle), signed with the company's Developer ID, notarized and stapled. Drag to Applications, or `~/Applications` for a standard user who can't write `/Applications`; no admin password. While macOS runs the app translocated (straight from the `.dmg` or Downloads), it says to move it first and refuses to connect agents, so no temporary path lands in Claude's or Codex's config. The app updates itself in place.
- **Windows:** a per-user installer that asks for no admin password: a signed setup `.exe` into the user's app folder; an `.msi` only if a managed install ever needs one. The app updates itself. Signing through Azure Artifact Signing if the company or Stephen qualifies, else an OV certificate; SmartScreen warnings fade as the download builds a reputation.
- **Linux:** Arch (an AUR `wanderbooks-bin` package), Debian/Ubuntu (`.deb` and our signed apt repository), Fedora (`.rpm` and our signed dnf repository), each installed and updated by the system's package manager (with `sudo`, as every package there is); and an AppImage that updates itself and needs no admin password, for every other distribution. The packages install the one executable, a `.desktop` entry and the icon.
- **One updater rule set** (D-24), whatever the tooling:
- Every update is verified twice: against our own release key (Ed25519 over a release manifest listing each file's sha256) and the system's code signing. Either failing refuses the update; nothing is half-installed.
- The updater restarts the books, it doesn't close them (3.9): `update.lock`, a clean stop after the request in progress, an atomic swap, a restart from the new executable; the viewer reopens and the agent bridges reconnect by themselves.
- When updates happen: the host checks for one daily while the books are open, downloads and verifies it, and stages it; it is applied at the next restart the owner accepts in the app, or by the helper just before it next starts the host. An install whose books are never opened is never touched.
- Format changes are the server's gate (3.9, `format_upgrade_pending`), not the updater's: the owner consents in the app after a verified backup with the keys. A failed update rolls back to the previous app, which can still open the books as long as no newer format was written. After consent there is no rollback for that book: only restoring the pre-upgrade backup as a separate book, which shows how many entries it lacks (3.9).
- Updates never start during a long job (a CPA package, an audit, an extraction backfill). Multi-call work like a reconciliation is journaled as it goes, so a restart between calls loses nothing.
- **Release keys.** Two Ed25519 keys, a primary on a hardware token held by the owner and a backup kept offline; CI builds and code-signs but holds neither, so it cannot sign a manifest. Rotating or revoking a key is a statement signed by the other. Each manifest carries a version that only increases and an expiry: from the feed, an older version than the one installed is refused (no rollback attack), and so is an expired manifest (no frozen feed). Going back to the kept previous app is a local action by the owner, not a feed update, and stays allowed.
- **Package-managed installs** (AUR, apt, dnf) are updated by the package manager, with its own signatures (our repository keys) and `sudo` as usual on Linux; our updater stays out, and `wanderbooks update` refuses there. The host notices the new executable and restarts when idle (3.9); the format gate still protects the books.
- **Connecting agents from the app:** the menu offers "Connect Claude Code" and "Connect Codex" (P2's `connect`, with the absolute path of the installed executable, which updates keep stable), so no one edits a `PATH`.
- Data root in each system's usual app-data folder (Application Support, `%LOCALAPPDATA%` (never the roaming profile), `~/.local/share`), as in 3.4 and 3.9.
- Release builds and the full test suite on all three systems in CI; package builds for Arch, Debian and Fedora in their own containers.
- The viewer menu of P2 (back up, export, restore, new books, close the books, connect an agent) is the app's whole surface beyond the viewer; there is no separate launcher app.
- **Costs (company expenses, recorded with their receipts):** the Apple Developer Program (the company's enrollment, attachments 76 and 77); Windows code signing (Azure Artifact Signing, about $10 a month where eligible, or an OV certificate, about $150 to $300 a year).
**Tests:** `install_needs_no_admin` (each system, clean machine); `update_replaces_all_parts_together`; `failed_update_rolls_back`; `unsigned_update_refused`; `server_runs_with_viewer_closed`; P2's restore tests on all three systems.
**Tests:** `install_needs_no_admin` (dmg, Windows installer, AppImage, clean machines); `translocated_app_refuses_to_connect`; `manifest_rollback_and_expiry_refused`; `key_rotation_statement_verified`; `package_upgrade_restarts_host`; `update_is_atomic`; `failed_update_rolls_back`; `unsigned_update_refused` (bad release-key signature, bad code signature, a file whose sha256 differs from the manifest: each refused, nothing changed); `update_closes_books_cleanly` (during a burst of posts; head verifies after); `update_waits_for_long_jobs`; `format_change_update_backs_up_and_asks` (declining leaves the books untouched and openable by the old version); `bridge_survives_update`; `linux_packages_install_and_upgrade` (AUR, apt and dnf repositories in clean containers; AppImage self-update); `server_runs_with_viewer_closed`; P2's restore tests on all three systems.
**Accept:**
- On a clean Windows, Mac and Linux machine: download, install, open, onboard and post a first entry with no terminal and no admin password.
- An update leaves every book's head and goldens unchanged.
- On a clean Windows and Mac machine, and on Linux with the AppImage: install, open, onboard and post a first entry with no terminal and no admin password. On Arch, Debian and Fedora: the same after the package manager's usual install.
- An update, on each system, leaves every book's head and goldens unchanged, and an agent session open across it carries on.
- A backup made on one system restores on each of the others.
---
@@ -1084,7 +1123,7 @@ Run from clients that hold only a URL and a credential: Claude Code started in `
1. **Journal forward compatibility.** Old GUIs or CLIs cannot read v2 events.
- The P0 `NewerJournal` gate ships first. The local GUI reads the journal directly, so it goes through the same gate.
- `wanderbooks update` upgrades every binary together.
- One executable (D-22): an update replaces it whole; the server's `format_upgrade_pending` gate (3.9) protects the books however it arrived.
- The replica (P9) carries a minimum version.
- Fixtures from every phase are kept.
- Downgrade is impossible after the first v2 event, so back up first (P1 says so).
@@ -1102,11 +1141,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. **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.
7. **The books are open only while something holds them** (D-21, D-23: the app, an agent session, a `serve` terminal; no background service).
- An agent session opens them itself through its bridge, so it needs no reconnect. A client connected over HTTP instead of the bridge holds no lease and needs them already open.
- The viewer reads the data root directly, so it works even if the server can't start; "Restore…" and "New books…" say why they can't run then.
- Backups run only while the books are open. The books change only then, so nothing goes unbacked; the archive process pushes anything left over, and a drill that fell due, at the next open.
- The host outlives the opener that started it, by design, until the last lease ends; it is never left running with no holder for more than 10 s. 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.
@@ -1145,9 +1184,12 @@ The owner decisions of 2026-10-16 (at the top of this document) replace the form
| 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-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-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 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. |
| 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. |
| D-22 | One app or several (owner, 2026-10-18: "My objective is simplifying installation and updates.") | **One app, one executable, separate processes.** `wanderbooks` is the only program installed; each role is a mode of it, run as its own process: the viewer (what opening the app shows), the books server and its host, the agent bridge (`wanderbooks mcp`), the archive process and the extractor. One thing to sign, notarize, ship and update. Mixed versions are still possible across a swap, so the host starts its children from its own executable and every handshake carries the version (3.9). The viewer's read-only rule never rested on a separate file: `ledger-gui` already links `ledger-store` and stays read-only through `Snapshot` and `tests/read_only.rs`. As its own process it keeps that, and on Linux it also confines itself read-only (Landlock). Each mode confines itself at start. The crates stay as they are; the executable is a thin dispatcher (P2). One exception: a server-only build (the `gui` feature off) for self-host (P9), so a headless machine carries no window libraries. |
| D-23 | When the books are open (owner, 2026-10-18: "Also when an agent starts") | Amends D-21. The books are open while anything that opened them runs: the app, an agent session (its bridge opens them, windowless, for the session), a terminal running `wanderbooks serve`, or a CLI command. Each holds a lease on the host's lease-only socket, so a crashed holder lets go by itself; local agents speak to the server over a peer-checked local socket, never TCP (3.9). The first opener starts the one host; later ones attach; when the last lease ends the books close 10 s later, cleanly. "Close the books" ends every lease at once. Still no background service: nothing starts at login and nothing runs after the last holder. An agent session started with the books closed needs no reconnect (P2). |
| D-24 | Installers and updates (owner, 2026-10-18: "A dmg for macOS, msi or whatever for windows, and packaged for Linux distributions. Win and Mac with updaters.") | macOS: a signed, notarized `.dmg`; the app updates itself. Windows: a signed per-user installer with no admin prompt (`.msi` only if a managed install needs one); the app updates itself. Linux: AUR, `.deb` with our apt repository, `.rpm` with our dnf repository (all updated by the package manager), and a self-updating AppImage. Every self-update is verified against our own release keys and the system's code signing; Linux packages are verified and updated by their package managers. An update restarts the books cleanly. A newer books format is the server's gate whatever the install: read-only until the owner consents, after a backup with the keys (3.9). "No admin password" holds for the dmg, the Windows installer and the AppImage; Linux packages use `sudo` as usual. Details and tooling in P11. |
### 10.2 Deferred