Add the microvm credential delivery plan and review prompt #32

Merged
vnprc merged 12 commits from agent/microvm-credential-delivery-plan into master 2026-08-12 19:49:02 +01:00
Member

Adds the dev plan and iterative review prompt for the third slice of milestone 4 in the microvm.nix adoption arc: moving a selected microvm guest's private key material off agenix and durable paths and onto per-boot delivery from host memory.

The slice implements parent contracts 7 (closed credential names and paths), the guest half of 8a (allod.microvm.guestCredentialRoot), 9 (one runtime plaintext root), 10 (a materializer boot service, not the current activation script), 11 (the SSH host key cannot auto-regenerate), 12 (no age in a microvm guest) and 14 (persistence and secrets never overlap). It excludes contract 8b's extendModules host integration, contract 15's networking, and the nested-boot tests.

What the plan settles

  • A closed, derived credential-name set of four names, three of them conditional on exactly the values that already gate today's age.secrets, so allod/archetypes#17's Forge opt-out keeps meaning one thing.
  • A consumer table with explicit owner and mode per file, replacing the current three-netrc fan-out with one.
  • services.openssh.hostKeys = [] plus an extraConfig HostKey, because pinned nixpkgs renders HostKey lines solely from cfg.hostKeys and gives sshd-keygen an empty script when that list is empty — pointing hostKeys at the runtime path instead would leave sshd-keygen armed to mint a replacement identity, which is the exact failure contract 11 forbids.
  • Twenty-six acceptance tests, of which eleven read generated artifacts — the rendered materializer unit, sshd_config, sshd-keygen.service, nix.conf, gitconfig, the Home Manager result and the guest closure — rather than option values.

Contract contradictions recorded

Two, both recorded in the plan rather than worked around: the guest option namespace (allod.microvm.guestCredentialRoot per contract 8a versus allod.archetypes.microvm.volumeImageRoot as the volumes slice landed it), and contract 9 forbidding /etc on a guest whose /etc is already tmpfs.

Risk

R0 for this PR: it adds two Markdown files to allod/strategy and changes no code.

The plan it describes is R3 High and says so, and says the score does not come down with its own validation because nothing in that slice boots a guest. Every claim about credential receipt, materializer ordering and sshd startup is an inference from pinned upstream source. The plan carries an explicit gate: no machine is enabled on the microvm runtime on the strength of that change.

Validation

Plan conforms to dev-plans.md: Tracking Issue, Goal, Scope, Risk Assessment, Interface Contracts, Agent Gates, Acceptance Tests, Rollback Plan all present; one PR, so per-PR risk does not apply.

Empirical claims in the plan were measured against the pinned sources before writing, not asserted: the microvm.nix and nixpkgs store paths were confirmed by comparing nix hash path against the flake.lock narHashes; QEMU's FW_CFG_MAX_FILE_PATH arithmetic, sshd's HostKey and sshd-keygen behaviour with an empty hostKeys, agenix's mkIf (cfg.secrets != {}) wrapper and its identityPaths default, and systemd's fw_cfg import path and per-unit credential modes were each read from source.

The review prompt carries a Review Evidence table and is set up for rotating passes to convergence, which this arc's parent plan required and which the slice's risk level warrants. Pass 1 is scheduled cross-vendor because the plan was authored by claude-opus-5.

Refs allod/strategy#20
Refs allod/archetypes#29

Adds the dev plan and iterative review prompt for the third slice of milestone 4 in the microvm.nix adoption arc: moving a selected microvm guest's private key material off agenix and durable paths and onto per-boot delivery from host memory. The slice implements parent contracts 7 (closed credential names and paths), the guest half of 8a (`allod.microvm.guestCredentialRoot`), 9 (one runtime plaintext root), 10 (a materializer boot service, not the current activation script), 11 (the SSH host key cannot auto-regenerate), 12 (no age in a microvm guest) and 14 (persistence and secrets never overlap). It excludes contract 8b's `extendModules` host integration, contract 15's networking, and the nested-boot tests. ### What the plan settles - A closed, derived credential-name set of four names, three of them conditional on exactly the values that already gate today's `age.secrets`, so `allod/archetypes#17`'s Forge opt-out keeps meaning one thing. - A consumer table with explicit owner and mode per file, replacing the current three-netrc fan-out with one. - `services.openssh.hostKeys = []` plus an `extraConfig` `HostKey`, because pinned nixpkgs renders `HostKey` lines solely from `cfg.hostKeys` and gives `sshd-keygen` an empty script when that list is empty — pointing `hostKeys` at the runtime path instead would leave `sshd-keygen` armed to mint a replacement identity, which is the exact failure contract 11 forbids. - Twenty-six acceptance tests, of which eleven read generated artifacts — the rendered materializer unit, `sshd_config`, `sshd-keygen.service`, `nix.conf`, `gitconfig`, the Home Manager result and the guest closure — rather than option values. ### Contract contradictions recorded Two, both recorded in the plan rather than worked around: the guest option namespace (`allod.microvm.guestCredentialRoot` per contract 8a versus `allod.archetypes.microvm.volumeImageRoot` as the volumes slice landed it), and contract 9 forbidding `/etc` on a guest whose `/etc` is already tmpfs. ## Risk R0 for this PR: it adds two Markdown files to `allod/strategy` and changes no code. The plan it describes is R3 High and says so, and says the score does not come down with its own validation because nothing in that slice boots a guest. Every claim about credential receipt, materializer ordering and sshd startup is an inference from pinned upstream source. The plan carries an explicit gate: no machine is enabled on the microvm runtime on the strength of that change. ## Validation Plan conforms to `dev-plans.md`: Tracking Issue, Goal, Scope, Risk Assessment, Interface Contracts, Agent Gates, Acceptance Tests, Rollback Plan all present; one PR, so per-PR risk does not apply. Empirical claims in the plan were measured against the pinned sources before writing, not asserted: the microvm.nix and nixpkgs store paths were confirmed by comparing `nix hash path` against the `flake.lock` narHashes; QEMU's `FW_CFG_MAX_FILE_PATH` arithmetic, sshd's `HostKey` and `sshd-keygen` behaviour with an empty `hostKeys`, agenix's `mkIf (cfg.secrets != {})` wrapper and its `identityPaths` default, and systemd's fw_cfg import path and per-unit credential modes were each read from source. The review prompt carries a Review Evidence table and is set up for rotating passes to convergence, which this arc's parent plan required and which the slice's risk level warrants. Pass 1 is scheduled cross-vendor because the plan was authored by `claude-opus-5`. Refs allod/strategy#20 Refs allod/archetypes#29
Replaces four claims that were inferences with measured behaviour from the
pinned nixpkgs, systemd 258.7 and OpenSSH 10.3p1 sources.

