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
@@ -63,14 +63,10 @@ impl Home {
|
||||
.prefix(&format!("wb-{tag}-"))
|
||||
.tempdir()
|
||||
.unwrap();
|
||||
let out = Command::new(env!("CARGO_BIN_EXE_wanderbooks"))
|
||||
.args(["setup", "--home"])
|
||||
.arg(dir.path())
|
||||
.env_clear()
|
||||
.env("PATH", "/usr/bin:/bin")
|
||||
.env("HOME", dir.path())
|
||||
.output()
|
||||
.unwrap();
|
||||
let mut setup = Command::new(env!("CARGO_BIN_EXE_wanderbooks"));
|
||||
setup.args(["setup", "--home"]).arg(dir.path());
|
||||
clean_env(&mut setup, dir.path());
|
||||
let out = setup.output().unwrap();
|
||||
assert!(out.status.success(), "{out:?}");
|
||||
Self {
|
||||
dir,
|
||||
@@ -79,7 +75,7 @@ impl Home {
|
||||
}
|
||||
|
||||
pub fn root(&self) -> PathBuf {
|
||||
self.dir.path().join(".local/share/wanderbooks")
|
||||
ledger_store::root::default_root(self.dir.path())
|
||||
}
|
||||
|
||||
pub fn run(&self) -> PathBuf {
|
||||
@@ -190,14 +186,14 @@ impl Server {
|
||||
.append(true)
|
||||
.open(home.dir.path().join("server.log"))
|
||||
.unwrap();
|
||||
let mut child = Command::new(env!("CARGO_BIN_EXE_wanderbooks"))
|
||||
let mut command = Command::new(env!("CARGO_BIN_EXE_wanderbooks"));
|
||||
command
|
||||
.arg("server")
|
||||
.arg("--root")
|
||||
.arg(home.root())
|
||||
.current_dir(cwd)
|
||||
.env_clear()
|
||||
.env("PATH", "/usr/bin:/bin")
|
||||
.env("HOME", home.dir.path())
|
||||
.current_dir(cwd);
|
||||
clean_env(&mut command, home.dir.path());
|
||||
let mut child = command
|
||||
.env("XDG_CONFIG_HOME", home.dir.path().join(".config"))
|
||||
.envs(env.iter().map(|(k, v)| (*k, v.as_os_str())))
|
||||
.stdin(Stdio::piped())
|
||||
@@ -248,10 +244,10 @@ pub fn mcp_http_builder(home: &Home) -> reqwest::ClientBuilder {
|
||||
/// [`mcp_http_builder`], for the root's `run/` folder `run`.
|
||||
pub fn mcp_http_builder_at(run: &Path) -> reqwest::ClientBuilder {
|
||||
// The short path is good in this process only, while it lives: kept.
|
||||
let short = ledger_store::root::SocketPath::new(run, "mcp.sock").unwrap();
|
||||
let path = short.path().to_path_buf();
|
||||
std::mem::forget(short);
|
||||
reqwest::Client::builder().unix_socket(path)
|
||||
let endpoint = ledger_mcp::ipc::Endpoint::new(run, "mcp.sock").unwrap();
|
||||
let builder = ledger_host::channel::http_builder(&endpoint);
|
||||
std::mem::forget(endpoint);
|
||||
builder
|
||||
}
|
||||
|
||||
/// The settings a settings file's text gives, as books.set_setting changes
|
||||
@@ -836,20 +832,71 @@ pub fn book_files(dir: &Path) -> std::collections::BTreeMap<PathBuf, String> {
|
||||
/// Copy the tree `from` into `to`, private as the server keeps a book
|
||||
/// (folders 0700, files 0600).
|
||||
pub fn copy_private(from: &Path, to: &Path) {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::create_dir_all(to).unwrap();
|
||||
std::fs::set_permissions(to, std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||
private_folder(to);
|
||||
for entry in std::fs::read_dir(from).unwrap().flatten() {
|
||||
let (src, dst) = (entry.path(), to.join(entry.file_name()));
|
||||
if entry.file_type().unwrap().is_dir() {
|
||||
copy_private(&src, &dst);
|
||||
} else {
|
||||
std::fs::copy(&src, &dst).unwrap();
|
||||
std::fs::set_permissions(&dst, std::fs::Permissions::from_mode(0o600)).unwrap();
|
||||
private_file(&dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Make the folder `dir` private as the server keeps one (0700; on
|
||||
/// Windows this account's alone, by an owner-only DACL).
|
||||
pub fn private_folder(dir: &Path) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||
}
|
||||
#[cfg(windows)]
|
||||
ledger_store::winsec::set_owner_only(dir).unwrap();
|
||||
}
|
||||
|
||||
/// Make the file `path` private as the server keeps one (0600; on
|
||||
/// Windows this account's alone).
|
||||
pub fn private_file(path: &Path) {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600)).unwrap();
|
||||
}
|
||||
#[cfg(windows)]
|
||||
ledger_store::winsec::set_owner_only(path).unwrap();
|
||||
}
|
||||
|
||||
/// A child process's environment cleared down to what a program needs to
|
||||
/// run and to find `home` as its own: PATH and HOME on Unix; the system
|
||||
/// folder, a PATH of the system's, the profile and the temporary folder on
|
||||
/// Windows (a program started with nothing at all can't load its DLLs).
|
||||
pub fn clean_env(command: &mut Command, home: &Path) {
|
||||
command.env_clear();
|
||||
#[cfg(unix)]
|
||||
{
|
||||
command.env("PATH", "/usr/bin:/bin").env("HOME", home);
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
let root = std::env::var_os("SystemRoot").unwrap_or_else(|| r"C:\Windows".into());
|
||||
let system32 = Path::new(&root).join("System32");
|
||||
command
|
||||
.env("SystemRoot", &root)
|
||||
.env("windir", &root)
|
||||
.env(
|
||||
"PATH",
|
||||
std::env::join_paths([system32, root.clone().into()]).unwrap(),
|
||||
)
|
||||
.env("USERPROFILE", home)
|
||||
.env("HOME", home)
|
||||
.env("TEMP", std::env::temp_dir())
|
||||
.env("TMP", std::env::temp_dir());
|
||||
}
|
||||
}
|
||||
|
||||
/// A test's place: a home (whose folder holds the test's own files, its
|
||||
/// settings file among them) and the books [`BOOK`] in its root, served
|
||||
/// by a server [`Place::start`] starts, and starts again on each next
|
||||
@@ -912,8 +959,7 @@ impl Place {
|
||||
let dir = self.books_data();
|
||||
let fresh = !dir.join("journal.jsonl").exists();
|
||||
if dir.exists() {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700)).unwrap();
|
||||
private_folder(&dir);
|
||||
}
|
||||
let cwd = self.cwd.borrow().clone();
|
||||
let cwd = cwd.as_deref().unwrap_or(self.home.dir.path());
|
||||
|
||||
Reference in new issue
Block a user