P2 slice 4: access
Agent tokens (wb_ and 64 hex digits), one book and one name each, kept only as SHA-256 in a chain log and in the OS keyring; made, listed and revoked only through the admin socket (`wanderbooks token ...`). The door checks them, scopes cover every tool, every call is in the access log (fail closed), and the owner passphrase is an Argon2id hash. Setup's terminal test waits for its prompts. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01T3gXMVt7XJPi6ens1or9Dj
This commit is contained in:
1 parent
a298d154b6
commit
d1f3ad891a
28 files changed
+5504
-116
No files matched your search
+35
-3
@@ -165,7 +165,7 @@ Both protocol eras are served: `initialize` and `server/discover` are both built
|
||||
| `books:admin` | tokens, access log, backup run, consolidate, invite | owner's agent (local default), owner |
|
||||
| `books:view` | remote replica only (P9) | viewer client only, never agents |
|
||||
|
||||
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).
|
||||
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). *Until P7 moves TINs to the human pages, the three tools that can still put a full TIN out (`vendor_1099` with `include_tins`, `export_cpa_package`, `get_w9`) need books:admin, and sealing one (`set_tin`, `set_w9`) needs books:post (P2 slice 4, review).*
|
||||
|
||||
- **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>` (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.
|
||||
@@ -191,6 +191,16 @@ No scope unwraps the vault. Unwrap happens only inside an `/h/` request that has
|
||||
|
||||
**Human factor for `/h/`:** passkey with user verification (webauthn-rs; localhost is a secure context), or the owner passphrase (Argon2id, RFC 9106, m=64 MiB, t=3, p=1). Locally the passphrase is the default. Cookies are `__Host-` and SameSite=Strict, with CSRF tokens and CSP `default-src 'self'`. URL-mode elicitation is used where the client declares it, and the server requires that the session's `sub` equal the token's `sub`.
|
||||
|
||||
**As built (P2 slice 4), and where it departs:**
|
||||
- **Tokens** are `wb_` and 64 hex digits (32 random bytes), one book and one name each (`agent` unless named), kept only as a SHA-256 in `auth/principals.jsonl`: a chain of `token.created` (with the token it replaces) and `token.revoked` records, each with its own hash besides the chain's (so a change to the last record is found too), replayed at start; one that can't be read refuses every agent with why (the list of books still answers). Checked in constant time against every live token. Made, listed and revoked only through the admin socket: `wanderbooks token create --book ID [--name N] [--scopes read,write,post,admin] [--replace] | list | revoke | print`. A token made is kept in the keyring before anything is said of it, or revoked again; `print` is the only way its text is shown. `connect` (slice 8) will mint through the same path.
|
||||
- **Kept in the OS keyring** (Secret Service on Linux, through keyring-core; macOS and Windows stores come with P11), under service `wanderbooks` and an account naming the root by a hash of its path, the book and the token's name. The bridge (`wanderbooks mcp --book ID [--token NAME]`) reads it before each new session and each request until it has one, and again when the door refuses it, so a token made or replaced mid-session is used from the next call (with a new session: a session belongs to the token that opened it); without one, the server's refusal says how to make one. Tests keep tokens in files under the root's `run/` (the `file_keyring` knob).
|
||||
- **The door** (`/b/{id}/mcp`): no token or an unknown one, 401 with `WWW-Authenticate: Bearer` (the scheme in any case); another book's, 403; a session is bound to its token when opened and used only with it (403), forgotten when it ends, is found gone (an idle one, when next used), or its token is revoked or replaced; an id the server never opened binds nothing; past 100,000 bindings an initialize is refused, never a binding dropped. Each refusal at the door is recorded in that book's access log (action `connect`).
|
||||
- **Scopes** (`ledger-mcp/src/scopes.rs`): a table of every tool. Read-only tools need books:read; a tool with `confirmed` needs books:write to preview and books:post to act; tools that post at once, settings commits, voids, renumbering, corrections and role changes need books:post, and so do the controls on posting (setting an approval rule, retiring a convention, approving an entry) and sealing a TIN (`set_tin`, `set_w9`, which can replace one); `books.access_log` and the tools that can still put a full TIN out (`vendor_1099`, `export_cpa_package`, `get_w9`) need books:admin. A token is offered only the tools it can call (the list is then cached per caller, `cacheScope: private`); a call beyond its scopes is refused, unrun, naming the scope. Reading a resource needs books:read. A local token gets all four unless told otherwise.
|
||||
- **The access log** (`books/<id>/access.jsonl`, own genesis) records every tool call and resource read, refusals included: who (token id, name), the client as it named itself (cut to 64 characters, any full TIN masked), the request id, the tool or resource, a SHA-256 of the arguments' canonical JSON with what may be a TIN masked first (a value under a key naming one, any key or text holding one in a shape the TIN scanner knows, any number of eight digits or more: an unsalted digest of a TIN whose last four the books show is broken by 10^5 guesses; a file's base64 is hashed as it is, never spelling a TIN in the clear), what came of it (`ok`, `error`, or `refused` with a code), and the journal's head after the call's own last write, if it wrote, whatever it answered (taken under the store's lock as the call lets go). A call's record is written on its own blocking thread while it still counts as running (one that panics part way included, with what it wrote), so a close waits for it too, and before the answer goes; one that can't be written stops the book (no more requests until a restart) and the answer says the call ran (a call refused as not run, by a close or a stopped server, keeps its "not run" code). A tool no table names is refused unrun. Known gap: a crash of the process between a call's journal write and its record leaves the journal ahead of the log for that call. A damaged log, or one holding a record that isn't an access, keeps its book closed, with why. `books.access_log` (admin) reads it newest first, filtered by token, action or outcome, paged with `before`; over stdio (no log) the tool isn't offered.
|
||||
- **The owner passphrase** is an Argon2id hash (m=64 MiB, t=3, p=1, its own salt) in `auth/owner.json`, replaced whole; 12 to 1024 characters, no other rule (NIST SP 800-63B); a check refuses a hash weaker than that (changed by hand). `setup` at a terminal asks for it (twice); elsewhere it says to run `wanderbooks passphrase`, which works only at a terminal, unechoed, and asks for the one in force before a change. (Advisory, like everything local: a process running as the human can drive a pseudo-terminal and set the first one.) A stored hash of another Argon2 version, with a short salt or output, or asking a check for more than 1 GiB, 16 passes or 8 lanes is refused as changed by hand. The command line wipes token texts and passphrases from memory once used (the server and the bridge, which hold tokens for their lifetime, don't). `doctor` says whether it is set and lists the tokens. Its uses (`/h/` pages, consents) come with the slices that build them.
|
||||
- **Known, product-wide (for P7):** the TIN scanner (`has_full_tin`) takes hyphens and spaces as separators, not commas or parentheses, so "123,45,6789" passes it in party fields and the access-log digest alike.
|
||||
- **Departures:** rate limits (tower-governor) wait for P9: on a socket only this account can reach, nothing could be limited that couldn't read the files instead. Principals are per book, not lists of books. `initialize` (the connect text, composed from the book's settings) is given to any valid token and isn't recorded. Changing the token record holds its lock across one fsync, briefly delaying token checks.
|
||||
|
||||
### 3.4 Data layout (server-owned data root, 0700)
|
||||
|
||||
```
|
||||
@@ -1097,7 +1107,7 @@ Run from clients that hold only a URL and a credential: Claude Code started in `
|
||||
- Agent context on receipts never exceeded 100K tokens.
|
||||
- The owner's sign-off is journaled as `decision.recorded`.
|
||||
|
||||
### P11: One app for Windows, Mac and Linux (D-19, D-22, D-24)
|
||||
### P11: One app for Windows, Mac and Linux (D-19, D-22, D-24, D-27)
|
||||
|
||||
**Scope:**
|
||||
- **Tooling: cargo-packager (D-25).** Its `cargo packager` builds `Wanderbooks.app` and the `.dmg`, the NSIS setup `.exe` (in its default `CurrentUser` mode: no admin, metadata under `HKCU`), the `.deb`, the AppImage, and the pacman `PKGBUILD` the AUR package starts from; `cargo-packager-updater` updates the formats that update themselves (`app`, `nsis`, `appimage`; `wix` too, if an `.msi` is ever made). It has no `.rpm`, so cargo-generate-rpm makes that. Both tools are pinned in `mise.toml`, like every tool. Checked 2026-10-18 against its source (packager 0.11.8, updater 0.2.3, both pre-1.0): the updater verifies each bundle's minisign (Ed25519) signature against one public key and by default takes only a newer semver than the running one. It has no expiry, no second key and no key rotation, so our signed release manifest (below) is checked first, and the bundle's own minisign signature after it; the primary release key is the minisign key. P11's first step is a short trial of all six formats on clean machines; anything it can't meet goes in the change log before we build around it.
|
||||
@@ -1113,12 +1123,20 @@ Run from clients that hold only a URL and a credential: Claude Code started in `
|
||||
- **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`.
|
||||
- **The agent dock and books homes (D-27; owner, 2026-10-19: "a one stop shop … it boots up, you get the chat interface").** The app runs the agent itself, in a terminal docked inside its window, so a person who installs Wanderbooks needs nothing else open:
|
||||
- **The CLIs are found or installed, never bundled.** The app looks for an installed `claude` and `codex` and offers the missing one through its vendor's official installer. Bundling is ruled out: Claude Code isn't redistributable, both release far faster than we do, and a second copy means a second login and a second set of permission rules.
|
||||
- **The dock** is a full-width panel along the bottom of the main window (not the sidebar), resizable and collapsible, so the reports stay in view while the agent works: posts appear within a second and `books.show` moves the view, which is the point. Its header picks the agent: Claude Code by default, Codex if installed, remembered with the viewer's preferences. The terminal is `alacritty_terminal` rendered in egui (`egui_term`, or our own view over it), on a pseudo-terminal: Unix PTYs on Linux and macOS, ConPTY on Windows 10 1809 and later. `/login`, slash commands and the agents' own permission prompts work as in any terminal; we build no chat UI of our own (a native one over the Agent SDK or `codex app-server` stays a later option).
|
||||
- **The agent is started by the viewer's helper** (3.9), not as the viewer's child: the viewer stays read-only and confined (3.6) while the agent runs unconfined, as the human, like one started in a terminal.
|
||||
- **Each book has a books home**, the agent's working folder: a visible, ordinary folder named for the company (`~/Documents/Wanderbooks/<Company>/`; the user's Documents folder on Windows), never the data root and nothing inside it. It holds only what makes an agent started there work with that book: an `AGENTS.md`/`CLAUDE.md` pointer ("call `books.start`"), the agent's project-level MCP configuration for that book (`wanderbooks mcp --book <id>`), and `.claude/settings.json` allowing the wanderbooks tools so the daily flow doesn't prompt. No books data, no keys, no token. The agents' "trust this folder?" question is then about a harmless folder, and is answered by the person, once; the app never edits Claude's or Codex's own settings to pre-trust it (whether `wanderbooks connect codex`, which already writes Codex's configuration, may mark the books home trusted is asked of the owner when it is built). The books home is a convenience: agents started anywhere else keep working (I-11).
|
||||
- **The first company's project folder becomes its books home.** After the P2 cutover clears its crumbs (10.3), `~/Documents/the first company-Books` holds exactly the pointer and the agent configuration: the first books home, and the template `New books…` makes the others from.
|
||||
- **Windows.** The pseudo-terminal is ConPTY (it rewrites some escape sequences and has resize quirks; neither stops a terminal app). Each agent's own Windows support is checked in the first-step trial on a clean machine: Claude Code's need for Git for Windows, Codex's native support (historically WSL-first). An agent that isn't good enough there isn't offered in the dock on Windows, and the app still works as a viewer with agents started elsewhere.
|
||||
- Open questions for the trial: whether Codex reads a per-folder MCP configuration (historically only the global `~/.codex/config.toml`; if not, `connect codex` writes one named server per book there); the exact layout of each agent's project files at that time.
|
||||
- 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` (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.
|
||||
**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`; `dock_runs_the_chosen_agent` (on each system: the dock starts Claude Code, and Codex where offered, in the book's books home, through the helper; the viewer stays confined); `books_home_holds_no_books_data` (only the pointer and the agents' configuration; no path into the data root); `agent_missing_offers_the_installer`; P2's restore tests on all three systems.
|
||||
|
||||
**Accept:**
|
||||
- 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.
|
||||
@@ -1199,6 +1217,9 @@ The owner decisions of 2026-10-16 (at the top of this document) replace the form
|
||||
| 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. |
|
||||
| D-25 | Packaging tool (owner, 2026-10-18: "Cargo packager looks good. Would like to use that.") | **cargo-packager** and its updater (crabnebula-dev, MIT/Apache-2.0) for the `.app`/`.dmg`, the per-user NSIS installer, `.deb`, AppImage and the AUR's `PKGBUILD`; cargo-generate-rpm for `.rpm`, which it lacks. Our release manifest (two keys, rotation, a version that only increases, an expiry) wraps its single-key minisign check (P11). |
|
||||
| D-26 | Where releases are hosted (owner, 2026-10-19: "I'm assuming the GH releases page will do the hosting?") | **GitHub Releases** on the public mirror (`swaits/wanderbooks`): each release carries the `.dmg`, the setup `.exe`, the AppImage, the `.deb` and `.rpm`, and the signed release manifest; the self-updaters read the manifest at the fixed `releases/latest/download/` address, so no server of ours is needed. CI builds and code-signs on release tags on GitHub's macOS, Windows and Linux runners and holds no release key (P11). The apt and dnf repositories' signed indexes are served from GitHub Pages on the same repository; the AUR package points at the Release assets. |
|
||||
| D-27 | The agent dock and books homes (owner, 2026-10-19: bundle the agents "then it'd be a one stop shop … a chat window maybe full span across the bottom"; "Yeah I like the idea of the embedded alacritty launching claude/codex … maybe MVP defaults to claude"; "you'd want like a 'books home' dir somewhere that makes sense so it has a place to work"; "add it to the plan!") | The app docks a terminal (alacritty_terminal; ConPTY on Windows) along the bottom of its window and runs Claude Code (the default) or Codex in it, found or installed through the vendors' installers, never bundled; started by the viewer's helper, in the book's **books home**: a visible folder per book holding only the agent's pointer and configuration for that book, never the data root. Trust in that folder is the person's to give. **Never required** (owner, 2026-10-19: "we wouldn't want to prevent people from using their OWN AI agent. They should never HAVE to use our built in tty+claude session! Ideally they could even use the claude.ai browser version to connect to it. And of course, eventually we'd host the entire MCP (and gui!) remotely ... use any agent they prefer to remote connect into that.. and they'd view the GUI in a browser."): the books stay fully usable from any agent the person runs themselves, locally or (P9) remotely, claude.ai in a browser included; the dock's one extra is pasting the app's suggested instructions straight into its session. A hosted MCP and a browser viewer are the later service (section 11). P11 (the agent dock and books homes). |
|
||||
| D-28 | Who does what, locally (owner, 2026-10-19: "Just have the agent ask for them and the user replies. The interface is through claude/codex and that's that. Don't fucking stray from it. TIN, W-9 is NOT critical or super secret PII." and "for downloading the CPA package or other assets, I don't mind if that can happen through the GUI app ... the GUI is for READING the data. The claude/codex is for MUTATAING (and reading) the data!!!") | **Two interfaces, no third.** Every change to the books goes through the agent (Claude Code or Codex) over the MCP; the agent asks the human for what it needs, TINs and W-9s included, and seals them in the vault (`books.set_tin`, `books.set_w9`): no human web pages, terminal commands or open ports for that locally. Reading and downloading (the CPA package and other exports, receipts, documents) happen in the viewer, which still never writes the books. This supersedes, for local books, the `/h/` pages of 3.3, 4.7 and P7's TIN entry: I-7's "the agent never holds a full TIN" becomes "a TIN passes through the agent only on its way into the vault; no result shows one". `/h/` pages remain for self-hosted books reached remotely (P9). |
|
||||
|
||||
### 10.2 Deferred
|
||||
|
||||
@@ -1217,6 +1238,17 @@ These are the only open owner questions. Ask them just before the move, one at a
|
||||
|
||||
**Q-3. After the move, should the old copies of the books leave the project folder?** The project folder still holds the old journal and receipts (`books-data/`), the old sealed tax ID numbers (`vault/`), and the folder's git history, which contains the journal and receipts too. **Recommended: move them into the private folder's legacy area.** That is what "hidden away" means; otherwise an agent working in the project folder can still read the old copies. Nothing is deleted.
|
||||
|
||||
**Crumbs to clear at the cutover** (owner, 2026-10-19: "migrate the first company books into the new scheme, data directory, clear out the crumbs"). After the adoption verifies, nothing of the old arrangement may be left where an agent or a later setup trips on it; every item is moved per Q-3 (into the root's `legacy/`, nothing deleted) or removed if it holds no books data:
|
||||
- `books-data/` (journal, blobs, database, lock, `gui.log`) and `vault/`, and the project folder's git history holding them (Q-3);
|
||||
- the v1 vault key outside the books, `~/.config/wanderbooks/vault.key` (adoption copies it to the book's `keys/legacy.key`; the old one goes to `legacy/` with the vault);
|
||||
- `wanderbooks.toml` (its settings are in the books since P1; only `[viewer] open_at_start` remains, which the app's own preferences replace);
|
||||
- the scaffold: `justfile`, `scripts/`, `.claude/settings.json`'s SessionStart hook, `.codex/hooks.json`; `AGENTS.md`/`CLAUDE.md` become the 3-line pointer;
|
||||
- the agents' old registrations: Claude Code's stdio `wanderbooks` server from `just mcp-add` (replaced by `wanderbooks connect`), and Codex's;
|
||||
- old binaries: `ledger-mcp`/`ledger-gui` from `just build` or `cargo install` (the one `wanderbooks` executable replaces them);
|
||||
- the viewer's old preferences file, eframe's `app.ron` in `$XDG_DATA_HOME/wanderbooks` (the data root's default place): the viewer moves it on its first run (ledger-gui prefs.rs), and `setup` must not refuse the default place for holding only that file (slice 8 handles it: moved to the viewer's own folder first);
|
||||
- the agent's own notes that name the old paths (its memory), rewritten to the new arrangement.
|
||||
The cutover's check adds: the project folder holds none of the above, and `wanderbooks doctor` is clean.
|
||||
|
||||
*For the engineer:* Q-1 was D-3, Q-2 was D-4 and Q-3 was D-2. Q-1's technical side is settled: `archive.git` is seeded from the existing git history, filtered to the journal and blob paths, includes the vault ciphertext, and is age-encrypted as a whole on push (3.4, 5.5).
|
||||
|
||||
---
|
||||
|
||||
Reference in new issue
Block a user