- sshd with an empty hostKeys list falls back to compiled-in host key paths,
  so an explicit HostKey directive is mandatory rather than convenient, and
  the pinned build-time sshd -G -T check exits 0 on that state.
- An empty hostKeys list also leaves sshd-keygen with no ExecStart, which
  systemd refuses to load; mask the unit instead.
- A bare LoadCredential is non-fatal on a missing credential; the absolute
  /run/credentials/@system source form fails the unit, so it is the first
  fail-closed gate.
- install(1) truncates in place and is not an atomic replace.

Also corrects why contract 5 matters: qemu_fw_cfg must be in the initrd
kernel modules for closure inclusion, because systemd's own kmod_setup loads
it before import_credentials, not because of systemd-modules-load ordering.
Ordering moves to Before=sysinit.target with DefaultDependencies=no, which
precedes every consumer instead of an enumerated list that goes stale.
Preserved after an agent session limit interrupted the pass.

Refs allod/strategy#20
The half-applied fold left the plan less coherent than either endpoint.
Its Scope and Risk Assessment announced two materializer services and a
two-file Git design, while the Interface Contracts and every acceptance
test still described one service and one gitconfig.

Keep the half that is complete. The contract 1 pathErrors split, the
union-of-names rule, the comma rule on credentialFiles values, and the
note about a value rule with no reachable fixture are all self-consistent
and stay.

Revert the half that is only announced. Rewriting the contracts and the
acceptance tests to match would be authoring a security-boundary design
under the guise of reconciling one, and the reasoning that motivated it
was not recorded.

Carry both design changes forward as questions the first pass must
answer, in Focus Areas 2 and 3, so the direction is not lost by being
reverted. Record in the review prompt that no pass has run, that all
three existing commits are the author own unreviewed work, and that the
measured claims in the second one are themselves prime review targets.

Refs allod/strategy#20
All eleven findings applied. The four blockers change the design, so Scope,
Risk Assessment, the numbered Interface Contracts and the numbered Acceptance
Tests were each walked through for every one of them.

Two materializer units, not one (finding 2). A single unit made an optional
Forge credential able to stop the SSH host key from being published, which
strands the guest with sshd failed. Contract 6 now specifies
allod-microvm-host-key.service (always composed, owns ssh-host-key and the
root directory, is what sshd requires) and allod-microvm-forge-credentials.service
(composed only when Forge names exist, owns everything else, required by
nothing). Tests 13 and 14 assert the disjoint LoadCredential sets, the absent
Forge unit on privacy and no-Forge fixtures, that no unit anywhere requires
the Forge unit, and that a fixture merging or swapping the two sets fails.

The Forge opt-out fixture was asserting an impossible machine (finding 1).
withTokens changes only the two token files while inventory keeps
allod-dev.forge_key = "allod_vm", so the opted-out shape is
[ forge-ssh-key ssh-host-key ], not [ ssh-host-key ]. Contract 2 now states
the two gates are independent and mkDevVm takes forgeKey ? machines.<name>.forge_key
on the same pattern as runtime, tokenFile and httpsTokenFile. Test 2 is three
fixtures covering both gates separately and together.

The user's Git helper still pointed at persistent home (finding 3). Contract 4
gains the helper-reset design for both /etc/gitconfig and the user's own
git/config, quoting git 2.51.2 gitcredentials.adoc:199-200 on the empty-string
reset, and modules/home-shared.nix takes a credentialRoot argument. Test 19
reads both files, each in its own rendering.

Reactivation ordering was incomplete (finding 4). requiredBy creates the pull
with no ordering; both units now carry sysinit-reactivation.target in before
as well, matching userborn.nix, and test 13 asserts both edge kinds.

