Model service machines: no runtime, x86 only, no guest sizing #14

Merged
vnprc merged 4 commits from agent/service-machine-schema into master 2026-09-02 01:18:48 +01:00
Member

Inventory can now describe a rented, internet-facing machine that this fleet did not create, under a new service type. Nothing about any existing machine changes: allod-dev, privacy-1 and nexus evaluate to exactly what they did before, and scripts/vm-specs.json is byte-identical, because no service machine is added — the first one is added when its host is rented. What proves it is nix flake check: four checks covering the three refusals this type exists to make, plus the shape rules everything else classifies on. If it is wrong, revert the commits; nothing downstream reads a service machine yet.

An adversarial review pass by a second model found five issues after the first commit; the fixes are in b527536 and the findings are recorded in a comment below. One mattered beyond this PR's own scope: platform validation was not in the chain that machines consumers force, so a machine with no platform reached archetypes unvalidated.

What changed

A service machine is a host some provider runs. Three schema rules follow from that, and each is an evaluation error rather than a field that is quietly ignored:

  • No runtime. The existing "hypervisors carry no runtime" rule widens to a runtimeFreeTypes = [ "hypervisor" "service" ] set. A hypervisor is in it because it runs the guests; a service machine is in it because some provider's hypervisor runs it, nothing here can see or configure that, and there is no guest module to select. The rejected alternative — runtime = "rented" — is recorded in the code comment: it keeps every machine uniform at the cost of putting a fictional virtualisation system in a field that otherwise names real ones.
  • platform must be x86_64-linux. platform says nothing about who hosts the machine, so lib.systems.flakeExposed would accept ARM today and the first ARM machine would fail somewhere confusing during provisioning. The refusal is early and names the missing cross-architecture build path.
  • No memory_mb, vcpus, disk_gb, mac. For a guest these are instructions a hypervisor acts on; on a rented machine they would describe what is being paid for, and nothing in the file distinguishes the two.

Service machines are excluded from lib.vmSpecsJson for the same reason hypervisors are: that file tells host tooling how to size and start guests.

runtimeDiagnostics becomes machineDiagnostics and grows from four diagnostic sets to eight. Four are the runtime rules, two are the service rules, and two — missingType/nonStringType — are shape rules the review showed were missing. The disjointness property the original design argues for is preserved and now stated for the wider set: a machine with exactly one problem still trips exactly one diagnostic, which is what lets each fixture be pinned to the diagnostic it claims to exercise rather than to "evaluation failed for some reason".

Validation moved into the chain consumers force

checkedMachines forces mkVmSpecs, so anything asserted there reaches a consumer reading raw machines — the surface archetypes actually uses. Platform validation was not there: it hung off lib.supportedPlatforms, which that path never forces, so a machine with no platform was handed out unvalidated and only failed later somewhere less obvious. missingPlatform and invalidPlatform are now diagnostics asserted inside mkVmSpecs, alongside the new type shape rules, and supportedPlatforms derives from the same chain.

Inventory deliberately does not validate the type value against a set of known archetypes. That set belongs to archetypes, which owns the builders and already rejects a machine whose type has no builder.

Checks

runtime-fact-mutations keeps its name deliberately. allod/archetypes reads inventory.checks.<system>.runtime-fact-mutations by name and throws when it is absent, because enum validation for the runtime fact is this repo's contract to that one; renaming would break that link for no gain. A comment in the file records this. It gains fixtures for a service machine declaring a runtime, for missing and non-string type, for missing and invalid platform, and for a service machine with no platform at all — that last one trips missingPlatform rather than serviceForeignPlatform, which is the guard the review showed was a hole.

service-machine-mutations is new: a positive fixture proving the rules admit the machine they describe and that it stays out of vmSpecsJson, then one sabotage per rule, each pinned to its own diagnostic and each proven to fail the real mkVmSpecsJson path.

diagnosticFields is derived from builtins.attrNames (machineDiagnostics { }) rather than hand-written. The review reproduced the alternative: add a diagnostic and forget the list, and pinnedTo silently stops looking at it.

New exports

