a73x

7e20404d

docs: the Darwin spellings and the claims that had drifted past them

a73x   2026-09-04 10:16

Commit message
docs: the Darwin spellings and the claims that had drifted past them

Nine corrections the final whole-branch review found, all of them a
sentence that was true before this branch and is half-true after it.

CLAUDE.md: three invariants named only the Linux mechanism, so a reader on
a Mac would look for a link, a directory and an anonymous fd that are not
there. The self-exec is `std.fs.selfExePath` on Darwin, the e2e_03 pins go
through the oracle's `pid_exe`/`pid_comm`/`pid_args` (ps and lsof there,
/proc here), and the upgrade manifest rides an unlinked mkstemp file.
`server_test_session.zig` is 1722 lines, so "none over 1.7k" is now 1.8k.

README: the macOS section said which SDK and which package to install and
never where the pinned Zig goes, which is the one thing a Mac checkout
gets wrong first.

decisions.md: the less-side dropped byte is OPEN, in the word the README
and the ledger use for it; the heredoc-backtick lint the entry wished for
exists and is wired into `make check`; and the second Darwin entry's "four
operations" listed three plus a test, where the first entry counts
`peerCred` as the fourth and the test as a fifth.

bans.sh: most ptyclient scripts are quoted heredocs, so the rule is for
the ones that interpolate a port or a fingerprint; and the fixture's opener
is built because a written one would make the sweep read this file, not
because the file holds no angle brackets -- the awk program above spells
two of them.

os_oracle.sh: the timeout exceptions missed e2e_10_agent.sh's backgrounded
flip leg and named one wsclient wrapper where there are three.

e2e_11_select.sh: the Darwin clock is perl, not gdate.

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

