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.
197 lines
12 KiB
Markdown
197 lines
12 KiB
Markdown
# 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.
|