lib.runtimeFreeTypes and lib.isGuestMachine. The review found archetypes keeping three separate copies of this classification, one of which had already gone stale against the service type and would have aborted on the first rented host. Machine facts belong here; the export is what stops a fourth copy appearing.

Risk

R1. The change is additive to a schema whose only consumers read guest machines, and no service machine exists for anything to read. The widened runtime-free filter is a no-op while service is unused.

The one behavioural change reaching existing data is that type and platform shape errors now fail on the machines path rather than only via supportedPlatforms. Valid data is unaffected; invalid data fails earlier and more legibly than before.

Validation

nix flake check passes: vm-specs-json, repository-registry, runtime-fact-mutations, service-machine-mutations.

Every witness was confirmed non-vacuous by sabotaging production and observing red:

  • Replacing the serviceForeignPlatform assertion with assert true turned foreign platform: fails mkVmSpecsJson red while its pinnedTo half stayed green — exactly the split those two halves exist to distinguish.
  • Deleting mac from serviceGuestFields turned three lines red.
  • Removing platform from privacy-1 and forcing machines now fails with inventory machines missing platform: privacy-1; before the fix it returned successfully.

Two defects in the checks themselves were found and fixed this way rather than by inspection: the per-field sizing fixtures originally derived from the very list under test, so deleting a field deleted its own witness; and the field-list comparison pinned list order, so a harmless reorder turned it red. It now compares sorted sets against an independently written list.

scripts/vm-specs.json needs no regeneration; the vm-specs-json drift check confirms it still matches.

Closes allod/inventory#13