Findings not applied as written: none. Two were applied differently from the
letter of the finding, both recorded in the plan:

- Finding 6 asked the plan to either define a real normalization operation or
  correct the parent contract. The parent plan converged at its own review pass
  10 and is not this slice's to edit, so the empirical result is recorded as a
  third entry under "Contract contradictions found" -- neither microvm.nix nor
  systemd normalizes these names and Nix attribute keys are already unique --
  with what it implies for parent contract 7, and the impossible collision
  fixture is deleted from test 7.
- Finding 10 asked for either identifier-based citations or a refresh of every
  line number. Both, split by source: pinned store paths keep line numbers
  because the revision is pinned; every allod/archetypes flake.nix citation is
  now an identifier plus a quoted construct, with a standing note at the top of
  Interface Contracts saying why. Remaining working-tree citations
  (allod/nexus, allod/profiles, modules/netrc.nix) were each verified accurate
  against the tree.

Findings 5, 7, 8, 9 and 11 applied as written: mkAfter with a last-occurrence
assertion and an injected unrelated extraOptions line as the preservation
control, plus why nix.settings.netrc-file cannot substitute; the impossible
relative-value sabotage deleted with upstream's type error named as the
enforcement evidence; synthetic AGE-SECRET-KEY-1 and OpenSSH-header canaries so
each closure detector has its own positive control; the nexus launcher
described as the same atomic-publish principle rather than the same
implementation; and the fixture plan rebuilt around exhaustive pure-validator
tables with one wiring sabotage per assertion family.

Two things found while folding that pass 1 did not raise, both now in the plan:

- The two Git config files render the same setting differently. environment.etc
  gitconfig text is raw, so it renders helper = store --file=... unquoted, while
  Home Manager goes through lib.generators.toGitINI whose mkValueString quotes
  every string, rendering helper = "store --file=..." and helper = "". A needle
  built for one file silently does not match the other.
- nixpkgs defines systemd.services.sshd under mkIf (!startWhenNeeded), so an
  unconditional systemd.services.sshd.requires would materialize an sshd.service
  unit in the socket-activated case where nixpkgs renders none. The sshd wiring
  is gated the same way sshd's own module gates itself.

Refs allod/archetypes#29
Refs allod/strategy#20
Review pass 1 has run, so the prompt's claim that no pass had run is retired
and the evidence table has its first real row. Three questions the Focus Areas
carried as open are closed by pass 1's answers; the questions that replace them
are verification of the fixes, because a design changed by a review has not
been reviewed.

Pass: 1
Model: gpt-5.6-sol
Reasoning effort: reported as xhigh by the runner, invoked with
model_reasoning_effort=high. The reported value is what is recorded, with the
discrepancy attached: dev-plans.md warns that an unsupported level can fall
back silently rather than erroring, so a reported level is evidence of what
applied and an invoked level is not.
Reviewed commit: e7530df, full pass, read-only.
Fixes: 91ec55e.

Findings new to this pass: 11 -- 4 BLOCKER, 6 GAP, 1 SIMPLIFY.

1. [BLOCKER] Forge opt-out shape contradicts the name derivation. The plan's
   test asserted that nulling both token files yields [ ssh-host-key ], but
   withTokens does not touch forge_key and inventory keeps allod-dev.forge_key
   set, so the real shape keeps forge-ssh-key. Origin: original-plan defect.
   Repair commit: 91ec55e.
2. [BLOCKER] One materializer lets an optional Forge failure disable repair
   SSH. Per-credential validation is fatal to the unit and sshd requires that
   unit, so a bad Forge credential stopped the host key from being published.
   Origin: original-plan defect. Repair commit: 91ec55e.
3. [BLOCKER] The user's Git helper still pointed at persistent home. Rewiring
   /etc/gitconfig alone leaves home-shared.nix's higher-precedence
   credential.helper = store reading and writing plaintext outside the runtime
   root. Origin: original-plan defect. Repair commit: 91ec55e.
4. [BLOCKER] Reactivation ordering was incomplete. requiredBy on
   sysinit-reactivation.target creates the pull but no ordering, so a rebuild
   could reactivate a consumer before the materializer rewrote its file.
   Origin: introduced by 9e82896. Repair commit: 91ec55e.
5. [GAP] mkForce on nix.extraOptions cannot be defended by counting netrc-file
   lines, because anything mkForce discards is absent from the artifact the
   check reads. Origin: original-plan defect. Repair commit: 91ec55e.
6. [GAP] The normalized-name contract disappeared while its impossible texture
   remained: nothing on this path normalizes credential names, so the demanded
   collision case cannot be constructed. Origin: introduced by 94c5ad6.
   Repair commit: 91ec55e.
7. [GAP] A sabotage was mandated that the plan itself proves cannot exist --
   a relative microvm.credentialFiles value is rejected by types.path before
   any assertion runs. Origin: original-plan defect. Repair commit: 91ec55e.
8. [GAP] The closure positive control validated only the .age filename
   detector, leaving both content detectors deletable with the check still
   green. Origin: original-plan defect. Repair commit: 91ec55e.
9. [GAP] The claimed atomic-write precedent is not the shape cited: the nexus
   launcher stages a whole directory and publishes it once, not a per-file
   temporary and rename. Origin: introduced by 9e82896. Repair commit: 91ec55e.
