a73x

dd868aeb

docs: record the platform layer and the macOS measurements

a73x   2026-09-03 15:10

Commit message
docs: record the platform layer and the macOS measurements

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SakwJEwD9dXBoRP5kWbemW

docs/decisions.md
Old New
@@ -8128,3 +8128,128 @@ status` says the stripe is 11 rows so the arithmetic is the layout's, not
8128 the comment's. Graded by mutation: with the row translation capped at one 8128 the comment's. Graded by mutation: with the row translation capped at one
8129 row every older leg passes and only the stacked leg fails. The suite's 8129 row every older leg passes and only the stacked leg fails. The suite's
8130 scenario pin goes from 111 to 112. 8130 scenario pin goes from 111 to 112.
8131
8132 ## 2026-09-03 — the platform layer (macOS port, step 2)
8133
8134 Every OS-specific spelling under `src/` now lives in `src/os/`, one row per
8135 side. `server_os` holds what only the daemon does — the pty fork, the
8136 detached daemon fork, peer credentials, an anonymous fd for the upgrade
8137 manifest, `exitNow`, `closeFrom`, the pty's mode and foreground pgid, the
8138 window-size ioctl and the stale-image verdict — and `client_os` holds the
8139 few the wall and askpass do: pid, euid, peer credentials, the parent walk,
8140 window size and a pty pair. `spawn` moved in beside them. Each root is the
8141 CONTRACT — a doc comment per operation says what it guarantees and which
8142 failure it prevents — and `impl` switches on `builtin.os.tag`, so a build
8143 for an OS with no arm is a `@compileError` at that switch rather than a
8144 link error or a runtime surprise. The client row is deliberately SEPARATE
8145 from the server's rather than one `os` row: the client never links a fork
8146 or a pty, and an app that links the engine and a client must not either.
8147
8148 **The gate is folder rule 7.** No production line under `src/`,
8149 `src/engine/`, `src/client/`, `src/tui/`, `src/server/` or `src/cli/` may
8150 spell `std.os.linux`, `/proc`, `memfd`, `close_range`, `exit_group`,
8151 `so.peercred`, `so_peercred`, `iocsptlck` or `iocgptn`. `src/os/` is absent
8152 from that folder list on purpose: its children may spell anything, and its
8153 roots have no reason to. Comments count, as they do for rule 4, because a
8154 comment naming a Linux mechanism is one that goes stale the day a second
8155 arm exists. Four needles are spelled to catch a name in both the form Zig
8156 writes it and the form C and our own prose do — `so.peercred` and
8157 `so_peercred` for `std.posix.SO.PEERCRED` and `SO_PEERCRED`, where a bare
8158 `peercred` would have banned `client_os.peerCred`, the very operation
8159 callers are supposed to reach for; `iocsptlck` and `iocgptn` drop the
8160 leading T so they catch `std.posix.T.IOCGPTN` as well as `TIOCGPTN`.
8161
8162 **Measured before the design, on a Linux host.** zig 0.15.2 cross-compiles
8163 a libc program using `posix_openpt`, `kqueue`, `libproc` and `dyld` to
8164 aarch64-macos with no SDK, and links Mach-O with its own linker — LLD
8165 refuses ("using LLD to link macho files is unsupported") — so `linkerFor`
8166 asks for `use_lld` on every target except Darwin. ngtcp2 1.25.0 and
8167 wolfSSL 5.9.2 cross-build with `WOLFSSL_SYS_CA_CERTS=no` (mux is PSK-only
8168 and the system-CA path wants Security.framework) and a
8169 `CMAKE_FIND_ROOT_PATH` fence at the target prefix, without which ngtcp2
8170 finds the host's `libwolfssl.so`. `std.posix.socket` and `std.posix.accept`
8171 already emulate `SOCK_CLOEXEC`, `SOCK_NONBLOCK` and `accept4` on Darwin
8172 with a trailing `fcntl`, so no socket site needed a row at all. ghostty-vt
8173 links three C++ libraries whose build.zig files each call ghostty's
8174 `apple_sdk.addPaths`, which resolves the HOST libc on a Linux host and
8175 fails the C++ compile on glibc headers — upstream code build.zig cannot
8176 reach, the cross-compile blocker that leaves the build-host decision open,
8177 and the reason this step stops at the platform layer and writes no
8178 `_macos.zig` arm.
8179
8180 **One amendment to the spec, made while writing the row.** The spec's
8181 `openPty` plus `becomeSession` are one `forkPty` instead: `forkpty(3)`
8182 exists on both OSes, returning pid 0 in the child exactly as `fork` does,
8183 so the child code stays one branch. Zig ships no `<util.h>` for Darwin,
8184 which is a missing HEADER and not a missing symbol — that arm declares it
8185 with one `extern "c" fn forkpty` line rather than reimplementing the pty
8186 open as `posix_openpt` + `grantpt` + `unlockpt` + `ptsname`.
8187
8188 **The QUIC prefix and the linker follow the TARGET, never the host.**
8189 `quicDeps` names one word per prefix — `native` for the host's own libc,
8190 `musl` for the static release, and `<arch>-<os>` for any other cross target
8191 — shared with `build-deps.sh`, `make deps`, `make clean-deps` and wan.sh's
8192 musl cross-build, so a third OS is one more `case` arm in the script and
8193 nothing in build.zig. A cross build that reused the host's prefix would
8194 link x86_64 Linux archives into an aarch64 macOS binary.
8195
8196 **Two behaviours changed shape on Linux without changing outcome.** The
8197 stale-image verdict is an inode compare against what the image's path held
8198 at boot, not the kernel's ` (deleted)` suffix: `noteBootImage` stamps the
8199 path and its inode once, and every later ask re-stats that path, so a
8200 rename-over reads stale and a file that lands there afterwards can never
8201 promote a stale daemon back to current. The two failures are kept apart. A
8202 path this OS will not name at all records NOTHING and answers false —
8203 unknown is not stale, and a wall must not dress a healthy box in a warning
8204 over a refused readlink. A path that IS named but holds nothing records a
8205 BORN-STALE state, the path with no ident, and answers stale for the life of
8206 the process, because that daemon is already executing an image no path
8207 holds. Second, `xdg.reapDeadPid` asks `kill(pid, 0)` rather than
8208 `access(/proc/PID)`: EPERM is alive-but-not-ours and keeps the entry, and
8209 ESRCH is the one answer that means the pid is gone.
8210
8211 **`sockpath.runtimeDir` owns the default socket directory.** One switch —
8212 `$XDG_RUNTIME_DIR` on Linux, and still no fallback, because a guess cannot
8213 make two binaries agree on one daemon — so the daemon, the client and the
8214 askpass listener agree by construction and another OS spells its own
8215 default in one place.
8216
8217 **The shell harness asks the OS through named helpers.** `test/e2e_lib.sh`
8218 holds one name per question a pin asks about a pid, an fd table, a bound
8219 UDP port or a file's mode: `pid_alive`, `pid_exe`, `pid_comm`, `pid_args`,
8220 `pid_children`, `pid_fd_count`, `pid_fd_targets`, `pid_holds_unix_sock`,
8221 `pid_rss_kb`, `udp_local_bound`, `udp_table`, `file_mode`, `file_size`,
8222 `sha256_of`. The SPELLING of the question now lives in one place per OS and
8223 the question itself stays in the group file, so a second OS adds a
8224 `case "$_os"` arm here and changes no group file. `oracle_selftest` runs
8225 once before the first group and is the helpers' own pin: a helper that
8226 quietly stopped answering — a missing binary, a `/proc` a sandbox will not
8227 show — would not fail a pin loudly, it would make every pin that reads it
8228 agree with anything, and the suite would go green having tested nothing.
8229 Its subject is off-origin on every dimension the helpers could accidentally
8230 hold constant: a child that is not pid 1, more open fds than the three any
8231 fixture would hold, an argv worth losing, and a child of its own. The
8232 socket arm is asked in both directions, of a listener that holds the path
8233 and of a shell that does not, because a `pid_holds_unix_sock` answering yes
8234 to everything would pass the one pin that reads it just as happily as a
8235 correct one.
8236
8237 **How the wire claim was graded, and what the cross-version gate could not
8238 say.** `make ci` is green. `test/xversion.sh` is NOT, and was not green
8239 before this branch either: its preflight demands a `muxd` in the old
8240 prefix and it drives that side as `muxd run`, so the branch base — one
8241 `mux` binary since v0.0.1-16 — cannot serve as its old side at all, and
8242 its pins still assert a PRE-M18 old side ("the daemon created session zz —
8243 this is not a pre-M18 daemon", "it decoded frames an old client has no arm
8244 for"). Against the released v0.0.1-15 tarball it answers 5 passed, 7
8245 failed. So the claim was graded DIFFERENTIALLY instead: the same gate, the
8246 same v0.0.1-15 old side, run once with this branch's binary as the new
8247 side and once with the branch base's, gives the identical verdict pin for
8248 pin — 5 passed, 7 failed, the same seven messages. The four real
8249 compatibility pins are among the passes in both runs: a new client driving
8250 an old daemon's default session over a socket and over QUIC, an old client
8251 driving the new daemon's, and an old `muxa`'s empty `status_req`. The
8252 refactor moved no byte on the wire. The gate itself needs its old side
8253 re-pinned to a version that still exists; that is not this branch's work.
8254
8255 Spec: `docs/superpowers/specs/2026-09-03-macos-port-design.md`.