# Row 187: activate sets, carries and drops the Console's proof pair Brief: `docs/plans/2026-10-20_activate-proof-pair.md` (blob 5940115e3744). Gitea #1685. This is step A0 of mos-0928 Part B and builds the tool only. Part B waits for Jason (J1-J6, P19 385). Owner Darkwing, reviewer Filbert, gate owner Sage. ## Files | File | What | |---|---| | `build.patch` | the change, `git diff f81ebfbae` over the three paths below | | `candidate-manifest.sha256` | one line per changed path | - Base: f81ebfbae (later than the brief's 16a936c79). None of the three paths changed between them. - Changed: `scripts/stack-release.sh`, `packages/cli/tests/stack-release.test.mjs` and `docs/TOOLS.md`. No webui change. Line numbers below are the candidate's unless they name f81ebfbae or `webui-service.sh`. ## Design ### Flags (changes 1 and 2) `parse_opts` takes `--proof-header NAME`, `--proof-file PATH` and `--no-proof`, for `activate` only (:82-84). After the loop, one pair flag without the other, or `--no-proof` with the pair, is a usage error (:97-98). ### Where the pair goes: `proof_for_target` (:694) `console_for_target` builds `RUN_CONSOLE` from the pointer and settles `--tester-port` as before, then calls `proof_for_target` (:686). That function strips any pair out of `RUN_CONSOLE`, notes whether the target's `packages/webui/src/cli.mjs` holds `--proof-header`, and picks one case: | Case | `RUN_CONSOLE` | `proof:` plan line | Steps log | Refusal | |---|---|---|---|---| | `--no-proof`, pair in the pointer | pair dropped | `dropped; --no-proof drops header H` | `drop --proof-header H --proof-file F; --no-proof was given` | none | | `--no-proof`, no pair | unchanged | `off; --no-proof was given and the pointer holds no pair` | says there is nothing to drop | none | | new pair | pair in place of any old one | `on, header H, file F` | none | `proof-target` on an unsupported target | | no pair anywhere | unchanged | `off` | none | none | | pointer pair, unsupported target, rollback | pair dropped | `dropped; 's Console does not support --proof-header, so the rollback drops header H` | `drop ...; the target Console does not support it` | none | | pointer pair, otherwise | pair carried | `carried, header H` | none | `proof-target` on an unsupported activate, naming `--no-proof` | A new or carried pair then goes through `proof_checks`. `plan` prints the `proof:` line after the drop-tester line (:939), and `run_steps` writes the step line (:952). `pm-on` is unchanged and still writes the pointer's Console flags back as they are. T53 test 3 pins that. ### Refusals before anything stops (change 3): `proof_checks` (:745), `proof_file_problem` (:765) All go through `check_refuse`, so `--plan` lists them with the others, and a run exits 3 with no stop: - `proof-needs-preview`: the flags the run would install lack `--public-host`, `--auth-header` or `--allow-user`. The message names the missing ones. - `proof-header-invalid`: the header, lowercased, must match `^[a-z0-9][a-z0-9-]{0,63}$`, and must not be `host`, `origin`, `x-mosaic-tester` or the `--auth-header` value. - `proof-file-invalid`, with the first reason that applies: not absolute; a character outside `[A-Za-z0-9/._+-]`; a symlink; cannot be found; not a regular file; inside the repository or the copies (deviation 3); mode other than 600 or 400; owner other than the script's uid. The checks run on a carried pair too, on activate and on a rollback that carries it. A file moved, or `chmod`ed, since the pair went in refuses before the stop instead of failing the Console start. ### The value is never read (change 4) `proof_file_problem` uses `[ -L ]`, `stat -c '%u:%a:%F'` and `realpath -e`. None of them opens the file. Every T53 case runs a python3 inotify watch on each proof file it writes (`IN_ACCESS | IN_OPEN`), and `noLeak` asserts each watch counted 0 events. I used inotify because `/home` is mounted `noatime`, so atime can't show a read. `noLeak` also scans every file under the data root, units, stand-in state and bin directories, plus every run's stdout and stderr, for the sentinel value. Mutant m1 (below) is killed by the watch. ### Rollback and unsupported targets (change 5) See the table. A rollback must be able to go back to a copy older than row 178, so it drops the pair and says so. An activate to such a copy refuses `proof-target` unless `--no-proof` is given, so the proof goes off only when someone asks. ### docs/TOOLS.md (change 8) "Pinned stack update" usage now lists `[--tester-port N]`, which was missing, and the pair flags. One paragraph covers the pair's position, the live copy, carry, the rollback drop, the `proof-target` refusal, the other refusals, the `stat` and `realpath` checks and the plan lines. ## Deviations from the brief Sage accepted all three (sage-1267, sage-1269). 1. **An activate may name the pointer's copy when it changes the pair.** Brief test 2 (Part B's B3) and Part B's rollback line B-R1 (`activate mosaic-stack --pm --no-proof`, STATE mos-0954) both activate the pointer's copy, and `check_target` refused that with `already-active` (f81ebfbae :236, pinned by the refusal table entry at test :441). `check_target` (:243-250) now lets it through only when the run sets a pair, or gives `--no-proof` while the pointer holds one. A plain activate, `--tester-port` alone and `--no-proof` with no pair still refuse `already-active`. The run is an ordinary activate with FROM == TO. It stops both units, backs up the stopped store, installs and starts, and the log line carries `from == to`. `from_log previous` (:163) skips lines whose `to` is the pointer's copy, so a later rollback still goes to the earlier distinct copy. The plan lists 0 commits and, at Sage's ask, prints `note: is the pointer's copy; the run re-pins it and only the Console flags change` right after `to:` (:902). 2. **The pair goes after the preview flags and before `--send` and `--tester-port`, not last.** The brief says the argv ends with the pair, with `--tester-port` kept. `webui-service.sh` render writes the preview flags, then the pair (:202), then `--send` (:204), then `--tester-port` (:205), and `flags_drift` compares the pointer's flags with the unit argv as exact strings (:644). A trailing pair with a tester port would make the next run refuse `unit-flags-drift`. `proof_for_target` puts the new pair before the first `--send` or `--tester-port` (:717-725), in the pointer and the unit alike. 3. **One more `proof-file-invalid` reason: inside the repository or the copies.** Render refuses that file (`webui-service.sh` :198), which would end the run as an install failure after the stop. `proof_file_problem` checks it with `realpath` before anything stops (:774-775). ## Tests `packages/cli/tests/stack-release.test.mjs`: - The fixture `cli.mjs` for B, C1 and C gains a `--proof-header` comment, so those copies support the pair, as they support the tester port. A and S support neither. T178 rolls back to B with a pair, so the proof set must match the tester set. - `planTail` gains `proof: off`, and four T36 slices move by one for the new plan line. - `refused()` now records its outputs for the leak scan. - T53, eleven subtests: | Subtest | Brief tests | |---|---| | the pair goes after the preview flags, before `--send` and the kept `--tester-port`; status matches; a new pair (0400, `X-Proof-Two`) replaces it | 1, 9 | | activate of the live copy writes the pair; a later activate and a rollback carry it | 2, 3 | | only a pair change may name the live copy: plain, `--no-proof` without a pair and `--tester-port` refuse `already-active`; the log keeps from and to; plan lists 0 commits; rollback goes to the earlier copy | 2, deviation 1 | | live copy: the plan note, the plan's backup line, the log's backup = activation, backup SHA256SUMS = pre-run store sha, stop Console, stop bus, start bus, start Console, and steps `stopped both units` < `backup:` < `installed`; with a pair and with `--no-proof` | 2, Sage's ask | | `pm-on` keeps the pair | 3 | | `--no-proof` drops it from the pointer and unit and says so; with no pair it says so and passes | 4 | | each refusal exits 3 with no start or stop and one refused log line: `proof-target`; seven bad headers (`Host`, `origin`, `x-mosaic-tester`, the auth header `X-User`, `-lead`, `under_score`, 65 characters); eight bad files (inside the copies, relative, a space, a symlink, a directory, missing, 644, 640); `proof-needs-preview` | 5 | | `--plan` against A lists `proof-target`, `proof-needs-preview`, `proof-header-invalid`, `proof-file-invalid` and `unknown-kind-for-target`, and writes nothing | 5, 9 | | usage errors: one flag alone, `--no-proof` with either, the flags on rollback or pm-on; the usage text | 6 | | a rollback to A drops the pair and says so | 7 | | an activate to A refuses `proof-target`, then passes with `--no-proof` | 7 | Every subtest ends with `noLeak` (brief test 8). Each `--plan` call in T53 goes through `planOnly`, which asserts the plan left the units, the data root, the log and the starts and stops unchanged (brief test 9). Not tested: the uid case of `proof-file-invalid`. A file owned by another uid needs root to make. No test reads `docs/TOOLS.md`, and no test pins a hash of any of the three files (grep of `packages/*/tests`, `scripts/test-*.sh` and `tests` for the paths and for 64-hex pins). Only `stack-release.test.mjs` reads `scripts/stack-release.sh`. ## Results Run from a detached worktree of f81ebfbae with the change applied, `TMPDIR` a scratch directory outside `/tmp`, stamps from `date -u`. Nothing touched `~/.mosaic-dev`, a live unit or the slot file. Every case uses the test's stand-in `systemctl`, units, stores, data root and proof files. - `node --test packages/cli/tests/stack-release.test.mjs`, whole file: 144 tests, 144 pass, 0 fail (09:46:48Z to 09:51:35Z). - Base control: the base `scripts/stack-release.sh` under the new tests fails all eleven T53 subtests (09:53:01Z). The candidate control passes the same 18 tests (09:52:32Z). Mutants: each is one exact replacement in the candidate's `scripts/stack-release.sh`, run against the proof and tester tests (`^(T53|T178|T37|T38|T41|T42) `, 18 tests), then put back and checked by sha256. The runner is `~/darkwing-scratch/r187/run187.sh`, 09:46:48Z to 09:59:29Z. | Mutant | Edit | Result | Failing T53 subtests | |---|---|---|---| | m1 the run reads the proof file (brief) | `: "$(cat -- "$2")"` after the `proof_file_problem` call | killed, 11 fail | every subtest but test 6, whose usage errors end before any check; `noLeak` reports the watch's events (`the run opened or read the proof file`) | | m2 an unsupported target keeps the pair on rollback (brief) | drop `RUN_CONSOLE=("${without_proof[@]}")` in the rollback branch | killed, 2 fail | test 7 rollback | | m3 an activate to an unsupported target drops the pair without `--no-proof` (brief) | `elif [ "$supported" = 0 ]; then`, dropping `[ "$OP" = rollback ]` | killed, 2 fail | test 7 activate | | m4 `--no-proof` leaves the pair in the pointer but not the unit (brief) | `write_pointer` takes `P_CONSOLE` when `NO_PROOF=1` | killed, 5 fail | test 4, test 7 activate, both live-copy test 2 subtests | | m5 the mode check accepts 644 (brief) | `\|\| [ "$mode" = 644 ]` | killed, 3 fail | test 5, tests 5 and 9 | | m6 the live-copy exception takes `--no-proof` without a pair | `[ "$NO_PROOF" = 1 ] \|\|` without the pointer check | killed, 2 fail | test 2, only a pair change | | m7 the plan has no live-copy note | drop the `note:` line | killed, 2 fail | test 2, the note and backup subtest | (The fail counts include the T53 parent. T178, T37, T38, T41 and T42 pass under every mutant.) Candidate (`candidate-manifest.sha256`, sha256 52a5ecc1d143…): ``` 78642df0db7b6516f9072777d42fb56cfa01c0273f2f58973f3267aa3bd3d386 docs/TOOLS.md 95e44c2f7f1441cea23f5ba78076b1567e753e648f0a6cd11ffc77fb336bf1ac packages/cli/tests/stack-release.test.mjs 51d80b9886d56efb392185fdb5d25edf97ccdd07d74e467991a04b1260cd6bc3 scripts/stack-release.sh ``` `build.patch` (sha256 f4651832…) applies with `git apply --check` on f81ebfbae. ## For Sage's gate The brief asks for the cli suite and every `scripts/test-*.sh` from a git worktree of the candidate. I ran only `stack-release.test.mjs`, the one test that reads any of the three files.