Export the inventory runtime fact through vmFacts #27

Closed
opened 2026-07-31 12:08:11 +01:00 by allod-agent · 0 comments
Member

archetypes.vmFacts.<name> carries every provisioning fact the hypervisor needs except the one that decides how a machine boots. Export the inventory runtime fact through it, so host-side tooling can dispatch on the same discriminator the flake already uses at build time to pick a guest module.

Primary goals:

  • runtime becomes a first-class vmFactvmFacts.<name>.runtime is "libvirt" or "microvm" for every non-hypervisor machine, read from inventory and never defaulted.
  • Missing or unknown data fails loudly — a machine with no runtime, a non-string value, or a value outside the enum throws with a named diagnostic rather than producing a fact the host would act on.
  • Coherence is proven, not assumedvm-facts-coherence compares runtime against the committed scripts/vm-specs.json so vmFacts and inventory cannot drift apart silently.
  • Every new failure mode gets a sabotage fixturevm-facts-negative gains fixtures for the new validation, and the coherence comparison gets its own negative proof, each verified to go red when the thing it checks is removed.
  • Libvirt machines do not move — the four example machine toplevel derivations stay byte-identical.

Current state and specifics

vmFacts is built at flake.nix:42 from mkVmFacts in nix/vm-facts.nix. factFor at nix/vm-facts.nix:76-87 emits exactly five per-machine fields — ip, username, forgeKey, hostKeys, hostKeySecretFile — and runtime is not among them, even though the flake already reads machines.${name}.runtime at flake.nix:434 to select a guest module. The fact exists in the data and is used at build time; it just never reaches the host.

Hypervisors are excluded from vmFacts by includeMachine at nix/vm-facts.nix:45-51, which is what keeps this consistent with inventory's own design: inventory actively rejects a hypervisor that declares runtime, so no hypervisor entry needs a fake guest runtime.

The comparison data already exists. allod/inventory scripts/vm-specs.json carries runtime per machine — both public examples are "libvirt" — and its key set is exactly the non-hypervisor machines, the same set mkVmFacts produces.

The vm-facts-coherence check at flake.nix:960-1014 projects an explicit field set before each diff: { ip: .value.ip, forge_key: .value.forgeKey } against vm-specs.json, and { active, staged } against machine-host-keys.json. Because the projections enumerate fields rather than passing the object through, an added key passes silently and stays unverified. The projection has to name runtime for the three sources to be provably in agreement.

The vm-facts-negative check at flake.nix:1016-1071 holds five expectFailure fixtures over a synthetic baseData, each forcing one field of one machine through builtins.tryEval (builtins.deepSeq value true) and reporting the fixture name when evaluation wrongly succeeds. Its alpha-dev machine carries no runtime today. Validation in nix/vm-facts.nix is lexical throw through named helpers (requireIp at :21-25, requireHostKeys at :27-43), not NixOS assertions, so a new failure mode there is a throw in the same shape.

Neither existing check proves its own comparator works: vm-facts-negative observes only that evaluation failed, and vm-facts-coherence diffs real data that already agrees, so deleting a projection field leaves it green. Anything added here has to come with the negative case that would catch its own removal.

vmFacts is a public flake output read host-side by allod/nexus as <flake>#vmFacts (README.md:46), and mkVmFacts is re-exported in lib, so adding a field is a public interface change and is reviewed as one.

Scope

In scope: nix/vm-facts.nix for the new fact and its validation, and flake.nix for the two checks that must cover it.

Out of scope and tracked elsewhere: guest-module selection, which already reads runtime directly from inventory and does not change here; consuming the new fact host-side, which is the runtime-aware key rotation work in allod/nexus#21 and allod/nexus#22; and allod/inventory, whose half of this contract has already landed.

Part of allod/strategy#20

