a73x

997290ad

docs: one start verb, one fork, one owner of the sun_path bound

a73x   2026-08-30 08:13

Commit message
docs: one start verb, one fork, one owner of the sun_path bound

CLAUDE.md's daemon bullets and README's command list carried the two-verb
grammar. Both now say what the code says: `mux d start` is the daemon,
`-d` forks one, `main.forkDaemon` is the only fork under src/ and
build.zig rule 6 keeps it that way, and the bound is `sockpath`'s alone.

CLAUDE.md
Old New
@@ -182,12 +182,12 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`:
182 session, never a box: `reap` and `pumpOnce` answer nothing, a shell's code 182 session, never a box: `reap` and `pumpOnce` answer nothing, a shell's code
183 reaches that shell's own clients as `exit_status`, and an emptied daemon 183 reaches that shell's own clients as `exit_status`, and an emptied daemon
184 keeps serving so a birth can take the default name back. One idle process 184 keeps serving so a birth can take the default name back. One idle process
185 per machine is the accepted cost. `mux d run` exits 0 on every shutdown — 185 per machine is the accepted cost. `mux d start` exits 0 on every shutdown —
186 a supervisor reads nonzero on a clean stop as a crash — and nonzero only 186 a supervisor reads nonzero on a clean stop as a crash — and nonzero only
187 for the boot failures `main.run` picks before anything binds. 187 for the boot failures `main.run` picks before anything binds.
188 - **A daemon starts only when someone asks; a read never starts one, and 188 - **A daemon starts only when someone asks; a read never starts one, and
189 the ask is a WORD.** `mux d start`, `mux d endpoint --start`, or the local 189 the ask is a WORD.** `mux d start -d`, `mux d endpoint --start`, or the
190 client's own entry spawn are the only starters. Bare `mux d endpoint` and 190 local client's own entry ask are the only starters. Bare `mux d endpoint` and
191 `mux d proxy` answer what is on the box and exit 1 when nothing is — the 191 `mux d proxy` answer what is on the box and exit 1 when nothing is — the
192 wall polls `endpoint` over ssh once a second per host, and a poll that 192 wall polls `endpoint` over ssh once a second per host, and a poll that
193 started daemons undid every remote `mux d stop` within a second (found on 193 started daemons undid every remote `mux d stop` within a second (found on
@@ -201,16 +201,25 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`:
201 `asked` DEFAULTS to false: a new dial path that forgets the field starts 201 `asked` DEFAULTS to false: a new dial path that forgets the field starts
202 nothing, because a missing start says so on screen while a spurious one is 202 nothing, because a missing start says so on screen while a spurious one is
203 a daemon on someone else's box that nothing reports. 203 a daemon on someone else's box that nothing reports.
204 - **A local start execs THIS image; nothing looks a daemon up by name.** 204 - **The daemon starts itself, and it execs THIS image.** `mux d start` IS
205 `spawn.ensureDaemon` execs `spawn.selfExe` — the running image, `/proc/self/exe` 205 the daemon in the foreground; `-d` is the flag that forks one, and
206 read THROUGH to the file it names — with argv `mux d run …`. No `execvp`, no 206 `main.forkDaemon` is the only `posix.fork` under `src/` — build.zig's
207 PATH walk, so an auto-start can only run the binary that is already 207 folder rule 6, whose `except` names the one file. Every other starter
208 running. (It used to exec a `muxd` off PATH, and an e2e leg whose daemon 208 spells the argv and execs: `endpoint --start` calls in-process, the
209 local client's entry runs `mux d start -d --sock PATH` with its own fd 2
210 lent to it, so the daemon's refusals reach the user in the daemon's own
211 words. The fork execs `spawn.selfExe` — the running image,
212 `/proc/self/exe` read THROUGH to the file it names — with argv
213 `mux d start …`. No `execvp`, no PATH walk, so an auto-start can only run
214 the binary that is already running. (It used to exec a `muxd` off PATH, and an e2e leg whose daemon
209 had died graded an installed v0.0.1-10 with no agent code in it.) The 215 had died graded an installed v0.0.1-10 with no agent code in it.) The
210 resolution is not cosmetic: `comm` is the basename of the filename handed 216 resolution is not cosmetic: `comm` is the basename of the filename handed
211 to execve, so exec'ing the link itself names every daemon `exe` and hides 217 to execve, so exec'ing the link itself names every daemon `exe` and hides
212 it from `pgrep mux` and `killall mux`. Both pins are in `e2e_03_side` — 218 it from `pgrep mux` and `killall mux`. Both pins are in `e2e_03_side` —
213 `readlink /proc/PID/exe` for the image, `/proc/PID/comm` for the name. 219 `readlink /proc/PID/exe` for the image, `/proc/PID/comm` for the name,
220 and `/proc/PID/cmdline` for the argv the attach may say and no more.
221 The detached child's log is APPENDED: one xdg log serves every socket on
222 the box, so truncating would zero a daemon still writing to it.
214 Running the daemon IN the fork instead was tried and crashes every Debug 223 Running the daemon IN the fork instead was tried and crashes every Debug
215 build: `std.debug.MemoryAccessor` caches the pid it reads memory through, 224 build: `std.debug.MemoryAccessor` caches the pid it reads memory through,
216 so the child's first DebugAllocator stack trace calls `process_vm_readv` 225 so the child's first DebugAllocator stack trace calls `process_vm_readv`
@@ -224,7 +233,8 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`:
224 released v0.0.1-15 tarball). There is no in-place upgrade across the 233 released v0.0.1-15 tarball). There is no in-place upgrade across the
225 rename either: a ≤15 daemon probes the candidate for `muxd <version>` and 234 rename either: a ≤15 daemon probes the candidate for `muxd <version>` and
226 this binary answers `mux <version>`, so that one migration is 235 this binary answers `mux <version>`, so that one migration is
227 `mux d stop` then `mux d start`. 236 `mux d stop` then `mux d start -d` — as is 16 to the next release, whose
237 daemon execs its candidate as `d run` and finds no such verb.
228 - **The layout sidecar is derived convenience, not authored intent.** 238 - **The layout sidecar is derived convenience, not authored intent.**
229 `$XDG_STATE_HOME/mux/layout` stores the pane tree on last detach, from 239 `$XDG_STATE_HOME/mux/layout` stores the pane tree on last detach, from
230 every wall left on a TERMINAL (`shared.is_tty`) — a piped `mux` is a wall 240 every wall left on a TERMINAL (`shared.is_tty`) — a piped `mux` is a wall
@@ -247,7 +257,11 @@ paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`:
247 dropped on purpose. A return watermark is RE-STAMPED, never carried — it 257 dropped on purpose. A return watermark is RE-STAMPED, never carried — it
248 belongs to the seq space that minted it. Rollback is another exec, of the 258 belongs to the seq space that minted it. Rollback is another exec, of the
249 binary the manifest names, with `MUX_UPGRADE_ROLLBACK` as the loop guard. 259 binary the manifest names, with `MUX_UPGRADE_ROLLBACK` as the loop guard.
250 - No socket stealing: `mux d run` refuses a path another daemon owns. 260 - **The sun_path bound belongs to the one binder.** `sockpath.max_sun_path`
261 is private and `mux d` is `tooLong`'s only caller: it refuses at parse,
262 before any fork, on the stderr the asking client lent it. Everyone else
263 dials and reads the kernel's `NameTooLong`, which truncates nothing.
264 - No socket stealing: `mux d start` refuses a path another daemon owns.
251 265
252 ## Working rules 266 ## Working rules
253 267
README.md
Old New
@@ -500,6 +500,7 @@ has none.
500 ## Everything else 500 ## Everything else
501 501
502 ```sh 502 ```sh
503 mux d start [-d] # the daemon: in this shell, or -d to fork one and wait for it
503 mux d dump [--vt] # debug: print the authoritative grid (what the screen *should* be) 504 mux d dump [--vt] # debug: print the authoritative grid (what the screen *should* be)
504 mux d stats # live sessions by name + clients each; wire stats (deltas vs snapshot bytes) 505 mux d stats # live sessions by name + clients each; wire stats (deltas vs snapshot bytes)
505 mux d upgrade # exec a newly installed binary in place; every session keeps running 506 mux d upgrade # exec a newly installed binary in place; every session keeps running
@@ -557,7 +558,8 @@ ending, retry` rather than carrying a shell with no master across the exec.
557 Coming from v0.0.1-15 or older is the one upgrade `mux d upgrade` cannot 558 Coming from v0.0.1-15 or older is the one upgrade `mux d upgrade` cannot
558 do: that daemon probes a candidate for `muxd <version>` and this binary 559 do: that daemon probes a candidate for `muxd <version>` and this binary
559 prints `mux <version>`, so it refuses. `mux d stop` then `mux d start -d`, 560 prints `mux <version>`, so it refuses. `mux d stop` then `mux d start -d`,
560 once per box. `make install` deletes `muxd`, `muxa` and `muxweb` from the 561 once per box. A v0.0.1-16 daemon is the same story for the same reason: it
562 execs its candidate as `mux d run`, and the verb is `start` now. `make install` deletes `muxd`, `muxa` and `muxweb` from the
561 install directory on its way in — they are modes of `mux` now, and a 563 install directory on its way in — they are modes of `mux` now, and a
562 `muxd` left on PATH is a second daemon waiting for someone to start it. 564 `muxd` left on PATH is a second daemon waiting for someone to start it.
563 565