Files
stack/docs/plans/2026-10-20_activate-proof-pair.md

6.6 KiB

Brief: activate carries the row 178 proof pair

Sage, 2026-10-20, step A0 of mos-0928 Part B (agents/sage/work/window-plan/repin-rows-177-185.md § Part B). Gitea #1685. Owner Darkwing, reviewer Filbert, gate owner Sage. Starts after row 186, which touches the same script. Base: the refactor head once row 186 lands. Line numbers are at 1d7fd75e2.

This row builds the tool only. Turning the proof on for the live Console is Part B, which waits for Jason (J1-J6 on P19 385) and is not part of this row. Nothing here runs against ~/.mosaic-dev, the live units or the slot file. Every test uses the stand-in systemctl, units, stores and data root that packages/cli/tests/stack-release.test.mjs already builds, with a proof file the test writes itself.

Why

Row 178 gave the Console --proof-header NAME --proof-file PATH (packages/webui/src/serve.mjs proofOptions, :217). On the pinned stack the Console flags live in the pointer. flags_drift (:627) refuses unit-flags-drift when the unit differs, and console_for_target (:645) carries the pointer's flags unchanged. So a hand webui-service.sh install with the pair would make every later activate, pm-on and rollback refuse. activate --tester-port N is the only path that writes a new Console flag into the pointer today. Part B's B3 needs the same path for the pair.

Part B's rollback also needs a way to turn the proof off. A plain rollback goes to the log's previous distinct copy (from_log previous, :907). Today that is 89af38a83, which predates row 178, so the rollback would drop the proof only by also dropping rows 177-185. An activate of the live copy without the pair is the narrow way back, and today an activate carries the pair unchanged. So this row adds --no-proof too.

The change

  1. activate ID BUSINESS --proof-header NAME --proof-file PATH writes the pair into RUN_CONSOLE, so install_stack puts it in the Console unit and write_pointer puts it in the pointer, the same way --tester-port does (:659-668). It replaces any pair the pointer already holds. Both flags or neither; one alone is a usage error. Only activate takes them.
  2. activate ... --no-proof drops any pair from RUN_CONSOLE and says so in the steps log, the way DROP_TESTER does. With no pair in the pointer it is a no-op that says so. It can't go with the pair flags (usage).
  3. Before anything stops, the run refuses (exit 3, through check_refuse so --plan lists them) when:
    • proof-target: the target copy's packages/webui/src/cli.mjs doesn't take --proof-header.
    • proof-needs-preview: the Console flags the run would install lack any of --public-host, --auth-header or --allow-user, which proofOptions needs.
    • proof-header-invalid: the lowercased name doesn't match ^[a-z0-9][a-z0-9-]{0,63}$, or is host, origin, x-mosaic-tester or the lowercased --auth-header value. These are proofOptions' own rules (serve.mjs :203, :220-222).
    • proof-file-invalid: the path isn't absolute, holds a character outside [A-Za-z0-9/._+-] (the unit argv can't quote, :166), is a symlink, isn't a regular file, isn't mode 600 or 400, or isn't owned by the uid the script runs as (the Console unit's uid today; J2 changes that and gets its own row).
  4. The script never reads the proof file's contents. It checks the file with stat only. The value never appears in the pointer, the log, the steps log, the plan output or any refusal text.
  5. A rollback to a copy whose Console doesn't take --proof-header drops the pair and says so, as DROP_TESTER does for --tester-port, since a rollback is the way back and must not refuse on it. An activate to such a copy with a pair in the pointer refuses proof-target unless --no-proof is given, so an activate never turns the proof off unless the operator asked. Unlike --tester-port, the proof decides who can reach the Console. The plan prints the drop line. A copy that takes the pair carries it unchanged, as today.
  6. --plan prints one proof: line for an activate or rollback: proof: on, header NAME, file PATH for a new pair, proof: carried, header NAME when the pointer's pair goes on unchanged, proof: dropped; ... with the reason, or proof: off.
  7. pm-on is unchanged. It already writes the pointer's Console flags back as they are (:1008).
  8. docs/TOOLS.md "Pinned stack update" gets the two flags in the usage block and one paragraph on the pair, --no-proof and the drop.

Tests

  1. Activate with the pair: the pointer's console and the Console unit's argv end with --proof-header NAME --proof-file PATH, after the preview flags and with --tester-port kept.
  2. Activate of the live copy with the pair (Part B's B3 shape) passes and writes the pair.
  3. A later activate without pair flags carries the pair; pm-on keeps it.
  4. --no-proof drops it from the pointer and the unit, and the steps log says so. With no pair it says so and passes.
  5. Each refusal in change 3 exits 3 with no unit start or stop, and --plan lists it with any other refusals.
  6. Usage errors: one of the pair alone, --no-proof with the pair, either flag on rollback or pm-on.
  7. Rollback to a copy without proof support drops the pair and says so. Rollback to one with it carries the pair. Activate to a copy without support, with a pair in the pointer, refuses proof-target without --no-proof and passes with it.
  8. The value never leaks: the test's proof file holds a sentinel line, and no file under the test's state or data root, and no stdout or stderr of any run, holds it.
  9. --plan prints the proof: line for each case and writes nothing.

Mutants, each killed by some gate test (mos-0862):

  • the run reads the proof file (any cat or node read of PATH)
  • a target without proof support keeps the pair on rollback
  • an activate to a target without proof support drops the pair without --no-proof
  • --no-proof leaves the pair in the pointer but not the unit (drift)
  • the mode check accepts 644

Gate

Filbert approves the candidate manifest. Cli suite and every scripts/test-*.sh green on Sage's gate, run from a git worktree of the candidate. Touched files: scripts/stack-release.sh, packages/cli/tests/stack-release.test.mjs, docs/TOOLS.md, and the packet. Anything else gets named in BUILD.md with the reason. No change to packages/webui.

Out of scope

Running Part B or any step of it, writing the slot file (A1), the uid separation (J2), the k3s middleware (J3, J4), and any change to the Console's proof rules. Going live needs a re-pin window that Mos checks (A2), after Jason's go on Part B.