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 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");
  • cat can'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:

  • --plan prints store 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 --restore refuses restore-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 commit 7e7f8e208" and say "stack-release.sh runs 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) run scripts/stack-release.sh by 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 --plan prints the same lines unless the bus stops during the store read, so the scratch plan in agents/darkwing/work/queue-162/scratch-plan-8e7ea89a1.txt still 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 either ExecStart holds 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= or ExecStart holds a quote now makes activate refuse unit-flags-drift. systemctl --user show [email protected] -p Environment -p ExecStart shows it before a window.
  • The copy compare assumes the copy dir is $COPIES/$ID, which stack-copy.sh makes.