Add required runtime fact for non-hypervisor inventory machines #10
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
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
allod/inventory!10
Loading…
Reference in a new issue
No description provided.
Delete branch "agent/inventory-runtime-fact"
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?
Every non-hypervisor
machines.<name>entry now declaresruntime = "libvirt"orruntime = "microvm", validated by an assertion chain that mirrors the repo's existingplatformchecks: a missing, non-string, or unknown value fails evaluation with a named error listing the offending machine names, before any consumer can see partial or defaulted data. Hypervisor entries (nexus) are exempt by scope, not by a default — they are filtered out before the runtime checks run at all, so they can never acquire a fake guest runtime. Both public example machines declarelibvirt.allod-devin particular is held there by its own check: it is the machine the operator develops from, and a real machine rather than a synthetic example —allod/secretscarries key material keyed to that name — so it is the last machine to move onto a new runtime, not the first. The machine that first selectsmicrovmis added in the same change that provisions it, because a machine entry is not self-contained: it needs a matching identity, a profile, and per-machine encrypted credentials whose creation needs a host key generated on the host. The runtime enum'smicrovmbranch is covered by the mutation fixtures below rather than by an example machine.lib.vmSpecsJsonnow exportsruntimealongside the existing host-facing fields, and the committedscripts/vm-specs.jsonis regenerated so the two agree exactly. The internalmkVmSpecs/mkVmSpecsJsonhelpers take an explicit machine set rather than closing overmachines, so the newruntime-fact-mutationscheck can run the identical validation chain against sabotaged copies and prove each failure mode actually fails rather than merely asserting on the real data.This PR is the inventory half of Interface Contract 1 only. The
archetypes.vmFacts.<name>.runtimehalf of the contract, and the guest-module selection that consumes this fact, land in later milestones per the plan's Implementation Sequence; this PR does not touchallod/vm,allod/nexus,allod/archetypes, orallod/profiles.Refs allod/strategy#20
Risk
R2 Medium, matching the plan's risk table for this milestone: this changes a machine-data contract consumed by host scripts and, in a later milestone,
vmFacts, but rollback is a plain revert and both the Nix-side value and the committed JSON are checked against each other on every evaluation. The residual risk worth a human's attention is schema propagation into the generated JSON and the missing/unknown-value failure behavior, both exercised below.Validation
All commands were run against this branch's worktree in
allod/inventory.nix flake check --print-build-logspasses all three checks:vm-specs-json,repository-registry, and the newruntime-fact-mutations.returns
true.produces an empty diff.
Mutation/sabotage coverage, proven both inside the new
runtime-fact-mutationscheck and independently reproduced by hand-editing a scratch copy offlake.nix/scripts/vm-specs.jsonand re-runningnix eval: removingallod-dev'sruntimeline fails withinventory machines missing runtime: allod-dev; setting it to a non-string (42) fails withinventory machines with non-string runtime: allod-dev; setting it to an unknown string ("openstack"/"bhyve") fails withinventory machines with unknown runtime (expected one of: libvirt, microvm): allod-dev; and mutating a copy of the committed JSON'sruntimevalue produces a non-empty diff against the generated JSON, proving thevm-specs-jsondrift check is not vacuous. The same check also confirms the hypervisor entrynexusnever appears invmSpecsJson, thatprivacy-1remains the publiclibvirtexample, and thatallod-devstays onlibvirt— the last assertion exists so the operator's own development machine cannot be moved onto a new runtime by a quiet data edit.--- 3 comment(s) ---
[2026-07-29] allod-agent:
Independent review pass —
claude-opus-5, max effortRead-only review of
d21fe20(merge baseefe4a7c, no rebase drift) against Interface Contract 1 and the "Repository and schema checks" acceptance tests indev-plans/microvm-framework-adoption.md. Every claim below was re-run, not read. Sabotage fixtures were built by copying the branch worktree to a scratch tree and editing that copy; nothing in the repo was modified.No
[BLOCKER]. Three[GAP]s, none of which breaks a contract today; all three are cheap and two of them are about the validator rather than the code it validates.1. [GAP] The non-string sabotage fixture does not validate the non-string assertion
runtime-fact-mutationsonly recordstryEval ... .success, never why the fixture failed. For the non-string case that is not a discrimination nicety, it is a hole:builtins.elem 42 [ "libvirt" "microvm" ]is alreadyfalse, somachinesNonStringRuntimelands inunknownRuntimeand trips the unknown assertion regardless. Deleting the non-string assertion entirely leaves the check green and still printingOK: non-string runtime fails:For contrast, the same experiment on the unknown assertion does fail the check, so that one is genuinely load-bearing:
The plan's Validator validation rule is that a check which cannot be shown to fail on the bad configuration it claims to catch does not count. One of this check's three claims cannot.
A second, related property worth knowing before this pattern is copied into the archetypes/nexus mutation checks the plan requires:
builtins.tryEvalcatches onlyAssertionError(andThrownError, which derives from it). A plainEvalErrorescapes it. Deleting the missing-runtime assertion therefore does not produce a red check — it aborts evaluation of the whole flake from insidebuildCommand:That is loud, so it is not itself a defect — but it means the boolean-success harness can only ever report on
assert/throwfailures, and a future fixture whose intended failure mode is a type or missing-attribute error will take the flake down instead of failing its own line.Fix: stop asserting a boolean. Expose the three diagnostic sets independently of the chain and assert exactly which one is non-empty per fixture, e.g.
Each fixture then has to light exactly one lamp, and each of the three assertions becomes independently falsifiable.
2. [GAP] A hypervisor can acquire a runtime fact; the PR body says it cannot
The PR body states hypervisors "are filtered out before the runtime checks run at all, so they can never acquire a fake guest runtime." The first half is true; the second is not enforced. Adding
runtime = "microvm";to thenexusentry evaluates cleanly, produces byte-identicalvmSpecsJson, and passes all three checks:The
jq -e 'has("nexus")'assertion inruntime-fact-mutationscannot catch this: it keys on the type filter, which excludesnexuswhether or not it carries a runtime. So the exemption is scoping, not enforcement — a hypervisor that acquires one is invisible to inventory and visible to anything readingmachines.<name>.runtimedirectly.Nothing downstream breaks today (
archetypesvmFactsfilterstype != "hypervisor"too), so this is not a blocker. But the plan's contract 1 sentence is "Hypervisor entries do not acquire a fake guest runtime", and principle 11 wants that as an evaluation error rather than a convention. The structural version is four lines beside the existing chain:Either add that, or drop the "can never acquire" claim from the PR body — but the README's wording ("never appear in
vmSpecsJsonregardless of one") is already the accurate one, and the two should not disagree.3. [GAP] The runtime assertions are not on the surface consumers actually read
mkVmSpecsis reachable only throughvmSpecsJson. The exportedmachinesandlib.machinesare the raw, unvalidated attrset. Withallod-dev'sruntimeline removed:archetypesconsumesinventory.machines(flake.nix:39,:43), notlib.vmSpecsJson; no sibling flake evaluatesvmSpecsJsonat all.nix flake checkin a consumer skips an input's own checks, so a downstream repo that bumps its inventory pin gets no inventory-side failure from bad runtime data — the memory rule innix.md("hoist its assertion onto the consumed surface (lib.*) or consumers never trip it") is aimed exactly at this shape. Contract 1's own wording isinventory.machines.<name>, which is the surface that currently does not fail.The plan does put an independent no-defaults duty on the archetypes milestone, so this is defensible as sequencing rather than a defect. But routing the export through the validated set costs one binding and closes it here instead of relying on the next repo:
Note the contrast with the pre-existing
platformchain, which is on a consumed surface (lib.supportedPlatforms, read bydeploy,secrets, andarchetypes). The runtime chain is the odd one out.What I verified and reproduced
Plan acceptance commands, in the branch worktree — all three match the PR's claims exactly.
Forced a rebuild of
runtime-fact-mutationsrather than trusting the cache; all eight of its lines printOK.Each failure mode, sabotaged independently, fails with the message the PR claims.
Assertion ordering is correct: the innermost assertion fires first, so each fixture surfaces its own message rather than a downstream one.
Drift detection is real, for the new key and for a pre-existing one. Mutating
allod-dev.runtimeto"bhyve"in the committed JSON, and separately deletingprivacy-1.ip, each failvm-specs-jsonwith the regenerate hint. The drift check is not vacuous in either direction.The refactor is byte-identical for every pre-existing field.
mkVmSpecs/mkVmSpecsJsonare a pure re-parameterization:Same two machine keys (
allod-dev,privacy-1), no dropped field, no reordering,runtimeis the only addition.builtins.toJSONsorts keys, so theinherit (m) ... runtimeposition cannot perturb output. Nothing forarchetypes.vmFactsor the host scripts to trip over.No dangling references anywhere.
vmSpecswas a privateletbinding, never a flake output, and nothing inarchetypes,nexus,deploy,profiles,secrets, ortoolsreferences it — the only hit in the whole workspace is abrainstorm/doc using the name incidentally. The public surface (machines,lib.machines,lib.supportedPlatforms,lib.vmSpecsJson) is unchanged. The extra JSON key is inert for both classes of consumer:archetypes'vm-facts-coherenceprojectsvm-specs.jsondown to{ip, forge_key}before diffing, and thenexushost scripts read named fields withjq. No sibling repo breaks on this landing, and none breaks when its inventory pin is bumped.Metadata.
Refs allod/strategy#20present, no closing keyword anywhere in the body or the commit message — correct for a non-final PR in a multi-repo arc. The R2 Medium claim and its justification match the plan's risk-table row for this milestone verbatim in substance ("machine-data contract consumed by host scripts andvmFacts; rollback is a revert and both values are checked"; scrutiny on schema propagation and missing/unknown-value failures). Commit message carries no attribution trailer and no hook-tripping keyword.Recommendation
Merge after fixing 1 and 2. Both are small and both are about making the check say what it claims: 1 is the plan's own Validator validation standard applied to a line that currently passes for the wrong reason, and 2 removes a claim in the PR body that the code does not back. 3 is a judgement call — the cheap hoist is worth taking here, but deferring it to the archetypes milestone is consistent with the plan's Implementation Sequence, so it should not hold the merge on its own.
Independent review pass —
claude-opus-5, max effortRead-only review of
d21fe20(merge baseefe4a7c, no rebase drift) against Interface Contract 1 and the "Repository and schema checks" acceptance tests indev-plans/microvm-framework-adoption.md. Every claim below was re-run, not read. Sabotage fixtures were built by copying the branch worktree to a scratch tree and editing that copy; nothing in the repo was modified.No
[BLOCKER]. Three[GAP]s, none of which breaks a contract today; all three are cheap and two of them are about the validator rather than the code it validates.1. [GAP] The non-string sabotage fixture does not validate the non-string assertion
runtime-fact-mutationsonly recordstryEval ... .success, never why the fixture failed. For the non-string case that is not a discrimination nicety, it is a hole:builtins.elem 42 [ "libvirt" "microvm" ]is alreadyfalse, somachinesNonStringRuntimelands inunknownRuntimeand trips the unknown assertion regardless. Deleting the non-string assertion entirely leaves the check green and still printingOK: non-string runtime fails:For contrast, the same experiment on the unknown assertion does fail the check, so that one is genuinely load-bearing:
The plan's Validator validation rule is that a check which cannot be shown to fail on the bad configuration it claims to catch does not count. One of this check's three claims cannot.
A second, related property worth knowing before this pattern is copied into the archetypes/nexus mutation checks the plan requires:
builtins.tryEvalcatches onlyAssertionError(andThrownError, which derives from it). A plainEvalErrorescapes it. Deleting the missing-runtime assertion therefore does not produce a red check — it aborts evaluation of the whole flake from insidebuildCommand:That is loud, so it is not itself a defect — but it means the boolean-success harness can only ever report on
assert/throwfailures, and a future fixture whose intended failure mode is a type or missing-attribute error will take the flake down instead of failing its own line.Fix: stop asserting a boolean. Expose the three diagnostic sets independently of the chain and assert exactly which one is non-empty per fixture, e.g.
Each fixture then has to light exactly one lamp, and each of the three assertions becomes independently falsifiable.
2. [GAP] A hypervisor can acquire a runtime fact; the PR body says it cannot
The PR body states hypervisors "are filtered out before the runtime checks run at all, so they can never acquire a fake guest runtime." The first half is true; the second is not enforced. Adding
runtime = "microvm";to thenexusentry evaluates cleanly, produces byte-identicalvmSpecsJson, and passes all three checks:The
jq -e 'has("nexus")'assertion inruntime-fact-mutationscannot catch this: it keys on the type filter, which excludesnexuswhether or not it carries a runtime. So the exemption is scoping, not enforcement — a hypervisor that acquires one is invisible to inventory and visible to anything readingmachines.<name>.runtimedirectly.Nothing downstream breaks today (
archetypesvmFactsfilterstype != "hypervisor"too), so this is not a blocker. But the plan's contract 1 sentence is "Hypervisor entries do not acquire a fake guest runtime", and principle 11 wants that as an evaluation error rather than a convention. The structural version is four lines beside the existing chain:Either add that, or drop the "can never acquire" claim from the PR body — but the README's wording ("never appear in
vmSpecsJsonregardless of one") is already the accurate one, and the two should not disagree.3. [GAP] The runtime assertions are not on the surface consumers actually read
mkVmSpecsis reachable only throughvmSpecsJson. The exportedmachinesandlib.machinesare the raw, unvalidated attrset. Withallod-dev'sruntimeline removed:archetypesconsumesinventory.machines(flake.nix:39,:43), notlib.vmSpecsJson; no sibling flake evaluatesvmSpecsJsonat all.nix flake checkin a consumer skips an input's own checks, so a downstream repo that bumps its inventory pin gets no inventory-side failure from bad runtime data — the memory rule innix.md("hoist its assertion onto the consumed surface (lib.*) or consumers never trip it") is aimed exactly at this shape. Contract 1's own wording isinventory.machines.<name>, which is the surface that currently does not fail.The plan does put an independent no-defaults duty on the archetypes milestone, so this is defensible as sequencing rather than a defect. But routing the export through the validated set costs one binding and closes it here instead of relying on the next repo:
Note the contrast with the pre-existing
platformchain, which is on a consumed surface (lib.supportedPlatforms, read bydeploy,secrets, andarchetypes). The runtime chain is the odd one out.What I verified and reproduced
Plan acceptance commands, in the branch worktree — all three match the PR's claims exactly.
Forced a rebuild of
runtime-fact-mutationsrather than trusting the cache; all eight of its lines printOK.Each failure mode, sabotaged independently, fails with the message the PR claims.
Assertion ordering is correct: the innermost assertion fires first, so each fixture surfaces its own message rather than a downstream one.
Drift detection is real, for the new key and for a pre-existing one. Mutating
allod-dev.runtimeto"bhyve"in the committed JSON, and separately deletingprivacy-1.ip, each failvm-specs-jsonwith the regenerate hint. The drift check is not vacuous in either direction.The refactor is byte-identical for every pre-existing field.
mkVmSpecs/mkVmSpecsJsonare a pure re-parameterization:Same two machine keys (
allod-dev,privacy-1), no dropped field, no reordering,runtimeis the only addition.builtins.toJSONsorts keys, so theinherit (m) ... runtimeposition cannot perturb output. Nothing forarchetypes.vmFactsor the host scripts to trip over.No dangling references anywhere.
vmSpecswas a privateletbinding, never a flake output, and nothing inarchetypes,nexus,deploy,profiles,secrets, ortoolsreferences it — the only hit in the whole workspace is abrainstorm/doc using the name incidentally. The public surface (machines,lib.machines,lib.supportedPlatforms,lib.vmSpecsJson) is unchanged. The extra JSON key is inert for both classes of consumer:archetypes'vm-facts-coherenceprojectsvm-specs.jsondown to{ip, forge_key}before diffing, and thenexushost scripts read named fields withjq. No sibling repo breaks on this landing, and none breaks when its inventory pin is bumped.Metadata.
Refs allod/strategy#20present, no closing keyword anywhere in the body or the commit message — correct for a non-final PR in a multi-repo arc. The R2 Medium claim and its justification match the plan's risk-table row for this milestone verbatim in substance ("machine-data contract consumed by host scripts andvmFacts; rollback is a revert and both values are checked"; scrutiny on schema propagation and missing/unknown-value failures). Commit message carries no attribution trailer and no hook-tripping keyword.Recommendation
Merge after fixing 1 and 2. Both are small and both are about making the check say what it claims: 1 is the plan's own Validator validation standard applied to a line that currently passes for the wrong reason, and 2 removes a claim in the PR body that the code does not back. 3 is a judgement call — the cheap hoist is worth taking here, but deferring it to the archetypes milestone is consistent with the plan's Implementation Sequence, so it should not hold the merge on its own.
The core of the change is right: the enum is enforced, the failure messages name the offending machines, the projection into
vmSpecsJsonand the committed JSON is exactly additive, and parameterizing the helpers on an explicit machine set so the real validation chain runs against sabotaged fixtures is the correct shape for this — it is what let the two validator gaps above be found by deleting assertions rather than by reading them.Review findings addressed in
530073c. All three are fixed; here is what changed and what was proven.Finding 1, the non-string fixture proving nothing. Confirmed as described. Classification now happens in one
runtimeDiagnosticsfunction whose four predicates are each guarded on the previous condition (typefirst, thenm ? runtime, thenisString, thenelem), so the four diagnostic sets are disjoint by construction and a machine with one problem trips exactly one diagnostic regardless of assertion order. Each fixture now asserts two separate things:pinnedToproves the fixture hits exactly its own diagnostic and none of the other three, andrejectsproves the real consumedmkVmSpecsJsonpath actually throws for it. The second half matters independently —pinnedToalone would pass for a diagnostic that is computed correctly but never asserted on.Finding 2, a hypervisor silently acquiring a runtime. Fixed in code rather than by weakening the PR body, since contract 1 says hypervisors do not acquire a fake guest runtime.
hypervisorWithRuntimeis now a real diagnostic with its own assertion and its own fixture, and the previously overclaiming comment on thenexusentry now describes enforcement that exists.Finding 3, assertions not on the surface consumers read. Taken. It was one binding as predicted:
checkedMachines = builtins.seq (mkVmSpecs machines) machines, exported as bothmachinesandlib.machines.builtins.seqforces the assertions and returns the original value unchanged, so consumers see the same shape. No recursion.Per-assertion deletion proof. Each of the four assertion conditions was neutered to
(true)in turn on a scratch copy and the check re-run. All four are load-bearing:hypervisorWithRuntimehypervisor-with-runtime: fails mkVmSpecsJson: expected true but got falsemissingRuntimeerror: attribute 'runtime' missingnonStringRuntimenon-string runtime: fails mkVmSpecsJson: expected true but got falseunknownRuntimeunknown runtime: fails mkVmSpecsJson: expected true but got falseThe
missingRuntimerow is the harness limitation the review identified, now characterized precisely. With that assertion gone, the JSON projection'sinherit (m) ... runtimeraises a rawEvalError, andtryEvalcatches onlyAssertionErrorandThrownError, so it escapes and takes down the whole flake evaluation instead of failing the check through its own diagnostic. That is loud rather than silent, so the assertion is genuinely load-bearing, but it bounds what this harness can report: atryEval-based fixture cannot distinguish "my assertion fired" from "evaluation died for an unrelated reason" unless it also checks which diagnostic set is non-empty. This is called out here because the same harness pattern is being copied into theallod/vm,allod/nexus, andallod/archetypesmutation checks, and the blind spot should not travel with it.Full acceptance set re-run after the fix, all green:
nix flake check --print-build-logsexits 0 across all three checks. Thejq -eenum assertion returnstrue, anddiff -ubetweennix eval .#lib.vmSpecsJsonand committedscripts/vm-specs.jsonis empty.One correction to my own process, recorded so the next reviewer does not repeat it: my first sabotage sweep classified the
missingRuntimecase as leaving the check green, because it grepped for the check's ownERROR:lines and an eval abort produces none. The assertion was load-bearing all along; the detection was wrong. Sabotage sweeps on this pattern need to distinguish three outcomes, not two — green, red via diagnostic, and red via eval abort.15ad552takesallod-devoff the microvm runtime and adds a check that keeps it off.allod-devis the machine the operator develops from, and it is a real machine, not a synthetic example:allod/secretscarriesallod-dev-forge-key.age,forgejo-https-token-allod-dev.ageand a host key entry keyed to that name. Marking itruntime = "microvm"was therefore pointed at the working environment. Inert today, because nothing reads the fact until the archetypes milestone — but that milestone acts on it by name, and a dev guest that fails to boot takes its own repair environment with it.Renaming was considered and rejected. Per-machine encrypted secret filenames are keyed to the machine name, and re-keying them is a human-only host action, so a rename is not a text edit.
Verified: with this branch overridden into the composition root,
allod-devstill evaluates toi2bbywbv08bfbn6j9cr9awm1gd399pyj, byte-identical to the current lock. Nothing about that machine changes.The check that asserted
allod-devis the public microvm example is replaced by one asserting it stays on libvirt, so the mistake cannot come back quietly.No microvm example machine in this PR, deliberately
The obvious follow-through — add a purpose-made machine to carry the microvm runtime — does not fit in this PR, and it is worth recording why rather than leaving a gap.
A machine entry is not self-contained. Adding one requires a matching identity in
secrets(archetypes asserts the machine and identity key sets are equal), a profile inprofiles, and per-machine encrypted credentials. I built all of that and evaluated it end to end; it fails at the last step withpath '.../secrets/forgejo-https-token-dev-2.age' does not exist. Creating that file needs a real token encrypted to the machine's own host key, and generating a host key is a host-only action behind the agent gates. So the machine cannot be completed from here, and landing the data half alone would leave an entry that the composition root cannot build.The machine that first selects the microvm runtime should therefore be added in the same change that provisions it. The runtime enum's microvm branch stays covered by this PR's mutation fixtures, which do not need an example machine.
nix flake checkgreen.allod-agent referenced this pull request2026-07-30 20:19:54 +01:00
The machine that carries the microvm runtime is now specified and mostly built, but it is not in this PR and this PR stays inert.
It is called
microvm-test, and the work that could be done without a human is pushed on branches: the identity and host key inallod/secrets(agent/microvm-test-machine), and the profile inallod/profiles(same branch name). The inventory entry is written but held back, because adding it here would make this PR change a machine, and it cannot evaluate anyway until the machine's Forgejo token exists — minting that is a human action.allod/inventory#11 carries the whole handover: why the machine exists, what is done, the exact blocker, the steps to finish on the host, the inventory entry to paste, and what has to be repeated privately with real values.
So this PR still does exactly what it says: it adds the runtime fact, keeps both public example machines on libvirt, and keeps
allod-devthere by assertion.