Files
stack/docs/plans/2026-10-18_stack-release-hardening.md

4.0 KiB

Brief: stack-release.sh hardening after D1

Follow-up to row 162 (D1, #1659), from Filbert's non-blocking notes on round 1 (#1659 comment 28997). Darkwing drafted it (agents/darkwing/work/queue-162/followup-brief.md). Sage folded it in here and made the fifth note, the brief item 8 re-pin, a doc change in docs/plans/2026-10-18_stack-release.md.

Owner Darkwing, reviewer Filbert, gate owner Sage. Starts after row 162 lands and builds on that landing. Line numbers below are scripts/stack-release.sh as landed by row 162.

None of the four is a known failure today. Each one fails closed or can't be reached with the current pin. The fix makes each one refuse earlier, or tell the truth sooner, with codes row 162 already has. No new refusal code. Nothing here runs against the live stack: every test uses the stand-in systemctl, units and stores the row 162 tests already use.

Items

  1. Precheck unreadable drop-ins. drop_in_override (:503) scans the same three directories as install_guard in scripts/bus-service.sh (:100), but skips a directory or file it can't read ([ -d ], grep 2>/dev/null). install_guard dies on that same path after the stop, so the run rolls back and the live stack stopped for nothing. The precheck refuses drop-in-override with the path when a unit path, drop-in directory or .conf file exists but can't be read, using install_guard's own tests (-r, -x, a failed read). Test: a drop-in file with mode 000, and a directory with mode 000, each refuse before any systemctl stop in the stand-in log, and the units stay active.

  2. Close the is-active to read race. plan_store (:573, check at :577) and the --restore event count (:716) choose the held-store read or the stopped-store read from one is-active call. If the bus stops between the check and the readOnly open, the open can leave -wal or -shm beside a stopped store, and the next run refuses store-unclean. After a held-store read, check is-active again. If the bus stopped meanwhile, say so: --plan prints that the store state changed during the read and prints no migration line; rollback refuses restore-mismatch before anything stops. Test: a stand-in systemctl that answers active, then inactive.

  3. Compare the copy check with the target. The copy-check gate step (:373) runs stack-copy.sh check --business, which proves the bus, Console and CLI agree on one copy, not that it is the copy this run installed. Pass the expected copy id, or compare the check's reported id with the target, and fail the gate with copy-check on a mismatch. Test: a stand-in check that reports agreement on another copy fails the gate and rolls back.

  4. Don't split a quoted Environment=. read_units (:148) splits the systemctl show Environment= line (show at :144) on spaces with read -r -a (:157). systemd quotes a value that holds a space, so a MOSAIC_CLAUDE_BIN path with a space reads as a prefix of itself. Smallest fix: read_units returns 1 when that line holds a quote or backslash, so its callers refuse as they do for an unreadable unit (unit-flags-drift at :535, check-failed at :561; the status view at :743 then skips its unit flags line). Cover the ExecStart argv split (argv_of, :145, used at :153 and :159) the same way. Test: a stand-in unit with Environment="MOSAIC_CLAUDE_BIN=/a b/claude" refuses by name and never reads /a.

Acceptance

  • Each item's test, plus a mutant per item that removes the new check and fails that test.
  • packages/cli/tests/stack-release.test.mjs and every scripts/test-*.sh green on Sage's gate; Filbert approves the candidate manifest.
  • The diff touches only scripts/stack-release.sh, its test file and, if a verb's wording changes, the TOOLS.md "Pinned stack update" section.

Out of scope

The bus-side PM marker, parsing systemd quoting, anything in Mos's live window, and any run of adopt, activate or rollback against the live stack.