From 7b1774512b090419d8b0d7e28a97da19e71964a0 Mon Sep 17 00:00:00 2001 From: Stephen Waits Date: Thu, 5 Nov 2026 08:29:20 -0700 Subject: [PATCH] docs: record the in-app accept, the update notice and translocation (P11a) The interface spec gains `books.start`'s allocated `update_waiting` field and says only the owner can accept it. The packaging and architecture notes now describe the lease intent, the viewer's notice and "Check for updates", and the translocated-app refusal instead of listing them as not built. The testing record names each new test and the mutation targets it gives P12. The plan's P11a status and As-built block record what landed and what still waits: the Mac trial, the Windows installer (P11c's) and signing (D-64). --- docs/architecture.md | 16 ++++++++++++++- docs/mcp-interface.md | 9 ++++++++- docs/packaging.md | 33 ++++++++++++++++++++++++++----- docs/testing.md | 45 +++++++++++++++++++++++++++++++++++++++++++ plan/served.md | 4 ++-- 5 files changed, 98 insertions(+), 9 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 40f59e5..0be68b8 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -417,7 +417,19 @@ downgrading is theirs). Anything else (an AppImage, whose file is named by call is cut off), after a second check against the key, or by `wanderbooks update`, the owner's say-so, which reuses the staged copy. A local release is named in `/updates/feed` (a `file://` address, then - the key file), the same rule as `--trusted-key`. + the key file), the same rule as `--trusted-key`; +- **the owner accepts one in the app**: the viewer reads the staged + `meta.json` (`Owner::update_waiting`) and shows a notice with "Update + now", and Books has "Check for updates" (`Owner::check_for_updates`) and + "Update to X"; accepting is the host's lease intent `apply_update`, which + installs the staged copy as `update` does and lets the host's own + replaced-program watch restart onto it once no call runs. `books.start` + carries the same notice to an agent as `update_waiting`, with a next step + to tell the human; no tool accepts it; +- **a translocated macOS app refuses to connect agents** + (`start::translocation_refusal`): a quarantined download runs from a + read-only copy under `AppTranslocation` that vanishes with the app, so + the person moves it out of Downloads first. `deploy/packaging/` holds the Linux packaging: `packager.toml` (cargo-packager: `.deb`, AppImage, the pacman tarball with its PKGBUILD), @@ -428,6 +440,8 @@ throwaway-keyed feed on this machine) and `test-linux-packages.sh`. `.github/workflows/release.yml` builds them on a version tag. The macOS bundle is `packager-macos.toml`, `Info.plist` and `build-macos.sh` (an unsigned `.dmg` from CI's `macos` job; signing and notarization are step 5). +A translocated app (Gatekeeper's copy of a quarantined download) may run +but refuses to connect an agent, saying how to move it out of Downloads. ## The result envelope and the guards (`ledger-mcp`, P5) diff --git a/docs/mcp-interface.md b/docs/mcp-interface.md index 17ffa7a..4212d05 100644 --- a/docs/mcp-interface.md +++ b/docs/mcp-interface.md @@ -124,7 +124,8 @@ settings-and-conventions section says how to use them. codes), backup: { last_at, through_seq, stale }, prompts: [{ name, args }], needs_attention: { items: [{ id, severity, kind, title, tool?, args? }], counts: { kind: n } }, truncated?, handbook_sections, - format_upgrade?: { guard, format, adds, how }, next_steps: [{ tool | + format_upgrade?: { guard, format, adds, how }, update_waiting?: { version, + staged_at, how }, next_steps: [{ tool | prompt, args?, why, required }], isolation }`. The connect text names it as the first call. `book.url` is the book's path on this server (`/b/ID/mcp`), never a path on a disk. `you` is the token's own facts @@ -145,6 +146,12 @@ settings-and-conventions section says how to use them. (below); its next step comes first, before anything else, at every start until it is done: `books.handbook {section: "format-upgrade"}`, "Tell the human first: …". + `update_waiting` is there only while the host has downloaded and checked + a newer version of wanderbooks (`/updates/staged/`): its version, + when it was staged (UTC, RFC 3339) and how it may be accepted. Only the + owner can accept it, in the app (Books, then Update) or with + `wanderbooks update` at a terminal; no tool can. Its next step asks the + agent to tell the human, and nothing changes until they accept. `isolation` is `same_user (advisory)` locally (D-32): the books are hidden from agents in the owner's own account, not locked against them. On the owner's own machine, where the server runs as a separate account diff --git a/docs/packaging.md b/docs/packaging.md index 8e5c03f..136fecc 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -135,8 +135,17 @@ subset. **Not checked: that the `.dmg` builds, mounts or runs**: that needs the CI run (Actions, release, Run workflow, this branch) and then a Mac. Open items for the trial on a Mac: whether `wanderbooks update` replacing the program inside a signed `.app` breaks its signature (it will: step 5 -must decide, probably by updating a whole `.app`); the translocated-app -check (`translocated_app_refuses_to_connect`) is not built. +must decide, probably by updating a whole `.app`). **Translocation +(P11a step 6):** a `.dmg` downloaded from the internet keeps its +quarantine bit, so Gatekeeper may run the app from a read-only copy under +`/private/var/folders/…/AppTranslocation/…` that goes away when it +closes. `wanderbooks connect` (and the viewer's Connect, which is the same +function) refuses there, before a token is made or an agent's list is +touched, and says to move the app out of Downloads and open it again: +`ledger_host::start::translocation_refusal` reads the path the program +would give the agent. Checked on Linux by standing in for that path +(`translocated_app_refuses_to_connect`, `only_a_translocated_path_refuses_an_agent`); +the real Gatekeeper copy still needs the trial on a Mac. ## Updates staged by the host (step 4) @@ -148,9 +157,23 @@ first; `wanderbooks update` applies it too and reuses the staged copy; `update --check` says "already downloaded". A local release is named by `/updates/feed` (a `file://` address, then the key file). Without a compiled-in release key and without that file, the look does nothing but -log why. Not built: the in-app "restart now to update" choice and the -viewer's notice, which need an admin-socket intent and a place in the -viewer (see the unit's As-built block). +log why. + +The owner accepts a staged update **in the app** (P11a step 4). The +viewer reads `/updates/staged/meta.json` every few seconds +(`Owner::update_waiting`) and shows a standing notice above the page, +"Version X of wanderbooks is ready", with "Update now"; Books also has +"Check for updates" (`Owner::check_for_updates`, which looks at the feed +at once whatever the daily look did) and "Update to X" while one waits. +Accepting sends the host's lease intent `apply_update`; the host installs +the staged copy exactly as `wanderbooks update` does and its existing +watch for a replaced program (host.rs INVARIANT 7) restarts onto it once +no call is running, the books staying open and every holder's session +carrying on. With no host running there is no call to cut off, so the +viewer applies it itself. `books.start` tells an agent the same thing +(`update_waiting`) and adds a next step to tell the human; no tool can +accept it. The accepted update replaces the program file, not a whole +`.app`, which step 5 must revisit for a signed bundle. ## What `update` and `rollback` need from a release diff --git a/docs/testing.md b/docs/testing.md index 403f142..4285dcd 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -3823,6 +3823,51 @@ add), `swap_in`'s temporary-then-rename (`update_is_atomic`), `healthy` (`failed_update_rolls_back`), `install_kind` (the marker test), and `parse_version`'s rejections. +## P11a steps 4 and 6: the in-app accept, the viewer's notice, translocation (2026-11-05) + +The accept is the host's lease intent `apply_update` (the name +`plan/served.md` 8.0.3 allocates); the viewer's names are +`Owner::update_waiting`, `Owner::check_for_updates` and `Owner::apply_update`, +and the agent's is `books.start`'s `update_waiting`. + +- `crates/ledger-cli/tests/update.rs` (Linux): + `books_start_says_an_update_waits` — a quiet `books.start` has no + `update_waiting`; after the host stages a signed 9.0.0, `books.start` + names its version and staged time and says only the owner accepts it, + its next step says to tell the human, and no path appears in the answer; + `the_owner_accepts_a_staged_update_in_the_app` — accepting with nothing + staged is refused and changes nothing; once 9.0.0 is staged, the same + intent (`opener::ask_once_within` with `Intent::ApplyUpdate`, what the + viewer's `apply_update` sends) installs it, the program becomes the + release's bytes, the old version is kept under `updates/previous/`, and + an agent's bridge writes all through the host's restart onto it: every + account is in the chart exactly once. +- `crates/ledger-gui/tests/update_notice.rs`: + `the_menu_checks_for_updates_and_a_waiting_update_shows_a_notice_that_accepts` + (Books, Check for updates asks the owner and its words show; the standing + notice names the version; Update now asks the owner to apply it, and the + notice goes once it is installed) and + `books_outside_a_data_root_have_no_update_notice`. +- `crates/ledger-cli/tests/translocation.rs`: + `translocated_app_refuses_to_connect` — with the program's path standing + in for Gatekeeper's copy (`APPIMAGE` names a file under + `AppTranslocation`), `connect claude` is refused before a token is made + or a home is made, and the stand-in agent never runs; the same program at + an ordinary path connects. `crates/ledger-host/src/start.rs`: + `only_a_translocated_path_refuses_an_agent` (the marker is a path part, + not letters in a name). + +Not checked here: the real Gatekeeper copy and the `.dmg` (needs a Mac, +the owner's trial); the Windows installer job is P11c's, not on this branch. + +**For P12 (mutation targets, none run, D-33):** `update_waiting`'s +"newer than this program" comparison (a mutant that offers the running +version must die on `books_start_says_an_update_waits`'s quiet first +call), `staged_update_in`'s plausibility check (a staged version with a +path separator must not become a notice), and `translocation_refusal`'s +component comparison (a part that merely contains the word must not +refuse). + ## P5: the agent surface (2026-10-29) Every tool result carries the envelope (`next_steps`, `guards`, `warnings`, diff --git a/plan/served.md b/plan/served.md index 4e2966c..bf79eca 100644 --- a/plan/served.md +++ b/plan/served.md @@ -1645,7 +1645,7 @@ Each unit follows the 8.0.4 template. "A-n" is an owner answer numbered in 8.3.1 #### P11a: packaging and updates (D-19, D-22, D-24, D-25, D-26) -1. **Status:** in progress: step 0 landed 2026-10-29 (`5e220cca`) as far as the macOS store and the portable CI job; its keyring stores are removed by U0e (D-63), and the Windows half of step 0 moved to P11c (D-62). Steps 1 to 7 open, macOS and Linux first. P11 was split 2026-10-26 (D-54): P11a is installers and updates; P11b the agent dock and books homes. +1. **Status:** in progress: steps 0-4 and the macOS half of step 6 landed 2026-10-29 (the AppImage restart, the daily staged update, the unsigned `.dmg` job); the in-app accept, the viewer's update notice and "Check for updates", and the translocation refusal landed 2026-11-05. Step 5 (signing) is deferred by D-64; left: the release workflow's first run (the owner's click), the Windows installer (P11c's), the trials on a Mac and a Windows machine. P11 was split 2026-10-26 (D-54): P11a is installers and updates; P11b the agent dock and books homes. 2. **Depends on:** none in code. From the owner before the signing steps: the Apple Developer ID certificate and notarization credentials as CI secrets; the Windows signing identity (Azure Artifact Signing if eligible, else an OV certificate); GitHub Releases enabled on the public mirror; the release hardware token; clean VMs or runners for the install tests. Q-4 (keep the two-key release manifest of D-25, or minisign alone) before step 5. 3. **Feature and schema:** none. 4. **Build:** @@ -1660,7 +1660,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/update.rs`, `deploy/packaging/*`, `.github/workflows/release.yml`, `docs/packaging.md`; touches `commands.rs`, `crates/ledger-gui/src/owner.rs`, `mise.toml`. 6. **Tests and proofs:** named above. 7. **Done when:** (1) on a clean Linux VM the AppImage installs, opens, onboards and posts an entry with no terminal and no admin password, and updates itself from a test feed; (2) on Arch, Debian and Fedora the packages install and upgrade with the head unchanged; (3) an update leaves every book's head and goldens unchanged and a bridge session carries on across it; (4) a backup made on one system restores on another; (5) macOS and Windows: the same as (1), once the signing prerequisites exist (record which are done in the As-built block); (6) `just check` green. -8. **As built:** (steps 0 to 3 as far as stated, 2026-10-29; steps 4 to 7 are later waves.) **Step 0, partly.** Built: the macOS login Keychain store (`apple-native-keyring-store` 1.0.2, feature `keychain`) and the Windows Credential Manager store (`windows-native-keyring-store` 1.1.0) behind `tokens.rs::Keeper`, on the same `Items` trait and the same laws as the Secret Service (`os` modules per OS; shared `refused`, `make_in`, `read_item`, `remove_item`); `NOT_YET` now only for other OSes; the ignored `os_keyring_keeps_and_forgets` compiles on all three; the `portable` job in `.github/workflows/check.yml` (`macos-latest` gating, `windows-latest` allowed to fail) builds `ledger-cli --no-default-features`, `cargo build --workspace --all-targets`, `cargo test --workspace` and the ignored keyring test. Checked here: Linux only (fmt, clippy, the tokens tests); no macOS/Windows target is installed, so both stores and the job are proven only by the first CI run. **Not built, and not a `cfg` gap: the Windows host.** Windows data root from the known-folder API with an owner-only DACL, named pipes with a SID check, `flock` and the Unix-only `host`, `start`, `bridge`, `opener` and `local`/`admin` sockets are a port of several crates (inventory in `docs/packaging.md`); macOS needs the CI run to show which Linux-only items (`seccomp` in `refuse.rs`, `renameat2`, `close_range`) need fences. So the step 0 Done-when ("setup ... gui work on each OS") is open for both. **Step 1 (the trial): not run in that wave**: it needs clean macOS, Windows and Linux machines; `mise.toml` now pins `cargo-packager` 0.11.8, `cargo-packager-updater` 0.2.3, `cargo-generate-rpm` 0.21.1 and `docs/packaging.md` holds the empty table and what is known. **Steps 1 (Linux half), 2 and 3 (2026-10-29, branch `P11a`).** *Pins:* the EM removed step 0's early pins; `mise.toml` now pins `cargo-packager` 0.11.8 and `cargo-generate-rpm` 0.21.1, which `just package-linux` calls; **`cargo-packager-updater` is a library, not a tool, so it is not pinned** (the plan's three-tool list was wrong on that); `update.rs` verifies with `minisign-verify` 0.3 (the crate the updater uses) and the tests sign with the `minisign` 0.10 dev-dependency. *Step 1:* `docs/packaging.md` has the Linux rows filled from real builds (47 MB .deb, 36 MB .rpm, 44 MB AppImage, the pacman tarball and PKGBUILD) and eight findings; the `.dmg` and NSIS rows stay "(trial)" (need a Mac and Windows; Windows is P11c). The findings that shaped the build: a binary linked on this machine (glibc 2.44) does not start on Debian 13, so packages are built on `ubuntu-24.04` in CI and in `rust:1.99-trixie` locally (`just release-trixie`); the books refuse to run under Docker's default seccomp filter (tests use `seccomp=unconfined`); an AppImage's launcher appends arguments to the desktop file's `Exec`, so the desktop files say plain `wanderbooks` and a bare launch with no terminal is the viewer (`main.rs::is_bare_launch`); the AppImage needs a square icon (**a placeholder PNG is committed; the owner should replace it**). *Step 2:* `crates/ledger-host/src/update.rs` (`update`, `rollback`) and the two CLI modes: a manifest (version; per platform `url`, `sha256`, `signature` = base64 of the minisign `.sig`, the shape cargo-packager signs in), accepted only if higher, sha256 equal AND signature valid against `WANDERBOOKS_RELEASE_PUBKEY` (compiled in; **no build carries one until step 5**, so today a build updates only from a `file://` feed with `--trusted-key`); stage under `/updates//`, keep the running version under `updates/previous/` with the journal format it knew, swap by renaming a complete copy made beside the target, `--help` health check with automatic put-back; `rollback` refuses when a book declares a format or feature the kept version lacks; a package marks itself with `share/wanderbooks/install-method` and then both refuse. The restart is the host's existing one (nothing new in `host.rs`). Tests: `crates/ledger-cli/tests/update.rs` (`update_is_atomic`, `failed_update_rolls_back`, `unsigned_update_refused`, `bridge_survives_update`, `update_waits_for_a_running_call`, `package_managed_install_refuses_update`, `rollback_refuses_after_a_book_consented_to_a_newer_format`, `an_update_replaces_the_program_keeps_the_old_one_and_rollback_brings_it_back`) all pass (8 in 12 s); five unit tests in `update.rs`. *Step 3:* `deploy/packaging/` (`packager.toml`, the rpm table in `crates/ledger-cli/Cargo.toml`, `aur/PKGBUILD`, `build-linux.sh`, `repo-indexes.sh`, `local-release.sh`, `test-linux-packages.sh`), the `just` recipes `package-linux`, `local-release`, `release-trixie`, `test-linux-packages`, and `.github/workflows/release.yml` (Linux packages on a `v*` tag, attached to the release, apt and dnf indexes pushed to `gh-pages`; untested, GitHub only). `linux_packages_install_and_upgrade` is `test-linux-packages.sh`: install 0.1.0 from an apt, a dnf repository and `makepkg` over the AUR PKGBUILD in Debian trixie, Fedora and Arch containers, hold books open as an ordinary user, upgrade to 0.1.1, host restarted onto the new file, journal head unchanged, `update` refuses: passes on all three. By hand: the AppImage updated itself and rolled back against a feed signed by `cargo packager signer` (`just local-release`). *Done-when so far:* (1) **partly**: the AppImage opens (bare launch) and updates itself from a test feed, but "onboards and posts an entry with no terminal" is false today (the viewer opens only an existing book and `setup`/`books create` need a terminal; not in these steps), and a held-open books' host does not restart onto an updated AppImage (it runs from the old image's mount; fix needs the host to learn the image's path, a trust decision left to the EM); (2) true on Debian, Fedora and Arch containers; (3) true for a hand-placed program (`bridge_survives_update`) and for packages (the container test), not yet for the AppImage's host; (4) not touched; (5) and (6) not done (macOS/Windows need machines and signing; `just check` is the EM's). *Not done, as instructed:* step 4 (daily check, staged update applied at restart), step 5 (signing, Q-4), step 6 (macOS and Windows bundles), step 7 (architecture note is written; the website and `docs/parity.md` are not touched, since nothing is published). **Step 3's AppImage gap, step 4 and the macOS half of step 6 (2026-10-29, branch `P11a`).** *AppImage restart (EM decision):* `start.rs` lets `APPIMAGE` into the host's environment only when it is an absolute file (`own_appimage`; read about the host itself, never told to an agent, I-2), `this_executable()` returns that image, and `host.rs` tells a replaced program by (dev, inode, size, mtime) instead of (dev, inode); test `a_replaced_appimage_restarts_the_books` (the programs run from one copy, `APPIMAGE` names another). Not run against a real FUSE mount. *Step 4:* `update.rs::stage_if_due` (the host's look, at most once per `update_check` knob, default 24 h, noted in `updates/checked`; from a thread of the host's loop; downloads, checks sha256 and signature, writes `/updates/staged/{wanderbooks,meta.json}`; never swaps), `apply_staged` (called by `start.rs::start_host` before it starts a host, so never during a call; re-checks the signature; a staged file that fails or does not start is dropped), `update` reuses a staged copy and `--check` says it is downloaded, `/updates/feed` names a local `file://` feed and its key (INVARIANT 8's rule), `set_program_version` because packages rewrite only the cli's version. Tests in `crates/ledger-cli/tests/update.rs`: `the_host_stages_a_daily_update_and_the_next_start_applies_it`, `a_daily_update_that_is_not_ours_is_not_staged`, `update_installs_the_copy_the_host_staged_without_downloading_again`, `a_staged_update_that_was_tampered_with_is_dropped_not_applied`; all 13 in the file pass; `ledger-host` lib tests pass (74). **Not built, needs a name or a decision:** "applied at the next restart the owner accepts in the app" and the `books.start`/viewer notice. The accept is a new lease/admin intent (`Intent` in `lease.rs`) and the notice a place in the viewer (P11b's files); neither name is allocated in 8.0.3, so the owner accepts today with `wanderbooks update`, and `update --check` and the host log say a version is waiting. *macOS bundle:* `deploy/packaging/{packager-macos.toml,Info.plist,build-macos.sh}`, `just package-macos`, a `macos` job in `release.yml` (`macos-latest`, Apple silicon, uploads the `.dmg` as run artifact `macos-dmg-unsigned`, attaches it on a tag), `docs/packaging.md` macOS rows "built in CI, unsigned; the trial on a Mac is the owner's". Checked on Linux only: the config parses under cargo-packager 0.11.8's strict schema; the `.dmg` itself is unverified until a CI run (the owner: Actions > release > Run workflow > branch `P11a`; the EM pushes the branch first). Not built: translocated-app refusal, "Check for updates" in the viewer's menu, the Intel build, the Windows installer (P11c), signing (step 5). Known issue for step 5: `update` replaces the program inside the `.app`, which breaks a signed bundle's seal; step 5 must update a whole `.app`. +8. **As built:** (steps 0 to 3 as far as stated, 2026-10-29; steps 4 to 7 are later waves.) **Step 0, partly.** Built: the macOS login Keychain store (`apple-native-keyring-store` 1.0.2, feature `keychain`) and the Windows Credential Manager store (`windows-native-keyring-store` 1.1.0) behind `tokens.rs::Keeper`, on the same `Items` trait and the same laws as the Secret Service (`os` modules per OS; shared `refused`, `make_in`, `read_item`, `remove_item`); `NOT_YET` now only for other OSes; the ignored `os_keyring_keeps_and_forgets` compiles on all three; the `portable` job in `.github/workflows/check.yml` (`macos-latest` gating, `windows-latest` allowed to fail) builds `ledger-cli --no-default-features`, `cargo build --workspace --all-targets`, `cargo test --workspace` and the ignored keyring test. Checked here: Linux only (fmt, clippy, the tokens tests); no macOS/Windows target is installed, so both stores and the job are proven only by the first CI run. **Not built, and not a `cfg` gap: the Windows host.** Windows data root from the known-folder API with an owner-only DACL, named pipes with a SID check, `flock` and the Unix-only `host`, `start`, `bridge`, `opener` and `local`/`admin` sockets are a port of several crates (inventory in `docs/packaging.md`); macOS needs the CI run to show which Linux-only items (`seccomp` in `refuse.rs`, `renameat2`, `close_range`) need fences. So the step 0 Done-when ("setup ... gui work on each OS") is open for both. **Step 1 (the trial): not run in that wave**: it needs clean macOS, Windows and Linux machines; `mise.toml` now pins `cargo-packager` 0.11.8, `cargo-packager-updater` 0.2.3, `cargo-generate-rpm` 0.21.1 and `docs/packaging.md` holds the empty table and what is known. **Steps 1 (Linux half), 2 and 3 (2026-10-29, branch `P11a`).** *Pins:* the EM removed step 0's early pins; `mise.toml` now pins `cargo-packager` 0.11.8 and `cargo-generate-rpm` 0.21.1, which `just package-linux` calls; **`cargo-packager-updater` is a library, not a tool, so it is not pinned** (the plan's three-tool list was wrong on that); `update.rs` verifies with `minisign-verify` 0.3 (the crate the updater uses) and the tests sign with the `minisign` 0.10 dev-dependency. *Step 1:* `docs/packaging.md` has the Linux rows filled from real builds (47 MB .deb, 36 MB .rpm, 44 MB AppImage, the pacman tarball and PKGBUILD) and eight findings; the `.dmg` and NSIS rows stay "(trial)" (need a Mac and Windows; Windows is P11c). The findings that shaped the build: a binary linked on this machine (glibc 2.44) does not start on Debian 13, so packages are built on `ubuntu-24.04` in CI and in `rust:1.99-trixie` locally (`just release-trixie`); the books refuse to run under Docker's default seccomp filter (tests use `seccomp=unconfined`); an AppImage's launcher appends arguments to the desktop file's `Exec`, so the desktop files say plain `wanderbooks` and a bare launch with no terminal is the viewer (`main.rs::is_bare_launch`); the AppImage needs a square icon (**a placeholder PNG is committed; the owner should replace it**). *Step 2:* `crates/ledger-host/src/update.rs` (`update`, `rollback`) and the two CLI modes: a manifest (version; per platform `url`, `sha256`, `signature` = base64 of the minisign `.sig`, the shape cargo-packager signs in), accepted only if higher, sha256 equal AND signature valid against `WANDERBOOKS_RELEASE_PUBKEY` (compiled in; **no build carries one until step 5**, so today a build updates only from a `file://` feed with `--trusted-key`); stage under `/updates//`, keep the running version under `updates/previous/` with the journal format it knew, swap by renaming a complete copy made beside the target, `--help` health check with automatic put-back; `rollback` refuses when a book declares a format or feature the kept version lacks; a package marks itself with `share/wanderbooks/install-method` and then both refuse. The restart is the host's existing one (nothing new in `host.rs`). Tests: `crates/ledger-cli/tests/update.rs` (`update_is_atomic`, `failed_update_rolls_back`, `unsigned_update_refused`, `bridge_survives_update`, `update_waits_for_a_running_call`, `package_managed_install_refuses_update`, `rollback_refuses_after_a_book_consented_to_a_newer_format`, `an_update_replaces_the_program_keeps_the_old_one_and_rollback_brings_it_back`) all pass (8 in 12 s); five unit tests in `update.rs`. *Step 3:* `deploy/packaging/` (`packager.toml`, the rpm table in `crates/ledger-cli/Cargo.toml`, `aur/PKGBUILD`, `build-linux.sh`, `repo-indexes.sh`, `local-release.sh`, `test-linux-packages.sh`), the `just` recipes `package-linux`, `local-release`, `release-trixie`, `test-linux-packages`, and `.github/workflows/release.yml` (Linux packages on a `v*` tag, attached to the release, apt and dnf indexes pushed to `gh-pages`; untested, GitHub only). `linux_packages_install_and_upgrade` is `test-linux-packages.sh`: install 0.1.0 from an apt, a dnf repository and `makepkg` over the AUR PKGBUILD in Debian trixie, Fedora and Arch containers, hold books open as an ordinary user, upgrade to 0.1.1, host restarted onto the new file, journal head unchanged, `update` refuses: passes on all three. By hand: the AppImage updated itself and rolled back against a feed signed by `cargo packager signer` (`just local-release`). *Done-when so far:* (1) **partly**: the AppImage opens (bare launch) and updates itself from a test feed, but "onboards and posts an entry with no terminal" is false today (the viewer opens only an existing book and `setup`/`books create` need a terminal; not in these steps), and a held-open books' host does not restart onto an updated AppImage (it runs from the old image's mount; fix needs the host to learn the image's path, a trust decision left to the EM); (2) true on Debian, Fedora and Arch containers; (3) true for a hand-placed program (`bridge_survives_update`) and for packages (the container test), not yet for the AppImage's host; (4) not touched; (5) and (6) not done (macOS/Windows need machines and signing; `just check` is the EM's). *Not done, as instructed:* step 4 (daily check, staged update applied at restart), step 5 (signing, Q-4), step 6 (macOS and Windows bundles), step 7 (architecture note is written; the website and `docs/parity.md` are not touched, since nothing is published). **Step 3's AppImage gap, step 4 and the macOS half of step 6 (2026-10-29, branch `P11a`).** *AppImage restart (EM decision):* `start.rs` lets `APPIMAGE` into the host's environment only when it is an absolute file (`own_appimage`; read about the host itself, never told to an agent, I-2), `this_executable()` returns that image, and `host.rs` tells a replaced program by (dev, inode, size, mtime) instead of (dev, inode); test `a_replaced_appimage_restarts_the_books` (the programs run from one copy, `APPIMAGE` names another). Not run against a real FUSE mount. *Step 4:* `update.rs::stage_if_due` (the host's look, at most once per `update_check` knob, default 24 h, noted in `updates/checked`; from a thread of the host's loop; downloads, checks sha256 and signature, writes `/updates/staged/{wanderbooks,meta.json}`; never swaps), `apply_staged` (called by `start.rs::start_host` before it starts a host, so never during a call; re-checks the signature; a staged file that fails or does not start is dropped), `update` reuses a staged copy and `--check` says it is downloaded, `/updates/feed` names a local `file://` feed and its key (INVARIANT 8's rule), `set_program_version` because packages rewrite only the cli's version. Tests in `crates/ledger-cli/tests/update.rs`: `the_host_stages_a_daily_update_and_the_next_start_applies_it`, `a_daily_update_that_is_not_ours_is_not_staged`, `update_installs_the_copy_the_host_staged_without_downloading_again`, `a_staged_update_that_was_tampered_with_is_dropped_not_applied`; all 13 in the file pass; `ledger-host` lib tests pass (74). **Not built, needs a name or a decision:** "applied at the next restart the owner accepts in the app" and the `books.start`/viewer notice. The accept is a new lease/admin intent (`Intent` in `lease.rs`) and the notice a place in the viewer (P11b's files); neither name is allocated in 8.0.3, so the owner accepts today with `wanderbooks update`, and `update --check` and the host log say a version is waiting. *macOS bundle:* `deploy/packaging/{packager-macos.toml,Info.plist,build-macos.sh}`, `just package-macos`, a `macos` job in `release.yml` (`macos-latest`, Apple silicon, uploads the `.dmg` as run artifact `macos-dmg-unsigned`, attaches it on a tag), `docs/packaging.md` macOS rows "built in CI, unsigned; the trial on a Mac is the owner's". Checked on Linux only: the config parses under cargo-packager 0.11.8's strict schema; the `.dmg` itself is unverified until a CI run (the owner: Actions > release > Run workflow > branch `P11a`; the EM pushes the branch first). Not built: translocated-app refusal, "Check for updates" in the viewer's menu, the Intel build, the Windows installer (P11c), signing (step 5). Known issue for step 5: `update` replaces the program inside the `.app`, which breaks a signed bundle's seal; step 5 must update a whole `.app`. **The in-app accept, the viewer's notice and the translocation refusal (2026-11-05, branch `P11a`).** The accept is the allocated `Intent::ApplyUpdate` on the lease socket (three intents now); `host.rs` installs the staged copy from a thread and lets its replaced-program watch (INVARIANT 7) restart onto it once no call runs, and `commands.rs::apply_update` asks the host, applying directly only when no host runs (no call can be cut off). `owner.rs` reads `/updates/staged/meta.json` through `Owner::update_waiting` (every five seconds), shows a standing notice above the page with "Update now", and Books has "Check for updates" (`Owner::check_for_updates`, which looks at the feed at once whatever the daily look did) and "Update to X". `books.start` gains `update_waiting {version, staged_at, how}` (`kept.rs`) and a next step telling the agent to tell the human; no tool accepts. Tests: `books_start_says_an_update_waits`, `the_owner_accepts_a_staged_update_in_the_app` (`ledger-cli/tests/update.rs`), `the_menu_checks_for_updates_and_a_waiting_update_shows_a_notice_that_accepts`, `books_outside_a_data_root_have_no_update_notice` (`ledger-gui/tests/update_notice.rs`). **Translocation (step 6):** `start.rs::translocation_refusal` refuses `connect_agent` (the CLI's `connect` and the viewer's Connect) when the program's path has an `AppTranslocation` part, before a token or an agent's list is touched; tested on Linux by `translocated_app_refuses_to_connect` (standing in with `APPIMAGE`) and `only_a_translocated_path_refuses_an_agent`. **Still not built:** the real macOS trial, the Windows installer job (P11c), signing (step 5). The pause note expected a Windows installer job in the checkpoint; it held none, and P11c owns the Windows installer. #### P11b: the agent dock and books homes (D-27)