Inventory can now describe a rented, internet-facing machine that this fleet did not create, under a new `service` type. Nothing about any existing machine changes: `allod-dev`, `privacy-1` and `nexus` evaluate to exactly what they did before, and `scripts/vm-specs.json` is byte-identical, because no service machine is added — the first one is added when its host is rented. What proves it is `nix flake check`: four checks covering the three refusals this type exists to make, plus the shape rules everything else classifies on. If it is wrong, revert the commits; nothing downstream reads a `service` machine yet. An adversarial review pass by a second model found five issues after the first commit; the fixes are in `b527536` and the findings are recorded in a comment below. One mattered beyond this PR's own scope: platform validation was not in the chain that `machines` consumers force, so a machine with no `platform` reached `archetypes` unvalidated. ## What changed A `service` machine is a host some provider runs. Three schema rules follow from that, and each is an evaluation error rather than a field that is quietly ignored: - **No `runtime`.** The existing "hypervisors carry no runtime" rule widens to a `runtimeFreeTypes = [ "hypervisor" "service" ]` set. A hypervisor is in it because it runs the guests; a service machine is in it because some provider's hypervisor runs it, nothing here can see or configure that, and there is no guest module to select. The rejected alternative — `runtime = "rented"` — is recorded in the code comment: it keeps every machine uniform at the cost of putting a fictional virtualisation system in a field that otherwise names real ones. - **`platform` must be `x86_64-linux`.** `platform` says nothing about who hosts the machine, so `lib.systems.flakeExposed` would accept ARM today and the first ARM machine would fail somewhere confusing during provisioning. The refusal is early and names the missing cross-architecture build path. - **No `memory_mb`, `vcpus`, `disk_gb`, `mac`.** For a guest these are instructions a hypervisor acts on; on a rented machine they would describe what is being paid for, and nothing in the file distinguishes the two. Service machines are excluded from `lib.vmSpecsJson` for the same reason hypervisors are: that file tells host tooling how to size and start guests. `runtimeDiagnostics` becomes `machineDiagnostics` and grows from four diagnostic sets to eight. Four are the runtime rules, two are the service rules, and two — `missingType`/`nonStringType` — are shape rules the review showed were missing. The disjointness property the original design argues for is preserved and now stated for the wider set: a machine with exactly one problem still trips exactly one diagnostic, which is what lets each fixture be pinned to the diagnostic it claims to exercise rather than to "evaluation failed for some reason". ## Validation moved into the chain consumers force `checkedMachines` forces `mkVmSpecs`, so anything asserted there reaches a consumer reading raw `machines` — the surface `archetypes` actually uses. Platform validation was not there: it hung off `lib.supportedPlatforms`, which that path never forces, so a machine with no `platform` was handed out unvalidated and only failed later somewhere less obvious. `missingPlatform` and `invalidPlatform` are now diagnostics asserted inside `mkVmSpecs`, alongside the new `type` shape rules, and `supportedPlatforms` derives from the same chain. Inventory deliberately does not validate the `type` *value* against a set of known archetypes. That set belongs to `archetypes`, which owns the builders and already rejects a machine whose type has no builder. ## Checks `runtime-fact-mutations` keeps its name deliberately. `allod/archetypes` reads `inventory.checks.<system>.runtime-fact-mutations` by name and throws when it is absent, because enum validation for the runtime fact is this repo's contract to that one; renaming would break that link for no gain. A comment in the file records this. It gains fixtures for a service machine declaring a runtime, for missing and non-string `type`, for missing and invalid `platform`, and for a service machine with no `platform` at all — that last one trips `missingPlatform` rather than `serviceForeignPlatform`, which is the guard the review showed was a hole. `service-machine-mutations` is new: a positive fixture proving the rules admit the machine they describe and that it stays out of `vmSpecsJson`, then one sabotage per rule, each pinned to its own diagnostic and each proven to fail the real `mkVmSpecsJson` path. `diagnosticFields` is derived from `builtins.attrNames (machineDiagnostics { })` rather than hand-written. The review reproduced the alternative: add a diagnostic and forget the list, and `pinnedTo` silently stops looking at it. ## New exports `lib.runtimeFreeTypes` and `lib.isGuestMachine`. The review found `archetypes` keeping three separate copies of this classification, one of which had already gone stale against the `service` type and would have aborted on the first rented host. Machine facts belong here; the export is what stops a fourth copy appearing. ## Risk R1. The change is additive to a schema whose only consumers read guest machines, and no service machine exists for anything to read. The widened runtime-free filter is a no-op while `service` is unused. The one behavioural change reaching existing data is that `type` and `platform` shape errors now fail on the `machines` path rather than only via `supportedPlatforms`. Valid data is unaffected; invalid data fails earlier and more legibly than before. ## Validation `nix flake check` passes: `vm-specs-json`, `repository-registry`, `runtime-fact-mutations`, `service-machine-mutations`. Every witness was confirmed non-vacuous by sabotaging production and observing red: - Replacing the `serviceForeignPlatform` assertion with `assert true` turned `foreign platform: fails mkVmSpecsJson` red while its `pinnedTo` half stayed green — exactly the split those two halves exist to distinguish. - Deleting `mac` from `serviceGuestFields` turned three lines red. - Removing `platform` from `privacy-1` and forcing `machines` now fails with `inventory machines missing platform: privacy-1`; before the fix it returned successfully. Two defects in the checks themselves were found and fixed this way rather than by inspection: the per-field sizing fixtures originally derived from the very list under test, so deleting a field deleted its own witness; and the field-list comparison pinned list order, so a harmless reorder turned it red. It now compares sorted sets against an independently written list. `scripts/vm-specs.json` needs no regeneration; the `vm-specs-json` drift check confirms it still matches. Closes allod/inventory#13
Author
Member

Adversarial review pass (gpt-5.6-sol, xhigh) — findings and fixes

An independent read-only review was run against d86175e with a different model, driven to sabotage production code and observe whether the checks went red. It found five issues; four are fixed in b527536 and one is answered.

1. High, fixed — a machine with no platform reached machines consumers unvalidated. Platform validation hung off supportedPlatforms, which checkedMachines never forces, so the surface archetypes actually reads could hand out a machine with no platform at all. This was pre-existing, but this PR's own comment claimed the opposite, so it is fixed here rather than deferred: missingPlatform and invalidPlatform are now diagnostics in machineDiagnostics, asserted inside mkVmSpecs. Forcing machines now fails with inventory machines missing platform: <name> where it previously succeeded.

Verified by removing platform from privacy-1 and forcing f.machines: green before the fix, red after.

