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

-55
View File
@@ -539,16 +539,6 @@ impl Host {
Phase::Closing(reason) => HostMsg::Ending { reason, detail: None },
});
}
Event::Intent(Intent::NewBooks { id, currency }, answer)
if self.ready && self.phase == Phase::Open =>
{
// The server does it; the loop goes on meanwhile.
let socket = self.run.join(ledger_mcp::local::ADMIN_SOCKET);
tokio::spawn(async move {
let msg = new_books(socket, id, currency).await;
let _ = answer.send(msg);
});
}
Event::Intent(Intent::Close, answer) if self.phase != Phase::Open => {
// INVARIANT 11; closing anyway: already so.
let _ = answer.send(match self.phase {
@@ -701,27 +691,6 @@ impl Host {
},
Some(Reason::ClosedByOwner),
),
Intent::NewBooks { .. } if self.phase == Phase::Open => (
HostMsg::Refused("the books are still opening: try again in a moment".into()),
None,
),
Intent::NewBooks { .. } => (
HostMsg::Refused("the books are closing: nothing was done".into()),
None,
),
Intent::BackUpNow { .. } => (
HostMsg::Refused(
"the books back themselves up; to back up now, ask your agent (books.back_up_now)"
.into(),
),
None,
),
Intent::Restore { .. } => (
HostMsg::Refused(
"restore from a terminal for now: `wanderbooks books restore BACKUP`".into(),
),
None,
),
Intent::ConsentFormatUpgrade { .. } => (
HostMsg::Refused("no books are waiting for a format upgrade".into()),
None,
@@ -768,30 +737,6 @@ impl Host {
}
}
/// "New books…": the server makes them, through its admin socket.
async fn new_books(socket: PathBuf, id: String, currency: String) -> HostMsg {
use ledger_mcp::admin::{Request, Response};
let answer = tokio::task::spawn_blocking(move || {
ledger_mcp::admin::call(&socket, Request::Create { id, currency })
})
.await;
match answer {
Ok(Ok(Response::Created { id, problem: None })) => HostMsg::Done {
detail: Some(format!("new books {id} made")),
},
Ok(Ok(Response::Created {
id,
problem: Some(why),
})) => HostMsg::Refused(format!("new books {id} were made but didn't open: {why}")),
Ok(Ok(Response::Refused(why))) => HostMsg::Refused(why),
Ok(Ok(_)) => HostMsg::Refused("the books server answered oddly".into()),
Ok(Err(ledger_mcp::admin::CallError::NotSent(why))) => HostMsg::Refused(why),
// Sent, and no answer: never reported as not done.
Ok(Err(ledger_mcp::admin::CallError::Unknown(why))) => HostMsg::Uncertain(why),
Err(_) => HostMsg::Refused("the host hit an internal error".into()),
}
}
/// Take run/update.lock (exclusive), if it is free now.
fn take_update_lock(run: &Path) -> Result<std::fs::File, String> {
let file = std::fs::OpenOptions::new()