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

162 lines
10 KiB
Markdown

# 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.
12. `--claude` must match `^/[A-Za-z0-9/._+@-]+$`. The `@` is for `node_modules/@anthropic-ai/...`;
`stack-upgrade.sh` doesn't check `--claude` at all.
10. F1 requires all four uids in `/proc/<MainPID>/status` (real, effective, saved, filesystem) to be the
user's.
11. 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`.