# Row 77 (P19 #225): the Console runs a pinned copy, candidate packet, round 2 Author: Rocko. Reviewers: Darkwing (comment 27801) and Filbert (comment 27818), both "changes" on round 1 candidate `4927de66`. Gate: Sage. Base: `04db738a` (`base.txt`), current `refactor`, which includes row 61 (`6c42344d`) and row 81 (`c9a5650e`). Row 81 touched `packages/webui/README.md`; the round 2 change applied to it cleanly with a 3-way apply. At freeze `refactor` is at `a3f6e9e6`: no commit after the base touches this row's paths, `packages/tasks` or `packages/bus`, and `build.patch` passes `git apply --check` on the checkout. The packet is uncommitted. I made no commits, pushes or Gitea calls, read no token, and started, stopped or installed no unit. Tests use `--dir`, a stand-in `systemctl` and fixture copies only. The round 1 packet is unchanged one level up (`../`). This directory holds round 2 only; `build.patch` is the whole candidate against the base, not a delta on round 1. ## Files (`files.txt`, `candidate-manifest.sha256`) Round 1's eight files, with these round 2 changes: | File | Round 2 change | |---|---| | `scripts/webui-service.sh` | `bus_on_copy`, called by `install`; `packages/tasks/src` and `packages/tasks/fields.json` in `COPY_PATHS`; directories read-only; the temp trap restores write before removing | | `packages/webui/tests/service.test.mjs` | the stand-in answers the bus `list-units` and `show` calls; the bus-on-copy test | | `packages/webui/tests/pinned-copy.test.mjs` | directory modes, `sed -i` and a new file refused, the rename-race cleanup test, `GET /api/task-fields` from the copy, a `.refactor` refusal, the README in the copy, `gc.auto 0` in the fixture repository | | `docs/TOOLS.md`, `packages/webui/README.md` | the bus wording, the install refusal, read-only directories and removal, the rollback floor, the unpinned queue view, the update step not yet usable | | `agents/rocko/work/queue-77/r2/mutants.py` | 34 mutants (round 1's 18 plus 16) | | `agents/rocko/work/queue-77/byte-check.sh` | unchanged from round 1 | Round 1's `agents/rocko/work/queue-77/mutants.py` is superseded by `r2/mutants.py` and isn't in this candidate. ## Findings ### Darkwing F1, Filbert F1: a Console on a copy fails every human proof The bus accepts a human proof only when `argv[1]` is its own `human-cli.mjs` (`human.mjs:70`, `runtime.mjs:68`). A Console on a copy spawns the copy's `human-cli.mjs`, so with the bus on the checkout every bus call fails `human-required`. Fix: `install --copy ID` refuses unless the bus unit runs from the same copy (`bus_on_copy`, Sage approved the design). It reads the effective `ExecStart` with `systemctl --user show UNIT -p LoadState -p ExecStart`, because the live bus has a `pm.conf` drop-in and the unit file alone isn't the effective value. - With `--business ID` the unit is `mosaic-bus@ID.service`. Without it, every loaded `mosaic-bus@*.service` from `list-units --all` is checked. - Every `path=` in `ExecStart` must lie under `/`. A path that only shares the copy's prefix (`x/...`) refuses. - It refuses when the unit isn't loaded (`not-found`), when no bus unit is loaded, when `ExecStart` has no path, and when `list-units` or `show` fails. Each refusal is one line and changes nothing. - The check runs after `render` and before the running-state read, so nothing is written on a refusal. The bus runs from the checkout today, and `copy` doesn't include `scripts/mosaic` (the bus entry point). So `install --copy` refuses on the live system until a later row puts the bus on the copy. TOOLS.md and the README say the update step can't be used yet and why. I didn't add the bus paths to the copy: moving the bus is the later row (Sage: "Don't move the bus yourself"), and row 85 (#1579, one-copy stack) is that row. TOOLS.md's "the bus child come from the copy" now says the bus child is the human transport and that the bus checks its path. Tests (`service.test.mjs`, "install refuses unless the bus unit runs from the same copy"): - refused: the bus on the checkout, with and without `--business`; the prefix-only path; two units, one off the copy; `not-found`; no units loaded; no `ExecStart` path; `list-units` failing; `show` failing - accepted: the bus on the same copy, with and without `--business` The exact-calls assertion in the existing install test now lists the bus calls too. ### Darkwing F2, Filbert F2: row 61's task field table `reads.mjs` imports `packages/tasks/src/fields.mjs`, which reads `packages/tasks/fields.json` at request time. Both are in `COPY_PATHS`. The comment says why and says the test serves from a copy: a missing import fails it at load, and a missing file read at request time (`fields.json`, `RELEASE`, `roles/`) fails the route that reads it, which the test also GETs (Filbert's wording point). The serve test GETs `/api/task-fields` from the copy and expects 200 with a body equal to `packages/tasks/fields.json`. Without that GET, the "copy misses fields table" mutant survived; with it, it dies. ### Darkwing 3, Filbert N1: read-only directories `copy` now makes every directory 0555 (the top one included) as well as every file `a-w`, so a writer that replaces by rename (`sed -i`, git, editors) fails too. Because a read-only temp directory can't be removed, the trap runs `chmod -R u+w` before `rm -rf`. TOOLS.md and the README give the removal command: `chmod -R u+w DIR && rm -rf DIR`. Tests (`pinned-copy.test.mjs`): every directory is 0555; `sed -i` on a copied file fails and leaves it unchanged; writing a new file throws `EACCES`. A new test makes `mv` lose the rename race (a stand-in `mv` that creates the target and fails): `copy` reports `exists:`, and the copies directory holds only that one entry, so no read-only temp directory is left behind. ### Filbert N2: rollback floor (docs) A copy from before row 77 holds a `cli.mjs` that refuses `--checkout` ("unknown option"), so a unit pointing at it exits 2 at start. I took the docs-only option Filbert offered: TOOLS.md and the README say rollback reaches only ids at or after row 77's landing. I didn't add a refusal to `install`: a grep of the copy's `cli.mjs` would be a second source of truth for the flag set. Before the next row lands, the update step can't be used anyway (F1). ### Filbert N3: the queue view isn't pinned (docs) By design (the brief), the queue read runs from the checkout. The README's update-step paragraph and TOOLS.md say so: a landing on the queue read shows at once. ### Filbert's three surviving mutants | Survivor | Now | |---|---| | the commit's leading-character check deleted | killed: `.refactor` refusal case, expects exit 1 and "commit must be a commit id or branch name: .refactor" (mutant "commit may start with a dot") | | `chmod 0755 "$COPY_TMP"` deleted, top directory 0700 | that line is gone; the directory-mode check covers the top directory (mutants "copy dirs stay writable", "copy subdirs stay writable") | | `packages/webui/README.md` dropped from `COPY_PATHS` | killed: the copy test checks the README exists and is read-only (mutant "copy misses README") | ## Test flakes seen while building, and one fix - Round 2's first full run after the mutants had one failure whose name I didn't capture. To find it I ran the suite five more times with TAP output. That turned up three failures: - `bus-browser.test.mjs` "S5 pages in a browser": a 27 s condition timeout. - `bus.test.mjs` "humanCall kills a transport that runs past its timeout": the pid file was missing. Both are timing failures in files this row doesn't change, seen under a load average of 20-30. - The third was this row's own: the cleanup of the new pinned-copy `tmp` helper (`chmod -R u+w`) failed with ENOENT under the fixture repository's `.git/objects`. The fixture commits thousands of loose objects, which starts a detached `git gc --auto`, and gc deletes objects while chmod walks. I set `gc.auto 0` in the fixture repository rather than making the cleanup ignore errors. After that, six consecutive full runs at the earlier base `08941d8b` were 97 pass, 0 fail (row 81 adds two tests at `04db738a`). ## Mutants (`mutants.py`, `out/mutants.txt`) `python3 mutants.py ` applies each mutant alone to the candidate tree and runs `service.test.mjs` and `pinned-copy.test.mjs`. 34 mutants, all killed. The 16 new ones: - install skips the bus check - bus path prefix only - `not-found` accepted - `show` failure ignored - `list-units` failure ignored - no loaded bus accepted - no `ExecStart` path accepted - only the first bus unit checked - `--business` ignored for the bus - copy misses tasks src - copy misses the fields table - copy misses README - commit may start with a dot - copy dirs stay writable, copy subdirs stay writable (`-maxdepth 0`), and the trap leaves a read-only temp directory The run in `out/mutants.txt` is on the candidate at `04db738a`. ## Byte check (`out/byte-check.txt`, `../byte-check.sh`) Round 1's script, base `04db738a` against the candidate: `render` without `--copy` gives the same unit and exit code for all 8 flag sets, and the template body is unchanged. ## Gate (`out/gate/`, `out/gate.sh`, `out/after-gate.sh`) Sequential, in the scratch worktree at `04db738a` with the candidate and `node_modules` linked, full output kept per suite, load average 17-27: | Suite | Result | |---|---| | webui node (`packages/webui/tests/*.test.mjs`) | 98 pass, 1 fail (below); rerun 99 pass, 0 fail | | test-auth | 15 passed, 0 failed | | test-conductor | 17 passed, 0 failed | | test-config | 24 passed, 0 failed | | test-discord | 66 passed, 0 failed | | test-extension-package | 18 passed, 0 failed | | test-foundation | 44 passed, 0 failed | | test-queue | 27 passed, 0 failed (the canonical-root checks skip in a worktree, as always) | | test-release | 14 passed, 0 failed | | test-task | 98 passed, 0 failed | The gate's one webui failure: `bus.test.mjs` "humanCall speaks the human transport protocol without blocking the server" got `outcome-unknown` where it expects `invalid-response`. That call has a 1500 ms timeout around spawning a node child, and the load average was 27. Neither `bus.test.mjs` nor `packages/webui/src/bus.mjs` is in this row's diff. After the gate, `out/after-gate.sh` ran the whole webui suite again (99 pass, 0 fail) and `bus.test.mjs` alone five times (all pass). The only failure marker in `out/gate/` is that one, in `node-webui.txt`. `rocko-r77_default` was removed after the Docker suites. The TOOLS.md update-step paragraph was reflowed during the gate (whitespace only, prose); no suite reads `docs/TOOLS.md`. The mutants and the rerun ran after the reflow. ## For the reviewers - The live unit and the live bus are untouched. The live Console can't take this change through `install --copy` until a later row moves the bus; until then it stays on its current unit. - The design decision with the most weight: an install refusal rather than a Console that spawns the checkout's `human-cli.mjs`. The latter would keep proofs working but put an unpinned file in the pinned Console's trust path.