10. [GAP] Several code citations pointed at unrelated constructs after
    archetypes/flake.nix grew past 2000 lines. Origin: original-plan defect.
    Repair commit: 91ec55e.
11. [SIMPLIFY] The fixture plan reacted to memory pressure only after adding
    more than twenty full-system sabotages, and its stated remedy targeted the
    cheap half of the cost. Origin: original-plan defect. Repair commit:
    91ec55e.

SIMPLIFY sweep considered: one versus two materializer units; systemd-tmpfiles
instead of scripts; a cross-repo nix.settings change in allod/vm; shared
path and name validators; redundant consumer copies of the same credential;
generated-artifact mutation fixtures; and the full-system sabotage count. It
kept two units and the custom atomic materialization, rejected tmpfiles and
the cross-repo change, and replaced most repeated full NixOS fixtures with pure
validator tables plus one minimal wiring sabotage per assertion family.

Two defects were found while folding that pass 1 did not raise, both now in the
plan and in the prompt's list of unverified empirical claims: the two Git
config files render the same setting differently, since toGitINI quotes every
string while environment.etc text does not, so one needle cannot serve both;
and nixpkgs defines systemd.services.sshd under mkIf (!startWhenNeeded), so
unconditional requires wiring would materialize a unit nixpkgs renders none of.

Next pass: full pass over 91ec55e and this commit, since four blockers changed
the design rather than patching it and a scoped diff would miss the coherence
question. Recommended model claude-fable-5 at xhigh. claude-opus-5 authored
both the plan and the fixes and gpt-5.6-sol ran pass 1, so neither is the
rotation partner. This spends the cross-vendor rotation for one pass; take it
back at pass 3, most naturally with gpt-5.6-terra, or immediately at pass 2's
end if pass 2 finds little.

Model: claude-opus-5

Refs allod/archetypes#29
Refs allod/strategy#20
Contract 4 named the netrc consumer but left the mechanism that points Nix at
it in acceptance test 18 alone. That is the same contract-and-test split that
made the earlier interrupted fold incoherent, so the design decision now sits
beside the Git helper decision it mirrors: append with lib.mkAfter, never
mkForce a types.lines option, and let test 18 carry the reasoning.

Refs allod/archetypes#29
The next-pass line named only the fix commit, which went stale as soon as
90827b3 landed. It now names the range so pass 2 reads every commit since the
one pass 1 reviewed.

Refs allod/strategy#20
All eleven findings applied, none dropped. Pass 2 returned no blocker, so no
design changes here. What changes: three wrong empirical claims corrected, two
assertions that could not fail given a sabotage, two fixtures that could not be
written given a route, one contract deviation recorded, and one duplicate
fixture merged away. Scope, Risk Assessment, the numbered Interface Contracts
and the numbered Acceptance Tests were walked through for each.

Nothing asserted that the Forge unit ever starts (finding 2). By design nothing
requires it, so wantedBy = [ "sysinit.target" ] is its only boot-time start
path; deleting that line yields a guest that is not failed, has an empty
systemctl --failed, and is silently missing every Forge credential. Test 13
also read the reactivation edge off an artifact that never carries it: NixOS
renders wantedBy and requiredBy as .wants and .requires symlinks (pinned
nixos/lib/systemd-lib.nix), so sysinit-reactivation.target's fragment never
gains a Requires= line and the old assertion failed on a correct
implementation. Test 13 now asserts four symlinks under
environment.etc."systemd/system".source, paired with a sabotage that mkForces
the Forge unit's wantedBy empty and must lose exactly that one needle while
every fragment needle stays green. Contract 6 carries the mechanism; Scope and
Risk-Assessment scrutiny item 1 name the symlinks.

What nixpkgs gates around sshd was described wrongly (finding 1). Measured:
services.sshd under mkIf (!startWhenNeeded) and sockets.sshd under
mkIf startWhenNeeded are as claimed, but services."sshd@" is defined
unconditionally inside mkIf cfg.enable and is rendered on both branches. The
!startWhenNeeded gate stays, because an unconditional sshd.requires would still
materialize an ExecStart-less sshd.service on the socket branch; only the text
changes. The sshd@ edge needs no branch gate, and contract 6 and test 14 now
both forbid any fixture asserting an sshd@ attribute is absent on the default
branch, because it is present there.

The netrc row named readers nothing points at it (finding 3). Its consumer
column becomes the Nix daemon and any nix client's curl-based fetches via
nix.conf netrc-file; git's readers stay on the git-credentials row. The "one
netrc, not three" note now rests on a measured mechanism rather than the
libgit2 story: Nix 2.31.5 shells out to the git binary for git+https network
operations -- a PATH shim recorded git ls-remote --symref, then git -C <cache>
--git-dir . fetch, then symbolic-ref -- so those fetches resolve credentials
through git's own helper chain, which is exactly what contract 4 rewires. The
note states its own limit: it does not claim ~/.netrc has stopped working,
since git's HTTP transport may still consult it through curl and that was not
tested. It claims only that a user netrc is unnecessary here.

