docs/superpowers/plans/2026-09-03-macos-port-step2-platform-layer.md
Ref: Size: 74.1 KiB History
# macOS port, step 2: the platform layer — Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Every Linux-specific call in `src/` moves behind `src/os/` (one backend per side), the harness stops spelling `/proc`, and a source ban keeps it that way — with Linux behaviour unchanged and `make ci` green at every commit.
**Architecture:** Two new table rows, `server_os` and `client_os`, each a root file that is the interface and a `_linux.zig` child that spells the syscalls; the root selects the child by `switch (builtin.os.tag)`. `spawn` moves into the folder. `forkDaemon`'s fork and the peer-credential read leave `main.zig`. The shared root (`xdg`, `sockpath`) respells its Linux-isms in portable `std.c`. `test/e2e_lib.sh` gains named oracle helpers. build.zig gains folder rule 7 and a target word for the QUIC deps.
**Tech Stack:** Zig 0.15.2 (vendored at `deps/zig/zig`, system zig will not build this), POSIX sh for the harness, cmake for `deps/quic`.
**Spec:** `docs/superpowers/specs/2026-09-03-macos-port-design.md`
## Global Constraints
- Build with `deps/zig/zig` only. `make check` (fmt + unit tests + shell syntax + comment-claim refs) before every commit; `make ci` before delivery. Capture `$?` before piping.
- Linux behaviour does not change in this plan. Any test that passed before must pass after; no test is deleted, only respelled.
- No line outside `src/os/` may spell `std.os.linux`, `/proc`, `memfd`, `close_range`, `exit_group`, `PEERCRED`, `TIOCSPTLCK` or `TIOCGPTN` once Task 9 lands — comments included. Earlier tasks reword the comments they touch to name the operation (`server_os.anonFd`, `pid_alive`) instead of the mechanism.
- A file belongs to exactly one module. `src/os/` imports nothing of ours.
- Commit subjects are `type: what changed`, `type` one of `feat fix refactor test docs build chore`, no scope. Trailer lines: `Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>` and `Claude-Session: https://claude.ai/code/session_01SakwJEwD9dXBoRP5kWbemW`.
- Never `cat` `src/server/server.zig`, `src/tui/interact.zig` or `docs/decisions.md`; `grep -n` then `sed -n 'A,Bp'`.
- Every hand-run rig exports an isolated `XDG_STATE_HOME` and `XDG_RUNTIME_DIR`.
- A unit test that writes to stdout wedges `zig build test` silently. Tests print to stderr or nothing.
- Line numbers below are from HEAD `7b207001`; re-grep the anchor symbol before editing.
---
## File structure
| File | Responsibility |
|---|---|
| `src/os/server_os.zig` (new) | The daemon-side platform interface: one `pub fn` per operation, doc comment naming the failure it prevents, `impl` switch. `selfImageStale` lives here in full. Unit tests for every operation. |
| `src/os/server_os_linux.zig` (new) | Linux spellings only. `@cImport` of `pty.h`. No doc prose beyond why a spelling is what it is. |
| `src/os/client_os.zig` (new) | Wall/askpass-side interface: `peerCred`, `parentOf`, `winSize`, `openPtyPair`. Tests. |
| `src/os/client_os_linux.zig` (new) | Linux spellings. |
| `src/os/spawn.zig` (moved from `src/cli/`) | Unchanged contract; the `/proc` fallback arm is spelled per OS. |
| `src/server/pty.zig` | Loses its `@cImport`; calls `server_os`. |
| `src/cli/main.zig` | `forkDaemon` keeps the log/argv/poll policy, hands the fork to `server_os.forkDetached`; `peerPid` becomes a call to `server_os.peerCred`. |
| `src/server/server.zig` | `send`, the two `memfd_create` sites, `selfImageStale`, `PendingUpgrade.memfd` → `carrier`. |
| `src/server/quic_server.zig` | `initFromFd`'s `getsockopt`. |
| `src/client/askpass.zig` | `peerCred`, `parentOf`, `getpid`, `geteuid` sites. |
| `src/tui/interact.zig` | `ttySize`, `ptsPair`, `setTtySize`. |
| `src/tui/wallview.zig` | The askpass gate reads `sockpath.runtimeDir()`. |
| `src/xdg.zig`, `src/sockpath.zig` | `kill(pid,0)`, comptime `max_sun_path`, `runtimeDir()`. |
| `build.zig` | Two rows, `spawn` path, import grants, folder rule 7, rule 5/6 folder lists, `use_lld` gated off Darwin, `quicDeps` target word. |
| `deps/quic/build-deps.sh` | Target word `native|musl|aarch64-macos`; `uname`-gated `sha256sum`/`nproc`. |
| `Makefile` | `MUX_TARGET ?= x86_64-linux-musl` for `install`/`release`. |
| `test/e2e_lib.sh` | Oracle helpers; `timeout` shim. |
| `test/e2e_0{1,3,4,6,9}_*.sh`, `e2e_14`, `e2e_16`, `soak.sh`, `vm.sh` | Pins call helpers. |
| `CLAUDE.md`, `docs/decisions.md` | Table row, rule 7, the dated decision. |
---
### Task 1: The `src/os/` folder, two rows, and `spawn` moves in
**Files:**
- Create: `src/os/server_os.zig`, `src/os/server_os_linux.zig`, `src/os/client_os.zig`, `src/os/client_os_linux.zig`
- Move: `src/cli/spawn.zig` → `src/os/spawn.zig`
- Modify: `build.zig:117-270` (mod_table), `build.zig` rule 5 and 6 folder lists (~line 355-380), `CLAUDE.md` layout table
**Interfaces:**
- Produces: modules `server_os` and `client_os`, importable by the rows granted below. Both roots export `pub const impl` and a first operation each so the wiring is exercised: `server_os.getpid() std.posix.pid_t` and `client_os.getpid() std.posix.pid_t`. (Later tasks add the rest; `getpid` stays because Task 7's callers use it.)
- [ ] **Step 1: Write the four files**
`src/os/server_os.zig`:
```zig
//! The daemon's platform layer: every call whose spelling or existence
//! differs by OS, behind one name each. This root is the CONTRACT — a doc
//! comment per operation says what it guarantees and which failure it
//! prevents — and a child per OS spells the syscalls. A build for an OS
//! with no child is a compile error here, never a runtime surprise.
//!
//! Imports nothing of ours: the daemon, the pty and the CLI entry import
//! this, and folder rule 7 (build.zig) bans the raw spellings everywhere
//! else, so a new Linux-ism has one place to go.
const std = @import("std");
const builtin = @import("builtin");
pub const impl = switch (builtin.os.tag) {
.linux => @import("server_os_linux.zig"),
else => @compileError("mux has no server platform arm for " ++ @tagName(builtin.os.tag)),
};
/// This process's pid, for the pid-named directories the daemon's
/// successor reaps (`xdg.reapDeadPid`).
pub fn getpid() std.posix.pid_t {
return impl.getpid();
}
test "server_os: the arm compiles and answers for the process it is in" {
try std.testing.expect(getpid() > 0);
}
// Forces semantic analysis of every pub decl under `zig build test`, so an
// unreferenced operation must at least compile for this OS.
test {
std.testing.refAllDeclsRecursive(@This());
}
```
`src/os/server_os_linux.zig`:
```zig
//! Linux arm of `server_os`. Spellings only; the contract is in the root.
const std = @import("std");
pub fn getpid() std.posix.pid_t {
return std.os.linux.getpid();
}
```
`src/os/client_os.zig`:
```zig
//! The wall's and askpass's platform layer: the few calls the client side
//! makes that differ by OS. Same shape as `server_os` — this root is the
//! contract, a child per OS spells it — and deliberately a SEPARATE row:
//! the client never links a fork or a pty, and an app that links the
//! engine and a client must not either.
const std = @import("std");
const builtin = @import("builtin");
pub const impl = switch (builtin.os.tag) {
.linux => @import("client_os_linux.zig"),
else => @compileError("mux has no client platform arm for " ++ @tagName(builtin.os.tag)),
};
/// This process's pid, for `mux-ask-PID.sock` and the hub's banner.
pub fn getpid() std.posix.pid_t {
return impl.getpid();
}
test "client_os: the arm compiles and answers for the process it is in" {
try std.testing.expect(getpid() > 0);
}
test {
std.testing.refAllDeclsRecursive(@This());
}
```
`src/os/client_os_linux.zig`:
```zig
//! Linux arm of `client_os`. Spellings only; the contract is in the root.
const std = @import("std");
pub fn getpid() std.posix.pid_t {
return std.os.linux.getpid();
}
```
- [ ] **Step 2: Move spawn**
```bash
git mv src/cli/spawn.zig src/os/spawn.zig
```
- [ ] **Step 3: Wire the table**
In `build.zig`'s `mod_table`, before the `pty` row, add:
```zig
// The platform layer, one row per side (docs/superpowers/specs/
// 2026-09-03-macos-port-design.md). Leaves: they import nothing of ours,
// and folder rule 7 below bans every raw OS spelling outside src/os/.
.{ .name = "server_os", .path = "src/os/server_os.zig", .link_libc = true },
.{ .name = "client_os", .path = "src/os/client_os.zig", .link_libc = true },
```
Change the `spawn` row's path to `"src/os/spawn.zig"`.
Add `"server_os"` to the `imports` of rows `pty`, `daemon`, `mux`; add `"client_os"` to rows `client`, `wall`. (The grant check requires the root file to `@import` the name; Tasks 2–7 add those imports. Until then the build's grant check will FAIL — so in this task add the grants only for `spawn`'s existing importers, which do not change, and add each grant in the task that first imports it. Concretely: this task changes ONLY the `spawn` path and adds the two new rows.)
In `source_bans`, rules 5 and 6: add `"src/os"` to their `folders` lists (rule 5's shell ban and rule 6's fork ban apply to the new folder like any other). Rule 6's `except` stays `src/cli/main.zig` until Task 3 moves the fork.
- [ ] **Step 4: Update CLAUDE.md's layout table**
In the `## Layout` table add a row after `src/cli/`:
```
| `src/os/` | `server_os`(`server_os.zig`) — `server_os_linux` · `client_os`(`client_os.zig`) — `client_os_linux` · `spawn` — the platform layer, one row per side so the client never links a fork or a pty; imports nothing of ours (spec 2026-09-03) |
```
and in the prose paragraph that says `spawn` lives under `src/cli/` because it asks the OS whether it has a terminal, replace with: "`spawn` lives under `src/os/` with the rest of the platform layer: asking the OS for a terminal is platform code, and rule 4 forbids a client module from doing it."
- [ ] **Step 5: Build and test**
Run: `make check 2>&1 | tail -5; echo rc=${PIPESTATUS[0]}` (zsh: `make check > /tmp/c.log 2>&1; echo rc=$?; tail -5 /tmp/c.log`)
Expected: rc=0. If the grant check fatals on a stale `spawn` grant, the path change was missed.
- [ ] **Step 6: Commit**
```bash
git add src/os build.zig CLAUDE.md
git commit -m "refactor: a platform layer under src/os with one row per side"
```
---
### Task 2: The pty goes through `server_os`
**Files:**
- Modify: `src/os/server_os.zig`, `src/os/server_os_linux.zig`
- Modify: `src/server/pty.zig:1-160` (header, `spawnArgv`, `mode`, `fgPgid`, `resize`) and the test at `pty.zig:483-500`
- Modify: `build.zig` — grant `server_os` to row `pty`
**Interfaces:**
- Produces:
- `server_os.Winsize = std.posix.winsize`
- `server_os.ForkedPty = struct { pid: std.posix.pid_t, master: std.posix.fd_t }`
- `server_os.forkPty(ws: Winsize) error{ForkPtyFailed}!ForkedPty` — returns `pid == 0` in the child, like forkpty
- `server_os.exitNow(code: u8) noreturn`
- `server_os.closeFrom(first: std.posix.fd_t) void`
- `server_os.PtyMode = struct { icanon: bool, echo: bool }`
- `server_os.ptyMode(master) !PtyMode`
- `server_os.ptyFgPgid(master) error{IoctlFailed}!std.posix.pid_t`
- `server_os.setWinsize(master, ws: Winsize) error{IoctlFailed}!void`
- [ ] **Step 1: Write the failing test in `server_os.zig`**
Append before the `refAllDeclsRecursive` test:
```zig
test "server_os.closeFrom: a fd below the floor survives and one above does not" {
// pipe(2) sets no CLOEXEC, so a child that did not close would still
// hold pipe[1]. Asked through /dev/fd, which both OSes have.
const pipe = try std.posix.pipe();
defer std.posix.close(pipe[0]);
defer std.posix.close(pipe[1]);
var cmd_buf: [96]u8 = undefined;
const cmd = try std.fmt.bufPrintZ(&cmd_buf, "test -e /dev/fd/{d} && exit 3; exit 0", .{pipe[1]});
const argv = [_:null]?[*:0]const u8{ "/bin/sh", "-c", cmd.ptr };
const ws: Winsize = .{ .row = 24, .col = 80, .xpixel = 0, .ypixel = 0 };
const f = try forkPty(ws);
if (f.pid == 0) {
closeFrom(3);
std.posix.execveZ(argv[0].?, &argv, std.c.environ) catch {};
exitNow(127);
}
defer std.posix.close(f.master);
const r = std.posix.waitpid(f.pid, 0);
try std.testing.expect(std.posix.W.IFEXITED(r.status));
try std.testing.expectEqual(@as(u32, 0), std.posix.W.EXITSTATUS(r.status));
}
test "server_os.setWinsize then ptyMode: the master answers about the line discipline" {
const ws: Winsize = .{ .row = 31, .col = 101, .xpixel = 0, .ypixel = 0 };
const argv = [_:null]?[*:0]const u8{ "/bin/sh", "-c", "stty -echo; sleep 5" };
const f = try forkPty(ws);
if (f.pid == 0) {
std.posix.execveZ(argv[0].?, &argv, std.c.environ) catch {};
exitNow(127);
}
defer {
std.posix.kill(f.pid, std.posix.SIG.KILL) catch {};
_ = std.posix.waitpid(f.pid, 0);
std.posix.close(f.master);
}
// A fresh pty echoes; the shell turns it off. Polled, because nothing
// notifies a mode change.
var waited: usize = 0;
while (waited < 100) : (waited += 1) {
const m = try ptyMode(f.master);
if (!m.echo) break;
std.Thread.sleep(50 * std.time.ns_per_ms);
}
try std.testing.expect(!(try ptyMode(f.master)).echo);
// The foreground group is the shell itself while `sleep` is its child
// in the same group: fgPgid equals the pid forkPty returned.
try std.testing.expectEqual(f.pid, try ptyFgPgid(f.master));
try setWinsize(f.master, .{ .row = 10, .col = 40, .xpixel = 0, .ypixel = 0 });
}
```
- [ ] **Step 2: Run to verify it fails**
Run: `deps/zig/zig build test -Dtest-filter="server_os" 2>&1 | tail -5`
Expected: compile error, `forkPty` not found.
- [ ] **Step 3: Add the operations to the root**
In `src/os/server_os.zig`, after `getpid`:
```zig
pub const Winsize = std.posix.winsize;
pub const ForkedPty = struct { pid: std.posix.pid_t, master: std.posix.fd_t };
/// Fork with a fresh pty as the child's controlling terminal, sized before
/// the shell's first read so no program sees a 0x0 grid. Returns pid 0 in
/// the child, exactly as forkpty(3) does, so the child code that resets
/// signals and injects env stays where the fork is visible (pty.zig).
pub fn forkPty(ws: Winsize) error{ForkPtyFailed}!ForkedPty {
return impl.forkPty(ws);
}
/// A child's bail-out. Never `std.process.exit`: under link_libc that is
/// exit(3), which runs atexit and flushes stdio buffers the child inherited
/// from the parent — so the parent's pending bytes would be written twice.
pub fn exitNow(code: u8) noreturn {
impl.exitNow(code);
}
/// The fd barrier: every descriptor at or above `first` is closed in the
/// child before exec. CLOEXEC is set fd by fd, and an upgrade clears every
/// one and must seal them again — two hand-kept lists that would have to
/// agree, or the manifest carrier with the QUIC key bytes rides into the
/// shell. This needs no list.
pub fn closeFrom(first: std.posix.fd_t) void {
impl.closeFrom(first);
}
/// The two line-discipline bits that decide who echoes a keystroke, read
/// off the MASTER. Polled — the kernel notifies nobody when a mode changes.
pub const PtyMode = struct { icanon: bool, echo: bool };
pub fn ptyMode(master: std.posix.fd_t) !PtyMode {
return impl.ptyMode(master);
}
/// Foreground process group of the pty. Equal to the session's child pid
/// means no foreground job: the kernel's "command returned" with zero shell
/// cooperation, which is `mux a`'s `pgid` mechanism.
pub fn ptyFgPgid(master: std.posix.fd_t) error{IoctlFailed}!std.posix.pid_t {
return impl.ptyFgPgid(master);
}
/// Resize the pty; the kernel raises SIGWINCH in the session.
pub fn setWinsize(master: std.posix.fd_t, ws: Winsize) error{IoctlFailed}!void {
return impl.setWinsize(master, ws);
}
```
- [ ] **Step 4: Spell them in the Linux arm**
Replace `src/os/server_os_linux.zig` with:
```zig
//! Linux arm of `server_os`. Spellings only; the contract is in the root.
const std = @import("std");
const root = @import("server_os.zig");
const c = @cImport({
@cInclude("pty.h");
@cInclude("sys/ioctl.h");
});
pub fn getpid() std.posix.pid_t {
return std.os.linux.getpid();
}
pub fn forkPty(ws: root.Winsize) error{ForkPtyFailed}!root.ForkedPty {
var master: c_int = undefined;
var cws: c.struct_winsize = .{ .ws_row = ws.row, .ws_col = ws.col, .ws_xpixel = 0, .ws_ypixel = 0 };
const pid = c.forkpty(&master, null, null, &cws);
if (pid < 0) return error.ForkPtyFailed;
return .{ .pid = pid, .master = master };
}
pub fn exitNow(code: u8) noreturn {
std.os.linux.exit_group(code);
}
pub fn closeFrom(first: std.posix.fd_t) void {
// ENOSYS (pre-5.9 kernel) leaves the CLOEXEC flags to do the work alone.
_ = std.os.linux.syscall3(.close_range, @intCast(first), std.math.maxInt(u32), 0);
}
pub fn ptyMode(master: std.posix.fd_t) !root.PtyMode {
// On Linux the master shares one termios with the slave, so what the
// session did with tcsetattr is one syscall away.
const t = try std.posix.tcgetattr(master);
return .{ .icanon = t.lflag.ICANON, .echo = t.lflag.ECHO };
}
pub fn ptyFgPgid(master: std.posix.fd_t) error{IoctlFailed}!std.posix.pid_t {
var pgid: c.pid_t = 0;
if (c.ioctl(master, c.TIOCGPGRP, &pgid) < 0) return error.IoctlFailed;
return @intCast(pgid);
}
pub fn setWinsize(master: std.posix.fd_t, ws: root.Winsize) error{IoctlFailed}!void {
var cws: c.struct_winsize = .{ .ws_row = ws.row, .ws_col = ws.col, .ws_xpixel = 0, .ws_ypixel = 0 };
if (c.ioctl(master, c.TIOCSWINSZ, &cws) < 0) return error.IoctlFailed;
}
```
- [ ] **Step 5: Run the new tests**
Run: `deps/zig/zig build test -Dtest-filter="server_os" 2>&1 | tail -5`
Expected: pass. If `stty -echo` leaves echo on, the shell was not given the pty as its controlling tty — check `forkpty` returned the child's pid, not 0, to the parent.
- [ ] **Step 6: Rewire `pty.zig`**
Header: delete the `@cImport` block (lines 5-9) and add `const server_os = @import("server_os");`. Update the `//!` header's "forkpty with the user's shell" to "a pty forked through `server_os.forkPty` with the user's shell".
In `spawnArgv`: replace the `ws` declaration and the `forkpty` call:
```zig
const ws: server_os.Winsize = .{ .row = opts.rows, .col = opts.cols, .xpixel = 0, .ypixel = 0 };
if (opts.argv[0] == null) return error.EmptyArgv;
const f = try server_os.forkPty(ws);
const pid = f.pid;
const master = f.master;
```
In the child block: `std.os.linux.exit_group(126)` → `server_os.exitNow(126)`; the `close_range` line and its four-line comment →
```zig
server_os.closeFrom(3);
```
(the rationale now lives on `closeFrom` in the root); `std.os.linux.exit_group(127)` → `server_os.exitNow(127)`. The comment "exit_group, never std.process.exit — see spawn.zig's fork child" becomes "`exitNow`, never `std.process.exit` — see `server_os.exitNow` for why".
`Mode` and `mode()`:
```zig
pub const Mode = server_os.PtyMode;
pub fn mode(self: *const Pty) !Mode {
return server_os.ptyMode(self.master);
}
```
`fgPgid`: body becomes `return server_os.ptyFgPgid(self.master);`. `resize`: body becomes `return server_os.setWinsize(self.master, .{ .row = rows, .col = cols, .xpixel = 0, .ypixel = 0 });`.
Test "a daemon fd without CLOEXEC still does not reach the shell": `/proc/self/fd/{d}` → `/dev/fd/{d}`, and the comment "The child looks for its own copy" gains ": through /dev/fd, which every OS mux runs on has".
- [ ] **Step 7: Grant and check**
`build.zig`: row `pty` gets `.imports = &.{"server_os"}`.
Run: `make check > /tmp/c.log 2>&1; echo rc=$?; tail -5 /tmp/c.log`
Expected: rc=0.
Run: `E2E_ONLY=05_session make e2e > /tmp/e.log 2>&1; echo rc=$?; tail -3 /tmp/e.log`
Expected: rc=0 (sessions, exits and resizes through the real binary).
- [ ] **Step 8: Commit**
```bash
git add src/os src/server/pty.zig build.zig
git commit -m "refactor: the pty forks, seals and resizes through server_os"
```
---
### Task 3: The daemon fork moves to `server_os.forkDetached`
**Files:**
- Modify: `src/os/server_os.zig`, `src/os/server_os_linux.zig`
- Modify: `src/cli/main.zig:1271-1356` (`forkDaemon`), header exemption line ~4
- Modify: `build.zig` rule 6 `except`, and grant `server_os` to row `mux`
**Interfaces:**
- Produces: `server_os.forkDetached(exe: [*:0]const u8, argv: [*:null]const ?[*:0]const u8, stdin_fd: fd_t, out_fd: fd_t) error{ForkFailed}!std.posix.pid_t` — the child becomes a session leader, dup2s `stdin_fd` to 0 and `out_fd` to 1 and 2, execs `exe`, and exits 127 without atexit if the exec fails. Returns the child's pid to the parent only.
- [ ] **Step 1: Write the failing test in `server_os.zig`**
```zig
test "server_os.forkDetached: the child is a session leader writing to the fd it was given" {
// Asked of the OS: the child prints its own session id and pid; a
// detached daemon is its own session leader, so they are equal.
const pipe = try std.posix.pipe();
defer std.posix.close(pipe[0]);
const devnull = try std.fs.cwd().openFile("/dev/null", .{});
defer devnull.close();
const argv = [_:null]?[*:0]const u8{ "/bin/sh", "-c", "ps -o sid= -p $$ | tr -d ' '; echo $$" };
const pid = try forkDetached("/bin/sh", &argv, devnull.handle, pipe[1]);
std.posix.close(pipe[1]);
var buf: [64]u8 = undefined;
var n: usize = 0;
while (true) {
const got = try std.posix.read(pipe[0], buf[n..]);
if (got == 0) break;
n += got;
}
_ = std.posix.waitpid(pid, 0);
var lines = std.mem.tokenizeScalar(u8, buf[0..n], '\n');
const sid = lines.next() orelse return error.NoOutput;
const shpid = lines.next() orelse return error.NoOutput;
try std.testing.expectEqualStrings(shpid, sid);
try std.testing.expectEqual(pid, try std.fmt.parseInt(std.posix.pid_t, shpid, 10));
}
```
- [ ] **Step 2: Run to verify it fails**
Run: `deps/zig/zig build test -Dtest-filter="forkDetached" 2>&1 | tail -3`
Expected: compile error, `forkDetached` not found.
- [ ] **Step 3: Root and arm**
Root:
```zig
/// The repository's ONE fork that is not a pty: `mux d start -d`. The child
/// becomes a session leader, wires stdin to `stdin_fd` and both stdout and
/// stderr to `out_fd`, and execs `exe` with `argv` — a fresh image, because
/// `std.debug.MemoryAccessor` caches the pid it reads memory through and a
/// Debug child that kept running would inspect the parent and panic
/// (decisions.md, 2026-08-28). A failed exec exits 127 with no atexit.
/// Returns the child's pid; the parent decides how long to wait for it.
pub fn forkDetached(
exe: [*:0]const u8,
argv: [*:null]const ?[*:0]const u8,
stdin_fd: std.posix.fd_t,
out_fd: std.posix.fd_t,
) error{ForkFailed}!std.posix.pid_t {
return impl.forkDetached(exe, argv, stdin_fd, out_fd);
}
```
Linux arm:
```zig
pub fn forkDetached(
exe: [*:0]const u8,
argv: [*:null]const ?[*:0]const u8,
stdin_fd: std.posix.fd_t,
out_fd: std.posix.fd_t,
) error{ForkFailed}!std.posix.pid_t {
const pid = std.posix.fork() catch return error.ForkFailed;
if (pid != 0) return pid;
_ = std.os.linux.setsid();
std.posix.dup2(stdin_fd, std.posix.STDIN_FILENO) catch exitNow(127);
std.posix.dup2(out_fd, std.posix.STDOUT_FILENO) catch exitNow(127);
std.posix.dup2(out_fd, std.posix.STDERR_FILENO) catch exitNow(127);
std.posix.execveZ(exe, argv, std.c.environ) catch exitNow(127);
unreachable;
}
```
- [ ] **Step 4: Run the test**
Run: `deps/zig/zig build test -Dtest-filter="forkDetached" 2>&1 | tail -3`
Expected: pass.
- [ ] **Step 5: Rewire `forkDaemon`**
In `src/cli/main.zig`, add `const server_os = @import("server_os");` near the other imports. Replace from `const pid = std.posix.fork() catch {` through the end of the `if (pid == 0) { ... }` block with:
```zig
const pid = server_os.forkDetached(exe_z.ptr, argv.ptr, devnull.handle, log.handle) catch {
if (progress.tty) progress.emit("\n");
return error.SpawnFailed;
};
```
Keep the comment above it that says daemon-mode code owns the fork path, rewritten: "The fork itself is `server_os.forkDetached`; this function owns what goes INTO it — the log, the argv and the deadline — and folder rule 6 names that file as the one fork."
`build.zig`: rule 6 `.except = "src/os/server_os_linux.zig"`; row `mux` gains `"server_os"` in `imports`. CLAUDE.md: the invariant paragraph "The daemon starts itself, and it execs THIS image" says `main.forkDaemon` is the only `posix.fork`; change to "`server_os_linux.forkDetached` is the only `posix.fork` under `src/`, and `main.forkDaemon` is its one caller".
- [ ] **Step 6: Check and the boot leg**
Run: `make check > /tmp/c.log 2>&1; echo rc=$?; tail -3 /tmp/c.log` → rc=0.
Run: `E2E_ONLY=01_boot make e2e > /tmp/e.log 2>&1; echo rc=$?; tail -3 /tmp/e.log` → rc=0 (the `-d` start, `comm` and `exe` pins).
- [ ] **Step 7: Commit**
```bash
git add src/os src/cli/main.zig build.zig CLAUDE.md
git commit -m "refactor: the daemon fork is server_os.forkDetached, and rule 6 names it"
```
---
### Task 4: Peer credentials and the parent walk
**Files:**
- Modify: `src/os/server_os.zig`, `src/os/server_os_linux.zig`, `src/os/client_os.zig`, `src/os/client_os_linux.zig`
- Modify: `src/cli/main.zig:693-701` (`peerPid`) and its test at ~2021
- Modify: `src/client/askpass.zig:150-156, 298-304, 480-485, 509-527, 730-736`
- Modify: `build.zig` grant `client_os` to row `client`
**Interfaces:**
- Produces: `server_os.PeerCred = struct { uid: std.posix.uid_t, pid: std.posix.pid_t }`, `server_os.peerCred(fd) ?PeerCred`; `client_os.PeerCred` (same shape), `client_os.peerCred(fd) ?PeerCred`, `client_os.parentOf(pid) std.posix.pid_t` (0 for unknown or ≤0), `client_os.geteuid() std.posix.uid_t`.
- [ ] **Step 1: Failing tests**
In `server_os.zig`:
```zig
test "server_os.peerCred: the kernel names the peer of a socketpair as this process" {
var sp: [2]std.posix.fd_t = undefined;
try std.testing.expectEqual(@as(c_int, 0), std.c.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, &sp));
defer std.posix.close(sp[0]);
defer std.posix.close(sp[1]);
const cred = peerCred(sp[0]) orelse return error.NoCred;
try std.testing.expectEqual(getpid(), cred.pid);
try std.testing.expectEqual(std.c.geteuid(), cred.uid);
}
```
In `client_os.zig`:
```zig
test "client_os.peerCred and parentOf: asked of the OS, not a fixture" {
var sp: [2]std.posix.fd_t = undefined;
try std.testing.expectEqual(@as(c_int, 0), std.c.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, &sp));
defer std.posix.close(sp[0]);
defer std.posix.close(sp[1]);
const cred = peerCred(sp[0]) orelse return error.NoCred;
try std.testing.expectEqual(getpid(), cred.pid);
try std.testing.expectEqual(geteuid(), cred.uid);
try std.testing.expectEqual(std.c.getppid(), parentOf(getpid()));
try std.testing.expectEqual(@as(std.posix.pid_t, 0), parentOf(0));
}
```
- [ ] **Step 2: Verify they fail** — `deps/zig/zig build test -Dtest-filter="peerCred" 2>&1 | tail -3`, expect compile errors.
- [ ] **Step 3: Roots**
`server_os.zig`:
```zig
/// Who is on the other end of a unix socket, or null when the kernel will
/// not say (across a pid namespace, for one); callers then rely on socket
/// shutdown. The daemon uses the pid to wait for a client that vanished.
pub const PeerCred = struct { uid: std.posix.uid_t, pid: std.posix.pid_t };
pub fn peerCred(fd: std.posix.socket_t) ?PeerCred {
return impl.peerCred(fd);
}
```
`client_os.zig`:
```zig
/// Who is on the other end of the askpass socket. The 0700 runtime
/// directory is the boundary and mux takes it as found; where it is not
/// private, the uid here is what stops another local user raising a prompt
/// and reading the answer, and the pid is what attributes a prompt to the
/// ssh THIS wall spawned.
pub const PeerCred = struct { uid: std.posix.uid_t, pid: std.posix.pid_t };
pub fn peerCred(fd: std.posix.socket_t) ?PeerCred {
return impl.peerCred(fd);
}
/// The parent of `pid`, or 0 when the OS will not say or `pid` is not
/// positive. One step of the walk from an askpass helper up to the ssh a
/// dial spawned.
pub fn parentOf(pid: std.posix.pid_t) std.posix.pid_t {
if (pid <= 0) return 0;
return impl.parentOf(pid);
}
/// The effective uid, for the askpass caller check above.
pub fn geteuid() std.posix.uid_t {
return impl.geteuid();
}
```
- [ ] **Step 4: Linux arms**
`server_os_linux.zig`:
```zig
pub fn peerCred(fd: std.posix.socket_t) ?root.PeerCred {
const Ucred = extern struct { pid: std.posix.pid_t, uid: std.posix.uid_t, gid: std.posix.gid_t };
var cred: Ucred = undefined;
std.posix.getsockopt(fd, std.posix.SOL.SOCKET, std.posix.SO.PEERCRED, std.mem.asBytes(&cred)) catch return null;
if (cred.pid <= 0) return null;
return .{ .uid = cred.uid, .pid = cred.pid };
}
```
`client_os_linux.zig` (add `const root = @import("client_os.zig");`):
```zig
pub fn peerCred(fd: std.posix.socket_t) ?root.PeerCred {
const Ucred = extern struct { pid: std.posix.pid_t, uid: std.posix.uid_t, gid: std.posix.gid_t };
var cred: Ucred = undefined;
std.posix.getsockopt(fd, std.posix.SOL.SOCKET, std.posix.SO.PEERCRED, std.mem.asBytes(&cred)) catch return null;
return .{ .uid = cred.uid, .pid = cred.pid };
}
/// `/proc/<pid>/stat` field 4. Parsed from the LAST ')' rather than by
/// counting spaces: field 2 is the executable's name, unquoted, and a
/// program free to call itself `a b) c` is a program free to move every
/// field after it.
pub fn parentOf(pid: std.posix.pid_t) std.posix.pid_t {
var path_buf: [64]u8 = undefined;
const path = std.fmt.bufPrint(&path_buf, "/proc/{d}/stat", .{pid}) catch return 0;
var stat_buf: [512]u8 = undefined;
const f = std.fs.cwd().openFile(path, .{}) catch return 0;
defer f.close();
const n = f.read(&stat_buf) catch return 0;
const text = stat_buf[0..n];
const close = std.mem.lastIndexOfScalar(u8, text, ')') orelse return 0;
var it = std.mem.tokenizeScalar(u8, text[close + 1 ..], ' ');
_ = it.next() orelse return 0; // the run state
const ppid = it.next() orelse return 0;
return std.fmt.parseInt(std.posix.pid_t, ppid, 10) catch 0;
}
pub fn geteuid() std.posix.uid_t {
return std.os.linux.geteuid();
}
```
- [ ] **Step 5: Tests pass** — `deps/zig/zig build test -Dtest-filter="peerCred" 2>&1 | tail -3`.
- [ ] **Step 6: Rewire callers**
`main.zig` `peerPid`:
```zig
/// The peer's pid, or null when the kernel cannot expose it; callers then
/// rely on socket shutdown.
fn peerPid(fd: std.posix.socket_t) ?std.posix.pid_t {
const cred = server_os.peerCred(fd) orelse return null;
return cred.pid;
}
```
Its test: `std.os.linux.socketpair(...)` → `std.c.socketpair(...)` with `expectEqual(@as(c_int, 0), ...)`, and `std.os.linux.getpid()` → `server_os.getpid()`.
`askpass.zig`: add `const client_os = @import("client_os");`. Delete the local `Ucred` type (grep `const Ucred`), `peerCred` and `parentOf`; in `start` the path uses `client_os.getpid()`; in `serve`:
```zig
const cred = client_os.peerCred(c) orelse return;
if (cred.uid != client_os.geteuid()) return;
var p: Prompt = .{ .ssh_pid = dialOwner(cred.pid, client_os.getpid(), client_os.parentOf) };
```
The comment on `serve`'s uid line ("The 0700 runtime directory is the boundary…") moves to `client_os.peerCred`'s doc (it is there already); leave one line: "// Both checks are `client_os.peerCred`'s to explain."
The test "askpass.parentOf: the field it reads is the one the OS calls ppid" moves to `client_os.zig` (it is Step 1's test); delete it here. Any remaining `std.os.linux.getppid`/`getpid`/`geteuid` in askpass tests → `std.c.*`.
`build.zig`: row `client` gets `"client_os"` in `imports`.
- [ ] **Step 7: Check and the askpass leg**
`make check` rc=0; `E2E_ONLY=15_askpass make e2e` rc=0.
- [ ] **Step 8: Commit**
```bash
git add src/os src/cli/main.zig src/client/askpass.zig build.zig
git commit -m "refactor: peer credentials and the parent walk go through the os rows"
```
---
### Task 5: The upgrade manifest carrier is `server_os.anonFd`
**Files:**
- Modify: `src/os/server_os.zig`, `src/os/server_os_linux.zig`
- Modify: `src/server/server.zig:514-524` (`PendingUpgrade`), `:2258-2274`, `:2980-2994`, `:3008-3011` (comment), and every `memfd` in `server.zig` (grep)
- Modify: `src/server/server_test_upgrade.zig` (8 sites, grep `memfd_create`), `src/server/upgrade.zig` comments (grep `memfd`)
- Modify: `build.zig` grant `server_os` to row `daemon`
**Interfaces:**
- Produces: `server_os.anonFd(name: [*:0]const u8) error{CarrierFailed}!std.posix.fd_t` — an fd no path names once it returns, private to this uid, readable and writable, that survives an exec of this process (no CLOEXEC).
- [ ] **Step 1: Failing test**
```zig
test "server_os.anonFd: no path names it, and it is not CLOEXEC" {
const fd = try anonFd("mux-test-carrier");
defer std.posix.close(fd);
const st = try std.posix.fstat(fd);
try std.testing.expectEqual(@as(@TypeOf(st.nlink), 0), st.nlink);
const flags = try std.posix.fcntl(fd, std.posix.F.GETFD, 0);
try std.testing.expectEqual(@as(usize, 0), flags & std.posix.FD_CLOEXEC);
try std.posix.lseek_SET(fd, 0);
_ = try std.posix.write(fd, "abc");
try std.posix.lseek_SET(fd, 0);
var buf: [3]u8 = undefined;
try std.testing.expectEqual(@as(usize, 3), try std.posix.read(fd, &buf));
try std.testing.expectEqualStrings("abc", &buf);
}
```
- [ ] **Step 2: Fails** — `deps/zig/zig build test -Dtest-filter="anonFd" 2>&1 | tail -3`.
- [ ] **Step 3: Root and arm**
Root:
```zig
/// The upgrade manifest's carrier across `mux d upgrade`'s exec: an fd that
/// no path names once this returns, readable only by this uid, and NOT
/// CLOEXEC because the candidate must inherit it. It carries the QUIC arm's
/// raw key bytes, which is why "no path" is the property and not a nicety —
/// and why `closeFrom` seals it away from every session shell.
pub fn anonFd(name: [*:0]const u8) error{CarrierFailed}!std.posix.fd_t {
return impl.anonFd(name);
}
```
Linux arm:
```zig
pub fn anonFd(name: [*:0]const u8) error{CarrierFailed}!std.posix.fd_t {
return std.posix.memfd_create(std.mem.span(name), 0) catch error.CarrierFailed;
}
```
- [ ] **Step 4: Passes** — same filter.
- [ ] **Step 5: Rewire the daemon**
`server.zig`: add `const server_os = @import("server_os");`. `PendingUpgrade`:
```zig
// What the run loop needs to exec: the candidate's path and the carrier
// holding the manifest (`server_os.anonFd`). Set by validateUpgrade +
// writeManifestTo.
const PendingUpgrade = struct {
path: []const u8,
carrier: std.posix.fd_t,
};
```
Rename every `.memfd` field use to `.carrier` (grep `\.memfd`). At the `upgrade_req` site:
```zig
// Accepted: write the manifest to its carrier (not CLOEXEC —
// the new binary must inherit it), reply, and arm the exec.
const carrier = server_os.anonFd("mux-upgrade") catch return self.refuseUpgrade(i, "carrier");
self.writeManifestTo(carrier, self.version) catch {
std.posix.close(carrier);
return self.refuseUpgrade(i, "manifest");
};
```
and rename the local through the rest of that handler. In `checkManifestResume`:
```zig
const carrier = server_os.anonFd("mux-upgrade") catch
return a.dupe(u8, "check: cannot create the manifest carrier") catch null;
defer std.posix.close(carrier);
self.writeManifestTo(carrier, my_version) catch
```
and the `fd_str` line uses `carrier`. The `clearCloexec` comment: "the manifest memfd" → "the manifest carrier". Run `grep -n 'memfd' src/server/server.zig src/server/upgrade.zig src/server/server_*.zig` and reword every production-line comment hit to "carrier"; in tests replace `std.posix.memfd_create("...", 0)` with `server_os.anonFd("...")` (the child files import through `daemon`'s grant: `const server_os = @import("server_os");`).
`build.zig`: row `daemon` gets `"server_os"`.
- [ ] **Step 6: Check, the upgrade unit tests and the upgrade leg**
`make check` rc=0. `E2E_ONLY=14_upgrade make e2e` rc=0. (Its `memfd:mux-upgrade` fd-leak pin still greps `/proc` until Task 11; it must still pass now because the Linux carrier IS a memfd.)
- [ ] **Step 7: Commit**
```bash
git add src/os src/server build.zig
git commit -m "refactor: the upgrade manifest rides server_os.anonFd"
```
---
### Task 6: `selfImageStale` by identity, not by a kernel suffix
**Files:**
- Modify: `src/os/server_os.zig` (root-only implementation), `src/os/spawn.zig`
- Modify: `src/server/server.zig:3190-3208` (`selfImageStale` and its comment), the `Server.init` site (grep `pub fn init(`) to call `server_os.noteBootImage()`
**Interfaces:**
- Produces: `server_os.noteBootImage() void` (idempotent; records dev+ino of the running image), `server_os.selfImageStale() bool`.
- [ ] **Step 1: Failing test**
```zig
test "server_os.selfImageStale: a rename over the image's path is stale, an untouched path is not" {
// The test binary cannot be renamed under itself safely, so the rule is
// exercised on a copy in a temp dir through the same two functions with
// the path named explicitly.
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.writeFile(.{ .sub_path = "img", .data = "v1" });
var pbuf: [std.fs.max_path_bytes]u8 = undefined;
const path = try tmp.dir.realpath("img", &pbuf);
var ident = try imageIdent(path);
try std.testing.expect(!staleAgainst(ident, path));
try tmp.dir.writeFile(.{ .sub_path = "img.new", .data = "v2" });
try tmp.dir.rename("img.new", "img");
try std.testing.expect(staleAgainst(ident, path));
ident = try imageIdent(path);
try std.testing.expect(!staleAgainst(ident, path));
try tmp.dir.deleteFile("img");
try std.testing.expect(staleAgainst(ident, path));
}
```
- [ ] **Step 2: Fails** — filter `selfImageStale`.
- [ ] **Step 3: Root implementation (no arm: it is the same rule on every OS)**
```zig
/// Identity of a file: the pair a rename-over changes and a rebuild in
/// place does not.
const ImageIdent = struct { dev: u64, ino: u64 };
fn imageIdent(path: []const u8) !ImageIdent {
const st = try std.fs.cwd().statFile(path);
return .{ .dev = 0, .ino = st.inode };
}
/// Stale when the path now names a different inode than `at_boot`, or
/// nothing at all: `make install` and `mux d upgrade HOST` both rename a
/// new file over the running image, and the daemon keeps executing the
/// old one. Unknown is reported not-stale — a wall must not dress a
/// healthy box in a warning because a stat was refused.
fn staleAgainst(at_boot: ImageIdent, path: []const u8) bool {
const now = imageIdent(path) catch return true;
return now.ino != at_boot.ino;
}
var boot_image: ?struct { ident: ImageIdent, path: [std.fs.max_path_bytes]u8, len: usize } = null;
/// Record the running image's identity. Called once at daemon start; a
/// later call is a no-op, so the comparison is always against boot.
pub fn noteBootImage() void {
if (boot_image != null) return;
var buf: [std.fs.max_path_bytes]u8 = undefined;
const p = std.fs.selfExePath(&buf) catch return;
const ident = imageIdent(p) catch return;
var rec: @TypeOf(boot_image.?) = .{ .ident = ident, .path = undefined, .len = p.len };
@memcpy(rec.path[0..p.len], p);
boot_image = rec;
}
/// Has the file at the running image's path been replaced since boot.
/// Read fresh per ask: a rename lands under a running daemon at any moment,
/// and one stat per `sessions_req` is nothing.
pub fn selfImageStale() bool {
noteBootImage();
const b = boot_image orelse return false;
return staleAgainst(b.ident, b.path[0..b.len]);
}
```
Note `std.fs.File.Stat` carries `inode` and not `dev` in 0.15.2; the `dev` field stays zero and the comparison is by inode, which a rename-over on one filesystem always changes. If `statFile` returns a `dev` in this std, fill and compare it too.
- [ ] **Step 4: Passes** — filter `selfImageStale`.
- [ ] **Step 5: Rewire the daemon and `spawn`**
`server.zig`: delete the private `selfImageStale` and its comment block; every call becomes `server_os.selfImageStale()`. In `Server.init` (the daemon's constructor), add `server_os.noteBootImage();` as the first statement, with the comment "// Before any request can ask: the comparison is against the image that BOOTED, not the first one asked about."
`src/os/spawn.zig`: the `/proc/self/exe` fallback becomes per-OS:
```zig
/// The kernel's link to the running image, used only when the resolved
/// path is no longer executable — Linux keeps a live link after a rename-
/// over. On an OS with no such link the fallback is the resolved path
/// itself, and an exec of a replaced image fails where it always would.
pub const self_exe: []const u8 = switch (builtin.os.tag) {
.linux => "/proc/self/exe",
else => "",
};
```
with `const builtin = @import("builtin");`, and `execOrLink` returns `if (self_exe.len == 0) resolved else self_exe` in the fallback arm. The two tests keep their assertions; the second one wraps its `(deleted)` expectation in `if (builtin.os.tag == .linux)`.
- [ ] **Step 6: Check and the hosts leg (the `stale` word)**
`make check` rc=0. `E2E_ONLY=09_hosts make e2e` rc=0 — its stale pin at ~1657 renames a binary over the daemon's and reads `stale` off the wall.
- [ ] **Step 7: Commit**
```bash
git add src/os src/server/server.zig
git commit -m "refactor: a stale daemon image is an inode that moved, not a kernel suffix"
```
---
### Task 7: The mechanical spellings — `sendNoSig`, `sockType`, `getpid`, `geteuid`, `socketpair`, `ioctl`
**Files:**
- Modify: `src/os/server_os.zig`, `src/os/server_os_linux.zig`, `src/os/client_os.zig`, `src/os/client_os_linux.zig`
- Modify: `src/server/server.zig:177-186` (`Sink.send`), `src/server/quic_server.zig:318-338`, `src/server/server_agent.zig:131-136`, `src/server/shellint.zig:175-179,444-448`, `src/server/server_test_await.zig:372-376`, `src/server/server_test_harness.zig:221-227`
- Modify: `src/cli/webhub_main.zig:174-178`, `src/cli/muxa.zig:648-707` (three socketpairs), `src/cli/main.zig:2100-2108`
- Modify: `src/dial.zig:136-142`, `src/link.zig:305-310`, `src/engine/protocol.zig:2570-2576`, `src/xdg.zig:412-418`, `src/sockpath.zig:172-178`
- Modify: `src/tui/interact.zig:628-640` (`ttySize`), `:3626-3645` (`ptsPair`, `setTtySize`)
- Modify: `build.zig` grant `client_os` to row `wall`
**Interfaces:**
- Produces: `server_os.sendNoSig(fd, bytes) std.posix.SendError!usize` (non-blocking, never raises SIGPIPE); `server_os.sockType(fd) error{NotASocket}!u32`; `client_os.winSize(fd) ?std.posix.winsize`; `client_os.setWinSize(fd, ws) error{Unsupported}!void`; `client_os.openPtyPair() error{Unsupported}!struct { master: fd_t, slave: fd_t }`.
- Every test-only `std.os.linux.socketpair` becomes `std.c.socketpair`; every `std.os.linux.getpid/geteuid/getppid` becomes the row's function where a row is imported and `std.c.*` in tests.
- [ ] **Step 1: Failing tests**
`server_os.zig`:
```zig
test "server_os.sendNoSig: a closed peer is an error, not a signal" {
var sp: [2]std.posix.fd_t = undefined;
try std.testing.expectEqual(@as(c_int, 0), std.c.socketpair(std.posix.AF.UNIX, std.posix.SOCK.STREAM, 0, &sp));
defer std.posix.close(sp[0]);
std.posix.close(sp[1]);
// With SIGPIPE at its default this send would kill the test binary.
try std.testing.expectError(error.BrokenPipe, sendNoSig(sp[0], "x"));
try std.testing.expectEqual(@as(u32, std.posix.SOCK.STREAM), try sockType(sp[0]));
}
```
`client_os.zig`:
```zig
test "client_os.winSize reads what setWinSize wrote, off a real pty" {
const p = try openPtyPair();
defer std.posix.close(p.master);
defer std.posix.close(p.slave);
try setWinSize(p.master, .{ .row = 17, .col = 91, .xpixel = 0, .ypixel = 0 });
const ws = winSize(p.slave) orelse return error.NoSize;
try std.testing.expectEqual(@as(u16, 91), ws.col);
try std.testing.expectEqual(@as(u16, 17), ws.row);
}
```
- [ ] **Step 2: Fail** — filters `sendNoSig`, `winSize`.
- [ ] **Step 3: Roots**
`server_os.zig`:
```zig
/// A non-blocking send that cannot raise SIGPIPE: a client that hung up
/// mid-frame is an error the pump handles, never a signal that ends the
/// daemon. The daemon also ignores SIGPIPE process-wide; this is the half
/// that does not depend on the order of that ignore against a fork.
pub fn sendNoSig(fd: std.posix.socket_t, bytes: []const u8) std.posix.SendError!usize {
return impl.sendNoSig(fd, bytes);
}
/// The socket type of an fd, for refusing to adopt a stream fd as the
/// QUIC listener across an upgrade: a stream fd would accept a handshake
/// and then lose every packet to recvfrom.
pub fn sockType(fd: std.posix.fd_t) error{NotASocket}!u32 {
return impl.sockType(fd);
}
```
`client_os.zig`:
```zig
/// This terminal's size, or null when `fd` is not a terminal. The 0x0 case
/// and the daemon's floor are the caller's to judge (`interact.ttySize`).
pub fn winSize(fd: std.posix.fd_t) ?std.posix.winsize {
return impl.winSize(fd);
}
/// Size a pty. Test-only in practice, but a contract because the wall's
/// own `ttySize` is judged against it.
pub fn setWinSize(fd: std.posix.fd_t, ws: std.posix.winsize) error{Unsupported}!void {
return impl.setWinSize(fd, ws);
}
/// A real master/slave pty pair, the OS answering about the OS. Test-only:
/// the wall never opens a pty, it lives in one.
pub fn openPtyPair() error{Unsupported}!struct { master: std.posix.fd_t, slave: std.posix.fd_t } {
return impl.openPtyPair();
}
```
- [ ] **Step 4: Linux arms**
`server_os_linux.zig`:
```zig
pub fn sendNoSig(fd: std.posix.socket_t, bytes: []const u8) std.posix.SendError!usize {
return std.posix.send(fd, bytes, std.posix.MSG.DONTWAIT | std.posix.MSG.NOSIGNAL);
}
pub fn sockType(fd: std.posix.fd_t) error{NotASocket}!u32 {
var t: i32 = undefined;
var len: std.posix.socklen_t = @sizeOf(@TypeOf(t));
const rc = std.os.linux.getsockopt(fd, std.os.linux.SOL.SOCKET, std.os.linux.SO.TYPE, @ptrCast(&t), &len);
if (std.os.linux.E.init(rc) != .SUCCESS) return error.NotASocket;
return @intCast(t);
}
```
`client_os_linux.zig`:
```zig
pub fn winSize(fd: std.posix.fd_t) ?std.posix.winsize {
var ws: std.posix.winsize = undefined;
if (std.os.linux.ioctl(fd, std.os.linux.T.IOCGWINSZ, @intFromPtr(&ws)) != 0) return null;
return ws;
}
pub fn setWinSize(fd: std.posix.fd_t, ws: std.posix.winsize) error{Unsupported}!void {
if (std.os.linux.ioctl(fd, std.os.linux.T.IOCSWINSZ, @intFromPtr(&ws)) != 0) return error.Unsupported;
}
pub fn openPtyPair() error{Unsupported}!struct { master: std.posix.fd_t, slave: std.posix.fd_t } {
const master = std.posix.open("/dev/ptmx", .{ .ACCMODE = .RDWR }, 0) catch return error.Unsupported;
errdefer std.posix.close(master);
var unlock: c_int = 0;
if (std.os.linux.ioctl(master, std.os.linux.T.IOCSPTLCK, @intFromPtr(&unlock)) != 0) return error.Unsupported;
var idx: c_uint = 0;
if (std.os.linux.ioctl(master, std.os.linux.T.IOCGPTN, @intFromPtr(&idx)) != 0) return error.Unsupported;
var name_buf: [32]u8 = undefined;
const name = std.fmt.bufPrint(&name_buf, "/dev/pts/{d}", .{idx}) catch return error.Unsupported;
const slave = std.posix.open(name, .{ .ACCMODE = .RDWR, .NOCTTY = true }, 0) catch return error.Unsupported;
return .{ .master = master, .slave = slave };
}
```
- [ ] **Step 5: Pass** — both filters.
- [ ] **Step 6: Rewire every site**
- `server.zig` `Sink.send`: `.socket => |fd| server_os.sendNoSig(fd, bytes),`.
- `quic_server.zig` `initFromFd`: replace the `sock_type` block with `if ((server_os.sockType(fd) catch return error.NotAUdpSocket) != std.posix.SOCK.DGRAM) return error.NotAUdpSocket;` (add the import; it is a child of `daemon`).
- `server_agent.zig`, `shellint.zig` (both sites), `server_test_await.zig`: `std.os.linux.getpid()` → `server_os.getpid()`.
- `server_test_harness.zig` `connectedPair`: `std.c.socketpair(...)`, and `if (rc != 0) return std.posix.unexpectedErrno(std.posix.errno(rc));`.
- `webhub_main.zig`: `client_os.getpid()` — `webhub_main` is a child of `mux`, which imports `client`; import `client_os` through the `mux` row: add `"client_os"` to row `mux`'s imports.
- `muxa.zig` three sites, `main.zig:2025`, `dial.zig:139`, `link.zig:308`, `protocol.zig:2573`: `std.os.linux.socketpair` → `std.c.socketpair`, expectations `@as(c_int, 0)`. muxa's comment "`std.os.linux` because `std.posix` has no socketpair on 0.15.2" → "`std.c` because `std.posix` has no socketpair on 0.15.2".
- `main.zig:2104`, `sockpath.zig:175`: `std.os.linux.geteuid()` → `std.c.geteuid()`. `xdg.zig:415`: `std.c.getpid()`.
- `interact.zig` `ttySize`: the ioctl lines become `const ws = client_os.winSize(fd) orelse return null;` (keep the `isatty` guard and the floor). `ptsPair` body: `return client_os.openPtyPair();`. `setTtySize`: `return client_os.setWinSize(master, .{ .col = cols, .row = rows, .xpixel = 0, .ypixel = 0 });`. Add the import; `build.zig` row `wall` gets `"client_os"`.
- [ ] **Step 7: The ban is now satisfiable — prove it by grep**
Run: `grep -rn 'std\.os\.linux' src --include=*.zig | grep -v '^src/os/'`
Expected: no output. If anything remains, respell it as above.
- [ ] **Step 8: Check and the two legs that watch sockets**
`make check` rc=0. `E2E_ONLY=06_web make e2e` rc=0; `E2E_ONLY=10_agent make e2e` rc=0.
- [ ] **Step 9: Commit**
```bash
git add src build.zig
git commit -m "refactor: every remaining std.os.linux spelling goes through an os row or std.c"
```
---
### Task 8: The shared root — `reapDeadPid`, `max_sun_path`, `runtimeDir`
**Files:**
- Modify: `src/xdg.zig:178-184`, `src/sockpath.zig:8-12, 44-55`, `src/tui/wallview.zig:1866-1870` (grep `XDG_RUNTIME_DIR`), `src/cli/muxa.zig:966` (grep `XDG_RUNTIME_DIR`)
**Interfaces:**
- Produces: `sockpath.runtimeDir() ?[]const u8` — the directory the default socket and the askpass socket live in, or null with no fallback on Linux.
- [ ] **Step 1: Failing tests**
`sockpath.zig`, beside `sockPathFrom`'s tests:
```zig
test "sockpath.max_sun_path is the kernel's field less its NUL, not a number of ours" {
try std.testing.expectEqual(@sizeOf(@FieldType(std.posix.sockaddr.un, "path")) - 1, max_sun_path);
}
```
`xdg.zig`, extend the existing `reapDeadPid` test (grep `dead_dir`): after the reap, assert the `ours` entry is still present and add
```zig
// A pid that is alive but is not a mux — pid 1 — keeps its entry. The
// liveness question is `kill(pid, 0)`, which answers for every process
// this uid may signal and EPERM for the ones it may not; both are alive.
var b4: [48]u8 = undefined;
const init_dir = try std.fmt.bufPrint(&b4, "mux-agent-{d}-abc", .{@as(u32, 1)});
try tmp.dir.makePath(init_dir);
reapDeadPid(tmp.dir, "mux-agent-");
try tmp.dir.access(init_dir, .{});
```
(match the existing test's calling convention for `reapDeadPid`; grep the signature).
- [ ] **Step 2: Fail** — the `max_sun_path` test fails to compile if the constant is still a literal typed `comptime_int` equal to 107? It passes trivially on Linux (108−1). Make the test meaningful: assert `max_sun_path == 107` only under `if (builtin.os.tag == .linux)` AND that the comptime derivation is the source — i.e. after Step 3 the literal is gone. The reap test fails before Step 3 only if `kill(1, 0)` differs from `access("/proc/1")` — it does not. So this task's proof is the grep in Step 5, and the tests are the pin against regression.
- [ ] **Step 3: Respell**
`sockpath.zig`:
```zig
/// The usable bytes of `sockaddr_un.sun_path`: the field less the NUL.
/// Derived from the kernel's own struct rather than spelled — 108 on
/// Linux, 104 on the BSDs — and private, because every binary that once
/// re-compared it grew its own wording for the same refusal.
const max_sun_path = @sizeOf(@FieldType(std.posix.sockaddr.un, "path")) - 1;
```
```zig
/// The directory the default daemon socket and every per-wall socket live
/// in, or null. On Linux that is `$XDG_RUNTIME_DIR` and there is NO
/// fallback: a guess cannot make two binaries agree on one daemon, so the
/// caller names it with --sock. Another OS spells its own default here,
/// once, so the daemon, the client and the askpass listener agree by
/// construction.
pub fn runtimeDir() ?[]const u8 {
return switch (builtin.os.tag) {
.linux => std.posix.getenv("XDG_RUNTIME_DIR"),
else => @compileError("mux has no default runtime directory for " ++ @tagName(builtin.os.tag)),
};
}
pub fn defaultSockPath(alloc: std.mem.Allocator) ![]const u8 {
return sockPathFrom(alloc, runtimeDir());
}
```
(add `const builtin = @import("builtin");`).
`wallview.zig` ~1868: `std.posix.getenv("XDG_RUNTIME_DIR")` → `client.sockpath.runtimeDir()` (or however `sockpath` is reached there; `wall` imports `client`, and `client` imports `sockpath` — if `client.zig` does not re-export it, add `pub const sockpath = @import("sockpath");` beside its other re-exports). `muxa.zig:966` keeps its wording (`mux a` speaks JSON) but reads `sockpath.runtimeDir()`.
`xdg.zig` `reapDeadPid`:
```zig
const pid = std.fmt.parseInt(u32, rest[0..n], 10) catch continue;
// `kill(pid, 0)`: alive, or alive-but-not-ours (EPERM), keep; ESRCH
// is the one answer that means the pid is gone. A live pid's entry
// stays even when it is no longer a mux.
const alive = if (std.posix.kill(@intCast(pid), 0)) true else |err| err == error.PermissionDenied;
if (alive) continue;
```
Reword `xdg.zig`'s and CLAUDE.md's "whose pid `/proc` no longer has" to "whose pid the OS no longer has".
- [ ] **Step 4: Pass** — `deps/zig/zig build test -Dtest-filter="sockpath" 2>&1 | tail -3`, `-Dtest-filter="reapDeadPid"`.
- [ ] **Step 5: Grep**
Run: `grep -rn 'XDG_RUNTIME_DIR' src --include=*.zig | grep -v 'sockpath.zig' | grep -v '^\s*//'` — expected: only string literals in user-facing messages (the two refusal sentences), no `getenv`.
- [ ] **Step 6: Check and the askpass leg** — `make check` rc=0; `E2E_ONLY=15_askpass make e2e` rc=0.
- [ ] **Step 7: Commit**
```bash
git add src/xdg.zig src/sockpath.zig src/tui/wallview.zig src/cli/muxa.zig src/client/client.zig CLAUDE.md
git commit -m "refactor: the shared root asks the OS in portable words, and sockpath owns the runtime dir"
```
---
### Task 9: Folder rule 7, and the comments that would trip it
**Files:**
- Modify: `build.zig` `source_bans` (~line 340-385)
- Modify: every file `zig build check` names
- Modify: `CLAUDE.md` Layout prose (rules paragraph)
- [ ] **Step 1: Add the rule**
After rule 6 in `source_bans`:
```zig
.{
.rule = "7",
.folders = &.{ "src", "src/engine", "src/client", "src/tui", "src/server", "src/cli" },
// The raw spellings the platform layer exists to hold. `src/os/` is
// absent from the list on purpose: its children may spell anything,
// and its roots have no reason to. Comments count, as they do for
// rule 4 — a comment naming a Linux mechanism is one that goes
// stale the day a second arm exists.
.needles = &.{ "std.os.linux", "/proc", "memfd", "close_range", "exit_group", "peercred", "tiocsptlck", "tiocgptn" },
.why = "a call whose spelling differs by OS belongs in src/os/, behind a " ++
"server_os or client_os operation whose doc names what it guarantees; " ++
"everything else builds for every OS from the same line",
},
```
(needles are matched lower-cased, hence `peercred`.)
- [ ] **Step 2: Run the gate and fix every hit**
Run: `deps/zig/zig build check 2>&1 | grep 'folder rule 7' | head`
For each `path:line`, reword the comment to name the operation: `/proc` → "the OS" or the helper (`pid_alive`, `client_os.parentOf`); `memfd` → "the manifest carrier (`server_os.anonFd`)"; `exit_group` → "`server_os.exitNow`". Repeat until the command prints nothing. Do not write a `folder rule 7 exemption:` line anywhere; there is no legitimate one yet.
- [ ] **Step 3: Prove the rule fires**
```bash
printf 'const x = "/proc/self";\n' >> src/xdg.zig
deps/zig/zig build check 2>&1 | grep -c 'folder rule 7 broken'
git checkout -- src/xdg.zig
```
Expected: `1`, then a clean tree (`git status --short src/xdg.zig` empty).
- [ ] **Step 4: CLAUDE.md**
In the Layout section's rules paragraph, after rule 6's sentence, add: "Rule 7 is the platform ban: `std.os.linux`, `/proc`, `memfd`, `close_range`, `exit_group`, `PEERCRED` and the two Linux-only pty ioctls may appear only under `src/os/`, comments included — the roots `server_os.zig` and `client_os.zig` are the contract and their `_linux` children the spellings (spec 2026-09-03)."
- [ ] **Step 5: `make check`** rc=0.
- [ ] **Step 6: Commit**
```bash
git add build.zig src CLAUDE.md
git commit -m "build: folder rule 7 keeps every OS-specific spelling under src/os"
```
---
### Task 10: Harness oracle helpers in `e2e_lib.sh`
**Files:**
- Modify: `test/e2e_lib.sh` (add a `# ---- the OS oracle ----` section after `real_pid`)
- Modify: `test/e2e_01_boot.sh` (all `/proc`, `stat -c`, `sha256sum` sites: lines 57-65, 535-541, 787-800, 904, 1044-1059, 1152-1178, 926-949), `test/e2e_03_side.sh:196-222`, `test/e2e_04_handoff.sh:91, 208-211, 218, 482`, `test/e2e_lib.sh:303,324,333` (`--ppid`)
**Interfaces:**
- Produces (all POSIX sh, all print to stdout, all exit 1 when the OS will not say):
- `pid_alive PID` — exit 0 iff the pid exists
- `pid_exe PID` — the fully resolved path of the running image
- `pid_comm PID` — the process name as the OS reports it
- `pid_args PID` — the argv, space-joined
- `pid_children PID` — child pids, one per line
- `pid_fd_count PID` — number of open fds
- `pid_fd_targets PID` — one line per fd, what it points at (`socket:[ino]`, `/dev/ptmx`, `/memfd:name`, a path)
- `pid_holds_unix_sock PID PATH` — exit 0 iff `PID` has `PATH` open as a unix socket
- `pid_rss_kb PID`
- `udp_local_bound HEXADDR` — exit 0 iff a UDP socket is bound at `HEXADDR` (the `/proc/net/udp` spelling `0100007F:1F90`)
- `file_mode PATH` (octal, `600`), `file_size PATH` (bytes), `sha256_of PATH` (hex only)
- `timeout` — defined as a function ONLY when no `timeout` binary exists, dispatching to `gtimeout`
- [ ] **Step 1: The helpers**
Add to `test/e2e_lib.sh` after `real_pid`:
```sh
# ---- the OS oracle ------------------------------------------------------
# "Ask the OS about the OS, not the daemon" (CLAUDE.md). Every pin that
# reads a pid, an fd table or a bound port goes through these names, so
# the spelling lives in one place per OS. The Linux arm is /proc; another
# OS adds a `case "$(uname)"` arm here and NOTHING in a group file changes.
# Each prints to stdout and returns 1 when the OS will not say.
_os=$(uname)
pid_alive() { kill -0 "$1" 2>/dev/null || [ -d "/proc/$1" ]; }
pid_exe() { readlink -f "/proc/$1/exe" 2>/dev/null; }
pid_comm() { cat "/proc/$1/comm" 2>/dev/null; }
pid_args() { tr '\0' ' ' < "/proc/$1/cmdline" 2>/dev/null; }
pid_children() { ps -o pid= --ppid "$1" 2>/dev/null | tr -d ' '; }
pid_fd_count() { find "/proc/$1/fd" -mindepth 1 2>/dev/null | wc -l | tr -d ' '; }
pid_fd_targets() { readlink "/proc/$1"/fd/* 2>/dev/null; }
pid_holds_unix_sock() {
_ino=$(awk -v p="$2" '$NF == p {print $7}' /proc/net/unix | head -1)
[ -n "$_ino" ] && pid_fd_targets "$1" | grep -qx "socket:\[$_ino\]"
}
pid_rss_kb() { awk '/VmRSS/{print $2}' "/proc/$1/status" 2>/dev/null || echo 0; }
udp_local_bound() { awk -v h="$1" '$2==h{f=1} END{exit !f}' /proc/net/udp; }
file_mode() { stat -c %a "$1"; }
file_size() { stat -c %s "$1"; }
sha256_of() { sha256sum "$1" | cut -d' ' -f1; }
# GNU timeout is a binary here; a box without one names gtimeout, and a
# group file keeps spelling `timeout`.
command -v timeout >/dev/null 2>&1 || timeout() { gtimeout "$@"; }
```
(`_os` is set now so the step-3 arms have it; on Linux it is unused. If `make check`'s shell gate flags an unused variable, drop the line and add it with the first `case`.)
- [ ] **Step 2: A self-test of the oracle, run by the runner before any group**
Append to `test/e2e_lib.sh`:
```sh
# The oracle's own pin, off-origin on every dimension: a child that is not
# pid 1, holding MORE than the three fds a fixture would, on a socket path
# it did not inherit. A helper that answered "yes" for pid 1 or for fd
# count 3 would pass a fixture and fail a daemon.
oracle_selftest() {
_osock="$OUT.oracle.sock"
_opid=$(sh -c 'exec 5>/dev/null 6>/dev/null; sleep 30' & echo $!)
_okid=$(sh -c "sleep 30 & echo \$!")
pid_alive "$_opid" || { echo "e2e FAIL: oracle: pid_alive says a live sleep is dead"; exit 1; }
[ "$(pid_comm "$_opid")" = sh ] || { echo "e2e FAIL: oracle: pid_comm of a sh is $(pid_comm "$_opid")"; exit 1; }
pid_args "$_opid" | grep -q 'sleep 30' || { echo "e2e FAIL: oracle: pid_args lost the argv"; exit 1; }
[ "$(pid_fd_count "$_opid")" -ge 5 ] || { echo "e2e FAIL: oracle: pid_fd_count under 5 for a shell holding fd 5 and 6"; exit 1; }
[ "$(pid_exe "$_opid")" = "$(readlink -f "$(command -v sh)")" ] || { echo "e2e FAIL: oracle: pid_exe is $(pid_exe "$_opid")"; exit 1; }
[ "$(pid_rss_kb "$_opid")" -gt 0 ] || { echo "e2e FAIL: oracle: pid_rss_kb is 0"; exit 1; }
[ "$(file_mode "$OUT")" = "$(stat -c %a "$OUT")" ] || { echo "e2e FAIL: oracle: file_mode"; exit 1; }
kill "$_opid" "$_okid" 2>/dev/null; wait "$_opid" 2>/dev/null
_i=0; while pid_alive "$_opid" && [ "$_i" -lt 50 ]; do sleep 0.05; _i=$((_i+1)); done
pid_alive "$_opid" && { echo "e2e FAIL: oracle: pid_alive says a killed sleep lives"; exit 1; }
rm -f "$_osock"
ok "oracle: the OS answers the helpers by name"
}
```
and in `test/e2e.sh`, right after `. "$E2E_DIR/e2e_lib.sh"` and the hermetic setup (grep `OUT=`), add `oracle_selftest`. The runner's scenario-count pin at the bottom of `e2e.sh` goes up by one; edit that number.
- [ ] **Step 3: Rewrite the group pins**
Apply, file by file, then diff-read each hunk:
`e2e_lib.sh:303,324,333`: `$(ps -o pid= --ppid "$1" 2>/dev/null)` → `$(pid_children "$1")`.
`e2e_01_boot.sh`: `stat -c %a X` → `file_mode X`; `stat -c %s X` → `file_size X`; `sha256sum "$KEYOUT"` → `sha256_of "$KEYOUT"` (the comparison `SUM1 = SUM2` is unchanged); line 539 `grep -qi " $QHEX " /proc/net/udp` → `udp_local_bound "$QHEX"` (and `QHEX` must be the `ADDR:PORT` hex the existing `udp_local_bound` at 787 expects — read both sites, they already agree on spelling; delete the local `udp_local_bound` at 787 since the lib defines it); line 541 and 800 diagnostic `grep -i ... /proc/net/udp` → `pid_fd_targets` is not the answer here; keep them as `cat /proc/net/udp 2>/dev/null | grep -i "$QHEX" || true` wrapped in a lib helper `udp_table` (`udp_table() { cat /proc/net/udp; }`) — add it to the oracle section; line 904 `cat "/proc/$SPID/comm"` → `pid_comm "$SPID"`; lines 1052-1055 → `pid_holds_unix_sock "$ESPID" "$ESOWN"`; 1059 → `pid_comm`; 1154, 1178 → `pid_fd_count "$HFDPID"`; 1156's message keeps its words minus `/proc`.
`e2e_03_side.sh:198` → `PAEXE=$(pid_exe "$PAPID")`; 209 → `pid_comm`; 222 → `pid_args`. Comments at 196, 206, 212 reworded to say "the resolved image" / "the OS's name for the process".
`e2e_04_handoff.sh:91` (inside the ssh shim's heredoc): `$(cat /proc/$$/comm)` → `$(ps -o comm= -p $$)` and `$(cat /proc/$PPID/comm)` → `$(ps -o comm= -p $PPID)` (the shim runs as its own `sh`, and `ps -o comm=` is the same answer on both OSes); 208, 211, 482 → `file_mode`; 218 → `udp_local_bound "$HHEXUDP"`.
- [ ] **Step 4: Run the touched groups**
```bash
for g in 01_boot 03_side 04_handoff; do E2E_ONLY=$g make e2e > /tmp/e.$g.log 2>&1; echo "$g rc=$?"; done
```
Expected: three `rc=0`. Then `grep -n '/proc\|stat -c\|sha256sum\|--ppid' test/e2e_01_boot.sh test/e2e_03_side.sh test/e2e_04_handoff.sh test/e2e_lib.sh | grep -v '^\S*:[0-9]*:\s*#'` — expected: only the oracle section's own lines.
- [ ] **Step 5: Commit**
```bash
git add test/e2e_lib.sh test/e2e.sh test/e2e_01_boot.sh test/e2e_03_side.sh test/e2e_04_handoff.sh
git commit -m "test: the e2e pins ask the OS through named oracle helpers"
```
---
### Task 11: The remaining pins — upgrade, hosts, web, push, soak, vm
**Files:**
- Modify: `test/e2e_06_web.sh:539-541`, `test/e2e_09_hosts.sh:1293-1360, 276, 351`, `test/e2e_14_upgrade.sh:170, 219, 492-505`, `test/e2e_16_push.sh:160-161`, `test/soak.sh:103-131`, `test/vm.sh:146, 157` (these run on the remote Linux VM through `vssh` and STAY as `/proc` — the VM is Linux; add a comment saying so), `test/wan.sh:586, 1174` (Python and a comment; the Python `ps --ppid` → `ps -o pid= --ppid` stays Linux — wan.sh's boxes are Linux; comment it)
- [ ] **Step 1: Rewrite**
`e2e_06_web.sh:539`: `[ ! -e "/proc/$DWSHELL" ]` → `! pid_alive "$DWSHELL"`; 541 → `pid_args "$DWSHELL" 2>/dev/null`.
`e2e_09_hosts.sh:1347-1354`, the environ walk: replace the loop with
```sh
# Every `mux d start` on the box, by argv — `environ` is not readable on
# every OS and the socket path is in the argv anyway (`--sock PATH`).
for _np in $(ps -Ao pid= | tr -d ' '); do
pid_args "$_np" 2>/dev/null | grep -q "mux d start.*$HRUN" || continue
...the existing body that records/kills the stray...
done
```
where `$HRUN` is whatever variable the existing loop matched the environ's runtime dir against (read lines 1293-1360: the loop looks for a daemon born under that box's runtime dir; the argv carries `--sock` under that dir, so the match is on the sock path). 276, 351: `sha256_of`.
`e2e_14_upgrade.sh:170`: `readlink "/proc/$UPXPID/exe"` → `pid_exe "$UPXPID"` (note `pid_exe` is `readlink -f`; `$UPBIN` must be a resolved path — check how it is spelled and `readlink -f` it once where it is set). 219 → `pid_args "$(real_pid "$D69PID")"`. 492: `ls -l "/proc/$UPNEWSH/fd" | grep -c -E 'socket:|memfd:|/dev/ptmx'` → `pid_fd_targets "$UPNEWSH" | grep -c -E 'socket:|memfd:|/dev/ptmx'`; 496, 505 diagnostics → `pid_fd_targets`. 502 → `pid_fd_targets "$(real_pid "$D71PID")" | grep -c 'memfd:mux-upgrade'`. The `memfd:` needle in a SHELL file is fine (rule 7 scans `src/`), but the comment at 480 should say "the manifest carrier" so a macOS arm reading `lsof` output knows what it is looking for.
`e2e_16_push.sh:160-161`: `pid_exe "$PUSHDPID"`.
`soak.sh:103, 110, 131`: `pid_rss_kb "$PDPID"`, `pid_fd_count "$PDPID"` (soak.sh sources e2e_lib? grep; if not, source it the way e2e.sh does, or copy the three one-liners under the same names with a comment pointing at the lib).
- [ ] **Step 2: Run**
```bash
for g in 06_web 09_hosts 14_upgrade 16_push; do E2E_ONLY=$g make e2e > /tmp/e.$g.log 2>&1; echo "$g rc=$?"; done
```
Four `rc=0`. Then `grep -rn '/proc' test/*.sh | grep -v 'e2e_lib.sh\|vm.sh\|wan.sh' | grep -v ':\s*#'` — expected: nothing.
- [ ] **Step 3: Commit**
```bash
git add test
git commit -m "test: the upgrade, hosts, web, push and soak pins read the OS through the oracle"
```
---
### Task 12: Build and deps take a target word; the linker is gated
**Files:**
- Modify: `build.zig:16-41` (`quicDeps`), every `use_lld = true` (lines 680, 865, 871, 876, 881, 886, 950, 971)
- Modify: `deps/quic/build-deps.sh:25-31, 35, 52-60, 73, 88, 109, 124`
- Modify: `Makefile:43-44, 68-70` (install/release), and `deps:` target
- [ ] **Step 1: `quicDeps` picks a word from the target**
```zig
fn quicDeps(b: *std.Build, target: std.Build.ResolvedTarget) struct { dir: []const u8, step: *std.Build.Step } {
// One word per prefix, shared with build-deps.sh, `make deps`,
// `make clean-deps` and wan.sh's musl cross-build: `native` for the
// host's own libc, `musl` for the static release, and the target
// triple for any cross target — so a third OS is one more `case` arm
// in the script and nothing here.
const t = target.result;
const name = if (t.abi == .musl)
"musl"
else if (t.os.tag == builtin.os.tag and t.cpu.arch == builtin.cpu.arch)
"native"
else
b.fmt("{s}-{s}", .{ @tagName(t.cpu.arch), @tagName(t.os.tag) });
const run = b.addSystemCommand(&.{ "deps/quic/build-deps.sh", name });
...
```
(keep the rest; `builtin` is `@import("builtin")` at the top of build.zig — add if absent).
- [ ] **Step 2: Gate LLD**
Add one helper and use it at each of the eight sites:
```zig
/// Zig 0.15's self-hosted x86_64 ELF linker can't handle the .sframe
/// sections gcc >= 16's crt1.o emits, so ELF goes through LLD. LLD does
/// not link Mach-O, and Zig's own linker does — so Darwin is the one
/// target that must NOT ask for it.
fn linkerFor(c: *std.Build.Step.Compile) void {
c.use_llvm = true;
c.use_lld = !c.rootModuleTarget().os.tag.isDarwin();
}
```
Replace each `X.use_llvm = true; X.use_lld = true;` pair with `linkerFor(X);` (keep the wsclient comment at 940 that explains its exception).
- [ ] **Step 3: `build-deps.sh`**
```sh
T="${1:-native}"
case "$T" in
native | musl | aarch64-macos) ;;
*) echo "usage: $0 [native|musl|aarch64-macos]" >&2; exit 2 ;;
esac
```
Default `ZIG` stays; after the `zigcc-musl` wrapper add:
```sh
cat > "$W/bin/zigcc-aarch64-macos" <<EOF
#!/bin/sh
exec $ZIG cc -target aarch64-macos "\$@"
EOF
```
`sha256sum -c -` → a function at the top:
```sh
case "$(uname)" in
Darwin) sha_check() { shasum -a 256 -c - >/dev/null; }; NJOBS=$(sysctl -n hw.ncpu) ;;
*) sha_check() { sha256sum -c - >/dev/null; }; NJOBS=$(nproc) ;;
esac
```
and `sha256sum -c - >/dev/null` → `sha_check`, `-j"$(nproc)"` → `-j"$NJOBS"` (both sites). `XTRA`:
```sh
XTRA=""
WOLF_XTRA=""
case "$T" in
musl) XTRA="-DCMAKE_SYSTEM_NAME=Linux -DCMAKE_SYSTEM_PROCESSOR=x86_64" ;;
aarch64-macos)
# Cross to Darwin: find nothing on the host (ngtcp2 found the host's
# libwolfssl.so before this fence), and no system CA path — mux is
# PSK-only and the CA path wants Security.framework (measured
# 2026-09-03, see docs/decisions.md).
XTRA="-DCMAKE_SYSTEM_NAME=Darwin -DCMAKE_SYSTEM_PROCESSOR=arm64 -DCMAKE_FIND_ROOT_PATH=$OUT -DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY -DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY -DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ONLY"
WOLF_XTRA="-DWOLFSSL_SYS_CA_CERTS=no" ;;
esac
```
and pass `$WOLF_XTRA` on the wolfSSL cmake line. The `-march=x86_64_v3` stays in `zigcc-native` only (an x86 host); a Darwin native arm is step 3's.
- [ ] **Step 4: Makefile**
```make
# The target the installed and released binaries are built for. Static
# musl on Linux, for the reason above; another OS names its triple here.
MUX_TARGET ?= x86_64-linux-musl
```
and `-Dtarget=x86_64-linux-musl` → `-Dtarget=$(MUX_TARGET)` in `install:` and `release:`; `RELTAR`'s `x86_64-linux-musl` → `$(MUX_TARGET)`.
- [ ] **Step 5: Prove nothing moved on Linux**
```bash
deps/zig/zig build --summary none 2>&1 | tail -2; echo rc=$?
deps/zig/zig build -Dtarget=x86_64-linux-musl -p /tmp/claude-1000/xver-musl --summary none 2>&1 | tail -2
ls deps/quic/out
sh -n deps/quic/build-deps.sh && echo syntax-ok
```
Expected: both builds rc=0 with no dep rebuild (`out/native` and `out/musl` untouched, mtimes unchanged: `stat -c %Y deps/quic/out/*/lib/libngtcp2.a` before and after), `syntax-ok`.
- [ ] **Step 6: `make check`** rc=0. **Commit**
```bash
git add build.zig deps/quic/build-deps.sh Makefile
git commit -m "build: the QUIC prefix and the linker follow the target, not the host"
```
---
### Task 13: The delivery gate and the record
**Files:**
- Modify: `docs/decisions.md` (append), `README.md` if it names `/proc` or `XDG_RUNTIME_DIR` behaviour (grep)
- [ ] **Step 1: The full gate**
```bash
make ci > /tmp/ci.log 2>&1; echo rc=$?; tail -5 /tmp/ci.log
```
Expected: rc=0. If a leg fails, the fix is a `--fixup` on the task's commit.
- [ ] **Step 2: Cross-version**
```bash
make xversion-build > /tmp/xv.log 2>&1; echo rc=$?
make xversion > /tmp/xv2.log 2>&1; echo rc=$?; tail -5 /tmp/xv2.log
```
Expected: rc=0 both — the wire and the manifest did not change shape (the old side is `../mux-xver-old`, per the memory note; build its prefix first).
- [ ] **Step 3: The record**
Append to `docs/decisions.md`:
```
## 2026-09-03 — the platform layer (macOS port, step 2)
Every OS-specific spelling under src/ now lives in src/os/, one row per
side (server_os, client_os), gated by folder rule 7. Measured before the
design: zig 0.15.2 cross-compiles C to aarch64-macos without an SDK and
links Mach-O with its own linker (LLD refuses); ngtcp2 + wolfSSL
cross-build with WOLFSSL_SYS_CA_CERTS=no and a CMAKE_FIND_ROOT_PATH fence;
std.posix already emulates SOCK_CLOEXEC/accept4 on Darwin; ghostty-vt's
three C++ deps call apple_sdk.addPaths, which finds the HOST libc on a
Linux host — the cross-compile blocker that leaves the build-host decision
open. Two behaviours changed shape on Linux without changing outcome: the
stale-image verdict is now an inode compare against the image at boot
(was the kernel's ` (deleted)` suffix), and reapDeadPid asks kill(pid,0)
(was access(/proc/PID)). Spec: docs/superpowers/specs/2026-09-03-macos-
port-design.md.
```
- [ ] **Step 4: Autosquash and final check**
```bash
git rebase -i --autosquash main # not interactive here: use GIT_SEQUENCE_EDITOR=true git rebase -i --autosquash <base>
git log --oneline <base>..HEAD
make check > /tmp/c.log 2>&1; echo rc=$?
```
Expected: one commit per task, subjects as written above, rc=0.
- [ ] **Step 5: Commit the record**
```bash
git add docs/decisions.md README.md
git commit -m "docs: record the platform layer and the macOS measurements"
```
---
## Self-review against the spec
- **`src/os/` layer, two rows, `spawn` moved, `forkDaemon` and peer-cred out of `main.zig`:** Tasks 1, 3, 4.
- **Every named operation:** `exitNow` `closeFrom` (2), `forkPty` (2; the spec's `openPty`+`becomeSession` is folded into one `forkPty` because forkpty(3) exists on both OSes and the Darwin header gap is a one-line `extern` in the macOS arm — recorded as a spec amendment in the spec file), `ptyMode` `ptyFgPgid` (2), `anonFd` (5), `selfImageStale` (6), `sendNoSig` `peerCred` `forkDetached` `sockType` (3, 4, 7); client `peerCred` `parentOf` `winSize` `openPtyPair` (4, 7); `sockpath.runtimeDir` and `spawn.selfExe` switches (8, 6).
- **Rule 7 with comments counted:** Task 9.
- **Harness oracle, environ walk rewritten, fixture off-origin:** Tasks 10, 11.
- **QUIC target word, wolfSSL flags, `use_lld` gate, `MUX_TARGET`:** Task 12.
- **`make ci` and `make xversion` green:** Task 13.
- **Not in this plan, by the spec:** the `_macos.zig` arms, the two hardware probes, the harness's Darwin `case` arms, the build-host decision.
Type consistency: `Winsize` is `std.posix.winsize` throughout (fields `row col xpixel ypixel`); `PeerCred` is `{uid, pid}` on both rows; `ForkedPty` is `{pid, master}`; the harness helper names match between Task 10's definitions and Task 11's uses.