feat(host): the lease and MCP channels as named pipes with the SID check, and the host, bridge and commands on Windows (P11c)
Windows steps 3 of D-62. Every `cfg(unix)` gate on the host, the bridge, the opener, the books server and the command line is gone: they build on every OS, and the Unix behaviour is unchanged (the existing over-the-wire suites pass on Linux). `ledger_mcp::ipc` is the one seam for the channels. Unix: the same 0600 sockets as before, peer uid unchanged (the peer functions moved here). Windows: a named pipe `\\.\pipe\wanderbooks-<hash of the root's run/>-<name>` made with a DACL of one allow entry for this account's SID, remote clients refused, the first instance made once (a second server is refused, as a held lock refuses it on Unix), and 16 idle instances kept waiting so a client never finds all busy while the server makes the next. Where Unix reads `SO_PEERCRED`, each end of a pipe takes the other's process id (`GetNamedPipeClientProcessId`/`...ServerProcessId`) and compares that process's token user with its own SID; any doubt is "not us". Clients open with SECURITY_IDENTIFICATION so a pipe server cannot act as the client. A blocking client with a read limit is polled with PeekNamedPipe (a pipe read cannot be cut short), and a pipe that has gone reads as the end of the stream, as a closed socket does. Stop signals are Ctrl+C, Ctrl+Break, console close and shutdown where Unix has SIGTERM, SIGINT and SIGHUP. The host starts detached (DETACHED_PROCESS | CREATE_NEW_PROCESS_GROUP, leaving the opener's job when the job lets it: an agent's job must not take the host down, as an app's systemd scope must not on Linux) with a fixed environment; `close_range` and `seccomp` stay Linux-only (D-32). A replaced program is told by size and last write. Git for Windows is found and a quiet git is stopped with its tree (`taskkill /T`). The test harness of ledger-cli finds its root by `default_root`, cleans a child's environment per OS and reaches the server over its pipe; the bridge's scripted-server tests stay Unix-only. No new dependency (tokio's named pipes and reqwest's `windows_named_pipe` were already in the build); `windows-sys` stays in ledger-store. Tests: ledger-mcp `ipc::tests` (four on every OS, one Windows only), ledger-host `tests/windows_pipe.rs::a_second_user_cannot_open_the_pipe`, Windows variants of the host and start tests. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HS8WZziDnyxunC23MUxrLp
This commit is contained in:
1 parent
5f5dd8f7da
commit
bd6448532e
21 files changed
+1471
-410
No files matched your search
@@ -2,6 +2,7 @@
|
||||
//! (`server`) and the viewer (`gui`) as modes of itself, each with its own
|
||||
//! flags, and the server-only build has no viewer.
|
||||
|
||||
#[cfg_attr(not(unix), allow(unused_imports))]
|
||||
use std::process::{Command, Stdio};
|
||||
|
||||
#[cfg(all(feature = "gui", target_os = "linux"))]
|
||||
@@ -105,13 +106,12 @@ fn a_viewer_that_cannot_open_a_window_exits_1() {
|
||||
let server = common::Server::start(&home);
|
||||
home.create(common::BOOK, "USD");
|
||||
assert!(server.close().success());
|
||||
let mut child = wanderbooks()
|
||||
.args(["gui", "--root"])
|
||||
let mut gui = wanderbooks();
|
||||
gui.args(["gui", "--root"])
|
||||
.arg(home.root())
|
||||
.args(["--book", common::BOOK])
|
||||
.env_clear()
|
||||
.env("PATH", "/usr/bin:/bin")
|
||||
.env("HOME", home.dir.path())
|
||||
.args(["--book", common::BOOK]);
|
||||
common::clean_env(&mut gui, home.dir.path());
|
||||
let mut child = gui
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
@@ -147,7 +147,6 @@ fn the_server_only_build_has_no_viewer() {
|
||||
}
|
||||
|
||||
/// The books server needs a data root: with none, it says to run setup.
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn the_books_server_needs_a_root() {
|
||||
// No root yet: the books server says to run setup.
|
||||
@@ -258,52 +257,3 @@ fn the_file_commands_end_with_the_code_that_says_how() {
|
||||
);
|
||||
assert!(!root.exists(), "the books were reached for");
|
||||
}
|
||||
|
||||
/// Where local sockets aren't built yet (P11, Windows): the modes that
|
||||
/// hold the books say so and end with 2, never a success; setup makes
|
||||
/// the folder and ends with 0, asking for no passphrase (nothing there
|
||||
/// asks for one yet).
|
||||
#[cfg(not(unix))]
|
||||
#[test]
|
||||
fn without_local_sockets_the_modes_that_hold_the_books_say_so() {
|
||||
for mode in [
|
||||
"server",
|
||||
"host",
|
||||
"mcp",
|
||||
"serve",
|
||||
"close",
|
||||
"books",
|
||||
"doctor",
|
||||
"token",
|
||||
"connect",
|
||||
"passphrase",
|
||||
"upload",
|
||||
"download",
|
||||
] {
|
||||
let out = wanderbooks()
|
||||
.arg(mode)
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(out.status.code(), Some(2), "{mode}: {out:?}");
|
||||
assert!(
|
||||
String::from_utf8_lossy(&out.stderr).contains("to this system with P11"),
|
||||
"{mode}: {out:?}"
|
||||
);
|
||||
}
|
||||
let home = tempfile::Builder::new()
|
||||
.prefix("wb-one-nosock-")
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let out = wanderbooks()
|
||||
.args(["setup", "--home"])
|
||||
.arg(home.path())
|
||||
.env_remove("XDG_DATA_HOME")
|
||||
.stdin(Stdio::null())
|
||||
.output()
|
||||
.unwrap();
|
||||
assert_eq!(out.status.code(), Some(0), "{out:?}");
|
||||
let text = String::from_utf8_lossy(&out.stdout);
|
||||
assert!(text.contains("The books' private folder: made"), "{text}");
|
||||
assert!(!text.contains("passphrase"), "{text}");
|
||||
}
|
||||
Reference in new issue
Block a user