chore(nix/system): commit two staged ops scripts that were sitting untracked in the workbench tree - #1412
Conversation
…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
Audit round 1 — 2 🔴, 4 🟡, 4 🟢 · all fixed in
|
| 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
Audit round 2 — 1 🔴, 4 🟡, 6 🟢 · all fixed in
|
…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
Audit rounds 3-5 · ladder STOPPED at round 5Round 3 — 🔴
|
| 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
Gate verdict — both tiers PASS on the merged tree
|
Two scripts existed only in the workbench working tree, where a stray
git checkoutor 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.shnixos-26.11 removed
services.journald.extraConfig(mkRemovedOptionModuleinnixos/modules/system/boot/systemd/journald.nix), sonixos-rebuild switchdied on a failed assertion after anix-channel --update. The script rewrites the block toservices.journald.settings.Journal, and:extraConfigblock, and unless every line inside parses as ajournald.conf(5)keynix-instantiate --parse+nixos-rebuild dry-build, restoring the backup if either fails/etc/systemd/journald.confandjournalctl --disk-usagerather than asserting successAlready applied to the workbench. Verified live, not inferred:
/etc/systemd/journald.conf→[Journal]/Audit=keep/SystemMaxUse=2G(Audit=keepis 26.11's new explicit default)/run/current-system→nixos-system-nixos-26.11pre1068949.dc5d91f84032Verified before it was run, against a copy — no writes to
/etc/nixos: the rewrite parses,config.system.build.toplevelevaluates (assertion gone), and buildingconfig.environment.etc."systemd/journald.conf"showed theSystemMaxUse=2Gline survives the migration.scripts/diagnose-nix-disk.shThe 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 +L1for deleted-but-held files,/nix/store/.links, swapfile, reserved blocks. Sibling to the existingscripts/diagnose-disk-accounting.sh; no overlap withscripts/cleanup-disk.sh.Deliberately NOT in this PR
nix/system/apply-nebula-relay.shandcheck-nebula-relays.shwere also untracked locally, but those copies are stale orphans — byte-identical toe7f2e45a, an intermediate commit of the branch that merged as #1272.mainalready carries strictly further-along versions (480 vs 205 lines, and 360 vs 317), including a🔴 THIS RESTARTS THE MESHwarning my copies predate. Committing them would have reverted ~318 lines of that work.Gate
mainmoved from4f49f5dcto03d7e0admid-run and this branch is rebased onto the latter, so the gate is being re-run on that merged tree; result posted below.