Lands Darkwing's round 1 candidate, manifest 573ece95a4a9 (ad1b23ab2on darkwing/row240-r3, basec771a36f4), approved by Filbert (29945, rev 2696). Nine paths: scripts/case1-hostcheck.sh (new), scripts/case1-pins.sh (new), packages/cli/systemd/mosaic-hostcheck.service (new), packages/cli/tests/case1-hostcheck.test.mjs (new), scripts/stack-upgrade.sh, packages/cli/tests/stack-upgrade.test.mjs, docs/guides/host-run-sheet.md, docs/TOOLS.md and agents/darkwing/work/queue-240/BUILD.md. File modes match the candidate. case1-hostcheck.sh and stack-upgrade.sh are 100755. case1-pins.sh is 100644, because the host check sources it. Evidence: Darkwing's first slot run failed one test, the N1 test's after hook (ENOENT once the fixture had removed its root). The fixb45852daais test-only. The second run onb45852daawas green: singles 37/0, mutants M1-M8 8 of 8 killed, cli 639/0, scripts 585/0. Sincec771a36f4no commit touches the nine paths. Sage's gate on060a17b54plus the nine paths (06:45:05Z to 06:56:21Z): packages/cli 639 pass, 0 fail, and all ten scripts/test-*.sh 585 pass, 0 fail (out/, run.log). `queue review verify-commit 240` matches the manifest on all 9 paths. Filbert's notes: N1 (the boot unit orders only mosaic-bus@mosaic-stack) and N2 (the run sheet writes the apt hook before the first --check) are in docs/plans/DEFERRED.md. N3, repeated DRIFT lines after bwrap is closed, is by design and is noted with N2.
174 lines
13 KiB
Markdown
174 lines
13 KiB
Markdown
# Row 240 (I4): root's host check, its pins and the boot unit
|
|
|
|
Brief: `docs/plans/2026-10-20_installer-rows.md` section I4, blob `4a5a65020f00` (commit b00d7d94d, rev 2665,
|
|
the pin row 240 carries; the I3 carry items came in with it). Sage split it out of I3 (sage-1650). Gitea #1746, Vikunja P19 #432. Owner Darkwing, reviewer Filbert, gate owner Sage.
|
|
Design: row 195 DESIGN r6 B2, `agents/darkwing/work/queue-195/DESIGN.md` at b7239bb66, :262-303 (the script,
|
|
the three callers) and :839-860 (files, test cases, mutants). Where the brief and r6 differ, the brief wins.
|
|
|
|
## Files
|
|
|
|
Drafted on `darkwing/row240-r1` from d99166ade (row 233's candidate) while row 233's gate ran, then applied
|
|
as one patch onto row 233's landing, 57f59e0c3, on `darkwing/row240-r2` (sage-1753). It applied cleanly; between
|
|
the two bases the only change on this row's paths is 233's N1 test fix in `stack-upgrade.test.mjs`. Row 234
|
|
then landed as c771a36f4 on 57f59e0c3. Sage preferred that the gate run with it in the tree (sage-1773), so both
|
|
commits were picked onto c771a36f4 as `darkwing/row240-r3`. That was a clean pick: the 42 paths changed from
|
|
57f59e0c3 to c771a36f4 share none with this row's 9.
|
|
|
|
- `scripts/case1-pins.sh` (new, 0644): `PIN_PROFILES_VERSION`, `PIN_PROFILE_SHA256`, `PIN_BUBBLEWRAP_VERSION`,
|
|
r6 section 0's values.
|
|
- `scripts/case1-hostcheck.sh` (new, 0755): the seven items and `--check`, `--enforce`, `--upstream`.
|
|
- `packages/cli/systemd/mosaic-hostcheck.service` (new): the boot oneshot.
|
|
- `packages/cli/tests/case1-hostcheck.test.mjs` (new, 16 tests). The cli suite's `tests/*.test.mjs` glob picks
|
|
it up.
|
|
- `docs/guides/host-run-sheet.md` section 4: the install lines that replace "Install them from row 240's run
|
|
sheet lines once it lands", plus rollback, drift recovery and `--upstream` before a re-pin window. Sections
|
|
3, 6, 8 and 10 for the I3 carry items below.
|
|
- `scripts/stack-upgrade.sh` and `packages/cli/tests/stack-upgrade.test.mjs`: I3 N1 below.
|
|
- `docs/TOOLS.md`: a new subsection "Root's host check (`scripts/case1-hostcheck.sh`, row 240)" after the
|
|
upgrade driver's.
|
|
|
|
Brief line 257 said I3's `stack-upgrade.sh` doesn't change. Sage's ruling sage-1727 widened the row to it for
|
|
N1 alone. Its default `STACK_UPGRADE_HOSTCHECK=/usr/local/lib/mosaic-stack/hostcheck.sh` is where the run
|
|
sheet installs this script.
|
|
|
|
## Carried from I3
|
|
|
|
Filbert's row 233 approve (Gitea 29922) left five notes. Sage ruled N1, N4 and N5 into this row and N2 and N3
|
|
to DEFERRED.md (sage-1727).
|
|
|
|
| Note | Change |
|
|
|---|---|
|
|
| N1: `stack-upgrade.sh` exits 1 `args-failed` when the args move after A5 fails, but exit 1 means the old copy runs | The move's failure exits 5 `args-failed`. The header, TOOLS.md's step 5 and exit line, and run sheet section 6's exit list name exit 5 and what root does: move the new file the result line names into place. Test `row 240: I3 N1: a failed args move after A5 exits 5 ...` makes `/etc/mosaic-stack` read-only at confirm and checks exit 5, the step names, `args rc=1`, the result line, both units on the new copy, the old args file's bytes and the new file beside it. The step log regex takes `exit=5`. Mutant M8. |
|
|
| N4: section 3's `npm install` has no npm on root's PATH, and the `--claude` path is unnamed while `node_modules/.bin/claude` is a symlink `case2-unit.sh:140` refuses | The install line runs with section 2's pinned node first on PATH (the postinstall runs `node` too). The `--claude` path is `node_modules/@anthropic-ai/claude-code/bin/claude.exe`, and a `--version` line catches the postinstall's 500-byte stub. |
|
|
| N5: sections 8 and 10 lack precheck lines | Each gets "Precheck line: none" and its missing line. |
|
|
|
|
Exit 5 is new rather than 2 because 2 means both units are stopped, and here both run. Exit 4 stays usage.
|
|
|
|
The `claude.exe` path comes from the 2.1.304 package itself: `npm view` gives `bin.claude` as `bin/claude.exe`,
|
|
and its `install.cjs` hardlinks the platform binary over that placeholder, or copies it across devices, then
|
|
sets 0755. I downloaded the tarball into `/mnt/storage/scratch/tmp/r240/cc304` and read it; nothing ran. The
|
|
run sheet says a newer pin needs the same check.
|
|
|
|
## The brief, item by item
|
|
|
|
| Brief line | Where |
|
|
|---|---|
|
|
| 247 `case1-pins.sh` holds the three pins | `scripts/case1-pins.sh`; the pins test checks the values against r6's |
|
|
| 248-249 seven items, `--check` exits 0 or 1 | `items()` and the final dispatch; the check tests |
|
|
| 250-251 `--enforce`: `apparmor_parser -R`, `root:root 0700`, `DRIFT`, a log line, exit 0 | `enforce()` and `stop_bwrap()`; the four enforce tests |
|
|
| 252 `--upstream` compares the current deb to the pin | `upstream()`; the upstream test |
|
|
| 253 the boot oneshot | `mosaic-hostcheck.service`; the boot unit test |
|
|
| 254-255 fakes for every host tool, r6's seven case groups, seven mutants | the test file; Tests and Mutants below |
|
|
| 257 the I3 run sheet item points here | run sheet section 4 |
|
|
|
|
## Tests
|
|
|
|
Fakes on PATH, each logging its argv to `$HC/calls.log`: `aa-status`, `dpkg`, `dpkg-statoverride`,
|
|
`apparmor_parser`, `systemd-run`, `stat`, `chown`, `chmod`, `apt-get`, `dpkg-deb`. `host(t)` builds a scratch
|
|
root (`/etc`, `/proc/sys`, `/usr/bin/bwrap`, `/var/log`) and a lib dir with a copy of the script and fixture
|
|
pins that hold the fixture profile's sha256. Every run passes `--root`.
|
|
|
|
r6's case groups (:843-856) against the tests:
|
|
|
|
| r6 case | Tests |
|
|
|---|---|
|
|
| `--check` exits 0 with all seven, 1 with each failing alone, its line naming it | `check: exits 0 ...`, `check: exits 1 with each item failing alone ...`, plus one test per item for the item's edge cases |
|
|
| item 3: a second file naming `bwrap` as a word, `/{usr/,}bin/bwrap` included; passes with the profile alone | `check: item 3 ...` |
|
|
| item 4: `unpriv_bwrap (enforce)` or a trailing space fails; the argv carries `-p NoNewPrivileges=yes` and the 2a' properties | `check: item 4 ...` checks the whole argv |
|
|
| item 7: the floor at 732 fails | `check: item 6 ...; item 7 ...` (733 passes as the control) |
|
|
| `--enforce` exits 0 on a pass and every failure; unload, close, `DRIFT`, one log line; `DRIFT` present with every item passing still closes | `enforce: a pass ...`, `enforce: each failure ...`, `enforce: with DRIFT present ...` |
|
|
| `--enforce` never calls `dpkg-statoverride` with `--update`, `--add` or `--remove` | `enforce: no unload ...; never writes the statoverride` (every call is `--list /usr/bin/bwrap`) |
|
|
| a bubblewrap version other than the pin is a line, not a failure | `check: a bubblewrap version other than the install pin ...` |
|
|
| `--upstream` against a differing deb prints the difference, exits 0 | `upstream: ...` (also: a match, a deb with no profile, a failed download) |
|
|
| the unit: `Type=oneshot`, `After=apparmor.service`, `[email protected]`, runs `--enforce` | `boot unit: ...` |
|
|
|
|
The last test also checks that the repo script reads `scripts/case1-pins.sh` beside it, that bad usage exits
|
|
2, and that a non-root run without `--root` refuses.
|
|
|
|
Singles are the two files that run a changed script: `case1-hostcheck.test.mjs` and `stack-upgrade.test.mjs`.
|
|
Sage asked (sage-1759) whether `stack-release.test.mjs` or `stack-release-external.test.mjs`, which share
|
|
`stack-release-fixture.mjs`, run `stack-upgrade.sh` or `case1-pins.sh`. They don't. `grep -cF` for
|
|
`stack-upgrade`, `case1-pins`, `case1-hostcheck` and `hostcheck` gives 0 in all three files; the control
|
|
`stack-release.sh` gives 2 in the fixture and 8 in `stack-release.test.mjs`. `scripts/stack-release.sh` names
|
|
`stack-upgrade.sh` once, in a comment (:2061). The gate's cli suite runs both files anyway.
|
|
|
|
## Mutants
|
|
|
|
r6's seven (:858-860) and one for I3 N1, scripted in `~/darkwing-scratch/r240/mutants.sh` with r233's method: a checked backup,
|
|
an exact replacement that must match once, `bash -n`, the named killer by `--test-name-pattern`, the restore
|
|
and a sha256 check. A dry run matched all eight once each. Results below.
|
|
|
|
| Mutant | Change | Killer |
|
|
|---|---|---|
|
|
| M1 | `--enforce` exits 1 after the stop | `enforce: each failure exits 0 ...` |
|
|
| M2 | `[ ! -e "$DRIFT" ] \|\| had=1` becomes `true \|\| had=1` | `enforce: with DRIFT present ...` |
|
|
| M3 | item 1's `dpkg -S` becomes `owner=""; rc=1` | `check: item 1 fails when a package owns ...` |
|
|
| M4 | item 3's `grep -rlw bwrap` becomes `grep -rlF /usr/bin/bwrap` | `check: item 3 ...` |
|
|
| M5 | item 4's `[ "$label" = "$LABEL" ]` becomes a substring match | `check: item 4 ...` |
|
|
| M6 | item 4 drops `-p NoNewPrivileges=yes` | `check: item 4 ...` (argv) |
|
|
| M7 | item 7's `-gt` becomes `-ge` | `check: item 6 ...; item 7 fails with the floor at 732 ...` |
|
|
| M8 | `stack-upgrade.sh`'s `finish 5 args-failed` becomes `finish 1 args-failed` (I3 N1) | `row 240: I3 N1: a failed args move ...` |
|
|
|
|
## Deviations
|
|
|
|
- Repo name `scripts/case1-hostcheck.sh` (r6), installed as `/usr/local/lib/mosaic-stack/hostcheck.sh`, the
|
|
name in brief line 239 and I3's `stack-upgrade.sh` default. r6's apt hook line says `case1-hostcheck.sh`;
|
|
the run sheet's hook uses the installed name.
|
|
- `stat` is a fake too. r6 lists seven fakes; a test can't make a root-owned file, so item 5's `stat` reads a
|
|
fixture.
|
|
- Item 7 with no Console unit installed reads the port as 1023 and says so ("1023, because no Console unit
|
|
is installed"). Until I2 installs the unit the floor must be above 1023. A unit with no `--port` fails.
|
|
- Item 4 runs under `timeout 60`. A hung `systemd-run` would otherwise hang the apt hook and the boot unit.
|
|
|
|
## Decisions
|
|
|
|
- Fail closed where r6 is silent:
|
|
- pins with no 64-hex sha fail item 1;
|
|
- `dpkg -S` exit 1 (no owner) passes, exit 0 fails with the owner, any other exit fails with the code;
|
|
- a failed `aa-status` fails item 2 and counts as loaded, so `--enforce` tries the unload.
|
|
- `--root DIR` is the test seam (r6 :840). Root refuses it; a non-root user needs it. Both exit 2. Root runs
|
|
with `PATH=/usr/sbin:/usr/bin:/sbin:/bin`.
|
|
- `DRIFT` is append-only (`<stamp> <failed line>`), and `--enforce` never removes it. The log line is written
|
|
only when the stop runs: `<stamp> enforce stop items=<ids or none> drift-before=<0|1> unload=<rc=N|no-file|skipped> chown=N chmod=N`.
|
|
- `--upstream` exits 1 when the download or the unpack fails, since it then compared nothing. A deb without
|
|
the profile file is a difference (exit 0), per r6 :302-303.
|
|
- The bubblewrap note reads the version from `dpkg -s bubblewrap`.
|
|
- The run sheet's recovery steps (`chown root:mosaic`, `chmod 0750`, `apparmor_parser -r`, then `--check`
|
|
before root removes `DRIFT`) are mine. r6 says only that root removes `DRIFT` in a reviewed step.
|
|
|
|
## Run environment
|
|
|
|
Worktree `/mnt/storage/scratch/darkwing-r240b`, branch `darkwing/row240-r3` on c771a36f4, with `node_modules`
|
|
linked to the canonical checkout's. Node v26.8.1, kernel 7.2.2-1-cachyos, 16 CPUs, TMPDIR
|
|
`/mnt/storage/scratch/tmp/r240`, compose project `darkwing-r240b`. Sage's slot (sage-1773, 06:16Z). Each run is
|
|
one chain behind `busy.sh`, launched from `/`: `~/darkwing-scratch/r240/run-all.sh` checks the HEAD pin and a
|
|
clean tree, runs the singles, then the mutants (8 killed, none surviving, the tree clean after), then the gate
|
|
(the cli suite and every `scripts/test-*.sh`), each stopping the chain on failure, then sweeps `/proc` cwd links
|
|
for anything left in the r240 trees.
|
|
|
|
## Results
|
|
|
|
Run r1, 06:17:10Z on 47ec9fe2f, failed at the singles. `case1-hostcheck.test.mjs` passed 16/0/0, and
|
|
`stack-upgrade.test.mjs` went 20 pass, 1 fail. The failure was the N1 test's own `t.after` hook, which ran
|
|
`chmodSync(e.etc, 0o755)` after the fixture's cleanup had already removed the scratch root, so it threw ENOENT.
|
|
Every assertion in the test body had passed, exit 5 included. The fixture's cleanup runs `chmod -R u+w` on the
|
|
root before `rmSync`, so the 0555 dir never needed the hook. b45852daa drops it and changes nothing else. The
|
|
mutants and the gate didn't run. The sweep found nothing left. Sage counted r1 as this slot's one failure
|
|
(sage-1788).
|
|
|
|
Run r2, 06:20:41Z to 06:33:28Z on b45852daa, passed. The counts come from awk over the .out files
|
|
(`~/darkwing-scratch/r233/sum.sh`):
|
|
|
|
| Step | Result |
|
|
|---|---|
|
|
| `case1-hostcheck.test.mjs` | 16/0/0 |
|
|
| `stack-upgrade.test.mjs` | 21/0/0 |
|
|
| c0 control | exit 0 |
|
|
| M1-M8 | 8/8 killed, none survived, every restore's sha256 matched, tree clean after |
|
|
| `packages/cli` suite | 639/0/0 |
|
|
| `scripts/test-*.sh` | auth 15, conductor 17, config 31, discord 78, extension-package 18, foundation 44, land-work 86, queue 156 node + 28 selftest, release 14, task 98; 585 pass, 0 fail |
|
|
| Gate total | 11 suites, 1224 pass, 0 fail |
|
|
| Sweep | nothing left in the r240 trees; the compose network was removed |
|
|
|
|
This commit adds only these BUILD.md lines. No test reads BUILD.md.
|