feat(step7): host qualification and the execution binding, kept apart - #356
Merged
Merged
Conversation
Two artifacts, because they answer two questions. Qualification: does this one environment satisfy the T0-7 predicate. Execution binding: which Linux and Windows environments, which candidates and which instrument form THIS campaign. A utility that checks a CPU governor must not become the root of campaign identity, so `hostqual.py` neither issues nor owns the binding. `hostqual.py` splits evidence into the two classes that are not the same thing. DECLARED provisioning — dedication, absence of other users, hypervisor configuration — is content-addressed, shape-checked and never called machine proof: a guest OS cannot establish what the hypervisor is doing. MACHINE-OBSERVED is what a checker actually asserts: `manifest.provenance.ci == false` (the field is always present, so a predicate demanding its absence could never be met), `performance` governor on every applicable CPU plus an identified turbo mechanism, the Windows active plan with AC processor state pinned at 100/100, and the stratum's memory mechanism actually existing on the host. Quiesce is implemented exactly, not approximately: twelve 5 s intervals over the final minute, `/proc/stat` on Linux and `GetSystemTimes` on Windows, mean below 5 % and no interval above 20 %. A missing sample, a counter that went backwards or a zero denominator is NOT_ELIGIBLE rather than a skipped interval — an unreadable machine is not a quiet machine. A quiesce failure starts no clock, so it is not INVALID and costs no retry budget. A qualification is not a certificate of perpetual quiet. Session eligibility is proved again per session, against a fresh manifest, and the record names the execution binding, the qualification and that manifest — so a qualified host that this campaign never bound cannot be substituted into the middle of it. `execbinding.py` carries references and identity, never copies: duplicating the governor or the provisioning blob would create two copies of one fact, and two copies drift. It requires both strata, refuses an unqualified or mislabelled host, refuses a binding whose strata share one memory metric, and refuses to overwrite an existing binding — a rebuild before the first clock is legitimate but never silent. Both platform branches were driven live, not only by fixture. On Windows the first live run found a defect in this tool's own powercfg parse: it read the range's minimum as the current setting and reported 0 % on a machine pinned at 100 %. The block ends with the two current indices, AC then DC, and everything before them describes the possible range. Fixed here, with the labels never parsed — on this Russian Windows every one of them is localized. On Linux the tool correctly refuses WSL, where no cpufreq governor exists. step 7 host qualification controls: 10 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
The mini-review found a hole in the middle of a chain that was otherwise content-addressed end to end: a qualification said "this host satisfies T0-7" without naming WHICH T0. SHAs everywhere and the causation missing anyway. Q1 — `--qualify` now requires the T0 path and commit, proves the commit exists, that `blob_sha` really is `commit:path`, that the bytes hash to the recorded sha256, and that the document declares FROZEN. A qualification against a NOT_FROZEN T0 is refused, so today's reconnaissance cannot become campaign evidence by reuse. `execbinding` refuses a host qualified under a different T0 than the campaign binds. Q2 — `--environment-id` is gone. The id is the manifest's own observed value and the provisioning declaration must agree with it: one identity, not three strings that agree while everyone behaves. Q3 — provisioning is checked by VALUE, not only by shape. `dedicated_to_p022` and `no_concurrent_user_workload` must be true, `hosted_ci_runner` false, `is_vm` a real boolean; a VM must promise fixed vCPU, fixed RAM, no live migration and no dynamic memory, with `n/a` unavailable to it, and a physical host must answer those four with an explicit `n/a: <reason>` rather than bare booleans. A VM declaring `fixed_vcpu: false` could previously qualify. Q4 — per-session operator facts left the host record. "No campaign workload", "no interactive user", "no prohibited background job" are properties of a moment, and now live in a session declaration bound by sha256 into eligibility. Q5 — the 120 s window is waited, not asserted. The quiet minute really elapses before the twelve 5 s intervals; a control drives an injected clock and requires 60 + 60. A constant nobody waits for is documentation. Q6 — identity is compared as a whole. The qualification records a canonical hash of the manifest's identity block and the session must reproduce it, so a changed kernel, CPU count, RAM or toolchain can no longer walk past a matching fingerprint. Q7/Q8 — the candidate is hashed against the binding before the clock, and a new `--session-postflight` pass re-checks identity, power and candidate afterwards and requires closing-probe evidence. Preflight may not certify what a session did after it started. Q9 — power is a structured snapshot, compared by equality through the session. Owner ruling applied: Windows requires 100% on AC *and* DC, so a machine cannot be compliant while plugged in and change policy when the power source does. Q10/Q11 — the binding proves its inputs instead of trusting strings. The harness digest is recomputed from the instrument sources at the bound commit by the frozen formula without importing the harness; the workload manifest comes from that commit's git object; `--verify` re-proves T0, instrument, manifest, both qualifications and both candidates. Q12 — a preflight and a postflight carrying two different `execution_binding_sha256` values cannot meet in one session. Q13 — a provisioning template lands in `scripts/step7/examples/`, outside any evidence directory, marked EXAMPLE / NOT EVIDENCE, with no real identity in it, and a control keeps it valid under the rules it teaches. step 7 host qualification controls: 18 passed, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
…e campaign Two findings from the second Phase-2 review, both reproduced before repair. S-A — machine-produced artifacts were consumed as ordinary dicts. A document whose kind was "NOT A BINDING AT ALL" was accepted as the execution binding, and an arbitrary object carrying `identity` and `provenance` was accepted as an environment manifest: the probe returned an ELIGIBLE session from two handwritten files. The asymmetry was exactly backwards — the two DECLARED artifacts were validated by kind and schema, and the three machine ones were not. There is now one boundary. `load_artifact()` parses, proves the artifact by its own validator, and only then hands it over; no consumer reads a field before the type is established, and a refusal raises rather than becoming "not eligible" — an input that is not what it claims is an operator error, not a session that failed a predicate, so it exits 2. The envcapture schema string is duplicated rather than imported, with `hostqual-producer-schema` proving the copy still equals what the capture tool emits. One validator per artifact type, not five call sites: `validate_qualification` now lives in `execbinding` — the module that binds campaigns is the lower one, so `hostqual` reuses it without an import cycle. That immediately paid: the required A4 attack found that binding never checked a qualification's SCHEMA, only its kind. A wrong-schema qualification could be bound. It cannot now. S-B — `--verify` narrowed silently to whatever it was handed. Called without the strata it skipped the qualification and candidate checks and still printed "binding verified". Owner ruling applied: no partial mode under that name. The function refuses incomplete input maps — not only argparse, because the next caller may be a script — and the command line refuses before running anything. The string "binding verified" is now unreachable unless the full set was attempted and passed. P-A — the control inventory is checked by a control. The docstring list and the executed set must be the same set, because this class of defect has now been found twice and a third discovery wearing a new hat is not a surprise worth paying for. step 7 host qualification controls: 22 passed, 0 failed. Regression: envcapture 11/11, perf instrument 16/16, calibration freeze 7/7, training prereg 9/9. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
S-C, found by the review of the previous repair — and introduced by it. Closing
the artifact-boundary hole, I folded value judgements into the shape validator,
so `dedicated_to_p022: false` was refused exactly like a forged document. The
honest case that used to produce a record with `qualified: false` and
`single_tenant: fail` produced nothing at all: one line on stderr and no
artifact.
That erases negative attempts, which is how a laboratory ends up with machines
that pass on the first try because the other tries were never artifacts.
The boundary now separates the two questions it had merged:
artifact validity is this the artifact it claims to be — kind, schema,
types, applicability shape. Malformed is refused before
any record exists.
predicate outcome do the declared values satisfy the predicate. Every
failure reaches a real record.
So the string "false" is malformed, a missing key is malformed, `is_vm` answered
with "n/a" is malformed, a VM omitting a VM-only field is malformed, and a
physical host answering those with bare booleans is malformed — while the
boolean `false`, anywhere it is allowed, is a valid declaration that this host
does not qualify.
Exit codes now say which class occurred, and are written down rather than
implied:
0 valid artifact, positive outcome
1 valid artifact, NEGATIVE outcome — the record exists and says why
2 malformed input or operational misuse — no record is produced
The same split applies to the session declaration: an operator who truthfully
records that a prohibited job is running gets `eligible: false` with the reason,
not an error message and no evidence.
Three controls added, driven through the command line so the exit codes are
part of the proof: an honest provisioning negative, an honest VM negative and an
honest session negative each leave an artifact naming the failed check, while
`"false"` as a string leaves none and exits 2.
step 7 host qualification controls: 24 passed, 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
…flag bite Two freeze-blockers from the full T0 review, both of the same family: a chain that reads correctly and does not actually hold. F1 — the accepted D7 payload binds the reference, the harness digest and version, and the workload manifest. It does NOT bind the execution binding: `execution_binding_sha256` is absent from `D7_PAYLOAD_BINDING_KEYS`, the string `execution_binding` does not occur in the instrument at all, and the gate only checks that the listed keys are present — an extra key is tolerated and never verified. Meanwhile this tool's own docstring promised D7 would bind it. One accepted component was advertising a join another accepted component cannot see. A campaign link now makes the join by bytes: it names the execution binding, the D7 payload's sha256, its blob at a named commit, and the attestation's sha256. Preflight and postflight both re-prove it — a link naming a different binding, a freeze edited after the campaign was linked to it, or a campaign swapped between preflight and postflight each refuse — and the link is recorded in both session records. Admissibility under T0 is the existence of the postflight record, so evidence produced outside a campaign cannot acquire one. What this deliberately does not do: make the instrument's firewall aware of the campaign. `IdentityGate` still arms from the payload and attestation alone, so a decisive clock can physically run with no binding in existence; what cannot happen is that such a run becomes admissible. Teaching the firewall would edit `perf_baseline.py`, move `measurement_harness_digest` and reopen steps 4/5/6 — a price not paid for a property obtainable by gating admissibility instead of execution. F4 — `collection_authorized` was decoration. Both T0 readers accepted FROZEN regardless of the flag, and the fixtures had quietly encoded the intended rule all along. The two fields are now read as one authority state on both paths: FROZEN+true may proceed to the remaining gates, while FROZEN+false, NOT_FROZEN+false and NOT_FROZEN+true each refuse — the third by name, because an authorisation without a fixed protocol is a contradiction and honouring the flag over the contract is how a tool starts arguing with its own rules. A fixture defect surfaced on the way: two campaigns built from identical content in the same second produced the same commit sha, so the "different campaign" attack was proving that a link equals itself. Campaigns are now distinct documents. step 7 host qualification controls: 26 passed, 0 failed. Regression: envcapture 11/11, perf instrument 16/16, calibration freeze 7/7, training prereg 9/9. No instrument change; the harness digest does not move. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
The frozen T0 names an accepted harness identity that, on its own branch, does
not exist yet: `104c384d01bf` arrives with the memory-semantics repair, which is
a different pull request. That is fine while the merge order holds — and an
order held by agreement is one stray click from becoming an archaeological
artifact, with a document saying FROZEN in a tree where its referent is absent.
So the order is checked instead of promised, and checked against the TARGET
TREE rather than against pull-request numbers: a number proves someone pressed a
button and says nothing about what the merged tree contains. The gate refuses
unless, at the commit a merge would produce:
- T0 declares FROZEN *and* collection_authorized: true;
- the harness digest recomputed from that tree's own instrument sources, by
the frozen formula, equals the one T0 names;
- the policy freeze, the design constants and the training preregistration all
bind that same digest — present is not the same as re-bound;
- the capture, qualification and binding tools exist and actually enforce the
campaign link and the authority state;
- the step-7 note still revokes the automatic collection authority, so hosts
plus a binding cannot again be enough to start a clock.
The expected digest is read out of the frozen T0 and recomputed from the tree; a
control proves no digest literal lives in the gate's own source, because a gate
trusting its own constant would be checking itself.
Exercised against real merges, not only fixtures. The full chain
(#355 -> #356 -> #353 -> #354) is allowed. #354 alone is refused on three
predicates at once — the instrument hashes to 562a7f7232da where T0 names
104c384d01bf, the bindings are stale, the machinery is absent. #355 plus #354
without the machinery is refused on one. T0-0 turns out to need no predicate of
its own: #353 is an ancestor of the freeze commit, so it cannot be skipped.
merge gate controls: 7 passed, 0 failed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
The first revision proved words. `step7_machinery_present` asked whether the two tool files existed and whether certain identifiers appeared in them, which a file containing `def check_campaign_link(): pass` satisfies — and the fixture that was supposed to exclude that world was exactly that file. A check for the name of a mechanism is not a check of the mechanism. `step7_machinery_enforces` now extracts the target tree's own hostqual and execbinding, runs them in a subprocess against a synthetic campaign, and requires all four attacks to be refused: a link naming another execution binding, a freeze edited after the link was made, FROZEN with collection_authorized false (on both readers), and a campaign swapped between preflight and postflight. A tool that accepts any of them fails the gate, and the refusal says which attack got through. `steps_4_5_6_rebound` searched each artifact's text for the digest. The right digest sitting in any field — a comment, a history entry, a field nobody binds — was read as a binding. It now walks the exact path each artifact actually binds at: measurement_harness_digest in the policy freeze, bound_measurement_harness_digest in the design constants, bindings.measurement_harness_digest in the training preregistration. Anywhere else is not a binding, and the refusal names the path and both digests. Fixtures ship the real tools; the two attack controls mutate one enforcement point each and require the gate to notice. Twelve controls, four of them new: a permissive link check is refused, an authority check that always passes is refused, a stale binding with the right digest in a decoy field is refused, and correct exact fields are allowed. Against the real merges: the full chain is allowed (rc 0); #354 without #355 and #356 is refused on three predicates (rc 1); #354 with the instrument but without the step-7 tools is refused on one (rc 1). 30d2f32 is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
…thing Two holes in the proof, not in the mechanism. The authority table exercised three of the four states. R15 defines all four, and `NOT_FROZEN+false` was the one nobody checked — so a future tree in which a reader quietly authorised it would have passed the gate. It is in the table now, and `DEAD_AUTHORITY` has a twin that breaks `execbinding.t0_at` instead of `hostqual.bind_t0`: the gate's message claims enforcement on both readers, and a control that only breaks the first left half that claim resting on nothing. Both mutations must now be caught naming all three forbidden states. The campaign-swap witness asserted only that a swapped campaign is inadmissible. A `session_admissibility` returning `admissible: False` unconditionally would have satisfied it — the witness would have read "nothing is admissible" as "the swap was caught". It now requires the unchanged campaign to survive preflight to postflight first, and requires the swapped one to be refused by a reason naming the campaign link, so a broken closing probe cannot stand in for the continuity check. Two mutations prove each half bites: removing only the preflight/current campaign-link comparison while leaving `check_campaign_link` intact, and a postflight that refuses everything. A mutation that changes no bytes now raises rather than passing as a second positive control nobody reads as one. 15 controls, 0 failed. The probes are unchanged: full chain rc 0, #354 alone rc 1 on three predicates, #354 without the tools rc 1 on one. 30d2f32 is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
#357 built a gate that refuses the wrong merge. Nothing ran it. A weapon in a safe is not a guard at the door, so this is the wiring: a workflow that runs it against the commit a merge would produce, under two names a ruleset can require. Wiring it exposed two things the gate could not answer on its own. **Applicability.** A required check runs on every pull request in the repository. `mergegate.py` refuses a tree with no T0 in it — correctly, as a question about a contract that must be there — so as a required check today it would refuse every pull request, including this one. The wrapper settles it first: no frozen T0 at the merge commit means no merge can make the contract reachable, the check passes and says so. The moment a tree carries T0 every predicate applies in full. **Co-change.** The workflow that runs on a pull request is the one on that pull request. A branch could otherwise carry the freeze and a weakened gate together and be judged by the gate it brought with it. So the gate's own files — `mergegate.py`, this wrapper, the workflow — may not change in the same merge that introduces or changes the frozen contract. Repairing the gate on its own stays ordinary work; that distinction is a control, not a promise. The applicability decision lives in the tool that owns `T0_PATH`, never a second copy in YAML, and a control fails if the workflow ever grows one. Another fails if the contexts the workflow declares stop matching the ones a ruleset is told to require — a required check nothing reports waits forever and reads as protection. Exit codes are the step-7 three: 0 proceed or not applicable, 1 refused, 2 the question could not be asked. 11 wiring controls, 0 failed; the 15 gate controls unchanged. Both suites also run green on Linux, which is where the job will run. Against real commits: this branch's head is not applicable, the full chain is allowed, #354 alone is refused on three predicates. This is half the wiring. Until a ruleset requires these two contexts, the job reports and nothing is prevented. #354 stays a draft until it does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
ruff allows a sys.path insertion before an import, so the E402 suppression was covering nothing and RUF100 said so. Caught by running the CI-pinned ruff (0.15.8) rather than the one that happened to be installed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
Sixteen ruff findings, all introduced by this PR and none of them caught here before, because `ruff check .` was never run against this branch. `main` is clean, so every one of them is mine. Behaviour-preserving throughout: `datetime.UTC` for the deprecated alias, list unpacking for two concatenations, three long lines wrapped, and six unpacked names the controls never read prefixed with an underscore. The one that was almost a real defect is B023: three fixture paths were closed over by a lambda inside a loop. It never bit, because the lambda is called in the same iteration that builds it — but that is a property of today's control body, not of the code, and a later `refuses(...)` that defers the call would have made every attack in that loop test the last fixture three times. The names are bound as defaults now, so the control cannot start lying quietly. step-7 qualification 26/26, envcapture 11/11, perf instrument 16/16, ruff clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
Three ruff findings, all mine: an unsorted import block, one over-long line inside the witness source, and an unpacked name a control never reads. The long line is inside WITNESS_SOURCE, so wrapping it edits the script the gate runs against the target tree's tools. Hoisting the identity into a local changes nothing it does, and the controls and probes were re-run to say so rather than assumed: 15 merge gate controls pass, host qualification 26/26, and the three real merge probes still come back 0 / 1 / 1 with the same predicates. This is a lint pass only. No predicate, no attack and no message moved, and 30d2f32 is untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
If `--base` named a commit the clone does not carry, `blob_sha` returned None for every gate file on that side, the co-change rule read all of them as rewritten, and the merge was REFUSED — for a fact about the checkout's depth rather than about the merge. The workflow sets fetch-depth 0, so this would not have fired today; it would have fired the first time someone ran the wrapper by hand, or the first time a shallow clone was used to save a minute, and the refusal would have been about the gate being rewritten. A false refusal teaches people that the gate is noise, which is how a required check dies. The base is now validated like the merge commit, and an unreachable one is exit 2 with a message that names the reason. A control builds exactly that case and requires 2, refusing to accept 1. 12 wiring controls, 0 failed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
tests/run_tests.py imports every test_*.py and calls `run()` on it. The new suite called its entry point `main()`, so the runner reported it as a module with no run() and ended the whole test job — after my own controls had printed 15 green lines, which is exactly the shape of a failure that looks like someone else's. Found by reading CI rather than by running the file directly: `python tests/test_step7_mergegate_ci.py` passes either way, and that is precisely why the local run said nothing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
The previous commit gave this module a run() for tests/run_tests.py to call — onto a name a local helper already had. The helper won, the entry point was shadowed, and eight controls started invoking themselves recursively instead of the wrapper. Caught immediately by ruff (F811) and by the suite dropping to 4 passed, 8 failed, which is what a repository's own checks are for. The helper is `invoke` now and says in its docstring why it is not `run`. 12 wiring controls, 0 failed; ruff clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh
ci(step7): the merge gate becomes a check, not a script in a drawer
feat(step7): the merge order becomes a predicate on the target tree
19 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Phase 2 of the S8 repair chain. Stacked on #355 (the instrument's memory-metric repair), which it needs for the closed memory vocabulary.
Three artifacts, three questions, never merged:
and, separately, the execution binding: which Linux and Windows environments, which candidates and which instrument form this campaign.
hostqual.pyneither issues nor owns it — a utility that checks a CPU governor must not become the root of campaign identity. D7 later bindsexecution_binding_sha256rather than restating the machines.Qualification is versioned by T0
A qualification claims "this host satisfies T0-7", so it cannot float free of T0.
--qualifyrequires the T0 path and commit and proves: the commit exists,blob_shareally iscommit:path, the bytes hash to the recorded sha256, and the document declares FROZEN. A qualification against aNOT_FROZENT0 is refused — today's reconnaissance cannot become campaign evidence by reuse — and the binding refuses a host qualified under a different T0 than it binds.Every consumed artifact is proved before it is read
One boundary, one validator per artifact type: parse → prove kind/schema/shape → only then read semantics. Binding, qualification, environment manifest, preflight record, provisioning and session declaration all go through it.
Artifact validity is not predicate outcome. Malformed input — wrong kind, wrong schema, the string
"false"where a boolean belongs, a VM omitting a VM-only field — is refused before any record exists. A declared booleanfalseis not malformed: it is a perfectly good declaration that this host does not qualify, and it reaches a real artifact. The exit codes say which happened:The envcapture schema string is duplicated rather than imported (neither tool imports the instrument or the capture tool), with a control proving the copy still equals what the producer emits.
Evidence classes stay apart
Declared — provisioning and the per-session operator declaration. Content-addressed, checked by shape and value, never called machine proof: a guest OS cannot establish that no neighbour arrived on the same hypervisor.
dedicated_to_p022andno_concurrent_user_workloadmust be true,hosted_ci_runnerfalse,is_vma real boolean; a VM must promise fixed vCPU, fixed RAM, no live migration and no dynamic memory, withn/aunavailable to it, while a physical host answers those four with an explicitn/a: <reason>.Machine-observed —
manifest.provenance.ci == false(the field always exists, so demanding its absence could never be satisfied);performancegovernor on every applicable CPU plus an identified turbo mechanism; on Windows an accepted plan with processor state pinned at 100% on AC and DC, read by GUID and by position because every label on a non-English machine is localized; and the stratum's memory mechanism actually existing here.Identity, candidate, quiesce
/proc/statorGetSystemTimes, mean below 5 %, no interval above 20 %. A missing sample, a rewound counter or a zero denominator isNOT_ELIGIBLErather than a skipped interval. A quiesce failure starts no clock, so it is notINVALIDand costs no retry budget.execution_binding_sha256values cannot meet in one session.The binding proves its inputs, and
--verifyis totalThe harness digest is recomputed from the instrument sources at the bound commit by the frozen formula, without importing the harness; because the workload manifest is one of those sources, proving the digest at a commit also proves the manifest there.
--verifyre-proves T0, instrument, manifest, both qualifications and both candidates — and there is no partial mode under that name: incomplete inputs are refused by the function and by the command line, so the stringbinding verifiedcannot appear over a skipped component.Driven live, not only by fixture
The first live Windows run found a defect in this tool's own parser: it read the possible-range minimum as the current setting and reported
0%on a machine pinned at100%. Fixed — the block ends with the two current indices, AC then DC.Exploratory witness, not qualification evidence: on the development machine
processor_min_dc = 5. Under the AC+DC rule that host does not qualify, which is the point of the rule — a machine can look perfect on its current power source and carry a different profile the moment the source changes.Both refusals are correct, and both leave evidence: neither environment is a measurement host, the tool says so for the right reason, and the attempt is recorded rather than discarded.
step 7 host qualification controls: 24 passed, 0 failed.Regression: envcapture 11/11, perf instrument 16/16, calibration freeze 7/7, training preregistration 9/9.No real campaign qualification or binding has been created. T0 remains NOT_FROZEN;
collection_authorized: false.🤖 Generated with Claude Code
https://claude.ai/code/session_018xhcg5opoFbSTdYHpkSXCh