P2 slice 8: the viewer's menu, setup, no confinement (D-32)

No Landlock, no seccomp, no helper: isolation is "same_user
(advisory)". The lease socket keeps two intents (close, format-upgrade
consent). The viewer holds the books open while it runs and gains a
Books menu: New books, Back up now, Restore (preview, then confirm),
Close, Connect Claude Code / Codex. New commands `wanderbooks books
back-up` and `wanderbooks connect claude|codex`. The scaffold
(`new`/`init` and its templates) is gone; install.sh needs no sudo.

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-20 11:14:22 -06:00
1 parent 019d8d76a3
commit 3cf359a0cd
28 files changed
+1976 -1931

No files matched your search

+247 -13
View File
@@ -170,7 +170,7 @@ impl Drop for Home {
let deadline = Instant::now() + Duration::from_secs(10);
loop {
let mut found = false;
for mode in ["serve", "mcp", "helper", "host", "server"] {
for mode in ["serve", "mcp", "host", "server"] {
for pid in self.pids(mode) {
found = true;
// SAFETY: a process of this test's own root.
@@ -922,19 +922,15 @@ fn the_lease_socket_takes_only_the_owners_intents() {
),
(
"{\"proto\":1,\"intent\":{\"back_up_now\":{\"book\":\"acme\"}}}\n",
"books.back_up_now",
"not understood",
),
(
"{\"proto\":1,\"intent\":{\"restore\":{\"source\":\"/x\",\"id\":\"acme\",\"replace\":true}}}\n",
"wanderbooks books restore",
"{\"proto\":1,\"intent\":{\"restore\":{\"source\":\"/x\",\"id\":\"acme\"}}}\n",
"not understood",
),
(
"{\"proto\":1,\"intent\":{\"new_books\":{\"id\":\"beta\",\"currency\":\"USD\"}}}\n",
"new books beta made",
),
(
"{\"proto\":1,\"intent\":{\"new_books\":{\"id\":\"beta\",\"currency\":\"USD\"}}}\n",
"already books called",
"not understood",
),
] {
let answer = home.lease_line(line);
@@ -946,7 +942,7 @@ fn the_lease_socket_takes_only_the_owners_intents() {
}
let out = home.books(&["list"]);
assert!(
String::from_utf8_lossy(&out.stdout).contains("beta open"),
!String::from_utf8_lossy(&out.stdout).contains("beta"),
"{out:?}"
);
}
@@ -1920,9 +1916,8 @@ fn the_books_command_takes_its_flags_and_asks_before_adopting() {
/// A program that starts the bridge with its stderr closed (some launchers
/// do) still gets a working bridge, its descriptor 2 on /dev/null: Rust's
/// runtime opens it before main, so the helper's socket can't take its
/// number and be sent the error messages (asked in slice 3's mutation
/// testing: the helper's descriptor 3 then is never its own pair's end).
/// runtime opens it before main, so no socket the bridge opens can take
/// its number and be sent the error messages.
#[test]
fn a_bridge_started_without_stderr_keeps_its_descriptors_apart() {
use std::os::unix::process::CommandExt;
@@ -2464,3 +2459,242 @@ fn a_backup_is_restored_from_the_command_line() {
assert_eq!(start["head"]["seq"].as_u64(), Some(head), "{start}");
fresh.end();
}
/// A stand-in for an agent's own command line (`claude`, `codex`): it
/// notes each call's arguments, a line each.
fn fake_agent(home: &Home, name: &str) -> (PathBuf, PathBuf) {
use std::os::unix::fs::PermissionsExt;
let bin = home.dir.path().join("agent-bin");
std::fs::create_dir_all(&bin).unwrap();
let log = home.dir.path().join(format!("{name}.log"));
let script = bin.join(name);
std::fs::write(
&script,
format!("#!/bin/sh\nprintf '%s\\n' \"$*\" >> '{}'\n", log.display()),
)
.unwrap();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
(bin, log)
}
#[test]
fn an_agent_is_connected_and_reaches_the_books_by_what_it_was_given() {
let home = Home::new("connect");
let out = home.books(&["create", "beta"]);
assert!(out.status.success(), "{out:?}");
let (bin, log) = fake_agent(&home, "claude");
let connect = |agent: &str, path: &Path| {
home.cmd("connect")
.args([agent, "--book", "beta"])
.env("PATH", format!("{}:/usr/bin:/bin", path.display()))
.stdin(Stdio::null())
.output()
.unwrap()
};
let out = connect("claude", &bin);
assert!(out.status.success(), "{out:?}");
let said = String::from_utf8_lossy(&out.stdout);
assert!(said.contains("Made the books beta' agent token"), "{said}");
assert!(
said.contains("Claude Code reaches the books beta"),
"{said}"
);
let calls = std::fs::read_to_string(&log).unwrap();
let lines: Vec<&str> = calls.lines().collect();
assert_eq!(lines[0], "mcp remove --scope user wanderbooks-beta");
let add = lines[1];
assert!(
add.starts_with("mcp add --scope user wanderbooks-beta -- "),
"{add}"
);
assert!(add.contains(" mcp --book beta --root "), "{add}");
assert!(!said.contains("wb_"), "a token's text was shown");
// The command it was given reaches the books (with the token kept).
let given: Vec<&str> = add.split(" -- ").nth(1).unwrap().split(' ').collect();
let mut c = Command::new(given[0]);
c.args(&given[1..])
.env_clear()
.env("PATH", "/usr/bin:/bin")
.env("HOME", home.dir.path());
let mut agent = Agent::from(c);
agent.initialize();
let start = agent.call("books.start", json!({})).unwrap();
assert!(start["head"]["seq"].as_u64().is_some(), "{start}");
agent.end();
// Again: the token kept is used, not made anew.
let out = connect("claude", &bin);
assert!(out.status.success(), "{out:?}");
assert!(
!String::from_utf8_lossy(&out.stdout).contains("Made the books"),
"{out:?}"
);
// Codex: its own list, no scope.
let (bin, log) = fake_agent(&home, "codex");
let out = connect("codex", &bin);
assert!(out.status.success(), "{out:?}");
let calls = std::fs::read_to_string(&log).unwrap();
assert!(
calls
.lines()
.nth(1)
.unwrap()
.starts_with("mcp add wanderbooks-beta -- "),
"{calls}"
);
// An agent that isn't installed: said. (Never where a real one could
// be found and run: skipped on a machine with one installed system-wide.)
if ["/usr/local/bin/claude", "/opt/homebrew/bin/claude"]
.iter()
.any(|p| Path::new(p).exists())
{
return;
}
let empty = home.dir.path().join("empty-bin");
std::fs::create_dir_all(&empty).unwrap();
let out = home
.cmd("connect")
.args(["claude", "--book", "beta"])
.env("PATH", empty.display().to_string())
.stdin(Stdio::null())
.output()
.unwrap();
assert_eq!(out.status.code(), Some(1));
assert!(
String::from_utf8_lossy(&out.stderr).contains("isn't installed"),
"{out:?}"
);
}
#[test]
fn books_are_backed_up_now_from_the_command_line() {
let home = Home::new("back-up");
home.create("acme");
// None set up: said, and failing.
let out = home.books(&["back-up", "acme"]);
assert_eq!(out.status.code(), Some(1), "{out:?}");
assert!(
String::from_utf8_lossy(&out.stderr).contains("No backups are set up"),
"{out:?}"
);
let disk = home.dir.path().join("disk");
std::fs::create_dir(&disk).unwrap();
let mut a = Agent::start(&home, "acme");
a.initialize();
a.call(
"books.set_setting",
json!({"changes": [{"key": "backup.folder", "value": disk.display().to_string()}],
"decided_by": "Test Owner"}),
)
.unwrap();
let out = home.books(&["back-up", "acme"]);
assert!(out.status.success(), "{out:?}");
assert!(
String::from_utf8_lossy(&out.stdout).contains("Backed up to the backup folder"),
"{out:?}"
);
assert!(disk.join("acme.git").join("HEAD").is_file());
// A folder gone since: said, and --new-disk begins it again.
std::fs::rename(disk.join("acme.git"), home.dir.path().join("away.git")).unwrap();
let out = home.books(&["back-up", "acme"]);
assert_eq!(out.status.code(), Some(1), "{out:?}");
assert!(
String::from_utf8_lossy(&out.stderr).contains("holds no backup"),
"{out:?}"
);
let out = home.books(&["back-up", "acme", "--new-disk"]);
assert!(out.status.success(), "{out:?}");
assert!(disk.join("acme.git").join("HEAD").is_file());
a.end();
}
/// The viewer's owner (ledger-cli's `ViewerOwner`, over these ledger-host
/// functions): its hold is a lease of kind app; new books are made by the
/// server; "Close the books" ends every lease, the viewer's hold with it.
/// (A `serve` holds the books first: run in this test's process, a hold
/// that had to start the host would start this test's own program.)
#[test]
fn the_viewers_hold_new_books_and_close_the_books() {
use ledger_host::commands::{Common, close_books, hold_for_viewer, new_books};
let home = Home::new("viewer-owner");
home.create("acme");
let serving = serve(&home);
let common = Common {
root: Some(home.root()),
knobs: ledger_host::knobs::Knobs::parse(KNOBS).unwrap(),
};
let hold = hold_for_viewer(&common, Some("acme".into()));
assert!(
wait_for(20, || home
.lease_line("{\"proto\":1,\"leases\":null}\n")
.contains("\"app\"")),
"the viewer's lease never showed"
);
assert_eq!(
new_books(&common, "beta", "USD").unwrap(),
"Made new books \"beta\" (USD)."
);
assert!(
new_books(&common, "beta", "USD")
.unwrap_err()
.contains("already books called")
);
let out = home.books(&["list"]);
assert!(
String::from_utf8_lossy(&out.stdout).contains("beta open"),
"{out:?}"
);
let said = close_books(&common).unwrap();
assert!(said.starts_with("The books are closing"), "{said}");
// The viewer's hold lets go, for good (closed by the owner).
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let _ = tx.send(hold.join().is_ok());
});
assert_eq!(rx.recv_timeout(Duration::from_secs(30)), Ok(true));
serving.expect("closed", 30);
assert!(home.wait_closed(30));
// Closed: said so.
assert_eq!(close_books(&common).unwrap(), "The books are closed.");
}
#[test]
fn connect_makes_anew_a_token_the_keyring_lost_and_says_when_an_agent_refuses() {
use std::os::unix::fs::PermissionsExt;
let home = Home::new("connect-lost");
home.create("acme");
// The server keeps the books' agent token; this computer's keyring
// lost it.
std::fs::remove_dir_all(home.run_dir().join("test-keyring")).unwrap();
let (bin, _log) = fake_agent(&home, "claude");
let connect = |path: &Path| {
home.cmd("connect")
.args(["claude", "--book", "acme"])
.env("PATH", format!("{}:/usr/bin:/bin", path.display()))
.stdin(Stdio::null())
.output()
.unwrap()
};
let out = connect(&bin);
assert!(out.status.success(), "{out:?}");
assert!(
String::from_utf8_lossy(&out.stdout).contains("Made the books acme' agent token"),
"{out:?}"
);
// The agent's own command refuses: said, with what it said.
let refusing = home.dir.path().join("refusing-bin");
std::fs::create_dir_all(&refusing).unwrap();
let script = refusing.join("claude");
std::fs::write(
&script,
"#!/bin/sh\n[ \"$2\" = add ] && { echo 'no room at the inn' >&2; exit 1; }\nexit 0\n",
)
.unwrap();
std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755)).unwrap();
let out = connect(&refusing);
assert_eq!(out.status.code(), Some(1), "{out:?}");
let err = String::from_utf8_lossy(&out.stderr);
assert!(
err.contains("Claude Code didn't add the books: no room at the inn"),
"{err}"
);
}