Two promised fixtures had no route and now have one (finding 5). The contract 8
githubCredentialTargets refusal is proved by a fixture that injects a target,
but the module derives its own targets from the flake's secrets input and the
public template pins that empty. mkDevVm gains a defaulted
githubCredentialTargets on the exact forgeKey pattern, threaded through
mkGithubCredentialModule to modules/github-credentials.nix as a targets
argument and to the assertion, so both read one value. Scope and the Rollback
Plan record the second edited module and the third defaulted argument. Test 2's
three fixtures read config.allod.microvm.credentialNames, which exists only
under microvm, so they are built from runtime-module-selection's own devFixture
-- which already forwards runtime and identity to mkDevVm -- rather than from
dev-forge-opt-out's withTokens, which passes no runtime and lives in another
check's let. Test 25 records that dev-forge-opt-out is now unchanged.

The persistent-mount scanner had no way to fail (finding 4). Test 9 sabotages
the contract-14 assertion and dies at evaluation, so the artifact scanner never
runs against it. Test 23 gains one sabotage riding the unit-mutation fixture
that already exists: a script line copying a credential under the dev home
mount, which the scanner must name.

The split's deviation from parent contract 10 is now recorded (finding 8), as a
fourth entry under "Contract contradictions found". A broken Forge credential
fails its materializer and, by design, no consumer unit -- the parent contract's
last sentence read strictly. The parent plan is not edited; the reconciliation
goes through its own process, and one reading (contract 10 says "required", and
no Forge credential is required to boot) avoids the deviation entirely.

Applied as written, briefly: the appended /etc/gitconfig block opens its own
[credential] header so a future third definition cannot re-home the reset onto
another section, and test 19 asserts it by section rather than by presence
(finding 9); test 18's preservation control must be a setting the pinned Nix
recognizes, since pkgs.formats.nixConf runs nix config show in checkPhase and
promotes warnings to errors, so a synthetic name would fail the build of
nix.conf itself (finding 6); test 20 reads the rendered ~/.ssh/config text
rather than the programs.ssh option, since it sits in a section whose first
sentence bans option reads (finding 7), and its sabotage needle tolerates both
realizable shapes, since
demoting the mkForce collides with profiles' plain definition rather than
losing to it; and test 5's moved-credential-root fixture merges into the
moved-image-root fixture the check already forces (finding 11).

Applied differently from the letter, both recorded:

- Finding 3 also asks for a matching workspace memory edit to nix.md. That is
  not a plan change and is not in this commit; it is being handled separately.
  The plan carries the measured mechanism and its limits.
- Finding 10 offered two routes for the drift-prone citations. Taken: convert
  them, not narrow the policy. Every allod/nexus, allod/profiles and
  archetypes modules/ citation is now identifier plus quoted construct, so the
  policy sentence holds without exceptions -- which matters most for the nexus
  ones, since that module and launcher move under nexus issues 21 and 22 in
  this same arc.

Three things found while folding that pass 2 did not raise, all now in the plan:

- An unconditional sshd@ edge is only safe while sshd is enabled. All three
  edges sit under the same config.services.openssh.enable condition nixpkgs'
  own definitions use, or a guest with sshd disabled gets the stray
  ExecStart-less unit the branch gating exists to avoid.
- Test 19's new section-anchored assertion has no fixture and the plan says so
  rather than implying a pinned negative exists: reaching its failure needs a
  third gitconfig definition, which is another forced dev system for a header
  the implementation always emits.
- The two new sabotage fixtures cannot share the loaded-up unit-mutation
  fixture -- one needs every other needle green, the other needs evaluation to
  reach the rendered artifacts -- so they cost two forced systems where test
  5's merge gives one back. The memory budget records the net.

Refs allod/archetypes#29
Refs allod/strategy#20
Pass 2 found no blocker, so the prompt no longer treats the design as
unverified; it treats the newest text as unverified instead. The nine focus
areas become seven: everything pass 2 measured and named correct moves into a
new "Settled by pass 2" list, and what replaces it is the fold's own new
material -- the pull symlinks, the two threading routes, the [credential]
header, the measured netrc mechanism, and the modes-versus-consumers question
neither pass has touched. The evidence table gains its second row.

Stopping rule: not met, and not started. Pass 2 had no blocker but did have
original-plan gaps, so it is not the first of the two clean passes, and
review-introduced findings have outnumbered original-plan ones for one pass
only.

Pass: 2
Model: claude-fable-5
Reasoning effort: unconfirmed. The reviewer reported it could not introspect
the applied level. dev-plans.md warns that an unsupported level can fall back
silently rather than erroring, so an invoked level is not an observation and
nothing is recorded in its place.
Reviewed commit: e5cc878, full pass over e7530df..e5cc878, read-only.
Repair commit: 8bab682.

Findings new to this pass: 11 -- 0 BLOCKER, 10 GAP, 1 SIMPLIFY.

1. [GAP] systemd.services."sshd@" is not gated the way the plan claimed. It is
   defined unconditionally inside mkIf cfg.enable and rendered on both
   branches, so only sshd.service and sshd.socket are branch-gated; the design
   conclusion is unchanged and the !startWhenNeeded gate stays. Origin:
   introduced by 91ec55e. Repair commit: 8bab682.
