# Row 245 build: Duckworth's N1-N3 on `scripts/case1-accept.sh` (row 195 follow-up) Duckworth approved row 195's manifest round 1 (duckworth-0088, `agents/duckworth/work/row195/REVIEW-m1.md` at 6afcee2a5) with three notes that don't block. Row 195's code landed as b65014647, and the notes are in `docs/plans/DEFERRED.md` (d6cff0cdd). Row 245 carries them: brief `docs/plans/2026-10-20_hub-own-uid.md` ยง "Row 195 follow-up" at 62ddc96cc, owner Darkwing, reviewer Duckworth, gate owner Sage (sage-1923). The branch is `darkwing/row245`, on 62ddc96cc. Its first commit is a cherry-pick of `darkwing/row195-n1` 1e3c4767c, which was written on 37171f65b and never run. The script, test and TOOLS.md blobs are the same as in 1e3c4767c, and none of those paths changed between 37171f65b and 62ddc96cc. ## What changes N1. F9 ran `$ROOT$NODE` as root (`case1-accept.sh:298` at 37171f65b) before F10 checked that `--node` is the pinned path. Duckworth offered two fixes: F9's parse through `AS_USER`, or F10's path check first. I took the path check, plus one more: - F9 now runs node only when `--node` equals `/opt/mosaic-stack/node-<--node-pin>/bin/node`. - Then `not_root_only` walks from that file up to `/`. Each entry must be the running user's (root's on the host) with no group or other write, the rule `case2-unit.sh` `root_only` applied when it rendered the units. A symlink fails it, because its mode is 777. The first entry that breaks the rule goes in the evidence. I didn't run the parse through `AS_USER`. That would hand a check about a root file to the stack's user, and a process running as `mosaic` shares that node's uid. Whether it could change what node prints then turns on the host's ptrace setting, and the root-only check doesn't depend on that. Sage agreed in sage-1877. Rechecking ownership costs one loop and doesn't depend on how the host was installed. N2. The header (:9-15) and the `docs/TOOLS.md` entry now list what root runs: `systemctl show` and `is-enabled` (F1, P11), `curl` on the loopback healthz (F2), `sudo -l` (P1, P3), `sudo -u jarvis bwrap --version` and `hostcheck.sh --check` (P9), and node for F9 under the rule above. N3. F6 matched `--claude` by prefix, so `/opt/mosaic-stack/claude-/../claude-2.0.9/...` passed the path check. F6 now fails on any `..` segment, before it runs `--version`. F10 compares for equality, so it needed no change. Row 195's BUILD.md gets a correction entry for deviation 6. Row 195's DESIGN.md is unchanged. Its "either works" wording at `DESIGN.md:336-338` is superseded by gosalyn-0087 (root writes `dev.args` at W10), and it stays in place: an approved design isn't edited (sage-1877). ## Tests `packages/cli/tests/case1-accept.test.mjs`: - The node stub touches `node-as-root` in the state directory when something runs it outside the fake `sudo` (which sets HOME to the stack's home). `host()` sets the node file's directories and the scratch root to 0755, so the umask can't fail F9. - "every check passing" also asserts that root ran the pinned node. - New: "F9 won't run --node as root off the pin path or with a writable directory above it". Another node path fails F9 and F10, with no root run. A group-writable `bin/` and a symlinked node each fail F9 alone, with the bad path in the evidence and no root run. - The F6 test gains a `..` case: a path under `claude-/` by prefix that names 2.0.9's binary, which prints the pin. F6 alone fails, with "has a .. segment". That makes 23 tests. All 23 pass on Sage's slot (results below). ## Mutants The twelve from row 195 still apply, unchanged. These four are new, each one exact replacement: | # | Mutant | Expected killer | |---|---|---| | 13 | F9 runs any `--node` (the pin check becomes `true`) | F9 won't run (another node) | | 14 | F9 skips the ownership walk (`bad=""`) | F9 won't run (writable `bin/`, symlink) | | 15 | `not_root_only` checks only the file, not the directories above it | F9 won't run (writable `bin/`) | | 16 | F6 accepts a `..` segment | F6 stub, path and `..` | The brief's gate asks for a mutant for each of N1 and N3. Mutants 13-15 are N1's, and 16 is N3's. ## Slot plan Same shape as row 195's steps, on this row's tip: the single test file, then the sixteen mutants, then the gate (the cli suite and every `scripts/test-*.sh`), each behind the busy checks and chained with `&&`. The scripts are in `/mnt/storage/scratch/darkwing-245-out/`. ## Slot results (grant sage-1961, 2026-10-21) All three steps ran on 0590a29fc with node v26.8.1, launched from `/`. Both busy checks were CLEAR before each step. - Singles, 11:27:46-11:27:59Z: `case1-accept.test.mjs` passed 23, failed 0. - Mutants, 11:28:10-11:31:20Z: all 16 killed, each with exit 1. The runner restored the script, and its sha256 875bb5b7 matches the committed blob. Rows 1-12 died as in row 195. 13, 14 and 15 died on "F9 won't run --node as root off the pin path or with a writable directory above it". 16 died on "F6 fails on a stub that prints nothing, on a path outside claude- and on a .. segment". - Gate, 11:31:20-11:42:51Z: the cli suite passed 664, failed 0, skipped 0. Every `scripts/test-*.sh` exited 0: auth 15, conductor 17, config 31, discord 78, extension-package 18, foundation 44, land-work 86, queue 28 (its node part 156/0), release 14, task 98. That's 429 selftests, none failed. - Afterwards no process had its cwd in the worktree, the out dir or TMPDIR, and the `darkwing-r245_default` network was gone. Logs: `/mnt/storage/scratch/darkwing-245-out/` (`step1.log`, `mut.out`, `mut/m1-16.log`, `gate.out`).