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:
1 parent
019d8d76a3
commit
3cf359a0cd
28 files changed
+1976
-1931
No files matched your search
@@ -116,6 +116,15 @@ pub enum Request {
|
||||
#[serde(default)]
|
||||
expect_head: Option<(u64, String)>,
|
||||
},
|
||||
/// Back books up now (plan 3.7): to every target their settings name.
|
||||
BackUpNow {
|
||||
/// The books.
|
||||
id: String,
|
||||
/// The backup folder's disk is new: begin these books' backup
|
||||
/// there again.
|
||||
#[serde(default)]
|
||||
new_disk: bool,
|
||||
},
|
||||
/// Make a token for an agent (plan 3.3).
|
||||
TokenCreate {
|
||||
/// The books it opens.
|
||||
@@ -294,6 +303,21 @@ pub struct Restored {
|
||||
pub problem: Option<String>,
|
||||
}
|
||||
|
||||
/// How one backup target took a backup now.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BackedUpTarget {
|
||||
/// "repository" or "folder".
|
||||
pub target: String,
|
||||
/// Where (no path in the data root).
|
||||
pub at: String,
|
||||
/// The journal seq it holds now, if any.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub through_seq: Option<u64>,
|
||||
/// Why it holds no backup of the latest, if it doesn't.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub problem: Option<String>,
|
||||
}
|
||||
|
||||
/// The server's answer.
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case", deny_unknown_fields)]
|
||||
@@ -316,6 +340,14 @@ pub enum Response {
|
||||
RestorePreview(RestorePreview),
|
||||
/// A backup restored, and open.
|
||||
Restored(Restored),
|
||||
/// Books backed up now: their head, and each target's outcome (none
|
||||
/// named: none set up).
|
||||
BackedUp {
|
||||
/// The books' head seq.
|
||||
head_seq: u64,
|
||||
/// Each target.
|
||||
targets: Vec<BackedUpTarget>,
|
||||
},
|
||||
/// A token made: its text, shown this once (INVARIANT 6).
|
||||
TokenCreated {
|
||||
/// The token.
|
||||
@@ -371,6 +403,9 @@ pub fn call_within(
|
||||
Request::Create { .. } | Request::Adopt { .. } | Request::Restore { .. } => {
|
||||
Some("`wanderbooks books list` shows the books there are")
|
||||
}
|
||||
Request::BackUpNow { .. } => {
|
||||
Some("`wanderbooks books back-up` again backs up whatever it didn't")
|
||||
}
|
||||
Request::TokenCreate { .. } | Request::TokenRevoke { .. } => {
|
||||
Some("`wanderbooks token list` shows the tokens there are")
|
||||
}
|
||||
|
||||
@@ -490,6 +490,41 @@ impl Books {
|
||||
confirmed,
|
||||
expect_head,
|
||||
} => self.restore(id, source, replace, confirmed, expect_head),
|
||||
Request::BackUpNow { id, new_disk } => {
|
||||
let id = match BookId::parse(&id) {
|
||||
Ok(id) => id,
|
||||
Err(e) => return refused(e),
|
||||
};
|
||||
let backups = match self.held().get(id.as_str()) {
|
||||
Some(Book::Open { backups, .. }) => backups.clone(),
|
||||
Some(Book::Failed(why)) => {
|
||||
return Response::Refused(format!("those books didn't open: {why}"));
|
||||
}
|
||||
None => {
|
||||
return Response::Refused(format!(
|
||||
"there are no books called {:?}",
|
||||
id.as_str()
|
||||
));
|
||||
}
|
||||
};
|
||||
let rt = tokio::runtime::Handle::current();
|
||||
match rt.block_on(backups.run_with("asked", new_disk)) {
|
||||
Ok(status) => Response::BackedUp {
|
||||
head_seq: status.head_seq,
|
||||
targets: status
|
||||
.targets
|
||||
.into_iter()
|
||||
.map(|t| admin::BackedUpTarget {
|
||||
target: t.target,
|
||||
at: t.at,
|
||||
through_seq: t.through_seq,
|
||||
problem: t.last_error,
|
||||
})
|
||||
.collect(),
|
||||
},
|
||||
Err(why) => Response::Refused(self.hide(why)),
|
||||
}
|
||||
}
|
||||
Request::Adopt {
|
||||
id,
|
||||
source,
|
||||
|
||||
@@ -237,6 +237,10 @@ pub struct StartOut {
|
||||
pub handbook_sections: Vec<SectionOut>,
|
||||
/// What to do next, in order.
|
||||
pub next_steps: Vec<NextStepOut>,
|
||||
/// How the books are kept from agents: "same_user (advisory)", hidden
|
||||
/// from them in the human's own account, not locked against them
|
||||
/// (plan D-32): reach them only through these tools.
|
||||
pub isolation: String,
|
||||
}
|
||||
|
||||
/// One setting change.
|
||||
@@ -894,6 +898,7 @@ impl BooksServer {
|
||||
})
|
||||
.collect(),
|
||||
next_steps,
|
||||
isolation: "same_user (advisory)".to_string(),
|
||||
};
|
||||
// Fit (INVARIANT 4): whole lists if they fit; else summaries go,
|
||||
// then the lists shorten from the end, each saying how many more.
|
||||
|
||||
@@ -517,6 +517,8 @@ fn start_fits_in_8_kb() {
|
||||
let cut = start["truncated"].to_string();
|
||||
assert!(cut.contains("books.conventions"), "{cut}");
|
||||
assert!(start["conventions"].as_array().unwrap().len() >= 10);
|
||||
// isolation_label_is_advisory (plan P2, D-32).
|
||||
assert_eq!(start["isolation"], "same_user (advisory)");
|
||||
}
|
||||
|
||||
/// Review of P1 (2026-10-17): the text imported is the file the server
|
||||
|
||||
Reference in new issue
Block a user