Wire a microvm guest into the host that starts it #39
No reviewers
Labels
No labels
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
bug
duplicate
enhancement
help wanted
invalid
question
wontfix
No milestone
No project
No assignees
3 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
allod/archetypes!39
Loading…
Reference in a new issue
No description provided.
Delete branch "agent/microvm-host-guest-join"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
A hypervisor can now actually start a microVM guest. Until this change nothing connected the two halves: a guest declared which credentials it expects by name and supplied no values, the host asserted that both sides agreed, and no host ever composed a guest — so that agreement was checked against nothing. The host now hands each guest real file paths derived from its own credential root.
Nothing changes for any machine that exists today. No machine selects the microVM runtime, so all four example machines build to byte-identical derivations against current master, and a hypervisor whose fleet has no microVM machine composes none of the new modules at all — measured, not assumed.
What proves it: one check builds three hypervisor fixtures, forces each to a real buildable configuration, compares the two sides' actual values, then reads the resulting systemd drop-in and QEMU arguments at a non-default credential root. A second check walks every hypervisor this flake exports and requires each machine whose inventory entry selects the microVM runtime to be started by exactly one of them, so a deployment cannot silently un-wire one. The whole gate passes at 21 steps.
If this is wrong, revert the branch. Nothing here runs on a host, no deployed state or credential source is touched, and no machine starts a guest on the strength of it.
The owner review ruled on the five judgment calls below and sent two back: close the empty-guest-set seam, and stop overriding
restartIfChanged. Both are applied inf39197a, and the branch is merged up to master with theflake.lockconflict resolved.What this implements
Closes allod/archetypes#38,Refs allod/strategy#20— contracts 1a, 8b and 17, all inallod/archetypes.mkHypervisorcallsextendModuleson each selected guest configuration, adding one module whosemicrovm.credentialFilesis built from the host's evaluatednexus.microvm.hostPlaintextRootand the guest's own declaredcredentialNames.allod/nexusis not modified, never importsallod/archetypes, and never restates the name set.microvm.vms.<name>.evaluatedConfig, andinstall-microvm-<name>installs its runner — a different store path from the standalone result's.microvmGuestsstays a free builder argument so checks can drivemkHypervisorwith a guest set no inventory supplies; what a deployment consumes is constrained instead.checks/microvm-fleet-wiring.nixwalks the exported hypervisors and requires every microVM machine to appear in exactly one host's guest set.restartIfChangedis left at upstream's default, which isfalsefor anevaluatedConfig-shaped VM. A microVM guest is its own deploy target the way this fleet's libvirt VMs are, so a host rebuild stages the new guest boot image and leaves the running guest alone. The check pins the renderedX-RestartIfChanged=falserather than the option, so an upstream default change goes red.nexus.inputs.vm.follows = "vm"collapses the lock from 28 nodes to 18, leaving exactly oneallod/vmand one microvm.nix node. No revision moves; the two copies already happened to agree, which is precisely the coincidence this removes.Owner review decisions
The five judgment calls this PR raised, with what the owner ruled and what changed.
1. The join also sets the guest's
microvm.socket, which the issue does not mention. Kept as is.allod/nexusrefuses upstream's default — a bare"<hostName>.sock", relative to the unit's working directory — and requires a normalized absolute path strictly below/run/allod, outside both the plaintext root and the namespace roots (nix/microvm/host.nixcontract 8). Nothing inallod/archetypesorallod/vmsets it. So composing the host module produces a configuration that does not evaluate at all until someone does, and the issue's whole goal is a host that can start a guest. The join sets/run/allod/microvm-qmp/<machine>/control.sock, the layout nexus's own host fixtures already assume, as a literal rather than an option because no deployment has needed to move it. The owner's ruling: not configurable, because a fixed value cannot be misaimed and this PR's own overlap guard documents what settable roots cost; not inallod/vm, because a guest must not know host filesystem layout; and exporting it fromallod/nexusbeside the launcher that deletes at it is a defensible follow-up issue rather than a blocker, since contract 8 already prevents silent drift of the current split.2. Each wired VM sets
autostart = false. Accepted. Upstream defaults it to true andmicrovms.targetthen wants the unit, so wiring a guest without disarming it would make a host rebuild start it.allod/strategy#20's agent gates make "enable microvm autostart for a real machine" a human-only act, so the framework declares the VM and a deployment opts in. Measured: with the line removed,config.microvm.autostartcomes back as[ "allod-dev" ].3. The join asserts what
allod/nexuscannot, and nothing more. Accepted. For every wired guest, itscredentialFileskeys must equal its declaredcredentialNames— the one property nexus cannot state, because it must not learn the name set. The value rule stays in nexus, which already owns it; restating it here would duplicate a fact.The rule is scoped to the
evaluatedConfigshape, the only shape this builder produces. A deployment reaching past the join with upstream'sconfigorflakeshape is nexus's to judge, and it does: upstream injects its own microvm module into aconfig-shape VM, so nexus's reads succeed and produce named diagnostics. A guest that carries the microvm module but not this framework's option tree gets past nexus's reads and is reported here by name — the read is guarded rather than raw, because an unguarded dereference would throw while the merged assertion list was being built and mask every message nexus had already produced. That branch has no paired sabotage: constructing a microvm guest this framework did not build needs a second guest builder the check has no seam for, so it is carried the waymodules/microvm-credentials.nixcarries its name-length guard — documentation with teeth, said out loud rather than implied.4.
restartIfChangedwas overridden totrue. Sent back, and reversed. The original reasoning assumed a host rebuild is how a guest gets updated. In this fleet it is not: microVM guests are their own deploy targets the way the libvirt VMs are,rebuild-vm-from-hostswitches a guest in place, its services restart at service granularity, andallod/vmcontract 6a requires the writable store overlay and the nix daemon precisely so that works. A host rebuild must therefore stage the new guest boot image and leave the running guest alone, which is exactly upstream's default for a pre-evaluated guest. The override is dropped, and the check now pins the renderedX-RestartIfChanged=falsein the generated drop-in rather than the option that produced it —switch-to-configurationtreats the key as true when absent, so an upstream default change, or an upstream that stopped emitting the key, goes red instead of quietly making every guest restartable on the next host rebuild. Its paired sabotage forces the option back totrueand requires the same text to go true.5. The join guards an overlap
allod/nexusdoes not. Accepted. Nexus checks that a VM's QMP directory is not inside the host plaintext root, but not the reverse. Since this PR introduces the fixed QMP root, ahostPlaintextRootset below it would evaluate clean and then have the launcher'srm -rfdescend into the mounted credential tmpfs at the next VM start. The join refuses that at evaluation, with a paired sabotage.6. The QEMU command reconstruction moved to
checks/qemu-command.nix. Accepted. It was inline inruntime-module-selection, and this check is its second consumer — the credential-fw_cfgarguments are in the same rendered command as the network interface. A copy would witness its own reimplementation, the drift failuretesting.mdnames. The extraction is a pure move, including the divergence guard the moved-root test depends on;runtime-module-selectionstill passes.The empty-guest-set seam, and how it is closed
The implementation review found that
profileDataandmachine.profilemerge into builder arguments, so a deployment could passmicrovmGuests = { }and silently un-wire a machine whose inventory entry declares the microVM runtime, with every check still green. That was originally recorded rather than fixed, on the grounds that constraining the argument would break the seam the checks depend on. The owner's ruling was that a footgun does not stay open because it makes testing easier, and named the shape: constrain the outputs, not the builder.checks/microvm-fleet-wiring.nixdoes that. The builder argument stays free, so fixtures keep flowing through it. What the check pins is what deployments consume — for every hypervisor innixosConfigurations, the guest set that host actually composed, read offconfig.microvm.vmsand guarded because a libvirt-only host declares nomicrovmoption tree at all. The union of those sets must be exactly the machinesmicrovmMachineNamesderives from inventory, each wired once.Exactly once, not at least once, and that is a second thing the rule catches.
mkHypervisor's default hands every hypervisor the whole fleet's microVM machines minus itself, because no inventory fact says which host runs which guest. With one hypervisor that is correct; with two it would wire each guest into both, and two hosts starting one VM share its state directory and its QMP socket. The check refuses that fleet by name and the diagnostic says which fact is missing, rather than letting it ship.What it witnesses here, stated plainly. The public example fleet declares no microVM machine, so the fleet read is the empty set against the empty set; its live half is the guarded branch, since the example hypervisor composes no microvm option tree and an unguarded read would be a raw attribute error rather than an empty guest set. The rule itself is a function over data with three sabotages and two positive controls, so it can be shown to fail. What makes it load-bearing is the deploy flake, which reruns this suite against the real data repos — that is where a machine with the microVM runtime exists and where an un-wired one is refused. To show the fleet path itself reports rather than only the rule, the check's
machinesargument was temporarily doctored on this branch: adding aruntime = "microvm"machine turns it red withbeta-dev selects the microvm runtime and no exported hypervisor starts it, and removing the hypervisor turns it red withthe fleet exports no hypervisor, so this check walked nothing.Nothing here forces a toplevel and no fixture system is built, so the new check adds a walk rather than a fleet — it is one more gate step, not one more machine's worth of memory.
What earlier review caught, and what it says about the check
The plan behind this change was reviewed before implementation, and the review found a blocker that the first working version of the check could not see: reading
config.microvm.vmsor a rendered unit does not forceconfig.assertions. nixpkgs only enforces those throughsystem.build.toplevel. So the check passed while its own fixtures were configurations no host could ever build — the socket problem in decision 1 was sitting there unreported.The fix is finding 0 of the check: every positive fixture now forces
toplevel.drvPathand reports the failed assertion messages rather than a bare "did not evaluate". With the socket line removed, it now saysnexus microvm contract 8: allod-dev: microvm.socket must be one absolute path.Two smaller gaps from the same review are also closed: the filter that decides which machines a hypervisor picks up is now a named function exercised with a machines-shaped fixture (no inventory declares the microVM runtime, so it was otherwise only ever witnessed returning nothing), and the lock-graph reader is a function over parsed lock data with a doctored-lock sabotage, since it is a new validator.
A second pass then reviewed the implementation diff, and its findings are commented on this PR with what each one changed. The substantive ones: the root-overlap guard in decision 5; the guest-name-to-configuration mapping is now a named function witnessed by a fixture; a hypervisor no longer appears in its own guest set, so a hypervisor that somehow declared the microVM runtime is a refusal rather than a stack overflow; and decision 3's stated reasoning was simply wrong and is rewritten.
Risk
R3 High. Cross-repo interface plus generated lifecycle behaviour — a systemd drop-in, a unit's restart semantics, and the argument list a hypervisor hands QEMU. Not R4: no machine selects the runtime, no deployed state is touched, no real credential path exists anywhere in the change, and rollback is a straight revert.
The worst credible failure is not a wrong credential path. That one is already eval-blocked by machinery this change does not touch:
allod/nexusderives its expected value from the host option itself, so a wrong-root extension is refused at deployment evaluation regardless of any check here, and a path that survived would fail loudly at QEMU start on a missing file. The real residual is the class this slice structurally cannot witness — unit lifecycle at runtime. Whetherswitch-to-configurationleaves a changed guest running as intended, and how the launcher and generated unit interact on a real host, are settled by booting, which belongs to the parent arc's nested-boot slice. TherestartIfChangedreversal moves which behaviour is the untested one but not how much is untested: the drop-in's rendered value is pinned either way, and what happens on a real rebuild is not.The fleet-wiring rule's own residual is that its live half only runs where a microVM machine exists. On this repo it is empty-against-empty, and its teeth are in the deploy flake's rerun against the real data repos.
Most useful human scrutiny, in order: decision 1 and whether that socket path belongs to this repo; the fleet-wiring rule's exactly-once clause, which refuses a two-hypervisor fleet that would otherwise double-wire; then the rendered
microvm@<name>drop-in.Validation
Evidence beyond the gate, each run against this branch at
f39197a:toplevel.drvPathfor all four, onmasteratc02e4e9and on this branchflake.lockwholesale, thennix flake lockre-applied thefollowsvm, onemicrovm; revision set byte-identical to master'sgit merge-base --is-ancestor origin/master HEAD, and the forge's ownmergeableflagmergeable: truetoplevel.drvPathforced on both positive fixturescredentialFilesand every-fw_cfgargumentrestartIfChanged = truere-added to the jointruewherefalseexpected, drop-in missingX-RestartIfChanged=false, drop-in carryingX-RestartIfChanged=trueautostart = falsedeletedmicrovm.autostartbecomes[ "allod-dev" ]microvm.socketdeletedhostPlaintextRootinside the QMP rootmachinesargument doctored with aruntime = "microvm"entrybeta-dev selects the microvm runtime and no exported hypervisor starts itmachinesargument doctored to remove the hypervisorthe fleet exports no hypervisor, so this check walked nothingThe permanent sabotages are each pinned to the diagnostic they name rather than to "evaluation failed": in the join check, a standalone unextended guest wired into
evaluatedConfig, a host source set that does not match the guest, a host plaintext root nested inside the QMP root, arestartIfChangedforced true, anautostartforced true, and a doctored lock graph; in the fleet-wiring check, a declared machine no host starts, one guest wired into two hosts, and a host starting a machine inventory did not select — with two positive controls so a rule that always reported would not pass them.What this does not prove. No guest is booted and no host is rebuilt: this slice ends at evaluation and rendered artifacts. Everything is measured against the public example fleet, which is not the fleet that matters; a verification request for private machine data follows on the issue.
Review rotation, stated because it was thinner than the standard asks for. The cross-vendor pass
allod/memoryrecords as strongest was not callable — thecodexrunner in this VM is unauthenticated (HTTP 401) — so the rotation was Claude-side only, one model reviewing another's plan. It found the blocker above, so the pass was not decorative. The owner review then read the generated report and sent back two changes, both applied here; those two commits have not themselves had an independent adversarial pass.Review pass on the implementation diff
Read-only adversarial pass over
master...HEAD, one model rotated off the author. No blocker. It re-verified the PR body's numbers by execution — 20 gate steps, 28→18 lock nodes with every survivor at its old revision, all four machines'toplevel.drvPathidentical between merge-base8982381and branch HEAD — and confirmed the extraction tochecks/qemu-command.nixis a pure move, thatallod/nexusneeds no change, and that there is no evaluation cycle.Six findings, four of which I am fixing in a follow-up commit on this branch.
1. GAP — the production default path is unwitnessed, and a deployment can silently un-wire a declared microvm machine
fleetMicrovmGuestsfeedsmkHypervisor's default argument, but no check ever executes that default: every fixture passesguestsexplicitly, and no inventory machine declares the runtime. If thegenAttrsline read the wrong variable, all 20 steps would still pass — the byte-identical drvPath comparison cannot tell a working gate from an accidentally empty guest set. This is the failure the check's own header warns about, one seam further out.Fixing the mechanical half: the name→configuration mapping becomes a named function driven by a machines-shaped fixture, so the wiring itself is witnessed.
Not fixing, and recording instead:
profileDataandmachine.profilemerge into builder arguments, so a deployment can passmicrovmGuests = { }and un-wire a machine whose inventory says microvm, with every check still green. Constraining that would break the seam the checks themselves depend on. The issue's verification request 2 is the witness for it, which makes that request load-bearing rather than optional — noted on the issue.2. GAP — the QMP socket root and the host plaintext root can nest, and the failure is destructive rather than loud
The sharpest finding. Nexus's contract-8 assertion checks one direction only — that the QMP directory is not inside the plaintext root — so a
hostPlaintextRootset below the QMP root (/run/allod/microvm-qmp/credspasses its option regex) evaluates clean. At the next start of that VM the launcher'srm -rfon the QMP directory descends into the mounted plaintext tmpfs, destroying every VM's prepared credentials including a running sibling's.This PR is the party that introduced the fixed QMP literal, so the guard belongs here rather than in prose. Adding an assertion for the uncovered direction, with a paired sabotage.
3. QUESTION — decision 3's stated reasoning is wrong, and this module can mask nexus's diagnostics
The comment and the PR body both claim that a VM wired in upstream's other shapes raises a raw attribute error from
allod/nexusbefore any assertion here renders. Not true in either direction: aconfig-shape VM gets upstream's microvm module injected, so nexus's reads succeed and produce named diagnostics; and for a guest that has the microvm module but not this framework's option tree, the rawattribute 'allod' missingcomes from this file, thrown while the merged assertion list is built — masking nexus's renderable contract-8 and contract-17 messages.The decision stands; the reason recorded for it does not. Correcting the comment and the PR body, and guarding the read so this module stops masking nexus.
4. QUESTION — a hypervisor declaring the microvm runtime is infinite recursion
microvmMachineNamesfilters onruntimealone, so a hypervisor entry withruntime = "microvm"would put that hypervisor in its own guest set and recurse. Inventory forbids it (contract 1, asserted there), so this is unreachable from valid data — but it surfaces as a stack error rather than a refusal. Excluding the machine being built from its own guest set: a hypervisor never hosts itself, which is this repo's own truth rather than a restatement of inventory's rule.5. SIMPLIFY — stale path
flake.nixstill says the reconstruction lives innix/qemu-command.nix; it ischecks/qemu-command.nix. Fixing.6. Nits in this PR body
Decision 4 says the divergence guard is what test 2 depends on — it is test 3, the moved root. And the owner summary says "revert the commit" when the branch carries two. Both corrected in the body.
One observation, not a defect: test 1's host-source comparison round-trips the fixture's own input, since the synthetic sources are built from the same restated name list. Agreement is still established, transitively, through that restated literal and through nexus's set-equality assertion on the forced toplevel — but the comparison is weaker than it reads.
Owner review decisions on the five recorded judgment calls, after a full read of the generated report:
1. The empty-guest-set seam: close it. A footgun does not stay open because it makes testing easier. Requested shape: constrain the outputs, not the builder. Add a check that walks every host configuration the fleet exports and requires its composed guest set to equal the inventory-derived expectation — every machine whose inventory entry selects the microVM runtime appears in exactly one host's guest set. The builder argument stays free, so fixtures keep flowing through it; the pinned object is what deployments consume. The deploy flake already reruns this suite against the real data repos, so the rule holds fleet-wide.
2. Deferred boot test: accepted. Runtime restart and launcher behavior land in the later boot-test slice as planned.
3.
autostart = false: accepted. Declaring a machine and arming it stay separate human acts.4.
restartIfChanged = true: reverse it. MicroVM guests should behave like the fleet's libvirt VMs under deploys. Those VMs are their own deploy targets:rebuild-vm-from-hostswitches them in place, services restart at service granularity, the VM never reboots because of a deploy. The microVM guests were built for the same model — allod/vm contract 6a requires the writable store overlay and nix daemon precisely so in-place deploys work. Under that model a host rebuild must stage the new guest boot image and leave the running guest alone, which is exactly upstream microvm.nix's default for pre-evaluated guests. Requested change: drop thetrueoverride, and flip the check to pinX-RestartIfChanged=falsein the generated unit text so an upstream default change goes red instead of silently making guests restartable. The report's drift argument assumed host deploys are how guests get updated; in this fleet they are not.5. Control-socket path placement: keep as is. Not configurable — a fixed value cannot be misaimed, and this PR's own overlap guard documents what settable roots cost. Not in the vm repo — the guest must not know host filesystem layout. The one defensible alternative, exporting the value from allod/nexus beside the launcher that deletes at it, can be a small follow-up issue; contract 8 already prevents silent drift of the current split, so it does not block this PR.
Items 1 and 4 are requested changes on this PR; 2, 3, and 5 are accepted as shipped.
Owner review decisions implemented, and the branch synced to master
Both requested changes are in
f39197a, andff98236merges master and resolves theflake.lockconflict. The forge now reportsmergeable: true. Gate:./check.shpasses at 21 steps,./check-selftest.shPASS. All four example machines evaluate to the sametoplevel.drvPathas master atc02e4e9. The PR body is rewritten to match.Item 1 — the empty-guest-set seam: closed at the output
New check,
checks/microvm-fleet-wiring.nix, in the shape requested.microvmGuestsstays a free builder argument so fixtures keep flowing through it; what is pinned is what a deployment consumes. For every hypervisor innixosConfigurationsit reads the guest set that host actually composed offconfig.microvm.vms— guarded, because a libvirt-only host declares nomicrovmoption tree at all — and requires the union to be exactly the machinesmicrovmMachineNamesderives from inventory, each appearing once.Two notes on what that rule does beyond the un-wiring case.
Exactly once refuses a two-hypervisor fleet.
mkHypervisor's default hands every hypervisor the fleet's microVM machines minus itself, because no inventory fact assigns a guest to a host. With one hypervisor that is right; with two it wires each guest into both, and two hosts starting one VM share its state directory and its QMP socket. The rule reports that by name and says which fact is missing, rather than shipping it. If you would rather that case were silently allowed, say so — it is a one-line change to the rule, but it looked worse than the un-wiring it was written for.What it witnesses in this repo, said out loud. The example fleet declares no microVM machine, so the fleet read is empty against empty; its live half is the guarded branch on the example hypervisor. The rule is a function over data with three sabotages and two positive controls, so it can be shown to fail, and the deploy flake's rerun against the real data repos is what gives it teeth — which matches your note. To show the fleet path reports and not only the rule, I doctored the check's
machinesargument on this branch twice: adding aruntime = "microvm"entry turns it red withbeta-dev selects the microvm runtime and no exported hypervisor starts it, and removing the hypervisor turns it red withthe fleet exports no hypervisor, so this check walked nothing. Both reverted.Nothing in the new check forces a toplevel and no fixture system is built, so it is one more gate step rather than one more machine's worth of memory. It is a separate check rather than another section of
microvm-host-joinfor that reason too: that check already holds three hypervisor fixtures live andallod/archetypes#31is open about its peak.Item 4 —
restartIfChanged: reversedThe override is dropped. Your model is right and the report's drift argument assumed the wrong deploy path: guests are their own deploy targets,
rebuild-vm-from-hostswitches them in place, and contract 6a's writable store overlay and nix daemon exist so that works — so a host rebuild stages the new boot image and leaves the running guest alone, which is upstream's default for a pre-evaluated guest.The check is flipped as asked: it pins the rendered
X-RestartIfChanged=falsein the generated drop-in text, and asserts the absence of=true. The absence matters as much as the value, becauseswitch-to-configurationtreats a missing key as true, so an upstream that stopped emitting it would be as bad as one that flipped the default. The paired sabotage now forces the option totrueand requires the same text to go true. Measured in the other direction as well: re-addingrestartIfChanged = trueto the join turns the check red with all three findings — the option value, the missing=false, and the present=true.The option read (
equals "the wired guest's restartIfChanged" false) is kept alongside the text read so the two cannot drift silently apart.Items 2, 3 and 5 — accepted as shipped
No code change. The body now records your reasoning for each rather than presenting them as open questions, including that exporting the control-socket path from
allod/nexusbeside the launcher that deletes at it is a follow-up issue rather than a blocker. Say the word and I will open it.Residual
The two commits above have not had an independent adversarial pass of their own; the rotation recorded in the body covers the work up to
ee99a08.Contract 8b now disagrees with the code, and it needs a decision
Raising this rather than acting on it, because it is another repo and it is your call.
allod/strategydev-plans/microvm-framework-adoption.mdcontract 8b ends: "SupplyingevaluatedConfigrather thanconfigalso flips upstream'smicrovm.vms.<name>.restartIfChangeddefault to false … andswitch-to-configurationskips the unit on every rebuild instead of stopping it. Each selected microvm therefore setsrestartIfChanged = trueexplicitly."That last sentence is exactly what item 4 reversed. The reversal is right — the contract sentence carries the same assumption your review named, that a host rebuild is how a guest gets updated — but the contract text is now the written record of a rule this branch deliberately breaks, and nothing in
allod/strategysays otherwise.d4a8420records the disagreement beside the code so a reader who checks the contract knows which side is stale, and corrects one citation of mine while there: the new fleet-wiring rule is not contract 1a (that is the pin set) and not 8b (which covers how a host crosses into a guest it has already selected, not which guests it selects). No strategy contract states it; it is this repo's own property, since this repo owns the builders and thenixosConfigurationsoutput.Two things worth deciding:
allod/strategy.Happy to open one
allod/strategyPR covering both. It does not block merging this one — the code and the checks agree with each other and with your decision; only the plan text lags.Contract text updated in
allod/strategydirectlyDone on
masteras362bf30, no PR. Three edits, all indev-plans/microvm-framework-adoption.md:Contract 8b's closing rule is reversed. It now records that upstream's
falsedefault for anevaluatedConfig-shaped VM is kept deliberately, with your deploy-model reasoning, that a host rebuild re-runsinstall-microvm-<name>to stage the changed runner and leaves the running guest alone, and that the check pins the renderedX-RestartIfChanged=falseplus the absence of=truerather than the option.The acceptance bullet follows, and the nested test changed shape rather than just losing a clause. Item 10 listed "a rebuild stop driven by a guest change that moves the toplevel" among the stop paths whose
ExecStopPostbehaviour it asserts — under the new rule that is not a stop path at all, so it is removed from that list. New item 11 asserts the opposite behaviour: rebuild with a guest change, require themicrovm@<name>unit to keep its main PID and start timestamp acrossswitch-to-configurationwhileinstall-microvm-<name>re-runs andcurrentcomes to point at the new runner, then stop and start and require the changed guest to boot. It is written against a change that would have restarted underX-RestartIfChanged=true, so a fixture that failed to change anything cannot pass it. That is the behavioural half your decision needs and this slice cannot give.Contract 1b is added for the fleet-wiring rule, beside contract 1's runtime fact rather than under 8b: every machine whose inventory entry selects the microvm runtime appears in exactly one exported hypervisor's guest set, asserted on
nixosConfigurationsrather than the builder argument, with the exactly-once clause and its two-hypervisor consequence stated.I did not touch
review-prompts/microvm-framework-adoption-review.md. Its pass log records that pass 8 verifiedf3c93bc's explicitrestartIfChanged = trueheld — true as history, and rewriting a review record to match a later decision would falsify it.59cfc2cupdates this branch's two code comments accordingly: the join no longer records a pending follow-up, and the fleet-wiring check cites contract 1b instead of saying no contract states it. Gate still passes at 21 steps.Owner review — follow-up recorded, not a change request against this branch.
The
autostart = falsedecision stands as accepted: the default is correct and this is not a blocker for merge. What the owner asked about is the half the explainer already flags under "Unresolved operator interface" — there is no repository-specific opt-in option, documented deployment recipe, or sanctioned location for arming autostart, so today the only route islib.mkForcecopied out ofchecks/microvm-host-join.nix, which is evidence the setting is reachable rather than a policy.Recorded as allod/archetypes#47, proposing
nexus.microvm.machines.<name>.autostartas the surface — besidecredentialSources, which is the same class of per-machine deployment composition input — with the safe default explicitly unchanged.Context for whoever picks it up: the libvirt fleet gets reboot recovery today from nixpkgs'
virtualisation.libvirtd.onBoot = "start"default, which restores guests that were running before shutdown. The first real microVM machine loses that at its first unattended host reboot, and the manual fallback issystemctl start microvm@<name>. That moment is the trigger the issue names.Owner review, decision 1 — the
microvm.socketplacement follow-up is now recorded as allod/nexus#27.The ruling stands unchanged: the fixed value and its placement in the join are accepted, and nothing here blocks merge. The issue captures the alternative the review named —
allod/nexusexporting the path constructor beside the post-stop hook that recursively removes the resulting directory — so the reasoning is findable from the nexus side rather than only from this pull request's body.It is explicit that it closes no current gap: nexus duplicates no literal (
qmpDirectory = builtins.dirOf qmpSocket,nix/microvm/host.nix:84), both sides of the overlap assertion come from one string in one file, andchecks/microvm/isolation.nixalready witnesses that the removal spares a running sibling's socket and prepared credential. Its trigger is a second consumer of the QMP root, or the layout ceasing to be a fixed value.Owner review — the remaining suffixed lock labels were checked, and one more is collapsible. Recorded as allod/archetypes#48; not a change request against this branch.
After this branch the graph is 18 nodes with two suffixed pairs left, and they are not the same kind of thing.
inventory/inventory_215ad5528home-manager/home-manager_2bf9ce9fe/3ee51fbdagenixdeclares for its own moduleinventory_2exists becauseflake.nix:11redirects thesecretsinput's nixpkgs but not its inventory. One line takes the graph to 17, with no revision moving — the same coincidence this branch removed forallod/vm, and worth removing for the same reason: inventory is the sole source of machine facts, so two nodes at different revisions would haveallod/secretsderiving key material against one fleet definition while this repo builds another.allod/deploycarries the identical pair and needs the same redirect.home-manager_2is left alone deliberately. Those hold different revisions, everything first-party already follows tohome-manager_2, and this repo receives agenix as afollowsrather than a declared input, so the redirect could only land inallod/vmand would assert agenix's module works against a home-manager it did not pin. That is a compatibility bet rather than a duplicate removal, and it is out of scope in allod/archetypes#48 with that reasoning recorded.