From d8816f9ee47aa8568873f749e72b5e684e0d3f0f Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Thu, 29 Oct 2026 13:31:42 -0600 Subject: [PATCH] docs: tokens in a file, no keyring (U0e) Deploy recipe loses its D-Bus dance, install.sh its macOS skip, README and site their macOS caveat; packaging, architecture and testing records follow; U0e As built filled in (D-63). Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp --- README.md | 13 +++----- deploy/README.md | 31 +++++------------- deploy/wanderbooks-server.service | 2 +- docs/architecture.md | 2 +- docs/packaging.md | 9 +++--- docs/testing.md | 53 ++++++++++++++++++++++++++++--- install.sh | 11 ++----- plan/served.md | 2 +- site/index.html | 2 +- 9 files changed, 72 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 227292a..748343f 100644 --- a/README.md +++ b/README.md @@ -67,11 +67,9 @@ Sheet is frozen as of 2026-10-11. The remaining work, its order and its hand-off are `plan/served.md` section 8.0; the destination is one installed app for Mac, Windows and Linux (P11), then the hosted service the plan describes and does not build. Today the agent path runs on -Linux: tokens live in the OS keyring (the Secret Service on Linux, the -login Keychain on macOS, the Credential Manager on Windows; the last two -are written and compiled by the CI matrix, P11a step 0, and not yet -proven on a machine of ours); the rest of the host is Unix-only, so -Windows is WSL2 only until its sockets and data root are ported. +Linux and macOS: tokens live in a 0600 file under the private data root +(D-63); the rest of the host is Unix-only, so Windows is WSL2 only until +its sockets and data root are ported. See [`plan/STATUS.md`](plan/STATUS.md) for where things stand and [`plan/roadmap.md`](plan/roadmap.md) for where it's going. @@ -90,10 +88,7 @@ them; nothing needs sudo): curl -fsSL https://wanderbooks.ai/install.sh | sh -s -- acme ``` -(Linux today. On macOS the same line installs and sets up, and the -viewer runs; connecting an agent uses the login Keychain, new with -P11a step 0 and proven only by the CI matrix so far. On Windows run it -inside WSL2.) +(Linux and macOS. On Windows run it inside WSL2.) Already installed, three commands: `wanderbooks setup`, `wanderbooks books create acme`, `wanderbooks connect claude --book acme` (or `connect codex`). diff --git a/deploy/README.md b/deploy/README.md index 31edc85..3a72587 100644 --- a/deploy/README.md +++ b/deploy/README.md @@ -123,27 +123,12 @@ sudo -u wanderbooks -H /usr/local/bin/wanderbooks token print \ --root /srv/wanderbooks --book acme --name laptop ``` -`token create` keeps the text in the server account's OS keyring and -`token print` shows it once; copy it to that client and nowhere else. -Revoke it with `token revoke --book acme --name laptop` when a client is -lost. - -A headless machine has no Secret Service by default, and `token create` -refuses to mint a token it cannot keep. Run the two commands inside a -session that has one, for example: - -```sh -sudo -u wanderbooks -H dbus-run-session -- sh -c ' - gnome-keyring-daemon --unlock --components=secrets >/dev/null - /usr/local/bin/wanderbooks token create --root /srv/wanderbooks --book acme --name laptop - /usr/local/bin/wanderbooks token print --root /srv/wanderbooks --book acme --name laptop -' -``` - -The keyring lives only for that session, which is enough: the text is -copied to the laptop, and the server checks the token's SHA-256 from the -books, not from the keyring. (Making a token without keeping it anywhere -is not a command; if your machine has a persistent Secret Service, use it.) +`token create` keeps the text in a file in the server account's private +data root (mode 0600, readable by that account alone) and `token print` +shows it once; copy it to that client and nowhere else. It works on a +headless machine as it is. The server checks the token's SHA-256 from the +books. Revoke it with `token revoke --book acme --name laptop` when a +client is lost. ## 6. TLS and the door @@ -201,8 +186,8 @@ export WANDERBOOKS_ACME_TOKEN='' ``` The plan's local rule is that no client configuration holds a token (the -bridge reads the keyring). For a remote machine there is no command yet -that keeps a remote token in the client's keyring, so this recipe puts it +bridge reads its token file). For a remote machine there is no command yet +that keeps a remote token in the client's own token file, so this recipe puts it in Claude's configuration (mode 0600) or in Codex's environment. That gap is recorded in `plan/served.md` 8.2 P9a's As-built block. diff --git a/deploy/wanderbooks-server.service b/deploy/wanderbooks-server.service index 8a95a6d..75ae263 100644 --- a/deploy/wanderbooks-server.service +++ b/deploy/wanderbooks-server.service @@ -26,7 +26,7 @@ Type=simple User=wanderbooks Group=wanderbooks DynamicUser=no -# The account's own home (the keyring, when one runs; the recorded root). +# The account's own home (the recorded root). StateDirectory=wanderbooks Environment=HOME=/var/lib/wanderbooks WorkingDirectory=/ diff --git a/docs/architecture.md b/docs/architecture.md index 66e8e7b..d9e3f45 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -36,7 +36,7 @@ the specification in `bookspec/` (which never depends on the engine). | `ledger-import` | formats: QuickBooks CSV in; IIF, XLSX, Typst PDFs (statements, the invoice, labels) out | `ledger-core`, `ledger-reader` | | `ledger-package` | the shared read-only view over `Store` or `Snapshot` (`PackageBooks`): the CPA package, the 1099 worksheet, the invoice document, Needs attention | `ledger-core`, `ledger-store`, `ledger-config`, `ledger-import`, `ledger-vault` | | `ledger-mcp` | the books server: the MCP tools (122), resources, the handbook, scopes, the door and the access log, backups' scheduling | all of the above | -| `ledger-host` | the host and its leases, the agent bridge (`wanderbooks mcp`), tokens in the OS keyring (Secret Service, macOS Keychain, Windows Credential Manager), the owner's commands, uploads and downloads | `ledger-store`, `ledger-mcp`, `ledger-config` | +| `ledger-host` | the host and its leases, the agent bridge (`wanderbooks mcp`), tokens in 0600 files under the data root (D-63), the owner's commands, uploads and downloads | `ledger-store`, `ledger-mcp`, `ledger-config` | | `ledger-gui` | the viewer: read-only, live, egui; the Books menu hands work to the server | `ledger-core`, `ledger-store`, `ledger-config`, `ledger-import`, `ledger-package`, `ledger-vault`, `ledger-host` | | `ledger-cli` | `wanderbooks`, the one executable: every mode (`serve`, `server`, `host`, `mcp`, `gui`, `setup`, `books`, `token`, `connect`, `doctor`, …) and the over-the-wire test suites | everything | | `bookspec/checker`, `bookspec/runner` | the example schema and parser, the rule catalogue, the `Engine` trait; the per-rule runner | nothing of ours (the adapter in `ledger-core`'s tests depends on them) | diff --git a/docs/packaging.md b/docs/packaging.md index 1739682..dba150e 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -21,11 +21,10 @@ table is filled by the person who runs it. ## What step 0 found about the platforms (read before the macOS and Windows rows) -The agent path was Linux-only because of the keyring. Step 0 adds the -macOS login Keychain and the Windows Credential Manager behind -`ledger-host/src/tokens.rs::Keeper`; CI (`portable` job) is the evidence. -Beyond the keyring, the Windows host is a port, not a `cfg` gap. Today -Unix-only, found by grep (2026-10-29): +The agent path was Linux-only because of the keyring; D-63 removed the +keyring on every OS (tokens are 0600 files under the data root, unit U0e), +so nothing about tokens is OS-specific now. The Windows host is a port, +not a `cfg` gap. Today Unix-only, found by grep (2026-10-29): - `ledger-host`: `bridge`, `host`, `start`, `opener` and others are `#[cfg(unix)]` modules; `host.rs`/`start.rs` use `setsid`, `umask`, diff --git a/docs/testing.md b/docs/testing.md index 49a67b2..9699fdd 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -494,7 +494,9 @@ recorded once with refusals, a damaged token log refusing every agent), `open_while_held.rs` (the bridge picking up a token made mid-session), `setup.rs` (the passphrase prompts, at a real terminal). -**One keyring item per token (2026-10-22).** On the live books a +**One keyring item per token (2026-10-22; superseded by U0e, D-63: there +is no keyring, so no second item and no locked or unreachable one, and +the tests named below were replaced, see "U0e" at the end).** On the live books a reconnected bridge got no token from a keyring that couldn't be read, was told to "make a token", and after `token create --replace` ran on a token the keyring no longer gave while `download` read a revoked one. @@ -3086,14 +3088,14 @@ loopback door (`--http`) for Caddy or a tunnel. the repository's `p2-served` fixture into a desktop root at this version, copies the book as the transport (D-31), adopts it into `/srv/wanderbooks` behind `--isolation enforced --http`, mints a token - with the `file_keyring` test knob (a runner has no Secret Service), and - runs the test as the runner. `deploy/README.md` is the same recipe by + with `token create` as on a real server (the knob that once kept it in a + file is gone, U0e: D-63), and runs the test as the runner. `deploy/README.md` is the same recipe by hand; the job does not install the systemd unit (it drives `serve` with the same flags), so the unit's lines are checked by the test's file read and by review, not executed as a unit. - **Not covered:** the recipe walked on a clean VM by someone other than its author (Done when 2), a real TLS tunnel (Tailscale Funnel or Caddy) - and a real Secret Service on a headless server. Those are the owner's + and a headless server of the owner's own. Those are the owner's and the EM's checks; the As-built block says so. - **Docs:** `docs/architecture.md` "Self-host", `docs/mcp-interface.md` (`books.start`'s two `isolation` values), `docs/parity.md` (no semantic @@ -3533,6 +3535,11 @@ arms, the 8 KiB cut, the 64 KiB hand-over threshold and `page - 1`. ## P11a step 0: keyring stores for macOS and Windows (2026-10-29) +*Superseded the same day by D-63 (unit U0e, below): the `file_keyring` +knob, the `Items` trait, the OS stores, `os_keyring_keeps_and_forgets` +and the `portable` job's step that ran it are gone; the tests named here +no longer exist.* + No new test body: the keeper's laws are tested once, over the `Items` trait, through the `file_keyring` knob (`the_test_keyring_keeps_and_forgets`, `two_items_for_one_token_are_said_never_one_of_them_given`, @@ -3549,3 +3556,41 @@ Secret Service run needs a desktop): the CI run is the evidence. **For P12 (mutation targets, none run, D-33):** `tokens.rs`'s `os` modules are not reachable by mutation testing (they need the real OS store); the `Items` logic they share is, through the file store. + +## U0e: tokens in a file, the OS keyring removed (2026-10-29, D-63) + +An agent's token is a 0600 file under the root's `run/tokens/`, on every +OS, written atomically (a temporary file made 0600 before any byte is +written, synced, then renamed over the old one). Tests, all in +`crates/ledger-host/src/tokens.rs` unless noted: +`an_account_names_the_root_by_hash_and_the_book_and_token` (kept); +`the_token_file_keeps_and_forgets` (was `the_test_keyring_keeps_and_forgets`, +no knob); `a_token_file_is_owner_only` (new: mode 0600, folder 0700, a +second create for the same name replaces the first, one file and no +temporary left); `an_unreadable_token_file_is_an_error_never_no_token` +(new: a folder in the file's place; replaces +`a_locked_or_unreachable_keyring_is_an_error_never_no_token`). Deleted +because the case cannot exist with one file per token: +`two_items_for_one_token_are_said_never_one_of_them_given`, +`a_token_kept_is_read_back_or_refused` (the read-back is still done by +`commands.rs::make_token`, and the CLI test below covers a token the file +cannot keep) and the ignored `os_keyring_keeps_and_forgets`. +`bridge.rs`: `a_token_file_that_cant_be_read_is_said_never_make_a_token` +and `each_new_session_reads_the_token_file_again` (renamed from the +keyring versions; the unreadable case is a folder in the file's place). +`crates/ledger-cli/tests/open_while_held.rs`: +`token_check_says_where_the_token_file_stands_and_an_unreadable_one_is_said` +(was `token_check_one_item_per_token_and_a_keyring_that_cant_be_read`; the +second-item parts are gone, the file's mode is asserted) and +`connect_makes_anew_a_token_whose_file_is_gone_and_says_when_an_agent_refuses` +(renamed). `knobs.rs`: `file_keyring` is now refused like any unknown knob. +`two-user.yml` mints its tokens with no knob (provable only on GitHub). + +**For P12 (mutation targets, none run, D-33):** `tokens.rs`'s `Keeper::set` +(the temporary file's mode and the rename: a mutant that writes the final +file directly or with a wider mode must die on `a_token_file_is_owner_only`), +`Keeper::get`'s `NotFound` arm (a mutant that turns any error into "no +token" must die on `an_unreadable_token_file_is_an_error_never_no_token`), +`Keeper::delete`'s `NotFound` arm, and `Account::key`'s three parts +(`an_account_names_the_root_by_hash_and_the_book_and_token`); +`bridge.rs::token_file_refusal`'s `holds_token` guard. diff --git a/install.sh b/install.sh index 62e9b3a..35fdf5d 100755 --- a/install.sh +++ b/install.sh @@ -3,11 +3,8 @@ # # curl -fsSL https://wanderbooks.ai/install.sh | sh -s -- acme # -# Linux, and macOS with one gap: agents' tokens live in the OS keyring and -# only the Linux store exists until the macOS app (plan P11a), so on macOS -# this installs, sets up and starts the books but cannot connect an agent -# yet (it says so instead of failing). On Windows, run it inside WSL2 -# (Ubuntu): the viewer opens as a normal window on Windows 11. +# Linux and macOS. On Windows, run it inside WSL2 (Ubuntu): the viewer +# opens as a normal window on Windows 11. # # Installs Rust if needed, then the one `wanderbooks` program (into # ~/.cargo/bin), then makes the books' private folder (`wanderbooks setup`). @@ -79,9 +76,7 @@ if [ $# -gt 0 ]; then if ! "$wb" books list | cut -d' ' -f1 | grep -qx "$1"; then "$wb" books create "$1" fi - if [ "$os" = macos ]; then - say "Done. Your books \"$1\" exist and 'wanderbooks gui $1' shows them. Connecting Claude Code waits for the macOS keyring store (the app, plan P11a); until then agents connect from Linux." - elif command -v claude >/dev/null 2>&1; then + if command -v claude >/dev/null 2>&1; then "$wb" connect claude --book "$1" say "Done. Start Claude Code and say hello: your books \"$1\" are ready." else diff --git a/plan/served.md b/plan/served.md index 9114227..70ffeb5 100644 --- a/plan/served.md +++ b/plan/served.md @@ -1316,7 +1316,7 @@ Each unit follows the 8.0.4 template. "A-n" is an owner answer numbered in 8.3.1 5. **Files:** owns `crates/ledger-host/src/tokens.rs`; touches `knobs.rs`, `commands.rs`, `bridge.rs`, `crates/ledger-host/Cargo.toml`, `Cargo.lock`, the two CLI tests, `.github/workflows/two-user.yml`, `deploy/README.md`, `install.sh`, `README.md`, `site/index.html`, `docs/packaging.md`, `docs/testing.md`, `docs/architecture.md`. 6. **Tests and proofs:** the existing keeper tests in `tokens.rs` run against the file store (they already do, through the knob: they lose the knob); `a_token_file_is_owner_only` (new, `tokens.rs`: mode 0600 on Unix, created atomically, a second `create` for the same name replaces the first); the bridge and `open_while_held` tests unchanged in what they assert. No kernel, Kani or Lean change. 7. **Done when:** (1) `grep -ri "keyring\|keychain\|secret service\|credential manager" crates/ deploy/ install.sh README.md site/ .github/` finds only the history notes this unit leaves (one sentence in `tokens.rs`'s module doc saying what was removed and why, D-63); (2) `cargo tree -p ledger-host -e normal | grep -i keyring` prints nothing; (3) the named tests pass; (4) `two-user.yml` runs with no knob (the EM sees it green on GitHub); (5) `cargo build --workspace --all-targets` green. -8. **As built:** none yet. +8. **As built:** (2026-10-29, branch `U0e`, EM-approved Option A after a stop) Build steps 1-3 done, with these departures. (1) The unit said the keeper tests and the bridge and `open_while_held` tests were "unchanged in what they assert"; they were not, because several asserted keyring-only states (a second item in another collection, a locked or unreachable keyring), which one file per token cannot have. `tokens.rs` is now direct file get/set/delete: the `Keeper` trait, `Items`, `get_one`/`set_one`/`delete_all`, `KeyringError::Duplicates`, `TEST_LOCKED`/`TEST_UNREACHABLE`/`TEST_DEFAULT` and the `login` collection are gone; `TokenFileError` is one message. The file is `run/tokens/` (the knob's `run/test-keyring/default/` was renamed), the folder 0700, the file made 0600 as a temporary beside it, synced and renamed. (2) Tests: `an_unreadable_token_file_is_an_error_never_no_token` (a folder in the file's place) replaces the locked/unreachable test; `a_token_file_is_owner_only` is new; `two_items_for_one_token_are_said_never_one_of_them_given` and `a_token_kept_is_read_back_or_refused` are deleted (case cannot exist; `make_token` still reads back); `os_keyring_keeps_and_forgets` is deleted; bridge tests renamed to `a_token_file_that_cant_be_read_is_said_never_make_a_token` and `each_new_session_reads_the_token_file_again`; in `open_while_held.rs`, `token_check_one_item_per_token_and_a_keyring_that_cant_be_read` became `token_check_says_where_the_token_file_stands_and_an_unreadable_one_is_said` (second-item and unreachable-marker parts deleted, 0600 mode asserted, unreadable = a folder in the file's place) and `connect_makes_anew_a_token_the_keyring_lost_and_says_when_an_agent_refuses` became `connect_makes_anew_a_token_whose_file_is_gone_and_says_when_an_agent_refuses`. (3) User-facing strings say "the token file in the data root" (no path), in `commands.rs`, `bridge.rs`, `ledger-gui/src/owner.rs`, `ledger-mcp/src/local.rs`, `admin.rs`. (4) Also changed beyond the unit's list: `.github/workflows/check.yml` (the `os_keyring` step of the portable job deleted), `deploy/wanderbooks-server.service` (a comment). (5) `start.rs` and the `DBUS_SESSION_BUS_ADDRESS` scrub and its tests stay: they keep a bridge session from inheriting a D-Bus session for any reason, not only the keyring. (6) Existing Linux books whose tokens were in the Secret Service lose them (nothing migrates): `wanderbooks connect` or `token create --replace` makes them anew. (7) `two-user.yml` has no knob; only GitHub can prove it. Checks: fmt, clippy (`ledger-host`, `ledger-cli`), `cargo test -p ledger-host`, `open_while_held`, `format_upgrade`, workspace build. #### P4b S4: `documents_v2` diff --git a/site/index.html b/site/index.html index e47d4ea..4ef0fe6 100644 --- a/site/index.html +++ b/site/index.html @@ -292,7 +292,7 @@ footer { padding: 72px 0 48px; color: var(--faint); font-size: 14px; display: fl

The one line at the top does everything: installs Rust if you don't have it, builds wanderbooks, makes a private folder for your books, starts them, and connects Claude Code. Nothing needs administrator rights, and nothing keeps running in the background once you're done.

Linux

Needs a C compiler and git (build-essential on Debian and Ubuntu). The installer tells you if anything's missing.

-

macOS

Needs Apple's command line tools: xcode-select --install. The install, your books and the viewer work today; connecting Claude Code or Codex waits for the macOS keyring store, which comes with the app (soon).

+

macOS

Needs Apple's command line tools: xcode-select --install. The install, your books, the viewer and connecting Claude Code or Codex work today.

Windows

Run it inside WSL2 (wsl --install, then Ubuntu). On Windows 11 the viewer opens as a normal window. Native Windows is on the way.

Already installed? wanderbooks books create another, then wanderbooks connect claude --book another. wanderbooks gui opens the viewer; its Books menu backs up, restores, starts new books and closes them.