`archetypes.vmFacts.<name>` carries every provisioning fact the hypervisor needs except the one that decides how a machine boots. Export the inventory `runtime` fact through it, so host-side tooling can dispatch on the same discriminator the flake already uses at build time to pick a guest module. Primary goals: - **`runtime` becomes a first-class vmFact** — `vmFacts.<name>.runtime` is `"libvirt"` or `"microvm"` for every non-hypervisor machine, read from inventory and never defaulted. - **Missing or unknown data fails loudly** — a machine with no `runtime`, a non-string value, or a value outside the enum throws with a named diagnostic rather than producing a fact the host would act on. - **Coherence is proven, not assumed** — `vm-facts-coherence` compares `runtime` against the committed `scripts/vm-specs.json` so vmFacts and inventory cannot drift apart silently. - **Every new failure mode gets a sabotage fixture** — `vm-facts-negative` gains fixtures for the new validation, and the coherence comparison gets its own negative proof, each verified to go red when the thing it checks is removed. - **Libvirt machines do not move** — the four example machine `toplevel` derivations stay byte-identical. ### Current state and specifics `vmFacts` is built at `flake.nix:42` from `mkVmFacts` in `nix/vm-facts.nix`. `factFor` at `nix/vm-facts.nix:76-87` emits exactly five per-machine fields — `ip`, `username`, `forgeKey`, `hostKeys`, `hostKeySecretFile` — and `runtime` is not among them, even though the flake already reads `machines.${name}.runtime` at `flake.nix:434` to select a guest module. The fact exists in the data and is used at build time; it just never reaches the host. Hypervisors are excluded from vmFacts by `includeMachine` at `nix/vm-facts.nix:45-51`, which is what keeps this consistent with inventory's own design: inventory actively rejects a hypervisor that declares `runtime`, so no hypervisor entry needs a fake guest runtime. The comparison data already exists. `allod/inventory` `scripts/vm-specs.json` carries `runtime` per machine — both public examples are `"libvirt"` — and its key set is exactly the non-hypervisor machines, the same set `mkVmFacts` produces. The `vm-facts-coherence` check at `flake.nix:960-1014` projects an explicit field set before each diff: `{ ip: .value.ip, forge_key: .value.forgeKey }` against `vm-specs.json`, and `{ active, staged }` against `machine-host-keys.json`. Because the projections enumerate fields rather than passing the object through, an added key passes silently and stays unverified. The projection has to name `runtime` for the three sources to be provably in agreement. The `vm-facts-negative` check at `flake.nix:1016-1071` holds five `expectFailure` fixtures over a synthetic `baseData`, each forcing one field of one machine through `builtins.tryEval (builtins.deepSeq value true)` and reporting the fixture name when evaluation wrongly succeeds. Its `alpha-dev` machine carries no `runtime` today. Validation in `nix/vm-facts.nix` is lexical `throw` through named helpers (`requireIp` at `:21-25`, `requireHostKeys` at `:27-43`), not NixOS `assertions`, so a new failure mode there is a `throw` in the same shape. Neither existing check proves its own comparator works: `vm-facts-negative` observes only that evaluation failed, and `vm-facts-coherence` diffs real data that already agrees, so deleting a projection field leaves it green. Anything added here has to come with the negative case that would catch its own removal. `vmFacts` is a public flake output read host-side by `allod/nexus` as `<flake>#vmFacts` (README.md:46), and `mkVmFacts` is re-exported in `lib`, so adding a field is a public interface change and is reviewed as one. ### Scope In scope: `nix/vm-facts.nix` for the new fact and its validation, and `flake.nix` for the two checks that must cover it. Out of scope and tracked elsewhere: guest-module selection, which already reads `runtime` directly from inventory and does not change here; consuming the new fact host-side, which is the runtime-aware key rotation work in allod/nexus#21 and allod/nexus#22; and `allod/inventory`, whose half of this contract has already landed. Part of allod/strategy#20
vnprc closed this issue 2026-07-31 20:30:14 +01:00
Sign in to join this conversation.
No description provided.