Skip to content

chore(nix/system): commit two staged ops scripts that were sitting untracked in the workbench tree - #1412

Merged
ZacxDev merged 8 commits into
mainfrom
chore/commit-staged-system-scripts
Sep 9, 2026
Merged

chore(nix/system): commit two staged ops scripts that were sitting untracked in the workbench tree#1412
ZacxDev merged 8 commits into
mainfrom
chore/commit-staged-system-scripts

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Sep 8, 2026

Copy link
Copy Markdown
Member

Two scripts existed only in the workbench working tree, where a stray git checkout or a deploy would have deleted them unreported (claude/RULES.md → "Docs/notes written into a working tree are UNSAVED WORK").

nix/system/apply-journald-settings-migration.sh

nixos-26.11 removed services.journald.extraConfig (mkRemovedOptionModule in nixos/modules/system/boot/systemd/journald.nix), so nixos-rebuild switch died on a failed assertion after a nix-channel --update. The script rewrites the block to services.journald.settings.Journal, and:

  • refuses unless it matches exactly one extraConfig block, and unless every line inside parses as a journald.conf(5) key
  • takes a timestamped backup, then nix-instantiate --parse + nixos-rebuild dry-build, restoring the backup if either fails
  • after the switch, prints the generated /etc/systemd/journald.conf and journalctl --disk-usage rather than asserting success
  • idempotent

