Files
jason.woltje 158205c5cc lead: commit darkwing's untracked evidence (348 files: queue-137 queue-141 queue-150 queue-162 queue-165 queue-166 queue-167 queue-169 queue-179 queue-180 queue-181 queue-182 queue-185 queue-186 queue-187 queue-188 queue-190 queue-93)
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.
2026-11-02 15:05:14 -06:00

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'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; <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-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 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).

  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 <live copy> 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: <copy> 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.