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

10 KiB

Row 195 build: scripts/case1-accept.sh

Row 195 (#1693), the Case 1 install on 10.1.1.53. Design r7 at 5425ea2f3, approved by Duckworth (duckworth-0087, agents/duckworth/work/row195/REVIEW-r7.md at 58d930f89; Gitea 29962). Sage's go to build is sage-1817.

What this commit holds

  • scripts/case1-accept.sh (new, 100755): section 5's acceptance, F1-F11 and P1-P11.
  • packages/cli/tests/case1-accept.test.mjs (new): section 5's cases.
  • docs/TOOLS.md: the case1-accept.sh entry after the hostcheck entry.
  • This file.
  • DESIGN.md, doc-only fixes Sage named in sage-1817, with no new design round:
    • Duckworth's N1. Section 1's human-id row cited broker.mjs:245; the check is broker.mjs:321.
    • Row 226's state at :21 and :326. It said "unclaimed"; 226 is assigned to Gosalyn, briefed and not started (sage-1814).
    • :340 said I'd send the section 6 items to Gosalyn once 226 was claimed. I sent them on 2026-10-21 and Gosalyn acknowledged (gosalyn-0085), so the line now says that.

Nothing else changes. No case2-*.sh, stack-upgrade.sh, stack-release.sh, case1-hostcheck.sh or run sheet edit, and nothing touches .53 (sage-1809).

Deviations from section 5, and choices it left open

  1. P4's mark is the single token accepted Q182+Q183. Section 5 lists accepted Q182 and accepted Q183 as marks and gives P4 both. One line can carry one mark, and P4 is the only Q182 path, so the count list has accepted Q182+Q183 in place of accepted Q182.
  2. The test seam replaces getent. Under --root, the script reads DIR/etc/passwd and DIR/etc/group with awk. Without it, the same helper calls getent. The groups of a user are its primary gid, then each group whose member list names it, which is what id -G prints. So there is no fake id, and the script never calls id for another user.
  3. P10 reads the port floor from /proc/sys/net/ipv4/ip_unprivileged_port_start rather than calling sysctl, so the seam covers it the way hostcheck's item 7 is covered. There is no fake sysctl.
  4. There is no fake bwrap, claude, node or hostcheck.sh on PATH. The script runs each by the absolute path the args file or the install names, so the test puts stubs at those paths under --root, and the fake sudo runs the stack's user's commands from the tree.
  5. The args file must say --profile factory and name --business. Both are new refusals beyond stack-upgrade.sh's rules. A library file, or one for another business, would make F4 and F7 check the wrong thing. After --, --port and --tester-port must be numeric. The args file is the only source of the ports.
  6. F8, F9 and F11 read file metadata as root: stat, -L and find -maxdepth 1 -name '*.json'. None opens the credential. F9 parses managed-settings.json with the pinned node, run by root: that node and that file are both root's own, installed by ops-01.
  7. F7 reads launch.by from $HOME/.config/mosaic-dev/businesses/<id>.json as the stack's user. That path is businessFilePath(id) under the default config, which is what the unit uses. It doesn't load the copy's business.mjs as stack-release.sh's plugins launchby does. The user runs the whole check and prints one line, and root accepts only ok <instance> run <run> (sage-1653: root doesn't parse the user's output beyond a fixed pattern).
  8. F5 names a process claude when its argv0's basename or its comm starts with claude. The window confirms this against a real role session. If the bwrap'd claude shows another name, F5 fails closed and the fix is this match, not the label check.
  9. F3 and F5 run together. talk goes out in the background under timeout 330, and the /proc scan repeats every half second until it finds a labelled pair or talk exits. F3's line prints first, then F4, then F5, so the output keeps id order. F3 passes on exit 0 with the sent line and a reply line, both counted on stdout only (cli.mjs:395 and the formatMessage write); talk's error goes to stderr.
  10. --claude must match ^/[A-Za-z0-9/._+@-]+$. The @ is for node_modules/@anthropic-ai/...; stack-upgrade.sh doesn't check --claude at all.
  11. F1 requires all four uids in /proc/<MainPID>/status (real, effective, saved, filesystem) to be the user's.
  12. Evidence text the stack's user controls (talk's last line, the precheck's last line, F6's and F10's --version) is cut to printable ASCII, one line, 200 bytes.

Tests

packages/cli/tests/case1-accept.test.mjs, 22 tests. Every case builds a scratch host with every F check passing and every P check closed or accepted, then changes one thing:

  • all pass: exit 0, ids in order, each P mark, the count lines match the check lines, and the stand-in credential's value appears nowhere;
  • each F check failing alone exits 1 with only its line failing;
  • F3 on exit 0 with only the sent line, and its evidence taking talk's stderr;
  • F1 on a failed unit and on User=root; F2 on healthz failing and on another pid holding the port;
  • F5 on unpriv_bwrap (enforce), bwrap//&unpriv_bwrap (complain), a trailing space on either label, no bwrap of the user's, and passing on comm alone;
  • F6 on a stub that prints nothing and on a path outside claude-<pin>;
  • F7 on two live PM launches, a live one for another business, and no launch.by;
  • F8 on 0640, on another owner and on a symlink, never printing the credential;
  • F9 on a group-writable mode, another owner and a number for the flag;
  • F11 on one *.json under discord/, and passing on an empty directory;
  • each P check's unnamed mark, with the line count each fault makes: P6 one line per member, P9 one per missing item; P2, P5, P10 and P11 in more than one way;
  • every P check unnamed at once still exits 0;
  • nine bad args files refuse with exit 2 before any command runs, each with its own refusal text;
  • usage errors, and a user's run without --root, exit 2;
  • every in-instance call goes through sudo -u mosaic -H env PATH=<node dir>:/usr/local/bin:/usr/bin:/bin, read from the fake sudo's argv, and root's other sudo calls are exactly -l -U mosaic, -l -U localadmin and the jarvis bwrap probe;
  • the host check records --check and never --enforce.

Mutants

Section 5's twelve, each one exact replacement in scripts/case1-accept.sh, run by /mnt/storage/scratch/darkwing-195-out/mutants.py in Sage's slot (sage-1865). All twelve were killed. After each one the script's bytes were restored, and the final sha256 matched (d434f15ff638...). The killers below are the tests that failed in each mutant's saved log.

# Mutant Killed by
1 F1 passes on a MainPID owned by root each F alone
2 F2 accepts a listener on 0.0.0.0 each F alone
3 F3 passes without a reply each F alone; F3 exit 0 with no reply line
4 F5 matches a substring F5 labels (the trailing space)
5 F6 skips the --version check each F alone; F6 stub and path
6 F8 skips the owner check F8 0640, owner and symlink
7 F11 ignores discord/ each F alone; F11 *.json
8 P1 closes on any output P unnamed; every P unnamed
9 P5 maps to closed with jarvis in docker P unnamed; every P unnamed; P2 and P5 names docker
10 P9 drops the jarvis item P unnamed; P9 per item
11 P11 accepts any Before= P unnamed; every P unnamed
12 a P check's unnamed sets the exit to 1 P unnamed; every P unnamed; P10 and P11; P2 and P5; P9 per item

Mutant 1 survived the F1 variants test (a failed unit, User=root), because those faults fail F1 on the unit's state and User=, not on the uid line. Each F alone covers the uid line.

Pre-slot read review

Since every test run is slot work, a read-only subagent traced each case through the script before the first slot run, executing nothing. It found four defects, all fixed in this commit:

  • The --claude regex had no @, so every run refused the args file, the real .53 one included.
  • The fake stat logged the args-file call, so the bad-args test's "no command ran" check would fail.
  • The P5 and P6 faults each rewrote etc/group from the constant, so applying both lost P5's change.
  • F3 counted stderr into its line count, so a mutant dropping the exit check survived. F3 now counts stdout only, and a sent-only case covers the line count.

Run environment

The build commit ddf7dce6c sits on base 4ced5e63d (origin/refactor when it was pushed). It was first built on c515273db as 4a51d392c. Row 238 then changed docs/TOOLS.md, so I cherry-picked the three commits onto 4ced5e63d as darkwing/row195-b. git range-diff shows all three identical. node v26.8.1 on dragon-lin, every run behind my busy check and Sage's, chained with &&.

  • Step 1, sage-1829 (07:41:54Z to 07:42:05Z), TMPDIR /tmp/dw195: node --test packages/cli/tests/case1-accept.test.mjs, 22 pass, 0 fail, 11.0 s.
  • Step 2, sage-1865 (08:34:27Z to 08:36:21Z), TMPDIR /mnt/storage/scratch/tmp/r195: the twelve mutants above, all killed.
  • Step 3, the gate (08:36:21Z to 08:47:06Z), same TMPDIR, COMPOSE_PROJECT_NAME=darkwing-r195:
Suite Result
packages/cli (node --test tests/*.test.mjs) 662 pass, 0 fail, 0 skipped
test-auth.sh 15 passed, 0 failed
test-conductor.sh 17 passed, 0 failed
test-config.sh 31 passed, 0 failed
test-discord.sh 78 passed, 0 failed
test-extension-package.sh 18 passed, 0 failed
test-foundation.sh 44 passed, 0 failed
test-land-work.sh 86 passed, 0 failed
test-queue.sh 156 node pass, 0 fail; 28 passed, 0 failed
test-release.sh 14 passed, 0 failed
test-task.sh 98 passed, 0 failed

Summed: 818 node tests passed with 0 failed, and 429 selftests passed with 0 failed. The compose network was removed afterwards, and the sweep found no process left in the worktree, the output directory or the TMPDIR. This file's results are a doc-only commit on top of the build, so nothing that ran has changed.

Correction to deviation 6 (follow-up, 2026-10-21)

Deviation 6 says the node F9 runs is root's own. At 37171f65b the script didn't check that. F9 ran --node as root before F10's path check, so the header's "root itself only reads" was false (Duckworth's N1, agents/duckworth/work/row195/REVIEW-m1.md at 6afcee2a5). A host installed as designed was safe, because case2-unit.sh refused a non-root-owned --node. Row 245 carries the fix and its tests: agents/darkwing/work/queue-245/BUILD.md.