Unclaimed files left in the canonical checkout. Inventory: canonical-cleanup-inventory.md (commit-as-evidence groups). detect-secrets scan clean on 2026-11-02: hits are sha256 pins, git object ids, test sentinels already in tracked tests and unit-file lines.
12 KiB
Row 187: activate sets, carries and drops the Console's proof pair
Brief: docs/plans/2026-10-20_activate-proof-pair.md (blob 5940115e37). 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's16a936c79). None of the three paths changed between them. - Changed:
scripts/stack-release.sh,packages/cli/tests/stack-release.test.mjsanddocs/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; <target>'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-headeror--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 behost,origin,x-mosaic-testeror the--auth-headervalue.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
chmoded, 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).
- 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 <live copy> mosaic-stack --pm --no-proof, STATE mos-0954) both activate the pointer's copy, andcheck_targetrefused that withalready-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-proofwhile the pointer holds one. A plain activate,--tester-portalone and--no-proofwith no pair still refusealready-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 carriesfrom == to.from_log previous(:163) skips lines whosetois 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, printsnote: <copy> is the pointer's copy; the run re-pins it and only the Console flags changeright afterto:(:902). - The pair goes after the preview flags and before
--sendand--tester-port, not last. The brief says the argv ends with the pair, with--tester-portkept.webui-service.shrender writes the preview flags, then the pair (:202), then--send(:204), then--tester-port(:205), andflags_driftcompares the pointer's flags with the unit argv as exact strings (:644). A trailing pair with a tester port would make the next run refuseunit-flags-drift.proof_for_targetputs the new pair before the first--sendor--tester-port(:717-725), in the pointer and the unit alike. - One more
proof-file-invalidreason: 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_problemchecks it withrealpathbefore anything stops (:774-775).
Tests
packages/cli/tests/stack-release.test.mjs:
- The fixture
cli.mjsfor B, C1 and C gains a--proof-headercomment, 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. planTailgainsproof: 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.shunder 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.