2. [GAP] Test 13 read the reactivation edge off an artifact that never carries
   it, and nothing at all asserted the Forge unit's boot-time pull -- wantedBy
   and requiredBy render as symlinks, and losing the wantedBy line yields a
   guest with no failed unit and every Forge credential absent. Origin:
   wrong-artifact facet introduced by 91ec55e, missing-pull facet original-plan.
   Repair commit: 8bab682.
3. [GAP] The netrc row named readers nothing points at it, and "one netrc, not
   three" rested on a libgit2 story the pinned Nix does not match: Nix 2.31.5
   shells out to the git binary for git+https, so those fetches use git's own
   helper chain. Origin: original-plan defect. Repair commit: 8bab682.
4. [GAP] Test 23's persistent-mount scanner had no paired sabotage, and test 9
   cannot serve as one because it dies at evaluation before the artifacts are
   rendered. Origin: original-plan defect. Repair commit: 8bab682.
5. [GAP] Two promised fixtures could not be built from the described inputs:
   the githubCredentialTargets injection had no route past the module's own
   secrets lookup, and test 2's fixtures read a microvm-only option through a
   helper that passes no runtime. Origin: route facet original-plan, withTokens
   facet introduced by 91ec55e. Repair commit: 8bab682.
6. [GAP] Test 18's preservation control would have failed the build of nix.conf
   itself, because pkgs.formats.nixConf's checkPhase promotes an unknown-setting
   warning into an error. Origin: introduced by 91ec55e. Repair commit: 8bab682.
7. [GAP] Test 20 was an option read in a section whose first sentence bans
   option reads, and its sabotage phrasing described only one of the two shapes
   an implementer could write. Origin: original-plan defect. Repair commit:
   8bab682.
8. [GAP] A fourth contract contradiction was unrecorded: under the split a
   broken Forge credential fails its materializer and, by design, no consumer
   unit, which is parent contract 10's last sentence read strictly. Origin:
   consequence of 91ec55e's split. Repair commit: 8bab682.
9. [GAP] The appended /etc/gitconfig block opened no section of its own, so any
   third definition landing between the two blocks would silently re-home the
   helper reset onto another section while every needle stayed green. Origin:
   introduced by 91ec55e. Repair commit: 8bab682.
10. [GAP] The citation policy claimed every working-tree citation was in
    identifier form while five kept line numbers, including the nexus ones that
    move in this same arc. Origin: introduced by 91ec55e. Repair commit:
    8bab682.
11. [SIMPLIFY] Test 5 forced a second dev system for a non-default credential
    root when the check already forces one for a non-default image root, in a
    check whose own comment records that a forced fixture is not free. Origin:
    original-plan structure. Repair commit: 8bab682.

SIMPLIFY sweep considered: the second moved-root dev fixture, which was cut;
and, kept with reasons recorded, the name-rules-on-union redundancy that is
what gives those rules a reachable sabotage, the guest-side comma rule on
credentialFiles values, the per-batch memory measurement rule, the two
synthetic closure canaries plus the libvirt traversal control, the
systemd-tmpfiles rejection, the explicit before = [ "sshd.service" ] edge, and
the contract-4/test-18 reasoning split from 90827b3.

Next pass: full pass over e5cc878..HEAD -- 8bab682 and this commit -- with
recommended model gpt-5.6-terra, at xhigh or above, with the applied level
confirmed before it is recorded. It is the cross-vendor rotation dev-plans.md
calls strongest and the only roster entry both fresh to this feature and not
excluded: claude-opus-5 authored the plan and both folds, gpt-5.6-sol ran pass
1, claude-fable-5 ran pass 2. Full pass rather than a scoped diff, because
pass 2's yield was original-plan gaps plus fix-commit defects rather than a
thin pass, so the coherence question is live across the whole plan.

Refs allod/archetypes#29
Refs allod/strategy#20
Two blockers, both original-plan defects that passes 1 and 2 missed.

The shared Git credential file was wired to a helper that writes it. Pinned
git 2.51.2 documents credential-store's --file= as lookup and store, and it
resets the file's permissions to exclude other users. Git calls that write
path on its own through 'git credential approve' after a successful
authentication, so a root fetch could rewrite <root>/git-credentials as
root-private and strand the unprivileged user with nothing failing or
logging, while the user's own write could not take its lock file inside the
root-owned 0711 directory. Contract 4 now specifies a generated read-only
helper that delegates only 'get' and drains store/erase, both Git config
files name it by absolute store path, and test 19 runs it: fill returns the
credential, approve and reject leave bytes and mode unchanged, and the stock
store helper is the negative control that must change the mode.

The plan never asserted the parent's no-swap boundary. Materialized
credentials land in ordinary /run, which pinned systemd's mount-setup.c does
not mount noswap, unlike mount_credentials_fs() which requests it
deliberately for systemd's own credential filesystem. New contract 11
asserts swapDevices == [] and !zramSwap.enable, test 10 carries the
sabotage, and the /proc/swaps proof is handed to the nested-boot slice.

Findings summary: 2 BLOCKER, 9 GAP, 1 SIMPLIFY. All twelve applied.

1. [BLOCKER] Shared Git credential file wired to a mutating helper.
   git-credential-store both reads and writes and resets permissions, so a
   root fetch can strand the user. Original-plan defect. Fixed here.
