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
@@ -124,11 +124,20 @@ impl Git {
|
||||
.unwrap_or_default();
|
||||
dirs.iter()
|
||||
.chain(
|
||||
["/usr/bin", "/bin", "/usr/local/bin", "/opt/homebrew/bin"]
|
||||
.iter()
|
||||
.map(PathBuf::from)
|
||||
.collect::<Vec<_>>()
|
||||
.iter(),
|
||||
[
|
||||
"/usr/bin",
|
||||
"/bin",
|
||||
"/usr/local/bin",
|
||||
"/opt/homebrew/bin",
|
||||
// Git for Windows' own folders (a host started clean
|
||||
// has a short PATH).
|
||||
r"C:\Program Files\Git\cmd",
|
||||
r"C:\Program Files\Git\bin",
|
||||
]
|
||||
.iter()
|
||||
.map(PathBuf::from)
|
||||
.collect::<Vec<_>>()
|
||||
.iter(),
|
||||
)
|
||||
.filter(|d| d.is_absolute())
|
||||
.map(|d| d.join(if cfg!(windows) { "git.exe" } else { "git" }))
|
||||
@@ -266,8 +275,24 @@ impl Git {
|
||||
let kill_group = |pid: u32| unsafe {
|
||||
libc::kill(-(pid as libc::pid_t), libc::SIGKILL);
|
||||
};
|
||||
#[cfg(not(unix))]
|
||||
let kill_group = |_: u32| {};
|
||||
// Windows has no process group to signal: a git that has gone quiet
|
||||
// is stopped with the tree of processes it started (`taskkill /T`).
|
||||
// Once git has ended normally nothing is looked for (a process
|
||||
// spawned per git call would cost more than the leftovers it could
|
||||
// find).
|
||||
#[cfg(windows)]
|
||||
let kill_group = |pid: u32| {
|
||||
let _ = std::process::Command::new("taskkill")
|
||||
.args(["/T", "/F", "/PID", &pid.to_string()])
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
};
|
||||
#[cfg(unix)]
|
||||
let kill_leftovers = kill_group;
|
||||
#[cfg(windows)]
|
||||
let kill_leftovers = |_: u32| {};
|
||||
let status = loop {
|
||||
if let Some(status) = child.try_wait()? {
|
||||
break status;
|
||||
@@ -284,7 +309,7 @@ impl Git {
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
};
|
||||
// What it started may still hold its pipes: not waited for long.
|
||||
kill_group(child.id());
|
||||
kill_leftovers(child.id());
|
||||
let wait = Duration::from_secs(10);
|
||||
let stdout = stdout.recv_timeout(wait).unwrap_or_default();
|
||||
let stderr = stderr.recv_timeout(wait).unwrap_or_default();
|
||||
|
||||
@@ -415,11 +415,15 @@ pub fn pid_is_us(pid: u32) -> bool {
|
||||
}
|
||||
|
||||
/// The process at the client end of the pipe `handle` (a server's).
|
||||
pub fn pipe_client_pid(handle: std::os::windows::io::RawHandle) -> io::Result<u32> {
|
||||
pub fn pipe_client_pid(pipe: &impl std::os::windows::io::AsRawHandle) -> io::Result<u32> {
|
||||
let mut pid = 0u32;
|
||||
// SAFETY: a pipe handle the caller owns and an out value.
|
||||
if unsafe { windows_sys::Win32::System::Pipes::GetNamedPipeClientProcessId(handle, &mut pid) }
|
||||
== 0
|
||||
if unsafe {
|
||||
windows_sys::Win32::System::Pipes::GetNamedPipeClientProcessId(
|
||||
pipe.as_raw_handle(),
|
||||
&mut pid,
|
||||
)
|
||||
} == 0
|
||||
{
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
@@ -427,11 +431,15 @@ pub fn pipe_client_pid(handle: std::os::windows::io::RawHandle) -> io::Result<u3
|
||||
}
|
||||
|
||||
/// The process at the server end of the pipe `handle` (a client's).
|
||||
pub fn pipe_server_pid(handle: std::os::windows::io::RawHandle) -> io::Result<u32> {
|
||||
pub fn pipe_server_pid(pipe: &impl std::os::windows::io::AsRawHandle) -> io::Result<u32> {
|
||||
let mut pid = 0u32;
|
||||
// SAFETY: a pipe handle the caller owns and an out value.
|
||||
if unsafe { windows_sys::Win32::System::Pipes::GetNamedPipeServerProcessId(handle, &mut pid) }
|
||||
== 0
|
||||
if unsafe {
|
||||
windows_sys::Win32::System::Pipes::GetNamedPipeServerProcessId(
|
||||
pipe.as_raw_handle(),
|
||||
&mut pid,
|
||||
)
|
||||
} == 0
|
||||
{
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
@@ -440,13 +448,13 @@ pub fn pipe_server_pid(handle: std::os::windows::io::RawHandle) -> io::Result<u3
|
||||
|
||||
/// How many bytes wait to be read on the pipe `handle`, without reading
|
||||
/// them. A pipe whose other end has gone is `ERROR_BROKEN_PIPE`.
|
||||
pub fn pipe_bytes_waiting(handle: std::os::windows::io::RawHandle) -> io::Result<u32> {
|
||||
pub fn pipe_bytes_waiting(pipe: &impl std::os::windows::io::AsRawHandle) -> io::Result<u32> {
|
||||
let mut avail = 0u32;
|
||||
// SAFETY: a pipe handle the caller owns; only the available count is
|
||||
// asked for.
|
||||
let ok = unsafe {
|
||||
windows_sys::Win32::System::Pipes::PeekNamedPipe(
|
||||
handle,
|
||||
pipe.as_raw_handle(),
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
std::ptr::null_mut(),
|
||||
|
||||
Reference in new issue
Block a user