CLAUDE.md
Old New
@@ -33,7 +33,7 @@ Files are large and comment-dense (~37% of Zig bytes are whole `//` lines). Read
33 costs ~800k tokens; every token stays in context and is re-billed each turn. 33 costs ~800k tokens; every token stays in context and is re-billed each turn.
34 34
35 - **Never `cat` these:** `src/server/server.zig` (3.3k lines, ~31k tok; its tests are 35 - **Never `cat` these:** `src/server/server.zig` (3.3k lines, ~31k tok; its tests are
36 siblings in `src/server/`, none over 1.7k; the agent relay and the 36 siblings in `src/server/`, none over 1.8k; the agent relay and the
37 session table are `src/server/server_agent.zig` and `src/server/server_sessions.zig`), 37 session table are `src/server/server_agent.zig` and `src/server/server_sessions.zig`),
38 `src/tui/interact.zig` (4.8k), 38 `src/tui/interact.zig` (4.8k),
39 `docs/decisions.md` (8.7k). `test/e2e.sh` is a 188-line runner now; the 39 `docs/decisions.md` (8.7k). `test/e2e.sh` is a 188-line runner now; the
@@ -322,15 +322,18 @@ own. Test fixtures in `test/`:
322 local client's entry runs `mux d start -d --sock PATH` with its own fd 2 322 local client's entry runs `mux d start -d --sock PATH` with its own fd 2
323 lent to it, so the daemon's refusals reach the user in the daemon's own 323 lent to it, so the daemon's refusals reach the user in the daemon's own
324 words. The fork execs `spawn.selfExe` — the running image, 324 words. The fork execs `spawn.selfExe` — the running image,
325 `/proc/self/exe` read THROUGH to the file it names — with argv 325 `/proc/self/exe` read THROUGH to the file it names on Linux and
326 `std.fs.selfExePath` on Darwin, which has no such link — with argv
326 `mux d start …`. No `execvp`, no PATH walk, so an auto-start can only run 327 `mux d start …`. No `execvp`, no PATH walk, so an auto-start can only run
327 the binary that is already running. (It used to exec a `muxd` off PATH, and an e2e leg whose daemon 328 the binary that is already running. (It used to exec a `muxd` off PATH, and an e2e leg whose daemon
328 had died graded an installed v0.0.1-10 with no agent code in it.) The 329 had died graded an installed v0.0.1-10 with no agent code in it.) The
329 resolution is not cosmetic: `comm` is the basename of the filename handed 330 resolution is not cosmetic: `comm` is the basename of the filename handed
330 to execve, so exec'ing the link itself names every daemon `exe` and hides 331 to execve, so exec'ing the link itself names every daemon `exe` and hides
331 it from `pgrep mux` and `killall mux`. Both pins are in `e2e_03_side` — 332 it from `pgrep mux` and `killall mux`. Both pins are in `e2e_03_side`, and
332 `readlink /proc/PID/exe` for the image, `/proc/PID/comm` for the name, 333 they ask through `test/os_oracle.sh` (`pid_exe` for the image, `pid_comm`
333 and `/proc/PID/cmdline` for the argv the attach may say and no more. 334 for the name, `pid_args` for the argv the attach may say and no more) —
335 which is `readlink /proc/PID/exe` and the two other `/proc` reads on
336 Linux, and `ps` plus `lsof` on Darwin, where no such directory exists.
334 The detached child's log is APPENDED: one xdg log serves every socket on 337 The detached child's log is APPENDED: one xdg log serves every socket on
335 the box, so truncating would zero a daemon still writing to it. 338 the box, so truncating would zero a daemon still writing to it.
336 Running the daemon IN the fork instead was tried and crashes every Debug 339 Running the daemon IN the fork instead was tried and crashes every Debug
@@ -374,7 +377,8 @@ own. Test fixtures in `test/`:
374 the version rule, manifest and serving check stay daemon-side and are 377 the version rule, manifest and serving check stay daemon-side and are
375 never reimplemented client-side. No daemon on the box is install-only 378 never reimplemented client-side. No daemon on the box is install-only
376 success; no mux on the box is a refusal, because a push replaces an 379 success; no mux on the box is a refusal, because a push replaces an
377 install rather than inventing one. The manifest memfd carries only 380 install rather than inventing one. The manifest carrier (a memfd on Linux,
381 an unlinked `mkstemp` file at 0600 on Darwin) carries only
378 what cannot be rebuilt (pty fd + child pid, the VT dump and title, the 382 what cannot be rebuilt (pty fd + child pid, the VT dump and title, the
379 tracker, the QUIC arm's key bytes, the cumulative counters); scrollback, 383 tracker, the QUIC arm's key bytes, the cumulative counters); scrollback,
380 clients, delta trackers, agent channels and per-connection QUIC state are 384 clients, delta trackers, agent channels and per-connection QUIC state are
README.md
Old New
@@ -716,7 +716,10 @@ Intel build, no universal binary, and no way to cross-compile the Mac
716 binary from Linux, so a Mac binary is made on a Mac. 716 binary from Linux, so a Mac binary is made on a Mac.
717 717
718 You need Xcode's Command Line Tools for the SDK and `cmake` for the QUIC 718 You need Xcode's Command Line Tools for the SDK and `cmake` for the QUIC
719 dependencies: 719 dependencies, and the pinned toolchain in the same place a Linux checkout
720 puts it: unpack the aarch64 Zig 0.15.2 tarball so that the binary is at
721 `deps/zig/zig`. The Makefile spells that path and nothing else, so a
722 system `zig` is neither used nor enough.
720 723
721 ```sh 724 ```sh
722 xcode-select --install 725 xcode-select --install
docs/decisions.md
Old New
@@ -8472,7 +8472,10 @@ because tmux asks only that no other-user bit is set and this asks for
8472 exactly 0700. Linux keeps NO fallback: a guess cannot make two binaries 8472 exactly 0700. Linux keeps NO fallback: a guess cannot make two binaries
8473 agree on one daemon, so the caller names it with `--sock`. 8473 agree on one daemon, so the caller names it with `--sock`.
8474 8474
8475 **Four operations are a different mechanism, not a different spelling.** 8475 **Four operations are a different mechanism, not a different spelling**, and
8476 the `forkDetached` pin is a fifth difference of the same kind in the test
8477 rather than in the arm. The four are `closeFrom`, `anonFd`, `sendNoSig` and
8478 `peerCred`, which is the count and the list the step-3 entry above gives.
8476 `closeFrom` walks the fd table one close at a time from the floor to 8479 `closeFrom` walks the fd table one close at a time from the floor to
8477 `getdtablesize()`, because Darwin has no `close_range`; that is a few 8480 `getdtablesize()`, because Darwin has no `close_range`; that is a few
8478 hundred cheap EBADFs once per session start, between fork and exec, so 8481 hundred cheap EBADFs once per session start, between fork and exec, so
@@ -8490,10 +8493,13 @@ property of the socket rather than the send, and a REFUSED set is the
8490 answer: Darwin's `sosetopt` rejects every option with EINVAL once a socket 8493 answer: Darwin's `sosetopt` rejects every option with EINVAL once a socket
8491 is shut down both ways, which is exactly the hung-up state, so the arm 8494 is shut down both ways, which is exactly the hung-up state, so the arm
8492 reads that EINVAL as the peer being gone and returns `error.BrokenPipe`. 8495 reads that EINVAL as the peer being gone and returns `error.BrokenPipe`.
8493 Fourth, the `forkDetached` test asks `getsid(2)` rather than `ps`: macOS's 8496 `peerCred` needs two calls: Darwin's `LOCAL_PEERCRED` answers a `struct
8494 `ps` has no `sid` column and its `sess` column is a kernel address that 8497 xucred` with no pid in it, so the uid comes from `getpeereid` and the pid
8495 reads 0 for anyone but root, so the natural port of the Linux pin compared 8498 from `LOCAL_PEERPID`. And the fifth, in the test: the `forkDetached` pin
8496 0 against 0 and passed whatever the child did. 8499 asks `getsid(2)` rather than `ps`, because macOS's `ps` has no `sid` column
8500 and its `sess` column is a kernel address that reads 0 for anyone but root,
8501 so the natural port of the Linux pin compared 0 against 0 and passed
8502 whatever the child did.
8497 8503
8498 **A Darwin panic the gate found, and the shape of its fix.** Attaching a 8504 **A Darwin panic the gate found, and the shape of its fix.** Attaching a
8499 Mac client to a Linux daemon crashed with `attempt to unwrap error: 8505 Mac client to a Linux daemon crashed with `attempt to unwrap error:
@@ -8672,8 +8678,10 @@ made and re-run green on both OSes:
8672 the Mac, three out of three. So whatever discards it is on the far side 8678 the Mac, three out of three. So whatever discards it is on the far side
8673 of the pty and not an input byte mux drops — by elimination, since less's 8679 of the pty and not an input byte mux drops — by elimination, since less's
8674 own source was not read; a `TCSAFLUSH` on re-entering raw mode is what 8680 own source was not read; a `TCSAFLUSH` on re-entering raw mode is what
8675 behaves this way and is the guess. Nothing in the product changes and the 8681 behaves this way and is the guess. It stays OPEN, which is the word the
8676 leg no longer types into the pager at all. 8682 README and the ledger use for it: elimination says where the byte is not
8683 lost, and nobody has yet said where it is. Nothing in the product changes
8684 and the leg no longer types into the pager at all.
8677 Recorded because the shape — one byte, first write, one OS — is exactly 8685 Recorded because the shape — one byte, first write, one OS — is exactly
8678 what a real input bug would look like, and the next person to see it 8686 what a real input bug would look like, and the next person to see it
8679 should know these two probes exist. 8687 should know these two probes exist.
@@ -8691,9 +8699,11 @@ made and re-run green on both OSes:
8691 because its detector required the heredoc's delimiter to end the line and 8699 because its detector required the heredoc's delimiter to end the line and
8692 that opener has a trailing space after it; the replacement comment 8700 that opener has a trailing space after it; the replacement comment
8693 written for the third site had a backtick in it too. The rule is written 8701 written for the third site had a backtick in it too. The rule is written
8694 above each of the three heredocs, which is all that gates it. A lint 8702 above each of the three heredocs. A lint is better and it is small —
8695 would be better and it is small — match the opener anywhere on the line, 8703 match the opener anywhere on the line, skip a quoted delimiter, and refuse
8696 skip a quoted delimiter, and refuse a backtick in the body. 8704 a backtick in the body — so `test/bans.sh` has one, wired into `make
8705 check`, with a planted positive in both opener spellings and a quoted
8706 delimiter as the negative. The comments stay; the lint is what gates it.
8697 8707
8698 **Two groups stay RED, and both are red BY DESIGN — the same design.** 8708 **Two groups stay RED, and both are red BY DESIGN — the same design.**
8699 `sockpath.runtimeDir` has no fallback on Linux and falls back to 8709 `sockpath.runtimeDir` has no fallback on Linux and falls back to
test/bans.sh
Old New
@@ -118,9 +118,11 @@ must_skip src/server 'test "bans probe" {
118 # ---- the shell rule: no backtick inside an UNQUOTED heredoc ------------- 118 # ---- the shell rule: no backtick inside an UNQUOTED heredoc -------------
119 # 119 #
120 # In an unquoted heredoc the shell runs a backquoted word as a command 120 # In an unquoted heredoc the shell runs a backquoted word as a command
121 # substitution wherever it stands, comments included. Every ptyclient script 121 # substitution wherever it stands, comments included. Most ptyclient scripts
122 # in this suite is an unquoted heredoc — they interpolate a fingerprint or a 122 # in this suite are QUOTED heredocs and safe; the ones that interpolate a
123 # port — and this repo quotes a key or a verb in backticks everywhere else, 123 # fingerprint or a port have to leave the delimiter bare, and those are the
124 # ones this rule is for. This repo quotes a key or a verb in backticks
125 # everywhere else,
124 # so writing one into such a comment is the natural thing to do and it is a 126 # so writing one into such a comment is the natural thing to do and it is a
125 # trap. There IS an expect program on macOS: the shell starts it, it reads 127 # trap. There IS an expect program on macOS: the shell starts it, it reads
126 # the rest of the heredoc as its own script, and the group hangs until its 128 # the rest of the heredoc as its own script, and the group hangs until its
@@ -180,12 +182,14 @@ SHPROBE="$ROOT/test/zz_bans_heredoc_$$.sh"
180 # contain the very shape it refuses — and the trailing space after the 182 # contain the very shape it refuses — and the trailing space after the
181 # first opener is half of what is being pinned. 183 # first opener is half of what is being pinned.
182 # 184 #
183 # The opener is BUILT rather than written, so that no line of this file 185 # The FIXTURE's opener is BUILT rather than written. The sweep's opener
184 # holds the two angle brackets next to each other. Spelling them here would 186 # detector spares a quoted delimiter, and it sees a quote right after the
185 # make the negative sweep below read this script's own fixture as a heredoc 187 # two angle brackets in the awk program at the top of `heredoc_backticks`,
186 # and report its three backticks, and the only ways out of that are an 188 # so those literals do not fool it — but a bare opener written out here
187 # exception for this file or a rule that stops covering it. Neither is 189 # would make it read this script's own fixture as a heredoc and report the
188 # worth a pair of characters. 190 # three backticks planted below. The only ways out of that are an exception
191 # for this file or a rule that stops covering it. Neither is worth a pair of
192 # characters.
189 _a='<' 193 _a='<'
190 _hd="$_a$_a" 194 _hd="$_a$_a"
191 printf '%s\n' \ 195 printf '%s\n' \
test/e2e_11_select.sh
Old New
@@ -377,7 +377,7 @@ kill -STOP "$AGENT48PID"
377 set +e 377 set +e
378 # Assembled rather than written as one heredoc, because ONE of its lines 378 # Assembled rather than written as one heredoc, because ONE of its lines
379 # needs a value from THIS shell: the session times its own ssh-add, and the 379 # needs a value from THIS shell: the session times its own ssh-add, and the
380 # clock that does it is `date +%s%N` on one OS and `gdate +%s%N` on the 380 # clock that does it is `date +%s%N` on one OS and a perl one-liner on the
381 # other, so the text comes from now_ms_snippet. Both halves stay QUOTED 381 # other, so the text comes from now_ms_snippet. Both halves stay QUOTED
382 # heredocs — the lines around the seam spell `$?`, `$R` and a backticked 382 # heredocs — the lines around the seam spell `$?`, `$R` and a backticked
383 # `d` that all have to reach the SESSION as characters rather than be spent 383 # `d` that all have to reach the SESSION as characters rather than be spent
test/os_oracle.sh
Old New
@@ -315,11 +315,16 @@ eval "$(now_ms_snippet)"
315 # Almost every caller in this tree runs `timeout <seconds> CMD` in the 315 # Almost every caller in this tree runs `timeout <seconds> CMD` in the
316 # foreground over a single mux or ptyclient child, with no flags, and none 316 # foreground over a single mux or ptyclient child, with no flags, and none
317 # of the three differences can reach one. The exceptions, and why they are 317 # of the three differences can reach one. The exceptions, and why they are
318 # still safe: e2e_01_boot.sh backgrounds one client and reaps it by pid 318 # still safe: e2e_01_boot.sh and e2e_10_agent.sh each background one
319 # rather than by signalling the wrapper; e2e_05_session.sh wraps a wsclient, 319 # timeout-wrapped client, and both reap it with `wait` on the wrapper's own
320 # which is one child like the others; and test/vm.sh, test/mac.sh and 320 # pid rather than by signalling it — the agent group's `defer_kill` on that
321 # test/xos.sh wrap ssh, but those three drivers run on the Linux side of a 321 # pid runs only when the leg has already failed, and on this arm it would
322 # gate, where `timeout` is the GNU binary and this arm is never defined. 322 # leave the child to finish its own budget, which is a slower cleanup and
323 # not a different verdict; e2e_05_session.sh, e2e_06_web.sh and
324 # e2e_13_birth.sh wrap a wsclient, which is one child like the others; and
325 # test/vm.sh, test/mac.sh and test/xos.sh wrap ssh, but those three drivers
326 # run on the Linux side of a gate, where `timeout` is the GNU binary and
327 # this arm is never defined.
323 if command -v timeout >/dev/null 2>&1; then 328 if command -v timeout >/dev/null 2>&1; then
324 : 329 :
325 elif command -v gtimeout >/dev/null 2>&1; then 330 elif command -v gtimeout >/dev/null 2>&1; then