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 166 (#1663): stack-release.sh hardening after D1
Owner: Darkwing. Reviewer: Filbert. Gate owner: Sage.
Brief: docs/plans/2026-10-18_stack-release-hardening.md, from Filbert's notes on row 162 round 1
(#1659 comment 28997).
Base: 139e28673. origin/refactor is c3cd27345, and git diff 139e28673 c3cd27345 -- scripts packages/cli docs/TOOLS.md is empty.
Worktree: ~/darkwing-scratch/r166/wt.
Files
| File | Change |
|---|---|
scripts/stack-release.sh |
items 1 to 4 |
packages/cli/tests/stack-release.test.mjs |
T28 to T31, a bus-stops-after stand-in fault, and run() takes e.sr |
| this packet |
No verb's wording changes, so TOOLS.md stays as it is. No new refusal code.
Items
1. Unreadable drop-ins refuse in the precheck. drop_in_override used to skip a unit path or
drop-in directory that [ -d ] rejected and a .conf that grep 2>/dev/null couldn't read.
install_guard in bus-service.sh dies on the same path after the stop. The precheck now
refuses drop-in-override when:
- a unit path exists but isn't a readable, searchable directory ("cannot inspect unit path P");
- a drop-in directory exists but isn't a readable, searchable directory ("cannot inspect drop-ins D");
catcan't read a.conf("cannot read drop-in F").
A dangling symlink counts as existing (-e || -L), as in install_guard. A unit path that isn't
absolute refuses too, since install_guard refuses it.
2. The is-active to read race. After a held-store read, plan_store and the --restore
event count each check is-active again. If the bus stopped meanwhile:
--planprintsstore schema: unknown (the bus stopped during the read, so the store state changed; run --plan again)and leaves out the note, migration and restore lines;- rollback
--restorerefusesrestore-mismatch("the bus stopped while the events after STAMP were counted, so the store state changed; nothing was stopped").
3. The copy check names the target. The gate's copy-check step reads the check's bus: line
and fails with copy-check unless it is $COPIES/$ID, the copy this run installed. The gate
message says what it saw: "the bus, Console and CLI agree on X, not Y".
4. A quoted Environment= or ExecStart= is unreadable. read_units returns 1 when the bus
Environment= line or either unit's ExecStart argv holds ", ' or \. Callers refuse as
they do for an unreadable unit: activate and rollback with unit-flags-drift, adopt with
check-failed, and status prints unit flags: cannot read the units. Nothing parses systemd
quoting (out of scope in the brief).
Tests
| Test | Item | What it checks |
|---|---|---|
| T28 | 1 | three subtests, each a mode 000 path: the unit path, the bus drop-in directory and a drop-in file. Each activate exits 3 with the named refusal, no start or stop, both units active, the fingerprint unchanged and one more log line. Round 2 adds a fourth: a relative unit path (systemd/user) refuses "invalid systemd unit path", with the same checks |
| T29 | 2 | the stand-in is-active answers the real state once, then inactive. --plan prints the exact plan without migration lines and writes nothing; rollback --restore refuses restore-mismatch with no start, stop or post-attempt dir |
| T30 | 3 | a repo copy whose stack-copy.sh check reports agreement on the pointer's copy. activate C1 fails the gate with copy-check, names both copies, rolls back to B and logs gate-failed, copy-check, rollback ok |
| T31 | 4 | five unit forms (Environment="...", Environment='...', Environment=/a\ b, the bus ExecStart ... "--pm" and, from round 2, the Console ExecStart ... --business "acme"). Each: activate refuses unit-flags-drift, status prints the unreadable line, adopt refuses check-failed, and no error names /a |
T30 copies the fixture repo, so the check stand-in replaces stack-copy.sh for that test only.
T28's relative path comes from a unit-path fault file: the systemd-analyze stand-in prints its
text as one more unit path.
The fingerprint is left out there because the copy's webui-service.sh writes its own
--checkout.
Mutants
~/darkwing-scratch/r166/mutants.py: each test is run with --test-name-pattern. CONTROL makes
no edit and must pass all four tests.
| Mutant | Removes | Test |
|---|---|---|
| 1a | the unit path read check | T28 |
| 1b | the drop-in directory read check | T28 |
| 1c | the cat read, back to grep 2>/dev/null |
T28 |
| 1d | the relative unit path refusal (round 2) | T28 |
| 2a | plan_store's recheck |
T29 |
| 2b | the --restore count's recheck |
T29 |
| 3 | the copy compare with $COPIES/$ID |
T30 |
| 4a | the Environment= quote check |
T31 |
| 4b | the bus ExecStart argv quote check, the Console one kept (round 2 narrows it) |
T31 |
| 4c | the Console ExecStart argv quote check, the bus one kept (round 2) |
T31 |
Slot run (sage-0952, round 1)
In Sage's slot after row 165 round 2, in ~/darkwing-scratch/r166/wt (139e28673 plus the
candidate), TMPDIR=~/darkwing-scratch/r166/tmp.
| Run | Result |
|---|---|
stack-release.test.mjs, the whole file |
71 of 71 pass, T28 to T31 among them |
stack-copy.test.mjs, the other test that reaches stack-release.sh by name |
12 of 12 pass |
| awk sum over both logs | pass 83, fail 0 |
mutants.py |
CONTROL passes (T28 4, T29 3, T30 1, T31 5 subtests and parents), all 8 mutants killed |
Logs: ~/darkwing-scratch/r166/out/full-20261018T232350Z and
~/darkwing-scratch/r166/out/mutants-20261018T232619Z. Mutant 2b took 63 s against about 3 s for
the others, because without the recheck the rollback goes on with the restore. Its log shows the
store moved to bus.post-attempt-... and the backup restored, and the test fails on exit 2 where
it expects 3. I didn't time the steps inside that run. The script was byte-equal to its backup after the run, and
the sweep found no process.
A grep of packages, scripts and tools for stack-release finds the test file, the script
itself, two comments in stack-copy.sh and packages/cli/README.md:648. No test pins a hash of
either manifest file. Correction (round 2): round 1 said the grep found only the test file,
stack-copy.sh and the script, and left out the README line Filbert named in 29045. Its text
describes the verbs, and no verb's wording changes, so it still holds.
Round 2 (Filbert 29045)
Filbert approved round 1 on d0c58a37... with three non-blocking notes. Round 2 takes the two test
notes and the grep correction. It changes only the test file. scripts/stack-release.sh is still
0cfdfa4f... and its diff is still stack-release-sh.diff (490d6c99...), the script Mos read.
| Note | Round 2 |
|---|---|
1. T31 has no Console ExecStart form, so dropping && unquoted "$console_argv" would survive |
T31 gets a fifth form, the Console unit's --business "acme". Mutant 4b now drops only the bus check and the new 4c drops only the Console check. Each is killed |
| 2. The relative unit path refusal is untested | T28 gets a fourth subtest. A unit-path fault makes the systemd-analyze stand-in print systemd/user as a second unit path, and activate refuses drop-in-override "invalid systemd unit path systemd/user" before any stop. The new mutant 1d removes the refusal and is killed |
3. docs/TOOLS.md:389-390 doesn't say the copy check must name the target |
Doc-only, left as a follow-up (decision 137) |
The grep list leaves out packages/cli/README.md:648 |
corrected under "Slot run". The line is 648 at 139e28673 and 693 after row 156 (Filbert 29054); the text is unchanged |
Round 2 slot run (sage-0959)
In ~/darkwing-scratch/r166/wt, TMPDIR=~/darkwing-scratch/r166/tmp:
| Run | Result |
|---|---|
stack-release.test.mjs |
73 of 73 pass |
stack-copy.test.mjs |
12 of 12 pass |
| awk sum over both logs | pass 85, fail 0 |
mutants.py |
CONTROL passes (T28 5, T29 3, T30 1, T31 6 subtests and parents), all 10 mutants killed |
Logs: ~/darkwing-scratch/r166/out/full-20261018T235407Z and
~/darkwing-scratch/r166/out/mutants-20261018T235640Z. Mutant 2b again took about a minute (64 s),
for the reason under "Slot run". The script was byte-equal to its backup after the run, the
worktree matched the round 2 manifest and the sweep found no process.
Row 156 landed as 4f45cadc0 meanwhile. git diff 139e28673 4f45cadc0 touches none of the two
files, stack-copy.sh or bus-service.sh. It changes docs/TOOLS.md and packages/cli/README.md,
but no +/- line there names stack-release, stack-copy, bus-service or drop-ins. The round 2
build.patch applies with git apply --cached --check on a 4f45cadc0 index.
The candidate
139e28673 plus 2 changed files.
| File | sha256 |
|---|---|
candidate-manifest.sha256 (the 2 files) |
4fc0613f2f9dad09d9a2a4ed424f4cde807485c0cee3eb536fec5c768468aced |
build.patch (the whole diff against 139e28673) |
ecfe96dd22191810468007d83267a2e8b779d1329a608f6b454b565fb05e5575 |
stack-release-sh.diff (the script part only, for Mos, unchanged) |
490d6c9910648e74b56f9343a9381ad4ad977cbc5f7e13aa9ff8c434cabf88d3 |
scripts/stack-release.sh is 0cfdfa4f... and the test file is 123555d6.... Round 1 was manifest
d0c58a37... and build.patch f7c22a97...; the packet keeps them as
candidate-manifest-r1.sha256 and build-r1.patch. git apply --check -R build.patch is clean on
the worktree. The commit message is commit-msg.txt.
Which script the 8e7ea89a1 window runs
The canonical checkout's, not the pinned copy's. Traced from
agents/sage/work/window-plan/repin-8e7ea89a1.md (blob ca3baaf1, last commit 53f3f29a3):
- lines 21-24 set REPO to
/mnt/storage/src/mosaic-stack"at or after row 162's landing commit7e7f8e208" and say "stack-release.shruns from REPO, not from a copy"; - W0 (line 82) is
cd $REPO && scripts/stack-copy.sh copy 8e7ea89a1..., and W2, W2b, W3 and W5 (lines 84-88) runscripts/stack-release.shby that relative path, so from REPO's working tree; - the copy under
copies/8e7ea89a1...is only the install target (lines 23-24).
At a618528b2 the canonical scripts/stack-release.sh is blob 4b16e848, the same as at 7e7f8e208,
and the working tree has no change to it. So the window runs row 162's script as long as row 166
hasn't landed. If row 166 lands first, the window runs row 166's script, because "at or after
7e7f8e208" covers it. What that changes for the window's own steps:
- W3
--planprints the same lines unless the bus stops during the store read, so the scratch plan inagents/darkwing/work/queue-162/scratch-plan-8e7ea89a1.txtstill matches. - W5's gate also checks that the bus, Console and CLI agree on
copies/8e7ea89a1..., the copy W0 makes, rather than on any copy. - W2, W3 and W5 refuse if a live drop-in path can't be read, or if the bus
Environment=or eitherExecStartholds a quote. This row doesn't read live state.
Landing order (mos-0834, via sage-0956)
Mos ruled that row 166 is reviewed now and lands only after the 8e7ea89a1 window is done, so the
window runs blob 4b16e848, the script Mos rehearsed. Mos read the live units: neither has a quote
in Environment= or ExecStart, and neither has a drop-in. Mos reruns that check after the
window, before 166 lands. No fresh P2 is needed. If the window lands anything on
stack-release.sh first, I rebase and request review again.
Notes for Mos and the live window
- A live unit whose
Environment=orExecStartholds a quote now makes activate refuseunit-flags-drift.systemctl --user show [email protected] -p Environment -p ExecStartshows it before a window. - The copy compare assumes the copy dir is
$COPIES/$ID, whichstack-copy.shmakes.