Export the inventory runtime fact through vmFacts #30

Merged
vnprc merged 3 commits from agent/vmfacts-runtime into master 2026-07-31 20:30:13 +01:00
Member

archetypes.vmFacts.<name>.runtime now reports the machine's inventory runtime, so a host-side tool can tell a libvirt VM from a microvm without evaluating a NixOS configuration.

This is the remaining half of contract 1 in the microvm framework adoption plan — inventory's half (the required fact, its enum, lib.vmSpecsJson, and the committed scripts/vm-specs.json) already landed, and this adds the third source of truth that contract makes agree. It is sequenced ahead of the rest of milestone 4 because contract 18 makes both key-rotation tools dispatch on this exact attribute, so allod/nexus#21 and allod/nexus#22 cannot start until it exists.

factFor gains one field with a presence guard and a distinct non-string throw, in the file's existing helper style. There is no enum in mkVmFacts and no default: inventory owns the runtime enum, which this repo already recorded as a deliberate delegation, and the delegation is now made real on both sides — the pre-existing dependency on runtime-fact-mutations plus a new one on vm-specs-json, so the committed JSON this check diffs against is known fresh rather than assumed.

Plan: allod/strategy dev-plans/vmfacts-runtime-export.md (allod/strategy#31).

Risk

R2 Medium. vmFacts is a public flake output read host-side by allod/nexus during provisioning, rebuild, and key rotation, so changing its shape is a public interface change with cross-repo ordering. Held below R3 because the output change is purely additive and every consumer across seven repos was enumerated and independently re-derived: none iterates the per-machine attrset's keys, destructures without an ellipsis, pattern-matches, or compares whole objects, and no schema, key-set, or key-count assertion exists anywhere. allod/deploy is a bare passthrough. No host command, secret, provisioning action, or persistent state is touched, and rollback is a straight revert.

One thing the "additive" framing does not cover, stated plainly: mkVmFacts's input contract is a breaking change. machines.<name>.runtime is now required, so a caller passing its own machine set that previously evaluated will now fail with vmFacts.<name>: missing runtime. This is inert today — lib.mkVmFacts has no caller outside this repo — but a deploy flake author driving it directly needs the field.

The accepted gap: a caller passing non-inventory data to lib.mkVmFacts gets presence and type validation but no enum, so runtime = "bhyve" would evaluate. The real export cannot do this — inventory's checkedMachines trip-wire forces the enum assert before archetypes reads a value. The mitigation is on the consumer side, and it is a review requirement for the milestone 5 PRs: allod/nexus#21 and allod/nexus#22 must fail closed on an unrecognized runtime rather than defaulting to the libvirt branch.

Validation

The libvirt no-op, taken across the change — all four byte-identical to the pre-change values:

Machine drvPath hash
allod-dev 1lhcxfhy9iddfssp3df3r2zwkwnd3ilm
privacy-1 zzzaralwv032jdmbqc1q6ms9fm15w7yf
nexus fad4pq24f8iavzr3q369alz8932jq9q3
installer 2yj5038jnsgzbv6ghhmyafs5kspn9jrg

nix flake check --print-build-logs green, 10 checks, peak RSS 5.71 GB — unchanged, since this slice adds no derivation that evaluates a NixOS configuration. Contract 1's own acceptance command passes, and vmFacts still carries no hypervisor entry.

Every new assertion has a sabotage fixture verified to go red for its own diagnostic, then restored. Two independent adversarial review passes attacked the checks — neither by the agent that wrote the code under review — and each found the new coverage green where it should have been red. Both rounds are fixed here.

First pass:

  • The positive passthrough assertion could not detect a non-passthrough factFor. Both real machines and the fixture were "libvirt", so a factFor returning that literal instead of machine.runtime — a direct violation of the passthrough rule — left every check green. The fixture now uses a runtime that differs from the real fleet, and that difference is itself asserted rather than commented, so a later migration to microvm cannot silently restore the defect.
  • The drift proof's failure pin was too loose. It grepped the comparator's own error string, which is emitted for divergence in any projected field, and also when the candidate-side jq fails inside an if condition. A name-set mismatch, a malformed fixture, and drift on ip alone each satisfied it with runtime untouched.

Second pass, on the fix commit:

  • Pinning the facts side of the diff alone was still not enough. diff lines carry no machine context, so any fixture that adds an entry carrying the drift value satisfied the pin with zero drift on a real machine — reproduced by a one-character typo in the fixture's own override key, which turns the override into an addition and passed green. The pin is now two-sided, matching both halves of one changed hunk, so a pure addition (no < line) is rejected.
  • The comparator-error grep was unfalsifiable — the function returns non-zero only when the diff failed, which is exactly when that message was already emitted. It is removed rather than left as an assertion that cannot fail.
  • A comment over-claimed. It said the assertion proves nothing computes, normalizes, or defaults the value; lib.toLower was measured leaving both checks green. The comment now claims only what is proven: that no constant survives.

Five wrong-reason fixtures now go red — name-set mismatch, malformed candidate, ip-only drift, added machine carrying the drift value, and the typo case — and both grep patterns were broken deliberately to confirm they fail closed. The drift value is derived from the real one, so a legitimate inventory change cannot turn the fixture into a false alarm.

Closes allod/archetypes#27
Refs allod/strategy#20

`archetypes.vmFacts.<name>.runtime` now reports the machine's inventory runtime, so a host-side tool can tell a libvirt VM from a microvm without evaluating a NixOS configuration. This is the remaining half of **contract 1** in the microvm framework adoption plan — inventory's half (the required fact, its enum, `lib.vmSpecsJson`, and the committed `scripts/vm-specs.json`) already landed, and this adds the third source of truth that contract makes agree. It is sequenced ahead of the rest of milestone 4 because **contract 18** makes both key-rotation tools dispatch on this exact attribute, so allod/nexus#21 and allod/nexus#22 cannot start until it exists. `factFor` gains one field with a presence guard and a distinct non-string `throw`, in the file's existing helper style. There is no enum in `mkVmFacts` and no default: inventory owns the runtime enum, which this repo already recorded as a deliberate delegation, and the delegation is now made real on both sides — the pre-existing dependency on `runtime-fact-mutations` plus a new one on `vm-specs-json`, so the committed JSON this check diffs against is known fresh rather than assumed. Plan: `allod/strategy` `dev-plans/vmfacts-runtime-export.md` (allod/strategy#31). ## Risk R2 Medium. `vmFacts` is a public flake output read host-side by `allod/nexus` during provisioning, rebuild, and key rotation, so changing its shape is a public interface change with cross-repo ordering. Held below R3 because the output change is purely additive and every consumer across seven repos was enumerated and independently re-derived: none iterates the per-machine attrset's keys, destructures without an ellipsis, pattern-matches, or compares whole objects, and no schema, key-set, or key-count assertion exists anywhere. `allod/deploy` is a bare passthrough. No host command, secret, provisioning action, or persistent state is touched, and rollback is a straight revert. One thing the "additive" framing does not cover, stated plainly: **`mkVmFacts`'s input contract is a breaking change.** `machines.<name>.runtime` is now required, so a caller passing its own machine set that previously evaluated will now fail with `vmFacts.<name>: missing runtime`. This is inert today — `lib.mkVmFacts` has no caller outside this repo — but a deploy flake author driving it directly needs the field. The accepted gap: a caller passing non-inventory data to `lib.mkVmFacts` gets presence and type validation but no enum, so `runtime = "bhyve"` would evaluate. The real export cannot do this — inventory's `checkedMachines` trip-wire forces the enum assert before archetypes reads a value. The mitigation is on the consumer side, and it is a review requirement for the milestone 5 PRs: allod/nexus#21 and allod/nexus#22 must fail closed on an unrecognized runtime rather than defaulting to the libvirt branch. ## Validation The libvirt no-op, taken across the change — all four byte-identical to the pre-change values: | Machine | drvPath hash | |---|---| | `allod-dev` | `1lhcxfhy9iddfssp3df3r2zwkwnd3ilm` | | `privacy-1` | `zzzaralwv032jdmbqc1q6ms9fm15w7yf` | | `nexus` | `fad4pq24f8iavzr3q369alz8932jq9q3` | | `installer` | `2yj5038jnsgzbv6ghhmyafs5kspn9jrg` | `nix flake check --print-build-logs` green, 10 checks, peak RSS 5.71 GB — unchanged, since this slice adds no derivation that evaluates a NixOS configuration. Contract 1's own acceptance command passes, and `vmFacts` still carries no hypervisor entry. Every new assertion has a sabotage fixture verified to go red for its own diagnostic, then restored. Two independent adversarial review passes attacked the checks — neither by the agent that wrote the code under review — and each found the new coverage green where it should have been red. Both rounds are fixed here. First pass: - **The positive passthrough assertion could not detect a non-passthrough `factFor`.** Both real machines and the fixture were `"libvirt"`, so a `factFor` returning that literal instead of `machine.runtime` — a direct violation of the passthrough rule — left every check green. The fixture now uses a runtime that differs from the real fleet, and that difference is itself asserted rather than commented, so a later migration to microvm cannot silently restore the defect. - **The drift proof's failure pin was too loose.** It grepped the comparator's own error string, which is emitted for divergence in any projected field, and also when the candidate-side `jq` fails inside an `if` condition. A name-set mismatch, a malformed fixture, and drift on `ip` alone each satisfied it with `runtime` untouched. Second pass, on the fix commit: - **Pinning the facts side of the diff alone was still not enough.** `diff` lines carry no machine context, so any fixture that *adds* an entry carrying the drift value satisfied the pin with zero drift on a real machine — reproduced by a one-character typo in the fixture's own override key, which turns the override into an addition and passed green. The pin is now two-sided, matching both halves of one changed hunk, so a pure addition (no `<` line) is rejected. - **The comparator-error grep was unfalsifiable** — the function returns non-zero only when the diff failed, which is exactly when that message was already emitted. It is removed rather than left as an assertion that cannot fail. - **A comment over-claimed.** It said the assertion proves nothing computes, normalizes, or defaults the value; `lib.toLower` was measured leaving both checks green. The comment now claims only what is proven: that no constant survives. Five wrong-reason fixtures now go red — name-set mismatch, malformed candidate, `ip`-only drift, added machine carrying the drift value, and the typo case — and both grep patterns were broken deliberately to confirm they fail closed. The drift value is derived from the real one, so a legitimate inventory change cannot turn the fixture into a false alarm. Closes allod/archetypes#27 Refs allod/strategy#20
vmFacts.<name>.runtime is machines.<name>.runtime exactly as inventory
declares it: a passthrough, with presence and type validation in
vm-facts.nix's existing throw-helper style and no enum, since inventory
owns [ libvirt microvm ] and gets it by construction through its own
checkedMachines trip-wire.

vm-facts-coherence now diffs the new field against inventory's committed
scripts/vm-specs.json. Both sides are real data that already agree, so
extending the projection alone would leave a comparator never once
observed to fail; the projection-and-diff is therefore a shell function
run twice, on the real facts and on a fixture differing only in one
machine's runtime. It returns 1 rather than exiting so the sabotage
invocation cannot kill the builder under the stdenv's errexit, and its
failure is pinned to the specific diff error string so a jq or fixture
problem cannot pass for the drift it is meant to prove. The sabotaged
value is a valid enum member for the same reason. The check also gains a
dependency on inventory's vm-specs-json check, since a downstream flake
check does not evaluate an input's checks and the committed file's
freshness was otherwise assumed.

vm-facts-negative gains a missing-runtime fixture, a non-string fixture
and a positive passthrough assertion, and README enumerates the vmFacts
per-machine field set.

The four libvirt toplevel drvPaths are byte-identical across the change.

Refs allod/archetypes#27
Refs allod/strategy#20
Review found both new proofs passing the good case while a real defect
walked through them.

The positive passthrough assert could not see a non-passthrough factFor.
Both real machines and the fixture were libvirt, so a requireRuntime
whose success branch returned that literal — nothing computed,
normalized or defaulted, exactly what contract 1 forbids — left both
vm-facts-negative and vm-facts-coherence green. Verified. The fixture now
declares microvm, the enum member the real fleet does not use, and the
assert follows it, so any constant fails either the assert or the
coherence diff.

The drift pin did not pin to runtime. Calling the comparator from an if
condition suppresses errexit for its whole body, so a fixture that breaks
jq outright still reaches the diff with one side empty and emits the
comparator's own message. Three fixtures that never touch runtime — a
name-set mismatch, a candidate-side jq error, and drift on ip alone — all
left the check green. Verified. A second grep now reads the facts side of
the diff and demands the runtime field itself moved.

Also: the drift value is derived rather than hard-coded, so allod-dev
legitimately moving to microvm cannot false-red the check, and it feeds
both the fixture and the grep from one binding; each invocation writes
its own intermediate files; the error string has one definition; and the
comment about the builder's shell options is corrected to what was
measured inside it — errexit and pipefail on, nounset off, and function
bodies inheriting errexit unconditionally rather than via inherit_errexit.

Deviation from the dev plan, deliberately: contract 5 specifies the
fixture baseline as runtime = libvirt. That is what makes the passthrough
assert unfalsifiable, which is the defect the plan itself exists to
prevent, so the fixture uses microvm instead.

The four libvirt toplevel drvPaths are still byte-identical.

Refs allod/archetypes#27
Refs allod/strategy#20
The coherence check read only the diff's > lines, which carry no machine
context, so any fixture that merely added an entry carrying driftRuntime
satisfied it with zero drift on a real machine; a one-character typo in
the fixture's attribute name turned the override into an addition and
still passed. Bind realRuntime beside driftRuntime and require the
inventory side of the same hunk too, so the fixture proves the comparator
saw allod-dev's runtime replaced rather than a new name appear.

Drop the grep for the comparator's own ERROR line. project_and_diff
returns non-zero iff the diff failed, which is iff that message was
already echoed, so it was true on every path reaching it. The two-sided
pin covers what it looked like it covered: a jq blow-up leaves the facts
side empty, which yields < lines only, and the > grep goes red.

Say what the negative check's narrow assert actually proves - that no
constant survives, the fixture runtime and the fleet's differing - not
that nothing normalizes the fact; lib.toLower in requireRuntime's success
branch leaves both checks green. Assert the property that makes the split
work instead of claiming it in a comment: no machine in vmFacts may use
the fixture's runtime.
vnprc approved these changes 2026-07-31 20:30:08 +01:00
vnprc merged commit dab1201d49 into master 2026-07-31 20:30:13 +01:00
vnprc deleted branch agent/vmfacts-runtime 2026-07-31 20:30:14 +01:00
Sign in to join this conversation.
No description provided.