Files
jason.woltje 6d5545d442 row 245: row 195 follow-up, F9 runs node as root only at the pinned root-only path, the header and TOOLS.md list what root runs, F6 refuses .. after claude-<pin>/ (Duckworth N1-N3, #1693)
Darkwing's ef8b4439e (darkwing/row245), Duckworth round 1 APPROVE (rev 2849, comment 30043),
manifest 69bc05f4f706, 5 paths. Slot run sage-1961 (darkwing-0107): singles 23/0, mutants 16/16
killed, N1 and N3 among them. DESIGN.md:336-338 stays unedited, superseded by gosalyn-0087.
Sage gate on 856c1826b plus these 5 paths (12:24-12:35Z): case1-accept.test.mjs alone 23/0,
cli 681/0, every scripts/test-*.sh 429/0.
2026-10-21 07:35:39 -05:00

5.5 KiB

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-<pin>/../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-<pin>/ 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).