2. High, fixed — type was read before it was validated. isRuntimeFree does builtins.elem m.type, so a machine with no type aborted with a raw attribute 'type' missing — an error tryEval cannot catch, so no fixture could ever pin it, and machineDiagnostics did not have the "pure, non-throwing" property it claimed. missingType and nonStringType are now guarded diagnostics classified before anything reads the field.

Inventory deliberately does not validate the type value against a set of known archetypes. That set belongs to archetypes, which owns the builders and already rejects a machine whose type has no builder. Duplicating it here would be a second source of truth for a fact this repo does not own.

3. Medium, fixed — diagnosticFields was a second, unvalidated registry. A diagnostic added to machineDiagnostics but forgotten in the hand-written list would be silently skipped by pinnedTo, so a fixture tripping two diagnostics still reported as pinned to one. The reviewer reproduced this by adding an overlapping diagnostic and watching the check stay green. It is now derived: builtins.attrNames (machineDiagnostics { }).

4. Low, fixed — the guest-field witness pinned list order. Reordering serviceGuestFields changed no behaviour but turned the check red. Now compared as sorted sets.

5. Corrected a false comment. The PR claimed the service-shape rules assert before the runtime rules. They do not: runtimeDeclared asserts before it forces serviceShape, so a machine wrong in two ways reports the runtime message. Nothing depends on the order — every fixture declares exactly one problem, which is what the disjointness property buys — but the comment now says what actually happens.

Six new pinned fixtures cover the added diagnostics, including a service machine with no platform at all, which trips missingPlatform rather than serviceForeignPlatform. That case is exactly the hole finding 1 opened, so it is now a witness rather than an argument.

Also added

lib.runtimeFreeTypes and lib.isGuestMachine are now exported. The review found that archetypes keeps three separate copies of this classification and that one of them — runtime-module-selection's hypervisor-only filter — had already gone stale against the service type and would have aborted on the first rented host. That is fixed on the archetypes side; the export is what stops the next copy appearing.

Not changed

The reviewer suggested collapsing the four guest-sizing sabotages to one, citing the one-sabotage-per-validator policy. Kept: with the field list now compared as a set, the four fixtures are what stop a validator that only ever inspects the first field from passing. They are pure evaluation and cost nothing.

## Adversarial review pass (gpt-5.6-sol, xhigh) — findings and fixes An independent read-only review was run against `d86175e` with a different model, driven to sabotage production code and observe whether the checks went red. It found five issues; four are fixed in `b527536` and one is answered. **1. High, fixed — a machine with no `platform` reached `machines` consumers unvalidated.** Platform validation hung off `supportedPlatforms`, which `checkedMachines` never forces, so the surface `archetypes` actually reads could hand out a machine with no `platform` at all. This was pre-existing, but this PR's own comment claimed the opposite, so it is fixed here rather than deferred: `missingPlatform` and `invalidPlatform` are now diagnostics in `machineDiagnostics`, asserted inside `mkVmSpecs`. Forcing `machines` now fails with `inventory machines missing platform: <name>` where it previously succeeded. Verified by removing `platform` from `privacy-1` and forcing `f.machines`: green before the fix, red after. **2. High, fixed — `type` was read before it was validated.** `isRuntimeFree` does `builtins.elem m.type`, so a machine with no `type` aborted with a raw `attribute 'type' missing` — an error `tryEval` cannot catch, so no fixture could ever pin it, and `machineDiagnostics` did not have the "pure, non-throwing" property it claimed. `missingType` and `nonStringType` are now guarded diagnostics classified before anything reads the field. Inventory deliberately does not validate the `type` *value* against a set of known archetypes. That set belongs to `archetypes`, which owns the builders and already rejects a machine whose type has no builder. Duplicating it here would be a second source of truth for a fact this repo does not own. **3. Medium, fixed — `diagnosticFields` was a second, unvalidated registry.** A diagnostic added to `machineDiagnostics` but forgotten in the hand-written list would be silently skipped by `pinnedTo`, so a fixture tripping two diagnostics still reported as pinned to one. The reviewer reproduced this by adding an overlapping diagnostic and watching the check stay green. It is now derived: `builtins.attrNames (machineDiagnostics { })`. **4. Low, fixed — the guest-field witness pinned list order.** Reordering `serviceGuestFields` changed no behaviour but turned the check red. Now compared as sorted sets. **5. Corrected a false comment.** The PR claimed the service-shape rules assert before the runtime rules. They do not: `runtimeDeclared` asserts before it forces `serviceShape`, so a machine wrong in two ways reports the runtime message. Nothing depends on the order — every fixture declares exactly one problem, which is what the disjointness property buys — but the comment now says what actually happens. Six new pinned fixtures cover the added diagnostics, including a service machine with no `platform` at all, which trips `missingPlatform` rather than `serviceForeignPlatform`. That case is exactly the hole finding 1 opened, so it is now a witness rather than an argument. ## Also added `lib.runtimeFreeTypes` and `lib.isGuestMachine` are now exported. The review found that `archetypes` keeps three separate copies of this classification and that one of them — `runtime-module-selection`'s hypervisor-only filter — had already gone stale against the `service` type and would have aborted on the first rented host. That is fixed on the archetypes side; the export is what stops the next copy appearing. ## Not changed The reviewer suggested collapsing the four guest-sizing sabotages to one, citing the one-sabotage-per-validator policy. Kept: with the field list now compared as a set, the four fixtures are what stop a validator that only ever inspects the first field from passing. They are pure evaluation and cost nothing.
Author
Member

Second-round adversarial review — the fixes themselves

The first round reviewed d86175e; the fixes landed in b527536. Those fixes were written by the same author who wrote the defects and verified only by sabotages that author designed, so a second round was run with the fix commit as its target. It confirmed F1–F4 closed and found two new defects, both introduced by the fixes. Both are fixed in 50f0ec1 and 0c41feb.

1. High, fixed — the newly exported lib.isGuestMachine had no witness at all. The reviewer replaced it with isGuestMachine = _: true; and both mutation checks stayed green, because nothing in this repo consumes it — archetypes does. An exported interface that cannot be shown to fail is exactly what principle 11 refuses, and its consumers living in another repo is the reason it needs a witness here, not the reason it does not.

It was also partial: isGuestMachine { } raised a raw attribute 'type' missing that builtins.tryEval cannot catch, and isGuestMachine { type = 42; } returned true, silently classifying a malformed machine as a guest. That is the same defect this PR had just fixed internally by making machineDiagnostics non-throwing — reintroduced one commit later in the export.

Now: isGuestMachine is hoisted into the flake's let so the check drives the exact value consumers get rather than a second copy; it raises named, catchable errors for missing and non-string type; and service-machine-mutations carries seven witnesses over it. The reviewer's exact sabotage now fails with four errors.

2. Low, fixed — "shape before meaning" was false, and it had changed a diagnostic. A let-binding's assertions fire when the binding is forced, not where it is written. machineShape was threaded through as serviceShape's return value, which made it the last thing evaluated, so a machine missing both platform and runtime reported the runtime problem — where the old code reported the platform one. The comment claimed the opposite.

mkVmSpecs now ends in an explicit builtins.seq machineShape (builtins.seq serviceShape (builtins.seq runtimeShape ...)), so the order is produced by forcing rather than by layout. Verified: that same input now reports inventory machines missing platform: privacy-1, restoring the old diagnostic and making the claim true. The four runtime assertions are consolidated into one binding in written order, so written order and actual order now agree throughout.

The lib.runtimeFreeTypes and lib.isGuestMachine rows were also missing from the exported-outputs table; added.

What the second round confirmed

  • F1 closed. Removing platform from a real machine now fails through machines, lib.supportedPlatforms and checks with the intended diagnostic. No recursion from the reworked supportedPlatforms.
  • F2 closed on the validation path. Missing and non-string type reach their named diagnostics through checkedMachines and mkVmSpecsJson.
  • F3 closed for the current classifier: all diagnostic keys are unconditional members of machineDiagnostics, so builtins.attrNames (machineDiagnostics { }) yields the full set, and adding an overlapping diagnostic makes pinnedTo fail.
  • F4 closed. Reordering serviceGuestFields stays green; deleting mac goes red.
  • The typed filter does not create a bypass: type = 42 with guest sizing set is rejected as nonStringType on both machines and vmSpecsJson.
  • Every new fixture is pinned to the diagnostic it names, including the service-without-platform one.

One behavioural change is worth stating plainly rather than burying: lib.supportedPlatforms is now stricter. A machine with a valid platform but a missing runtime used to return a platform list from the old platform-only calculation; it now fails. That input was already invalid under checkedMachines, so this is fail-closed strengthening rather than a regression, but it is a real difference.

## Second-round adversarial review — the fixes themselves The first round reviewed `d86175e`; the fixes landed in `b527536`. Those fixes were written by the same author who wrote the defects and verified only by sabotages that author designed, so a second round was run with the fix commit as its target. It confirmed F1–F4 closed and found two new defects, both introduced by the fixes. Both are fixed in `50f0ec1` and `0c41feb`. **1. High, fixed — the newly exported `lib.isGuestMachine` had no witness at all.** The reviewer replaced it with `isGuestMachine = _: true;` and both mutation checks stayed green, because nothing in this repo consumes it — `archetypes` does. An exported interface that cannot be shown to fail is exactly what principle 11 refuses, and its consumers living in another repo is the reason it needs a witness here, not the reason it does not. It was also partial: `isGuestMachine { }` raised a raw `attribute 'type' missing` that `builtins.tryEval` cannot catch, and `isGuestMachine { type = 42; }` returned `true`, silently classifying a malformed machine as a guest. That is the same defect this PR had just fixed *internally* by making `machineDiagnostics` non-throwing — reintroduced one commit later in the export. Now: `isGuestMachine` is hoisted into the flake's `let` so the check drives the exact value consumers get rather than a second copy; it raises named, catchable errors for missing and non-string `type`; and `service-machine-mutations` carries seven witnesses over it. The reviewer's exact sabotage now fails with four errors. **2. Low, fixed — "shape before meaning" was false, and it had changed a diagnostic.** A let-binding's assertions fire when the binding is *forced*, not where it is written. `machineShape` was threaded through as `serviceShape`'s return value, which made it the last thing evaluated, so a machine missing both `platform` and `runtime` reported the runtime problem — where the old code reported the platform one. The comment claimed the opposite. `mkVmSpecs` now ends in an explicit `builtins.seq machineShape (builtins.seq serviceShape (builtins.seq runtimeShape ...))`, so the order is produced by forcing rather than by layout. Verified: that same input now reports `inventory machines missing platform: privacy-1`, restoring the old diagnostic and making the claim true. The four runtime assertions are consolidated into one binding in written order, so written order and actual order now agree throughout. The `lib.runtimeFreeTypes` and `lib.isGuestMachine` rows were also missing from the exported-outputs table; added. ## What the second round confirmed - **F1 closed.** Removing `platform` from a real machine now fails through `machines`, `lib.supportedPlatforms` and `checks` with the intended diagnostic. No recursion from the reworked `supportedPlatforms`. - **F2 closed** on the validation path. Missing and non-string `type` reach their named diagnostics through `checkedMachines` and `mkVmSpecsJson`. - **F3 closed** for the current classifier: all diagnostic keys are unconditional members of `machineDiagnostics`, so `builtins.attrNames (machineDiagnostics { })` yields the full set, and adding an overlapping diagnostic makes `pinnedTo` fail. - **F4 closed.** Reordering `serviceGuestFields` stays green; deleting `mac` goes red. - The `typed` filter does not create a bypass: `type = 42` with guest sizing set is rejected as `nonStringType` on both `machines` and `vmSpecsJson`. - Every new fixture is pinned to the diagnostic it names, including the service-without-platform one. One behavioural change is worth stating plainly rather than burying: `lib.supportedPlatforms` is now stricter. A machine with a valid platform but a missing `runtime` used to return a platform list from the old platform-only calculation; it now fails. That input was already invalid under `checkedMachines`, so this is fail-closed strengthening rather than a regression, but it is a real difference.
vnprc approved these changes 2026-09-02 01:18:38 +01:00
vnprc merged commit 0c41febed9 into master 2026-09-02 01:18:48 +01:00
vnprc deleted branch agent/service-machine-schema 2026-09-02 01:18:48 +01:00
Sign in to join this conversation.
No description provided.