2. [BLOCKER] No swap assertion. Parent contract 8a forbids a guest swap
   device and nothing in the plan asserted it, leaving materialized
   plaintext pageable. Original-plan defect. Fixed here.
3. [GAP] Fifth parent-contract contradiction missing. The plan refuses the
   registered-GitHub-target path contract 9 requires while claiming to
   implement 9. Original-plan defect. Fixed here: contradiction 5 recorded
   with the 8b follow-up named, and the Tracking Issue claim narrowed to
   'implements 9 and 10 in part'.
4. [GAP] Persistence scanner did not cover contract 14's surface. It read
   seven named artifacts, so a copy in an unrelated activation script or
   unit passed. Original-plan defect. Fixed here: test 23 enumerates every
   activation script, unit fragment, unit script and Home Manager artifact,
   filters to those naming a credential, and sabotages an unrelated
   activation script.
5. [GAP] Acceptance never pinned the owner/mode table. Test 15 checked
   mv -T but not the install -o/-g/-m arguments, missing the plan's own
   highest-risk failure. Original-plan defect. Fixed here, with a
   wrong-mode and wrong-owner mutation on the shared fixture.
6. [GAP] The promised libvirt scoping was not implemented. Scope claimed it
   while tests 24 to 26 left dev-forge-opt-out inheriting inventory's
   runtime and credential-profiles filtering on membership rather than
   runtime. Original-plan defect. Fixed here: three concrete filters and an
   explicit runtime on the fixture.
7. [GAP] The wantedBy rendering statement was false. nixpkgs'
   commonUnitText does emit [Install] WantedBy=. Introduced by 8bab682.
   Fixed here: wantedBy renders as both metadata and a .wants symlink,
   requiredBy only as .requires, and the symlink is what activates an
   immutable NixOS unit.
8. [GAP] 'Nothing requires Forge' contradicted the planned graph, which
   asks for requiredBy = sysinit-reactivation.target. Introduced by
   91ec55e. Fixed here: the claim is narrowed to consumers, and test 14
   asserts the inbound-edge set is exactly the boot .wants and reactivation
   .requires edges.
9. [GAP] The test-13 relocation sabotage could not fail the invariants it
   named. A true relocation leaves the two sets disjoint and preserves
   their union. Introduced by 91ec55e. Fixed here by reassignment, not
   rewording: relocation pins the per-unit set assertions, duplication pins
   disjointness, removal pins the union, and one shared-fixture mutation
   carries all three. The sabotage preamble gains the rule that closes the
   class, since this shape has now been found five times in this arc and
   never by a test.
10. [GAP] The section-anchored Git assertion lacked a canary unnecessarily.
    Introduced by 8bab682. Fixed here: the nearest-section predicate runs
    over synthetic text with an intervening [core] section, no NixOS system
    needed.
11. [GAP] The socket-activated and disabled sshd branches were untested.
    Introduced by 91ec55e. Fixed here: test 14 builds startWhenNeeded and
    enable = false on the privacy builder, because what is under test is
    where Allod put its three edges, not nixpkgs' branching.
12. [SIMPLIFY] Test 23 did not need another forced system. Introduced by
    8bab682. Fixed here: the memory budget carries a per-mutation table and
    the net forced-system change is zero, not plus one.

SIMPLIFY sweep: reconsidered recombining the two materializers, tmpfiles
versus units, shared validators and mutation fixtures, the defaulted builder
arguments, test-2 helper reuse, fixture merging, and per-principal Git
copies versus one shared read-only helper. The two-unit design and the
defaulted arguments remain justified. One cut: the standalone test-23 thunk.
The read-only helper is the cheaper of the two shapes blocker 1 admits, one
generated script against per-principal copies of the same secret.

Nothing found while folding that pass 3 missed, beyond one consequence of
its own fix: the correct helper is now a store path, so a bare 'store'
substring needle would match /nix/store and report a correct configuration
as broken. Test 19 records it.

Model: claude-opus-5
Pass 3 changes what the review record means. Pass 2 returned no blockers and
that read as evidence the design was right; pass 3, a different vendor, then
found two blockers that had been in the plan since its first draft and that
two frontier models had both read past. A clean pass from one model is
evidence about that model's coverage, not about the plan.

Stopping-rule state, plainly: the consecutive-clean-pass count remains zero.
Pass 3 had two blockers and six original-plan gaps. The other stopping
condition also reset rather than advancing: pass 3 split six original-plan to
six review-introduced, and six does not outnumber six.

Findings summary: 2 BLOCKER, 9 GAP, 1 SIMPLIFY. All twelve applied in
77a6cea; none dropped and none deferred.

1. [BLOCKER] Shared Git credential file wired to a mutating helper.
   git-credential-store both reads and writes and resets permissions to
   exclude other users, so a root fetch can strand the unprivileged user.
   Origin: original-plan. Repair commit: 77a6cea.
2. [BLOCKER] No swap assertion. Parent contract 8a forbids a guest swap
   device; nothing asserted it, and ordinary /run is not mounted noswap.
   Origin: original-plan. Repair commit: 77a6cea.