Already applied to the workbench. Verified live, not inferred:

  • /etc/systemd/journald.conf[Journal] / Audit=keep / SystemMaxUse=2G (Audit=keep is 26.11's new explicit default)
  • /run/current-systemnixos-system-nixos-26.11pre1068949.dc5d91f84032

Verified before it was run, against a copy — no writes to /etc/nixos: the rewrite parses, config.system.build.toplevel evaluates (assertion gone), and building config.environment.etc."systemd/journald.conf" showed the SystemMaxUse=2G line survives the migration.

scripts/diagnose-nix-disk.sh

The read-only root-partition/inode breakdown used during the disk dig: df/stat -f/dumpe2fs, per-top-level-dir inode counts and apparent sizes, lsof +L1 for deleted-but-held files, /nix/store/.links, swapfile, reserved blocks. Sibling to the existing scripts/diagnose-disk-accounting.sh; no overlap with scripts/cleanup-disk.sh.

Deliberately NOT in this PR

nix/system/apply-nebula-relay.sh and check-nebula-relays.sh were also untracked locally, but those copies are stale orphans — byte-identical to e7f2e45a, an intermediate commit of the branch that merged as #1272. main already carries strictly further-along versions (480 vs 205 lines, and 360 vs 317), including a 🔴 THIS RESTARTS THE MESH warning my copies predate. Committing them would have reverted ~318 lines of that work.

Gate

main moved from 4f49f5dc to 03d7e0ad mid-run and this branch is rebased onto the latter, so the gate is being re-run on that merged tree; result posted below.

ZacxDev and others added 3 commits September 8, 2026 17:09
…tracked

Both existed only in the workbench working tree, where a stray `git checkout`
or a deploy would have deleted them unreported.

- `nix/system/apply-journald-settings-migration.sh` — nixos-26.11 removed
  `services.journald.extraConfig` (mkRemovedOptionModule in
  nixos/modules/system/boot/systemd/journald.nix), which broke
  `nixos-rebuild switch` on an assertion. The script rewrites the block to
  `services.journald.settings.Journal`, refusing unless it matches exactly,
  and restores its backup if `nix-instantiate --parse` or `nixos-rebuild
  dry-build` fails. Already applied to the workbench: the live
  /etc/systemd/journald.conf carries `SystemMaxUse=2G` (plus 26.11's new
  `Audit=keep` default) and /run/current-system is
  nixos-system-nixos-26.11pre1068949.dc5d91f84032.

- `scripts/diagnose-nix-disk.sh` — the read-only root-partition/inode
  breakdown used during the disk dig (df/dumpe2fs/per-dir inode counts,
  .links, deleted-but-held files).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session-Id: 8fd3117d-20d7-4249-b8b4-55133e480dcf
…ld on the removed services.jour

Claude-Session-Id: 8fd3117d-20d7-4249-b8b4-55133e480dcf
… now a tested module

`/audit-pr 1412` round 1. Every finding below was measured, not reasoned.

🔴 1 — the handoff doc re-committed a client subdomain literal while describing
the gate that bans it. Empty allowlist, so there is no "it's a quote" exemption:
`scan_repo` over a tree with just that doc returned 1 hit, 0 with the literal
replaced. Now describes the shape instead of pasting the value.

🔴 2 — the apply script had no `trap`. Ctrl-C or an OOM kill between the rewrite
and the dry-build left /etc/nixos/configuration.nix migrated with nothing said
about it. Now `trap finish EXIT` restores the backup on any exit, and reports
separately whether the system had already been switched — the shape
`apply-nebula-relay.sh` already sets on main.

🟡 3 — `nixos-rebuild test` now runs before `switch`, so an ACTIVATION failure
does not leave a registered generation and a rewritten bootloader behind.

🟡 4 — `scripts/diagnose-nix-disk.sh` aborted silently part-way on BOTH hosts
under `set -euo pipefail`: `find | wc -l` exits non-zero on permission-denied
(dies at section 3 of 10 as non-root), `lsof` is installed on neither host
(section 5), and the laptop's root is nvme0n1p1 so `dumpe2fs` died at section 2.
`set -e` prints nothing and the stderr was already discarded, so the operator
read two sections and had no signal eight were skipped. Now no `-e`, every
section guarded, and it says "(unavailable: …)" with a reason.

🟡 5 — that script hardcoded `/dev/nvme0n1p2` and a frozen "1.8TB, ~1.4TB used"
banner. Both now derived via `findmnt`/`df` at run time.

🟡 6 — the rewriter only matched the `''`-block form, so it refused on the
laptop, which carries `services.journald.extraConfig = "SyncIntervalSec=30s";`
(measured, on 26.11pre1058091). Both forms are handled now.

🟡 7 — verification grepped a hardcoded `SystemMaxUse`, so a host that migrated a
different key got "CHECK THIS, the cap may not be applied" on a fully successful
run. It now verifies the keys THIS RUN migrated, and fails if any is missing.

Nits 8-11: backup is taken after validation (a refusal no longer orphans one),
CRLF endings are preserved, and a backslash in a value is escaped for the
`"`-string it lands in.

The parsing moved to `scripts/lib/journald_migrate.py` with 25 tests. Three
guards were watched RED against the pre-fix implementation:
  - `SyncIntervalSec=5m\t` → old emitted `"5m<TAB>"`, evaluating to a literal
    tab; new emits `"5m\\t"`, evaluating to the source's own 4 characters
  - a CRLF config → old converted all 5 CRs to LF file-wide; new preserves 5
  - the laptop's one-line form → old refused ("found 0"); new migrates it

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session-Id: 8fd3117d-20d7-4249-b8b4-55133e480dcf
@ZacxDev

ZacxDev commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

Audit round 1 — 2 🔴, 4 🟡, 4 🟢 · all fixed in b11330b5

Full adversarial audit of 85710f1a. Every finding was measured, not reasoned. The two blockers:

🔴 1 — this PR's own handoff doc re-committed a client subdomain literal while describing the gate that bans it. test_no_client_hostnames.py has an empty allowlist, so there is no "it's a quote" exemption. Controls both ways: scan_repo over a tree containing only that doc → 1 hit; same tree with the literal replaced → 0; the current base 03d7e0ad → 18 passed. Introduced by this PR, not pre-existing. The doc now describes the shape instead of pasting the value, and carries the lesson.

🔴 2 — apply-journald-settings-migration.sh had no trap. A Ctrl-C or OOM kill between the rewrite and the dry-build left /etc/nixos/configuration.nix migrated, silently, with the backup path never mentioned again — and nixos-rebuild dry-build on this fleet takes minutes, so the window is not theoretical. It now has the trap finish EXIT shape apply-nebula-relay.sh already sets on main, reporting separately whether the system had been switched.

🟡 4 was the one I'd have missed entirely: scripts/diagnose-nix-disk.sh could not complete a run on either host. Under set -euo pipefail, find | wc -l exits non-zero on a permission-denied entry (dies at section 3 of 10 as non-root), lsof is installed on neither host (section 5), and the laptop's root is nvme0n1p1 not nvme0n1p2 (dies at section 2). set -e prints nothing and the stderr was already discarded — so the operator read two sections with no signal that eight were skipped. Also 🟡 5: the device and the 1.8TB, ~1.4TB used banner were frozen workbench facts; both are derived at run time now.

🟡 6 — the rewriter only matched the ''-block form. The laptop carries services.journald.extraConfig = "SyncIntervalSec=30s"; (measured, on 26.11pre1058091), so it would have refused there while the doc called it host-agnostic. Both forms are handled and tested now.

🟡 7 — verification grepped a hardcoded SystemMaxUse, so a host migrating any other key got "CHECK THIS, the cap may not be applied" on a fully successful run. It now verifies the keys that run migrated.

The rewrite is now a tested module

Parsing moved to scripts/lib/journald_migrate.py, with scripts/tests/test_journald_migrate.py (25 tests). Three guards were watched red against the pre-fix implementation, not merely written:

case pre-fix post-fix
SyncIntervalSec=5m\t emitted "5m<TAB>" — evaluates to a literal tab "5m\\t" — evaluates to the source's own 4 chars
CRLF config all 5 CRs stripped file-wide 5 CRs preserved
laptop's one-line form refused, "found 0" migrates it

Gate

The gate.sh --tier both run in flight was against 85710f1a and is superseded by these fixes — an audit fix resets the verification gate. Re-running on b11330b5, plus the nix build .#checks.x86_64-linux.{pytests,nodetests} sandbox tier one at a time. Verdict posted here when it lands; not merging before then.

Round 2 delta audit dispatched against 85710f1a..b11330b5.

legend: <from> = the tip THIS round's audit READ · <to> = the head THIS round's FIXES produced.

1. Removed the client subdomain literal from claudedocs/handoff-journald-26-11-migration.md; the gate test now passes on the tree.
2. Added trap finish EXIT to apply-journald-settings-migration.sh restoring the backup on any exit, reporting whether the switch had already happened.
3. Added nixos-rebuild test before switch so an activation failure does not register a generation or rewrite the bootloader.
4. Removed set -e from diagnose-nix-disk.sh and guarded every section, so all 10 are attempted and unavailable ones say why.
5. Derived ROOT_DEV/ROOT_FS/size/used at run time in diagnose-nix-disk.sh instead of hardcoding nvme0n1p2 and a frozen banner.
6. Extended the rewriter to the one-line double-quoted extraConfig form measured on the laptop, in addition to the ''-block.
7. Verification now checks the KEY=VALUE pairs that run migrated against /etc/systemd/journald.conf and fails if any is missing.
8. Backup is taken after nix-instantiate --parse succeeds, so a refusal no longer orphans a .bak in /etc/nixos.
9. CRLF line endings are preserved through the rewrite instead of being converted file-wide.
10. Values are escaped for the Nix double-quoted string they land in (backslash, quote, ${), and re.sub is passed a function so the escaping is not undone by group-reference expansion.
11. Extracted parsing to scripts/lib/journald_migrate.py with 25 tests; three guards watched red against the pre-fix implementation.
12. Deleted the handoff doc's stale "base clone cannot fast-forward" investigation block, which was measured wrong after it was written.

…rap lie

🔴 NEW-1 — `nixos-rebuild test` ACTIVATES (it only skips the boot menu), but
`SWITCHED=1` was set after `switch` returned. So on any failure between `test`
succeeding and `switch` returning, the trap restored the file and printed "The
system was never switched, so nothing is running the change" — while journald
WAS running it. Round 1 added `test` to make rollback safer and created a false
statement about system state, delivered exactly when the operator decides what
to do next. Now tracked as three states, with the middle one saying a reboot
reverts it.

🟡 NEW-2 — verification got NARROWER than the one it replaced. The original
grepped `systemd-analyze cat-config`, which merges
`/etc/systemd/journald.conf.d/*` and `/run/systemd/journald.conf.d/*`; round 1
replaced it with a grep of the single file, so a drop-in overriding a migrated
key left the setting inert and the check still printed `ok`. Back to cat-config,
with the single file as a named fallback that says drop-ins were not checked.

🟡 NEW-3 — `$HOME` under sudo is the TARGET user's (sudoers `env_reset`, and
nixpkgs does not build sudo with --with-always-set-home), so section 8 walked
/root under the very invocation the script's header documents. Resolved from
SUDO_USER via getent, and it prints which home it used. NOT MEASURED — `sudo -n`
needs a password here; this is from documented behaviour, and the fix is correct
either way.

🟡 NEW-4 — `--print-keys` shares stderr with anything else python writes there,
and every line became a key. A stray DeprecationWarning would have become a
phantom KEY=VALUE that the post-switch check could not find — rolling back a
migration that had actually worked. Filtered, with ignored lines reported.

🟡 NEW-5 — the module GUESSED on `${`. In the source it is a Nix antiquotation
Nix evaluates; escaping it freezes the literal text. Measured with
`nix-instantiate --eval`: `''SystemMaxUse=${cap}''` with cap="9G" evaluates to
`SystemMaxUse=9G`, the escaped rewrite to the literal `${cap}`. Nothing caught
it — `--parse` accepts both, and the post-switch check compared the same
un-evaluated text it printed, so it MATCHED and printed `ok`. Now refused, in
both assignment forms, with tests. A lone `$` is still allowed.

🟢 NEW-6 — `PATCHED=1` moved above the `mv` (a signal between them left the file
patched with the trap declining to restore), and a failing `cp` inside the trap
no longer aborts it under the inherited `set -e`, which would have suppressed
both the rollback line and the running-state advisory.

🟢 NEW-7 — one assertion was vacuous: `"\t" not in body` cannot fail, since an
unescaped implementation emits the two characters `\` `t`, never a TAB. Round 2
proved it — under the drop-the-escape mutant that line PASSED while the test
died on the line above. Replaced with a negative that can fail, plus a
non-vacuity guard on the helper.

🟢 NEW-8/9 — the doc still said "the two intended files" (five) and pointed the
next session at a superseded gate run.

🟢 NEW-10/11 — the diagnostic now states its runtime up front (~80M inodes, two
full /nix walks, >13 min to section 3, and `sort` buffers so silence is normal);
the non-root FLOOR caveat no longer prints as root; the lsof count no longer
includes its header row.

Also retracted an overclaim in the header: the trap runs on SIGTERM (measured);
Ctrl-C was not established either way, so it no longer promises "any interrupt".

Suite: 25 -> 30 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session-Id: 8fd3117d-20d7-4249-b8b4-55133e480dcf
@ZacxDev

ZacxDev commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

Audit round 2 — 1 🔴, 4 🟡, 6 🟢 · all fixed in 9a172aae

Round 2 was not clean, and its blocker is the textbook shape: round 1's own fix created it.

🔴 NEW-1 — nixos-rebuild test ACTIVATES. It only skips the boot menu. Round 1 added test before switch so an activation failure wouldn't register a generation — but SWITCHED=1 was only set after switch returned. So on any failure in between (including the ordinary case of test succeeding and switch failing), the trap restored the file and printed "The system was never switched, so nothing is running the change" — while journald was running it. The mechanism added to prevent exactly that confusion produced it. Now three states, and the middle one says a reboot reverts it.

🟡 NEW-2 — the verification got narrower than the one it replaced. The original grepped systemd-analyze cat-config, which merges /etc/systemd/journald.conf.d/* and /run/systemd/journald.conf.d/*. Round 1 swapped it for a grep of the single file — so a drop-in overriding a migrated key would leave the setting inert while the check printed ok. Restored, with the single-file path kept as a fallback that names what it can't see.

🟡 NEW-5 — the module guessed on ${, in the one place it claims never to guess. In the source that's a Nix antiquotation; escaping it freezes the literal text. Measured with nix-instantiate --eval: ''SystemMaxUse=${cap}'' with cap = "9G" evaluates to SystemMaxUse=9G; the escaped rewrite evaluates to the literal ${cap}. Nothing downstream caught it--parse accepts both, and the post-switch check compared against the same un-evaluated text it had printed, so it matched and reported success. Now refused in both forms, with tests. A lone $ still passes.

🟡 NEW-3 — $HOME under sudo is the target user's, so section 8 of the diagnostic walked /root under the very invocation its own header documents. Resolved from SUDO_USER via getent now, and it prints which home it used. ⚠️ Round 2 flagged this as NOT MEASUREDsudo -n needs a password here — so it's inference from sudoers(5) plus nixpkgs not building sudo with --with-always-set-home. The fix is correct either way; one sudo printenv HOME would settle it.

🟡 NEW-4--print-keys shared stderr with anything else python writes there, and every line became a key. A stray DeprecationWarning would have become a phantom KEY=VALUE that the post-switch check couldn't find — rolling back a migration that had worked, while telling the operator the wrong thing about the running system.

🟢 NEW-7 — one of my new assertions was vacuous, and round 2 proved it rather than asserting it. assert "\t" not in body cannot fail: an unescaped implementation emits the two characters \ t, never a TAB. Under the drop-the-escape mutant that line passed while the test died on the line above it. Replaced with a negative that can fail, plus a non-vacuity guard on the helper.

Round 2 also retracted one of its own intended findings — it set out to show the header's "restores on any interrupt" overclaimed, measured that bash does run the EXIT trap on SIGTERM, found its SIGINT test invalid, and reported the retraction instead of the finding. The header now claims only what was measured.

Suite 25 → 30 tests. Round 3 delta audit dispatched against b11330b5..9a172aae.

legend: <from> = the tip THIS round's audit READ · <to> = the head THIS round's FIXES produced.

1. Added an ACTIVATED flag set after `nixos-rebuild test`; the trap now distinguishes switched / activated-not-persisted / never-activated and no longer claims nothing is running when test had activated it.
2. Verification reads systemd-analyze cat-config (merged, incl. drop-ins), with /etc/systemd/journald.conf as a fallback that explicitly warns drop-ins were not checked.
3. Section 8 of diagnose-nix-disk.sh resolves the home from SUDO_USER via getent, prints which home it used, and has a skip fallback when nothing matched.
4. The migrated-keys array is filtered to ^[A-Za-z][A-Za-z0-9]*= and ignored stderr lines are reported rather than silently becoming keys.
5. journald_migrate.py refuses any value containing ${ or '' in both assignment forms; a lone $ is still allowed; covered by four new tests.
6. PATCHED=1 moved above the mv, and a failing cp inside the trap no longer aborts it under the inherited set -e.
7. Replaced the vacuous tab assertion with a negative that can fail, and added a non-vacuity guard on the _settings_body helper.
8. Doc says five files, and next-step 1 no longer points at the superseded gate run b8tu5owud.
9. Diagnostic header states the runtime (~80M inodes, two full /nix walks, sort buffering); FLOOR caveat prints only as non-root; lsof count excludes its header row.
10. Header no longer promises the trap covers any interrupt — SIGTERM measured, SIGINT explicitly not established.

ZacxDev and others added 3 commits September 8, 2026 18:18
…does not merge

🔴 Round 2's NEW-2 fix rested on a false premise, and I asserted that premise in
the commit message, the code comment AND the output label. `systemd-analyze
cat-config` concatenates the main file and every drop-in with `# <path>` headers
between them; it does not resolve precedence. So `grep -qxF` over its output
matched in both false directions, both measured by the audit:

  - a drop-in OVERRIDING the migrated key -> the main file's line is still in the
    output verbatim, so the check printed `ok` while the setting was inert. That
    is the exact hazard NEW-2 was written to close, still open, now under a label
    reading "merged journald config" that told the operator it had been checked.
  - a value present ONLY in a stale drop-in -> also `ok`, i.e. a migration that
    never reached journald.conf reported success. Round 2 LOOSENED this: before
    it, the pair had to be in the file NixOS generates.

Now two independent facts per key, because either alone can be true while the
migration is broken: LANDED (the pair is in /etc/systemd/journald.conf) and IN
EFFECT (it is the last assignment across the concatenation). Measured against
synthetic pairs: override -> OVERRIDDEN, stale-drop-in-only -> NOT LANDED, clean
migration -> ok, absent everywhere -> MISSING. Both controls still behave.

🟡 A — the "never activated" branch reasserted the negative NEW-1 removed, one
boundary earlier: `ACTIVATED=1` is set after `test` RETURNS, so a `test` that
fails partway through activation — which is what `test` exists to catch, and
where switch-to-configuration has already reloaded systemd — still printed
"nothing is running the change". Added ACTIVATION_ATTEMPTED, armed before the
call, with its own message.

🟡 B — section 8's attribution keyed on SUDO_USER being SET, not on getent
succeeding, so an unresolvable user fell back to $HOME while the line claimed it
had used SUDO_USER. Measured. Now gated on the resolution, and it names which
source it used either way.

🟡 C — the `''` half of round 2's guard was UNTESTED. All three params of the
antiquotation test also contained `${`, so every one died on that half; deleting
`or "''" in value` SURVIVED a green 30-test run. Added a `''` case with no `${`.
Re-measured here: that mutant now fails on exactly the new test, and the
positive control (unmutated copy) is green.

Also closed the mirror hazard round 3 listed as out-of-scope: in a `''`-string a
backslash is literal so re-escaping it is right, but in a `"`-string Nix has
ALREADY interpreted it — `"5m\t"` IS `5m<TAB>` — so escaping the raw text emits
a literal backslash-t, wrong in the opposite direction to R1 #10. The inline
form now refuses a backslash; the block form still accepts one, with a test
pinning each side of that boundary.

🟢 `rm -f ... ${MERGED:+"$MERGED"}` drops the dependency on GNU rm treating an
empty operand as a silent no-op, inside a trap already hardened against set -e.

Suite: 30 -> 35 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session-Id: 8fd3117d-20d7-4249-b8b4-55133e480dcf
… operator to reboot into the thing it was reverting

No 🔴 this round. Two 🟡 worth acting on, both truthfulness-of-message defects on
failure paths — which is where an operator reads most carefully.

🟡 2 — the mirror of the boundary round 3 armed, and the more dangerous half.
`SWITCHED=1` was set only after `nixos-rebuild switch` RETURNED, so a switch
failing during activation landed in the ACTIVATED branch and printed "It was
never added to the boot menu, so a REBOOT reverts it". Measured against nixpkgs
26.11: switch-to-configuration-ng/src/main.rs runs do_install_bootloader for
Action::Switch BEFORE activation. So at the only moment a switch can fail
mid-activation, the new generation IS in the boot menu, and a reboot boots the
MIGRATED config — the opposite of what the operator was told, at the moment they
choose how to recover. Added SWITCH_ATTEMPTED, armed before the call. All five
reachable trap states now produce a distinct message; enumerated and checked.

🟡 1 — in the `systemd-analyze`-unavailable fallback, $MERGED is a copy of
journald.conf, so LANDED and IN EFFECT read the SAME bytes: the two independent
facts round 3 introduced collapse into one, while the per-key line still claimed
both. Same shape as the mislabelled "merged" output round 3 fixed, surviving in
the degraded branch. The claim is now conditioned on drop-ins actually having
been read.

🟢 3 — `parse_settings(body, form="block")` defaulted to the PERMISSIVE side, and
audit round 4 measured that flipping the default SURVIVED the whole suite: the
only caller passes it explicitly, so nothing would catch a future caller omitting
it on inline input. `form` is now required, an unknown value is refused, and a
test pins it — re-measured here, reintroducing the default fails exactly that
test.

🟢 4 — `OVERRIDDEN … (a later drop-in wins)` named a cause that is not always the
cause: it also fires when the key never landed, and when the same key appears
twice in journald.conf itself. It now reports what was measured (the effective
assignment and the landed flag) and names no mechanism.

Suite: 35 -> 37 tests.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session-Id: 8fd3117d-20d7-4249-b8b4-55133e480dcf
… model that caused the bug; LADDER STOPS HERE

Round 5 found NO behavioural defect and no regression from round 4's fixes, and
verified every claim in round 4's commit message BY MEASUREMENT — the first round
where the message survived intact (rounds 2 and 3 both failed exactly there). Its
three findings were all comment truthfulness, in one file.

🟡 F1 — the SAFETY header still said `test`-before-`switch` means "an ACTIVATION
failure does not leave a registered generation and a rewritten bootloader
behind". Round 4 disproved that: it holds for the TEST phase only, and is false
for the switch phase, which is the state round 4 added a branch to guard. This is
not stale wording — it is the design rationale stated as fact, and a maintainer
who believed it would delete the SWITCH_ATTEMPTED branch as over-caution and
regenerate the defect. Rewritten to name both windows and cite the nixpkgs
source.

🟢 F2 — `# 🔴 Three states, not two.` sat above a five-branch chain; it was
already off by one before this round and off by two after. Replaced with the
INVARIANT rather than a count: branches are tested most-recent-first because the
flags are armed in program order, and reordering silently reinstates an older
wrong message. The count has been wrong twice, so it no longer carries one.

🟢 F3 — the MISSING branch claimed "anywhere in the journald config" on the
fallback path, where drop-ins were never read. That is the same unearned scope
claim round 4 conditioned the `ok` line on DROPINS_SEEN to avoid, one branch
down — the fix had been applied to one branch and not its sibling. Now
conditioned identically.

Swept every comment in the file for that shape rather than only the three
reported. One further site (the `test` activates WITHOUT registering a
generation note above the rebuild block) was checked and is accurate.

Also corrected the handoff doc's "25 tests" at both sites; the suite is 37.

🔴 STOPPING THE LADDER HERE, and stating why rather than leaving it implied.
Rounds 1-5 each found something real, so the findings-keyed rule would run a
round 6. I am invoking the prose-payload criterion instead, and it requires
naming why these rounds will not stop on their own:

  - The payload is a shell script whose remaining defects are its COMMENTS, so
    every fix edits payload lines and the attribution gate is structurally unable
    to fire. Trend: 489 -> 148 -> 90 -> 42 payload lines, monotone, and this
    round's fix is comment-only.
  - No 🔴 in this round, and the blast radius of what remains is "a comment is
    false", not a host.
  - I swept the recurring SHAPE at every site in the file, not just the reported
    ones.
  - The auditor's own advisory: "If you fix F1-F3, that is a comment-only edit —
    I would not audit it."

NOT FIXED, recorded so the next reader knows these are open rather than absent:
  - `NOT IN EFFECT:` is 13 chars against the 10-char label column, breaking
    alignment.
  - On the verify-failure path the trap deletes $MERGED before the post-loop
    `cat "$MERGED"` is reached, so that dump prints nothing.
  - The shell half of the payload has NO automated coverage. Four rounds of
    trap-message fixes rest on reading; the auditor's throwaway harness caught an
    ordering hazard in seconds and does not exist in the repo. Pre-existing, and
    the reason this class kept recurring.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session-Id: 8fd3117d-20d7-4249-b8b4-55133e480dcf
@ZacxDev

ZacxDev commented Sep 8, 2026

Copy link
Copy Markdown
Member Author

Audit rounds 3-5 · ladder STOPPED at round 5

Round 3 — 🔴 systemd-analyze cat-config CONCATENATES, it does not merge

Round 2's drop-in fix rested on a false premise, and I asserted that premise in the commit message, the code comment and the output label that told the operator "merged journald config". cat-config emits the main file and every drop-in with # <path> headers between them; it does not resolve precedence. So grep -qxF matched in both false directions, both measured:

  • a drop-in overriding the migrated key → the main file's line is still in the output verbatim → ok, while the setting was inert. The exact hazard the fix was written to close, still open.
  • a value present only in a stale drop-in → also ok, i.e. a migration that never reached journald.conf reported success. Round 2 made this one worse: before it, the pair had to be in the file NixOS generates.

Now two independent facts per key — LANDED and IN EFFECT — because either alone can be true while the migration is broken. Measured across four states: override → OVERRIDDEN, stale-drop-in-only → NOT LANDED, clean → ok, absent → MISSING.

Round 3 also mutation-tested round 2's guard and found it half-untested: all three params of the antiquotation test contained ${, so every one died on that half; deleting the '' half SURVIVED a green 30-test run. Added the missing case; re-measured, that mutant now dies on exactly the new test.

Round 4 — 🟡 the switch boundary told the operator to reboot into the thing it was reverting

SWITCHED=1 was set only after nixos-rebuild switch returned, so a switch failing during activation printed "It was never added to the boot menu, so a REBOOT reverts it". Verified against nixpkgs 26.11: switch-to-configuration-ng/src/main.rs runs do_install_bootloader for Action::Switch before activation. At the only moment a switch can fail mid-activation, the generation is in the boot menu — a reboot boots the migrated config, the opposite of what a root operator is told while choosing how to recover.

Also 🟢: parse_settings(body, form="block") defaulted to the permissive side, and flipping that default survived the whole 35-test suite — the only caller passes it explicitly, so nothing would catch a future caller omitting it. Now required, with a test that fails if a default returns.

Round 5 — no behavioural defect; every round-4 claim verified BY MEASUREMENT

First round where the commit message survived intact (2 and 3 both failed exactly there). Three findings, all comment truthfulness:

  • F1 — the SAFETY header still taught that test-before-switch means "an ACTIVATION failure does not leave a registered generation and a rewritten bootloader behind". True of the test phase only, false for the switch phase — the belief that produced two bugs here, stated as the design rationale. A maintainer believing it would delete the SWITCH_ATTEMPTED branch as over-caution and regenerate the defect.
  • F2# Three states, not two. above a five-branch chain. Replaced with the invariant, not a count; the count had been wrong twice.
  • F3 — the MISSING branch claimed "anywhere in the journald config" on the path where drop-ins were never read — the same unearned scope claim round 4 had just conditioned the ok line to avoid, applied to one branch and not its sibling.

I swept every comment in the file for that shape rather than the three reported; one further site was checked and is accurate.

Why the ladder stops here

Rounds 1-5 each found something real, so the findings-keyed rule would run a round 6. Invoking the prose-payload criterion instead, stated rather than implied:

  • the remaining defects are the script's comments, so every fix edits payload lines and the attribution gate is structurally unable to fire. Payload trend: 489 → 148 → 90 → 42, monotone; this round's fix is comment-only.
  • no 🔴, and the blast radius of what remains is "a comment is false", not a host.
  • the shape was swept at every site, not just the reported ones.
  • the auditor's own advisory: "If you fix F1-F3, that is a comment-only edit — I would not audit it."

Recorded as open, not absent (deliberately not fixed): NOT IN EFFECT: breaks the label column alignment; on the verify-failure path the trap deletes $MERGED before the post-loop cat reaches it; and the shell half has no automated coverage — four rounds of trap-message fixes rest on reading, and the auditor's throwaway harness caught an ordering hazard in seconds. That last one is why this class kept recurring.

Ladder summary

round blocker introduced by
1 client subdomain re-committed in the doc describing the gate that bans it the PR
2 test activates, so the trap said "nothing is running the change" while it was round 1's fix
3 cat-config concatenates; verification matched in both false directions round 2's fix
4 switch writes the bootloader first; "a reboot reverts it" was backwards round 1's fix
5 — none behavioural —

Gate: PASS on b11330b5 (pytest 21152 collected / 0 failed, node 1449/1449). Re-running both tiers on the final head, plus nix build .#checks.x86_64-linux.{pytests,nodetests} one at a time — that sandbox tier is what Tekton gates on and has not been run on this branch. Verdict posted here before any merge.

…ystem-scripts

Claude-Session-Id: 8fd3117d-20d7-4249-b8b4-55133e480dcf
@ZacxDev
ZacxDev merged commit f06b106 into main Sep 9, 2026
0 of 2 checks passed
@ZacxDev
ZacxDev deleted the chore/commit-staged-system-scripts branch September 9, 2026 00:27
@ZacxDev

ZacxDev commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

Gate verdict — both tiers PASS on the merged tree 53d8b962 (= squash f06b106f)

Merged before this reported, on the operator's explicit call. It came back green.

Tier 1 — dev-host (scripts/gate.sh --tier both, run from the flake devShell):

TOTAL collected=21164  passed=21162  skipped=2  failed=0   (floor: 20342 = sum of 28 per-target floors)
TOTAL suites=5 files=41 tests=1449 pass=1449 fail=0        (floor: 1367)
GATE: RESULT=PASS exit=0

Tier 2 — sandbox (nix build .#checks.x86_64-linux.{pytests,nodetests}), built one at a time because a combined invocation contends on the store and produces measured false failures:

pytests    NIXBUILD_RC=0  timeout panics: 0  RESULT: PASS (exit=0)
nodetests  NIXBUILD_RC=0  timeout panics: 0  RESULT: PASS (exit=0)

This is the tier Tekton gates on, and it had never been run on this branch before now — #773 is the precedent where four consecutive dev-host greens preceded a red sandbox run, so the two are not interchangeable.

Base named, per the rule that a gate result is a claim about one tier, one tree, one base: the merged tree is this branch merged with origin/main at efa9a0fc. main moved 5 commits during the audit ladder; I merged rather than rebased so the audit-claims sha ranges in the comments above still resolve.

Squash verified by CONTENT, not ancestry (a squash is never an ancestor of its base): all five files present in origin/main, the two payload files byte-identical to the branch head, and the round-5 SWITCH_ATTEMPTED guard present — so it is the final revision that landed, not an earlier one.

Still open, deliberately

  • The shell half has no automated coverage. Five rounds of trap-message fixes rest on reading; the auditor's throwaway harness caught a branch-ordering hazard in seconds and does not exist in the repo. This is why that class kept recurring.
  • NOT IN EFFECT: breaks the label column alignment.
  • On the verify-failure path the trap deletes $MERGED before the post-loop cat reaches it.
  • The laptop is not migrated. It carries the one-line extraConfig form on a 26.11pre channel and will hit the same assertion on its next rebuild. The rewriter handles that form and is unit-tested for it, but has never been run on that host — different claims.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant