Read composed machines or own fixtures, never the data inputs by name #91
No reviewers
Labels
No labels
blocked
bug
decision
duplicate
enhancement
help wanted
invalid
landed?
question
ready-to-merge
stale
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/archetypes!91
Loading…
Reference in a new issue
No description provided.
Delete branch "agent/fork-safe-checks"
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?
Framework checks no longer read the
secrets,profilesandinventorydata inputs by name, so a deployment fork can run the whole check suite over its own machines instead of excluding the checks that name the template's.Nothing changes for any running machine. The three composed machines —
allod-dev,privacy-1andnexus— build to byte-identical derivations, measured on the branch and on its base. No rebuild is needed and nothing outside this repo changes.What changes is the suite. Eleven of nineteen checks passed only on the public template data, because they reached into the data inputs by name: a machine called
allod-dev, a credential calledallod-dev-host, a ciphertext filename with a machine name inside it, an inventory that happens to export a check calledruntime-fact-mutations. On a real deployment those checks fail, and the only option was to switch them off and lose the coverage. Every check now reads either the machines as the deployment composes them, or fixtures it builds itself inside this repo.A second synthetic data trio under
checks/fork-fixture/and a new./fork-safety.shkeep it true: a check that names template data fails there the day it is written, rather than on a fork after merge. The framework also no longer requires a deployment's hypervisor to be callednexus.What proves it:
./check.shgreen at 32 steps,./fork-safety.shgreen at 19 of 19 where it was 11 red, and identical derivation paths for every machine. If it is wrong, revert the merge.The rule
A framework check reads exactly one of two things:
machineConfigurationsas the consuming deployment composes them, iterated dynamically and never indexed by a literal name. Whatever names, identities and credentials the deployment supplies are what the check sees. This is the kind that finds real leaks on real data.checks/fixtures/, a platform taken frominventory.lib.supportedPlatforms.Reading a data input's contents by name is neither, and is a defect whether or not the template happens to satisfy it today.
One line this makes explicit, because the rule is unusable without it: a data repo's file layout is contract; its names are not. Production itself reads
${secrets}/git/protected-branchesand${inventory}/scripts/vm-specs.json, and every fork has those files because a fork is a fork of the repo.secrets/forgejo-https-token-allod-dev.ageis different in kind — the name is data. Without that distinction the rule would condemn working production paths.The seam
mkDevVm,mkPrivacyVmandmkHypervisoreach readplatform = machines.${name}.platformwith no override, andmkDevVm/mkHypervisorclosed over thesecretsinput directly. That is why every fixture in the suite had to borrow a real machine name and inherited a real identity with it.platform,runtime,secrets— plushardwareandpreferencesModuleonmkHypervisor— are now parameters with their current values as defaults. Noprofilesparameter was added:definition,preferencesModuleandpiCatalogalready cover every read of that input.Widening the seam had a cost that the review caught.
machineConfigurationsmergesprofileDataand inventoryprofilesettings into builder arguments, so making these parameters would have let the profiles layer override inventory'splatformandhardwareand the identity source itself — keys that were previously unexpected arguments and therefore evaluation errors.productionFactContractnow pins them after the merge, in the shapeproductionPiContractbeside it already established.runtimestays overridable, deliberately:guestModuleFor's throw exists to guard exactly that path.Scope beyond the issue's list
The issue named five checks. The witness found nine, and two framework defects behind them:
host-user-credentialsmkHypervisor { name = "nexus"; }.vm-facts-coherenceinventory.checks.<system>.vm-specs-json, exactly asruntime-module-selectiondepended onruntime-fact-mutations. It now derives the freshness property frominventory.lib.vmSpecsJson, a data surface, instead of delegating to another repo's check.agent-vm-status,pi-integrationmachineConfigurationsandvmFactsby the literalallod-dev.identitieskeyed by a literalnexusnexus, and it masked all eight per-check defects behind one error. Now keyed offnexusIdentity.hostname, which secrets already declares andmkHypervisoralready consumes. Owner-approved. Making that identity plural is tracked as allod/archetypes#89.pi-integrationneeded one further split. It asserts the composed dev machine has Pi installed, and Pi comes from the definitions repo, not from archetypes — so on a fork whose definitions do not install it, the check fails for a reason that is not the framework's. The Pi-dependent assertions moved onto a fixture machine of the check's own making; everythingmodules/ai-agents.nixgenerates on every dev machine stays on the composed machine, which is where it catches real leaks in real generated text.Risk
R3. The composition is provably unchanged, rollback is a straight revert, and nothing touches secrets, provisioning or deployed state. It is not lower because the builder signature is a cross-repo interface:
profileDataand inventory'sprofilesettings are merged into builder arguments, so widening the parameter set widens what a fork's data can reach — which is precisely the defect the review found andproductionFactContractcloses.The useful human scrutiny is not on the seam — the derivation equality settles that — but on whether any rewritten check is now weaker than the one it replaced. A fixture that can no longer fail is worse than no fixture.
Validation
./check.sh(template data)./fork-safety.sh(fork data)allod-dev,privacy-1,nexusidentical to the branch base at81ca50bEvery negative and sabotage fixture touched was neutered, measured failing-to-fail, and reverted.
fork-safety.shis a pin witness, not a per-commit gate. It runs onenixprocess per check because the suite already peaks near this box's memory ceiling, and it belongs on the lock-bump path; it is deliberately not wired intocheck.sh, which would double every commit's evaluation.Review pass
One cold cross-family reviewer,
gpt-5.6-solvia codex athighreasoning effort, on commit313c5ea. Four findings, all verified against the source before being acted on, all fixed with a sabotage proving the fix can fail:profileDataoverridesecrets,platformandhardwareproductionFactContractpins them after the mergedev-forge-opt-outandhost-user-credentialsstopped covering profile-module interference with the credential contractpath/owner/group/modemkHypervisortakespreferencesModule, and fixtures pass an in-repo onefork-safety.shcould report success having built nothingThe credential sweeps deliberately do not compare
age.secrets.<name>.file. Which ciphertext a machine deploys is a per-machine builder argument thatprofileDatamay legitimately set, so comparing it would fail a fork for doing what the builder permits; the carve property of that file is swept for every dev machine byconsumed-file-carve-outalready. Whether those two arguments should join the pinned facts is a boundary question raised in allod/archetypes#90.Two things the review surfaced are tracked rather than fixed here:
check.shhas the same all-evaluated blind spotfork-safety.shjust lost, andfork-safety.shhas no standing selftest of the kindcheck-selftest.shgives its sibling. Both are allod/archetypes#90.Closes allod/archetypes#88
Framework checks reached into the secrets, profiles and inventory inputs by name: a machine called allod-dev, a credential called allod-dev-host, a ciphertext filename carrying a machine name, an inventory that happens to export a check called runtime-fact-mutations. The public data repos are templates with synthetic contents and a deployment forks them with its own, so those checks passed here and failed on every fork, whose only option was to exclude them and lose the coverage. A check now reads exactly one of two things: the composed machines as the consuming deployment composes them, iterated dynamically and never indexed by a literal name, or fixture inputs it constructs itself inside this repository. One line the rule needs to stay usable: a data repo's file layout is contract, since production itself reads ${secrets}/git/protected-branches, but its names are not. Rule two was unreachable before. mkDevVm, mkPrivacyVm and mkHypervisor each read machines.${name}.platform with no override, and mkDevVm and mkHypervisor closed over the secrets input directly, so every fixture had to borrow a real machine name and inherited a real identity with it. platform, runtime and secrets are now parameters defaulting to today's values, and hardware likewise on mkHypervisor, which imports machines.${name}.hardware straight into its module list. No profiles parameter: definition, preferencesModule and piCatalog already cover every read of that input. machineConfigurations passes what it passed before, and all three machine derivations are byte-identical. The witness is what keeps it true. checks/fork-fixture holds a second synthetic data trio sharing no name with the template's, and fork-safety.sh runs the whole suite against it with --override-input, one nix process per check because the suite already peaks near this box's memory ceiling. A check that names template data fails there the day it is written. It is a pin witness for the lock-bump path, not a per-commit gate. Nine checks were rewritten, not the five the issue named: the witness also caught host-user-credentials building its fixture as a literal nexus, vm-facts-coherence depending on an inventory-exported check exactly as runtime-module-selection did, and agent-vm-status and pi-integration indexing composed machines by a literal. It also found one defect outside the suite: identities bound the hypervisor under a literal nexus key, requiring every deployment to use that name. It is keyed off nexusIdentity.hostname now, which secrets already declares. Making that identity plural is a separate change. Refs allod/archetypes#88 Refs allod/archetypes#89