3. [GAP] Fifth parent-contract contradiction missing, and the tracking claim
   to implement contract 9 was wrong while the plan refuses the registered
   GitHub target path. Origin: original-plan. Repair commit: 77a6cea.
4. [GAP] Persistence scanner did not cover contract 14's surface, reading
   seven named artifacts instead of every generated one. Origin:
   original-plan. Repair commit: 77a6cea.
5. [GAP] Acceptance never pinned the owner/mode table, missing the plan's
   own highest-risk failure. Origin: original-plan. Repair commit: 77a6cea.
6. [GAP] The promised libvirt scoping was never implemented in tests 24 to
   26. Origin: original-plan. Repair commit: 77a6cea.
7. [GAP] The wantedBy rendering statement was false; nixpkgs' commonUnitText
   does emit an Install WantedBy line. Origin: introduced by 8bab682.
   Repair commit: 77a6cea.
8. [GAP] 'Nothing requires Forge' contradicted the plan's own requiredBy
   edge on the reactivation target. Origin: introduced by 91ec55e. Repair
   commit: 77a6cea.
9. [GAP] The test-13 relocation sabotage could not fail the invariants it
   named. Origin: introduced by 91ec55e. Repair commit: 77a6cea.
10. [GAP] The section-anchored Git assertion lacked a canary that costs
    nothing. Origin: introduced by 8bab682. Repair commit: 77a6cea.
11. [GAP] The socket-activated and disabled sshd branches were untested.
    Origin: introduced by 91ec55e. Repair commit: 77a6cea.
12. [SIMPLIFY] Test 23 did not need another forced system. Origin:
    introduced by 8bab682. Repair commit: 77a6cea.

SIMPLIFY sweep: the reviewer considered recombining the two materializers,
tmpfiles versus units, shared validators and mutation fixtures, the defaulted
builder argument, test-2 helper reuse, fixture merging, and per-principal Git
copies versus a read-only helper. The two-unit design and the defaulted
argument remain justified. One cut: the standalone test-23 thunk.

Effort and model id are unconfirmed for pass 3, and the pattern is now worth
recording rather than papering over. Pass 3 was invoked as gpt-5.6-sol with
model_reasoning_effort=xhigh, and its reviewer reported it could introspect
neither the deployed model id nor the applied effort. Two of three passes
could not confirm their own effort, so this plan's fix-stability tracking
rests on invocation records nothing inside the session corroborates. The
evidence table records invoked values marked unconfirmed, never promoted to
observations.

Next pass: pass 4, full pass over 756e64f..HEAD, which is 77a6cea plus this
commit. Recommend gpt-5.5 at xhigh. It is cross-vendor from the fix author,
has never seen this plan, and its roster precondition — that the 5.6 family
has already reviewed the feature — is met twice over. The roster tension is
recorded in the evidence section: the R3-appropriate tier is nearly spent,
gpt-5.6-terra is a tier below and arguably barred by two consecutive folds
introducing defects, and claude-opus-4-8 is the fix author's own family,
which is the weakest rotation available when cross-vendor rotation is the
only thing that has surfaced an original-plan blocker after a clean pass.

Model: claude-opus-5
Contributor

Approved for landing, and it lands by relay rather than by the merge button — so this PR will need closing by hand once it does.

The slice this plan describes is implemented and merged on allod/archetypes master as 233044f..8982381, so the plan should not sit in dev-plans/ describing finished work. It lands and is archived in one change: this branch's twelve commits carried forward unchanged in content, plus two more that archive this plan and prompt alongside seven other plans and five other prompts whose work has landed.

The relaying environment cannot push to this org, and the relay tool refuses merge commits, so these commits arrive rebased onto master with new hashes rather than through a merge. Content is identical. When they appear on master, close this with -d.

One note for the record, from the deployment that carries this arc: the guest half's tracking issue allod/archetypes#29 stays open, and the plan is being archived anyway because its own scope is complete — it excluded contract 8b, which is what allod/archetypes#39 is landing now.

Approved for landing, and it lands by relay rather than by the merge button — so this PR will need closing by hand once it does. The slice this plan describes is implemented and merged on `allod/archetypes` master as `233044f..8982381`, so the plan should not sit in `dev-plans/` describing finished work. It lands and is archived in one change: this branch's twelve commits carried forward unchanged in content, plus two more that archive this plan and prompt alongside seven other plans and five other prompts whose work has landed. The relaying environment cannot push to this org, and the relay tool refuses merge commits, so these commits arrive rebased onto master with new hashes rather than through a merge. Content is identical. When they appear on master, close this with `-d`. One note for the record, from the deployment that carries this arc: the guest half's tracking issue `allod/archetypes#29` stays open, and the plan is being archived anyway because its own scope is complete — it excluded contract 8b, which is what `allod/archetypes#39` is landing now.
vnprc force-pushed agent/microvm-credential-delivery-plan from 0c5d7445d9 to f73f728cb4 2026-08-12 19:48:36 +01:00 Compare
vnprc approved these changes 2026-08-12 19:48:56 +01:00
vnprc merged commit f73f728cb4 into master 2026-08-12 19:49:02 +01:00
vnprc deleted branch agent/microvm-credential-delivery-plan 2026-08-12 19:49:02 +01:00
Sign in to join this conversation.
No description provided.