Repository navigation
[Proposal] Milestone: full ACS-Core conformance (requirement ledger + behavioral tests, beyond adapter emission) #33
Description
Note: Scope proposed during PR #22 review; item selection and sequencing are for the workstream leads to decide, not settled here.
Context
PR #22 (reference adapters) now proves emission conformance: the reference adapters, driven through their real production entry points, emit schema-valid, signed ACS v0.1.0 Core traffic and honor the decisions the suite tests, validated against the canonical schemas by an independent oracle. Three concrete Guardian bugs found by direct probing (version forward-compat, malformed-ClientHello acceptance, non-object/batch JSON-RPC crash) were also fixed.
This is not the same as a full ACS-Core deployment conformance claim, which spans the adapter, the Guardian, the framework wiring, and production configuration together. This issue tracks that larger program so the emission work can ship with an honestly-scoped claim and the rest is visible, not silently implied.
Candidate scope (unordered — for lead prioritization)
-
Normative-requirement ledger. A machine-readable ledger over every MUST/REQUIRED in
docs/spec/conformance.mdand the sections it cites (~58 occurrences, some conditional), each row:- stable requirement ID + exact normative text
- role: adapter / Guardian / framework / deployment
- profile + applicability condition
- implementation location
- positive, negative, integration tests
- status: conformant / partial / deferred / not-applicable
CI fails when an applicable MUST has no falsifying test. Resolve the normative inconsistency the review flagged:
conformance.mdlists all five dispositions and Wrapped MCP as Core, while the suite/README treat some as conditional / "Core minus full Wrapped MCP" (see the Wrapped MCP item, and PR Set the ACS-Core floor: MODIFY and system/ping to SHOULD, Wrapped MCP and subagentStart as conditional MUSTs #21 which relaxes MODIFY/ping/MCP). -
Handshake & negotiation semantics. Beyond the three fixed bugs: PROVENANCE_REQUIRED, unknown-field handling, startup proceed/refuse, session-refusal. Adapters currently consume only
on_decision_failure; prove they honortimeout_config(incl. per-method),methods_evaluated,selected_transport, negotiated signature algorithms,skew_window_ms, accepted profiles. Reconcile the stdio advertisement inacs_commonwith HTTP-only transport. -
Two-sided live payload validation. Guardian must validate request envelopes for handshake/ping and hook payloads against method-specific schemas on the live path (not just locally). Prove: malformed ClientHello/ping/every hook payload rejected on the wire; valid unknown fields ignored; unsupported batches → -32600; adapters validate the full ServerHello + response-envelope schema, not only signature/binding; contradictory responses follow the failure posture.
-
Complete decision semantics. Negotiated timeouts applied; invalid MODIFY composition (wholesale+structured, overlapping paths) fails closed; live redaction / modified_content application; DEFER suspends until resolution or resolution_timeout_ms (not immediate deny/ask); cascading-DEFER bounds; ASK approver auth, recursive-ASK rejection, approver-incapable substitution, session-scoped Intent extension; side-effect-negative DENY tests on every platform.
-
Hook completeness (black-box). Real-framework tests attempting shell/process exec, file read/write/delete, network egress, MCP calls, subagent spawn, memory/knowledge access — assert the Guardian receives the hook BEFORE execution and DENY leaves a canary untouched. Cursor stays manual until a GUI/product-level harness exists.
-
Wrapped MCP: implement or formally remove from Core. Either implement + test full
protocols/MCP/*(intact wrapping, version negotiation, decision application, error mapping, request/result correlation), or change the normative Core definition so genericsteps/toolCallRequest/Resultcollapsing is explicitly conformant. Spec-owner decision — being taken to the ACS leads.
Profiles (do not implement until intending to claim)
- ACS-Trace — OTel/OCSF per supported step, decisions, provenance forwarding, non-blocking sink failure
- ACS-Inspect — pre-content
agbom/snapshot, Guardian acceptance, audit-chain inclusion, CycloneDX/SPDX/SWID - Inspect-Dynamic —
agbom/changedon every mutation - ACS-Provenance — strict schemas + deterministic provenance on every data-bearing field
- ACS-Crypto — ML-DSA-65 + verification/negotiation
- ACS-Audit — every ContextEntry carries request_hash
Deployment attestation
A preflight that verifies a running deployment: signing enabled / no ACS_DEV_MODE; ACS_DISABLED unset; HTTPS for non-loopback; secret-file perms + rotation; durable audit sink + fail-open alerts; approved framework/adapter versions + config hashes; no unexpected skips without recorded evidence.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsAccepted
Activity
Reference adapters: emission conformance, fail-closed enforcement, an…
astrogilda commentedon Aug 21, 2026
"CI fails when an applicable MUST has no falsifying test" is the load-bearing line in this issue, and it is the one with no instrument behind it. The ledger you describe cannot check that on its own. I have been running the thing that can for months, on a public conformance corpus for an in-toto attestation predicate, and the numbers are all published.
A ledger row proves a requirement is named. It cannot prove the named test would fail if the requirement were violated, and those are different properties that look identical in a table. The measurement is mutation: switch off exactly one rule in the reference implementation, replay the entire corpus against the weakened build, and see whether the corpus notices. My published baseline at the current head of the corpus is seven hundred and fifty-four single-site weakenings of the reference rail, recorded as four hundred and twenty-four killed, twenty-seven silent, two hundred and ninety-eight dead, and five inconclusive, where the file's own definitions are that killed means some vector goes from pass to fail, silent means the corpus sees the rail change and stays green, dead means nothing in the corpus depends on that rule at all, and inconclusive means the mutant did not build and terminate cleanly so nothing is asserted about it. The corpus is two hundred and fifty vectors, and it cites ninety-three normative conditions with at least one vector each. So on the ledger axis it looks complete. Twenty-seven weakenings still went through unnoticed, and two hundred and ninety-eight rules have nothing in the corpus depending on them. A conformance ledger built without this instrument will report those as covered, because they are covered. They are simply not forced, and forced is what your CI requirement actually means.
ACS should have a companion document to that baseline from day one of the program, because it is the artifact that buys the standard credibility with a buyer. A contributor will already take your word for it; a buyer will not. Mine is called "Where this suite is weak", it is generated from the mutation baseline on every run and never hand-written, and it states that of the ninety-three conditions the corpus cites, sixty-six are forced by a vector no other condition's vectors duplicate, twenty-seven are covered only redundantly, and zero are not forced at all. It also names the sites where "unforced" would be the wrong word, either a true equivalent mutant no vector can ever kill or a real rule an earlier check reaches first, and each of those annotations is itself falsifiable, because a site annotated as unkillable that is ever killed fails the gate. A suite that publishes only what it covers is asking to be taken on trust, which is the posture the conformance label is supposed to replace.
Every killed row is a tighten-only ratchet: a rule that has stopped being forced fails the build, and the baseline never quietly updates itself, which is the difference between a measurement and a status page. And the generated document is re-derived by a second implementation sharing no code with the generator, because drift and correctness are different properties and a
--checkflag only ever sees the first.On your requirement-ledger scope specifically, I would add a column and a companion file. The column is the mutation result per requirement, so a row reads conformant only when a weakening of that requirement is killed by a test the row names. The companion file is the one I did not know I needed until an independent implementer worked from my text alone: a registry of every interpretation decision a from-spec implementer must make, with the specification lines it turns on, the rival reading it was chosen over, and the vector that locks it. Mine carries twenty-one decisions, all classified forced, with no open corners, and it also carries an explicit list of nineteen of those twenty-one for which no differential witness has yet been produced, with the gate set to fail any id left on that list once it gains one. I mention the unfinished part because that list is the most useful thing in the file. Agreement between two implementations is not evidence that the text forces a reading; it is evidence that both made the same choice. For ACS,
methods_evaluatedsemantics, the §6.4 refusal-versus-failure question in the issue next door, and the Wrapped MCP disposition you flag here as needing a spec-owner decision are all exactly this shape: places where two conformant implementations can already diverge and nobody would find out until an incident.That is also the honest answer to your Wrapped MCP item. The normative inconsistency you name,
conformance.mdlisting all five dispositions and Wrapped MCP as Core while the suite and README treat some as conditional, is not resolvable by testing, because a test cannot tell you which of two documents the working group meant. What testing does is put a bound on how long it can stay unresolved: a requirement ledger with a mutation column reports it immediately as a Core requirement with no forcing test, and it stays on the report until somebody decides, which is a good deal cheaper than finding out from a vendor's marketing page.One governance rule I would want written down at the start, while it is still cheap, since this issue is about a claim as much as it is about tests: the party that scores an implementation must not be the party that shipped it, and every recorded run states its independence relation. The only reason my own numbers mean anything is that an unrelated author wrote a from-spec verifier with its own parser, canonicalizer, Merkle root and signature tier, with no sight of my reference code, and scored one hundred and seventy-nine out of two hundred and thirty-two blind. The corpus records that blind figure as the headline and the later two-hundred-and-thirty-two-out-of-two-hundred-and-thirty-two directed run underneath it, with a note that the directed pass followed a specification diff already read and therefore says nothing about whether the text determines the reading. Fifty-three mismatches published as the headline number is worth more to a buyer than a clean sheet, because it is the only version of the number that could have come out differently.
Everything above is public and Apache-2.0 at
github.com/astrogilda/aee-conformance: the mutation baseline, the generated weakness page, the interpretation registry and the gates that hold all three. I have put the longer version of this, covering the registry row shape and the steward question, on #19. Concretely, the first cycle I would take on here: extract the ledger you describe, one row per MUST inconformance.mdand the sections it cites, with the role, profile and applicability columns you list plus the mutation column; run the first forcing campaign against the reference Guardian and publish the baseline and the generated weakness document alongside it; and wire the falsifying-test gate so it fails on an applicable MUST whose weakening nothing catches, rather than on an applicable MUST with no test named. Tell me which piece is most useful first and I will start there.ryjen commentedon Aug 25, 2026
One additional behavioral conformance case may fit #33: valid authorization for action A should not count as successful execution if the downstream effect materially corresponds to B instead.
A simple way to think about it is: permission to do one thing is not proof that the system actually did that thing.
Proposed paired case:
Positive control:
This is intentionally different from #29/#30. Those cases primarily test whether an action should be admitted before execution. This case starts after a valid authorization and asks whether the effect that actually occurred still corresponds to the action that was authorized.
The property under test is:
A valid authorization for
Aestablishes thatAwas permitted under the modeled policy inputs. It does not establish that the framework, tool adapter, or downstream provider actually performedA, and it should not retroactively authorize a materially differentB.ACS already has useful request/result correlation through
toolCallRequest,toolCallResult, andrequest_id_ref. I would not treat the result hook alone as authoritative proof of what physically happened, though. The stronger behavioral assertion is to correlate the authorized request with independently observed execution evidence or deterministic terminal-state evidence.For example, if an authorized operation is:
but the downstream effect instead changes:
the authorization record can still remain valid and immutable while the execution-correspondence check fails. That distinction seems useful because it separates “was this action allowed?” from “is this the action that actually happened?”
I would keep this at the behavioral/integration conformance layer initially rather than proposing a schema change. If the #33 workstream finds that current request/result correlation is insufficient to express the test cleanly, that would be the point to discuss whether additional representation is needed.
Scope is intentionally narrow. This would not claim production complete mediation, honesty of arbitrary provider receipts, absence of unobserved side effects, correctness of every provider-specific semantic mapping, or universal TOCTOU protection. It would test only the authorized-action versus observed-effect correspondence boundary.
narko4u commentedon Aug 28, 2026
Three strong contributions are already on the table: the ledger itself, the mutation-forcing instrument that makes "has a falsifying test" measurable, and the authorized-action vs observed-effect correspondence case. One dimension is missing from all three, and it is the one that decides what the conformance label is worth to a buyer: who can check the claim from outside the deployment, and against what.
Everything in the current scope is producer-side. Mutation forcing proves the reference implementation's rules are testable, which is a property of the test suite; it does not prove a specific vendor deployment's Guardian enforced them, which is a property of that deployment. The independence rule governs who does the scoring. The remaining question is what the scored artifact can say about a different deployment, and the ledger has no vocabulary for it today. "Independently observed execution or terminal state" is the right direction, but it needs the same question answered: independently by whom, observed against what basis, on a path the authorization cannot influence? If the terminal-state evidence is produced by the same framework that executed the action, the result is correlation, not correspondence.
Proposal, additive to the ledger:
Witness_scope column. Each requirement row names who can observe its satisfaction: SELF (the deployment's own reports), PEER (another party within an agreement), EXTERNAL (a party outside the trust domain). Most enforcement rows will be SELF-only under the current wire format. That is a finding, not a failure, and it should be visible in the ledger.
Verification-basis column. What artifact the witness checks against: wire transcript, signed decision records, policy state, terminal-state evidence, anchor commitments. Where the only basis is the deployment's own report, the row is SELF-only.
Claim-scope rule. A conformance claim must state its witness_scope. "Emission-conformant" and "operationally conformant, self-attested" are honest claims; unqualified "conformant" overclaims, because mutation forcing and a live deployment are different objects. The deployment attestation section already names this shape (config hashes, no unexpected skips without recorded evidence); it just does not label the scope of those checks.
The milestone should treat "an enforcement requirement with no EXTERNAL witness basis" as a first-class open item, not a silent default. That gives the program a concrete way to absorb the audit-chain and trace questions raised in #18 and #36: they are not separate concerns, they are rows in this ledger with an empty witness_scope column, and the program can now say which ones.
Happy to draft the column definitions and the claim-scope wording for the milestone if the leads want it.
narko4u commentedon Aug 28, 2026
Addendum to milestone scope: witness_scope and verification-basis columns, and the claim-scope rule (additive to the normative-requirement ledger)
1. Witness_scope column (one value per requirement row)
Assignment rule: a row is SELF unless a concrete artifact path exists that lets the named witness check the requirement without relying on the enforcement point's own account. Most enforcement rows are SELF-only under the current wire format. That is a finding, not a failure, and it is recorded as such.
2. Verification-basis column (one or more values per row)
Where the only basis is the deployment's own report, the row is SELF-only and the basis is recorded as such.
3. Claim-scope rule
Unqualified "conformant" overclaims and must not be used: mutation forcing and a live deployment are different objects, and the claim must say which one is attested and by whom.
4. First-class open item
An enforcement requirement with no EXTERNAL witness basis is recorded in the ledger as an open item, not a silent default. The ledger then answers the audit-chain and trace questions from #18 and #36 directly: they are rows with an empty witness_scope column, and the program can say which ones they are.
giskard09 commentedon Aug 28, 2026
Read the addendum — the witness-scope/verification-basis split is the right cut, and it gives a vocabulary the conversation in #18 was missing a name for. Mapped what we actually have against it, since a practitioner's eye is only useful with real rows in it, not hypothetical ones:
action_ref+cross-system-verification-v1: EXTERNAL. A non-trusting third party with the four preimage fields, the public trail record, and the tx_hash recomputes the hash and checks the on-chain anchor payload — no cooperation from the emitter, no Giskard infrastructure required. Basis:anchor_commitment+ the equivalent ofwire_transcript(the public trail record itself). Scope of the claim: it proves the record is internally consistent and anchored — it does not prove the action was authorized or that the fields are a true account of what happened. That boundary is already written into our spec; your claim-scope rule gives it a name we didn't have ("emission-conformant"-adjacent, not "operationally conformant").context_digest: SELF, today. Flagged this in [Bug] Audit chain trust depends on a single Guardian, no cross-Guardian verification protocol #18 too — the property only holds once a verifier already holds the disclosed context set from the emitter. No artifact path lets an EXTERNAL or PEER witness check that the assembled context was complete or that two parties independently assembled the same one. Your assignment rule calls this correctly on its own.The Guardian-to-Guardian reconciliation primitive from [Bug] Audit chain trust depends on a single Guardian, no cross-Guardian verification protocol #18: PEER, and unbuilt. Two parties in an agreement, neither trusting the other's internal state — exactly the scope your table names but the ledger has no row for yet.
Happy to take a pass at draft column values for our own artifacts if that's useful groundwork before the leads decide sequencing — but that's your call on scope, not ours to push.
narko4u commentedon Aug 28, 2026
This mapping is exactly the kind of real row the ledger needs, thanks for doing it against actual artifacts.
Reading the three against the tables:
action_ref+cross-system-verification-v1= EXTERNAL, basisanchor_commitment+wire_transcript(the public trail record). Claim scope: emission-conformant, it proves internal consistency and anchoring, not authorization or truthful accounting. That boundary was already in your spec, the rule now names it.context_digest= SELF under the assignment rule, until an artifact path lets an EXTERNAL or PEER witness re-derive the assembled context without emitter cooperation. Guardian-to-Guardian reconciliation = PEER, an open row in the ledger, the unbuilt primitive from #18.Yes to the draft column values for your artifacts. Concrete rows with real artifact paths make the vocabulary testable before leads sequence it. PEER stays open until the primitive exists.
giskard09 commentedon Aug 28, 2026
Appreciate you closing the loop on this — agreed on all three rows. PEER stays open on our side too; no artifact to point to yet, will flag if that changes.
astrogilda commentedon Aug 29, 2026
The witness-scope column measures who holds the record. There is a second question it does not reach, which §8.6 already anticipates about the Guardian. Under the HMAC baseline the published, signed head "does not bind the Guardian itself: the Guardian holds the symmetric key and can re-sign a rewritten head." So EXTERNAL custody of a Guardian-signed head is still the Guardian's own account of itself. Scope moved; forgeability did not.
This separate axis is the focus of #36. Emission is observability, not evidence, precisely because an external collector holding emitted records has EXTERNAL scope over an artifact the emitter produced.
The basis/method split merged in in-toto/attestation#570 handles this: every row carries basis (substrate or artifact) and method (live or reconstructed). Both stay individually readable because a reduction over them obscures which one is weak. The forcing case is why they are required and not advisory. A party holding the enclosing envelope key but not the substrate observation key can move every row to artifact, drop the observation records the artifact rows no longer need, and emit a statement that is well formed, carries no substrate evidence, and is byte-identical to one an honest producer with no substrate vantage emits from the same configuration. No function of the carried bytes separates them. Only a required per-row declaration does.
Applied to the three rows you assigned: anchor_commitment and wire_transcript both sit at EXTERNAL, and they differ on this axis. A transcript recorded by a party on the wire is substrate; a transcript the emitter produced and handed over is artifact, at the same scope. Without the column the ledger cannot express the difference, and the PEER row inherits the same ambiguity before the primitive exists.
The change is small: basis and method on the ledger tables, the three rows filled in, and negative vectors that fail a verifier accepting an artifact-basis record as substrate. I will put it up. The vectors are what make the column testable, not merely declared.
narko4u commentedon Aug 29, 2026
This is the right extension, and the two-axis point is the one that matters. The witness_scope column says who holds the record; it does not say whether the record is forgeable by the party it describes. EXTERNAL custody of a Guardian-signed head is still the Guardian's own account of itself, exactly as you put it. Scope moved, forgeability did not.
The basis/method split from in-toto/attestation#570 is the clean way to carry that second question, and required rather than advisory is correct for the forcing case you describe. A party holding the enclosing envelope key but not the substrate observation key can reclassify every row to artifact, drop the observation records those rows no longer need, and emit a statement that is well formed, carries no substrate evidence, and is byte-identical to an honest producer's output. No function of the carried bytes separates them. Only a required per-row declaration does.
Applied to the three rows we assigned: anchor_commitment and wire_transcript both sit at EXTERNAL scope, and they differ exactly on this axis. A transcript recorded by a party on the wire is substrate; a transcript the emitter produced and handed over is artifact, at the same scope. The column makes that difference expressible, and the PEER row inherits the same clarity before the primitive exists, which is better than leaving the ambiguity open.
The negative vectors are what make this testable instead of declared. A verifier that accepts an artifact-basis record as substrate must fail them, and the corpus should carry both the passing honest rows and the failing reclassified rows so the distinction is enforced, not asserted.
One framing note for the ledger: keep basis and method individually readable rather than reducing them to a single strength value. The weak property is the one that matters, and a reduction over both axes can hide which one is weak. The per-row declaration is the load-bearing part, so the schema should require it rather than default it.
giskard09 commentedon Aug 29, 2026
Good split, and it maps cleanly onto our own primitive without the label existing yet. In cross-system-verification-v1, steps 1-2 (recompute_action_ref, match_trail_record) operate on the trail_record the emitter produced and handed over — that's wire_transcript, artifact-basis. Steps 3-4 (fetch_chain_anchor, match_anchor_payload) go to a public RPC we don't control and cross-check the on-chain tx against the artifact — that's anchor_commitment, substrate-basis. The spec already states the property this axis names ("No trust in the trail emitter"), and the fixture vector is a real, checkable case: tx 0x7fd0a8ededd1feb65ab37b3324218a0386dbf124174cf122bffc40717c057b84 on Base mainnet, independently fetchable right now.
One gap, stated rather than found by someone else: this is spec-level, not code-level yet. There's no bundled reference validator for cross-system-verification-v1 the way action-ref-v2 has one, so the basis split above is what the spec requires, not something a test suite currently enforces with negative vectors. If the ledger's negative-vector requirement extends here, we'd need to write that validator first, not just point at prose.
narko4u commentedon Aug 29, 2026
The step-by-step mapping is the strongest confirmation the axis is real: steps 1-2 on the emitter's own trail record (wire_transcript, artifact-basis) vs steps 3-4 on a public RPC you don't control (anchor_commitment, substrate-basis), with "no trust in the trail emitter" already written into the spec. The fixture tx on Base mainnet makes it checkable, not just asserted.
And the gap is real, not a courtesy: spec-level basis split without a reference validator means the property is required, not enforced. The negative-vector discipline from the ledger work applies directly here. We have built fail-closed validators with reject vectors before, and we are happy to help write the cross-system-verification-v1 reference validator that enforces the basis split, if that is useful groundwork before the leads decide sequencing. Your call on scope.
One convergence note: the same basis and claim-scope vocabulary is being proposed on the record side in the OT-WG evidence spec (aaif/wg-observability-and-traceability#37), so the two efforts are converging on the same primitive from different ends.
giskard09 commentedon Aug 29, 2026
Agree the axis is real and the ledger note is right — declaring basis and method separately (rather than a single reduced strength value) is the load-bearing part, and defaulting either one is exactly how the reclassification attack you describe stays undetected.
On the reference validator: we'd rather not build it solo and hand it over — happy to co-build instead. We can bring the canonical contract for
cross-system-verification-v1(what each row is actually allowed to assert) plus a first pass at negative vectors (artifact-basis records misclassified as substrate, the reclassification case above, and the PEER row's open ambiguity you flagged). You bring the fail-closed validator pattern you've already shipped elsewhere. We have direct precedent for this shape: our action-ref-v2 validator is going through independent conformance review with aeoess right now (aps-conformance-suite#42, four rounds so far, CI green, open — two remaining blockers are on the suite side, not ours) — same discipline, worth reusing here rather than reinventing.On sequencing: this is the leads' call, not ours — we'll follow whatever order they set. If it helps, we can post the contract + negative-vector draft in this thread first so there's something concrete to sequence around.
Good catch on the OT-WG convergence (aaif/wg-observability-and-traceability#37) — worth cross-referencing once both sides stabilize.
astrogilda commentedon Aug 30, 2026
Co-build works. The half you named as ours is built, so here is where the seam falls.
conformance/basis/on branchbasis-method-conformanceatf451b00. Twenty-one vectors, twenty-five negative controls, thirteen mutants. Remote CI is green on run 33281584924, with all five steps executed, none skipped. It follows PR #35 file for file, so it reviews as a sibling. The identifiers, the PASS, REJECT and UNMEASURABLE verdicts and the per-category positive control all come from PR #30, so there is no third convention to learn.Your constraint is enforced in code three ways, so that it holds without anyone remembering it. The runner refuses any adapter whose credited rows carry the two axes as anything but separate members, deciding that on the shape of the answer before it compares a single value, so an adapter that reduces them is turned away without ever being scored. The self-test ships a fake that collapses the axes into one strength value. Its verdicts are all correct and it is refused anyway, on shape, before any of them is read. A CI step scans for reduced-axis field names.
One thing I got wrong on the way and it is worth stating, because it changes the corpus, not the wording.
I set out to build a control proving a checker can tell an honest artifact row from a reclassified one. It cannot, and it must not, which is your own point about byte-identity. So the reclassification pair is byte-identical by construction and the suite requires that they be credited identically. The answer to reclassification is that it buys nothing, not that it gets caught. The runner refuses the whole suite if that pair ever diverges, so a later contributor cannot improve the corpus by handing the attacker a tell.
The grid came out with no contradictions inside it. All four combinations are legal, including artifact plus intercepted, which is live and still self-reported. That is what makes the axes irreducible: any faithful reduction gives the two middle cells the same value while they differ on everything that matters. Contradiction lives between a row's declaration and the records it carries, never between the members, so the vectors test that relation.
Three states sit outside it. Fail-closed for a missing or out-of-vocabulary value, unattested where a row is covered but signed outside the pinned roots, and declared-but-uncovered.
The canonical contract for
cross-system-verification-v1is the part I do not have and the part your offer fits exactly. What each row is permitted to assert is a vocabulary question, and the corpus currently carrieswitness_scopeas an open string precisely because that enumeration is not settled here. Typing the two axes as strings and not as enums is also deliberate: a vector whose point is an out-of-vocabulary value has to be able to carry one, and moving that into schema validation tests the schema, not the verifier.The PEER ambiguity is untouched.
On sequencing, the leads have not spoken on this thread or on #30, #35, #36 or #37, and the last merge here predates #29. So I would put something concrete in front of them and let them sequence it. If you post the contract in this thread the two halves meet, and the vectors follow it wherever it is decidable against them.
Separately, the observability working group cross-reference is worth following up and I had not seen
aps-conformance-suite#42. Four rounds of independent review on a validator is worth reading before we design ours.giskard09 commentedon Aug 30, 2026
Contract for
cross-system-verification-v1, two rows (mapped from the four canonical steps indocs/spec/cross-system-verification.md):wire_transcript— covers steps 1-2 (recompute_action_ref,match_trail_record). basis:artifact— both steps operate on thetrail_record, which the emitter produces and hands over. What it asserts: the four preimage fields hash to the statedaction_ref, and the record under verification is the one being checked. What it does NOT assert: that the action was authorized, that the trail corresponds to anything real off-record, or that the emitter is honest — an emitter can construct a self-consistent faketrail_recordend to end, and these two steps alone cannot detect that. witness_scope:EXTERNAL(per the 28/8 row).anchor_commitment— covers steps 3-4 (fetch_chain_anchor,match_anchor_payload). basis:substrate— the transaction is fetched from a public RPC the emitter doesn't control ("no trust in the trail emitter," verbatim from spec). What it asserts: this exactaction_refexists as on-chain payload at the statedtx_hash. What it does NOT assert: that the preimage fields hashed into thataction_refare truthful — the chain only sees the digest, never the fields. witness_scope:EXTERNAL.Both rows stay open string, not enum, for the same reason your corpus does: an out-of-vocabulary basis has to be nameable, not rejected by schema before the verifier ever sees it.
PEER stays untouched, as you said — no primitive on our side yet.
One place I'm less sure and want to flag rather than assert: whether
wire_transcriptandanchor_commitmentshould be two separate corpus rows or one row with an internal two-step basis split, given they're both required for the sameverification_refto be conformant (spec invariant 3 — all four steps mandatory, none optional). Your call on which shape fits the runner better.28 remaining items
Mayur021 commentedon Oct 5, 2026
Carrying one category over from #19 now that it closes here, because it is the one thing in that thread still unnamed and it does not appear in this one. The handshake and negotiation bullet lists startup proceed/refuse, but that is an adapter honouring a posture it negotiated, not the case where nothing was negotiated at all.
The resolved position there was three verdicts plus a coverage rung rather than a wider enum, which composes better and I agree with it. What still needs naming sits at the top of that ladder: supported, configured, effective and observed all hold, traffic traversed the path, and no decision was rendered. Verdict PASS, coverage full, control absent.
On ACS that is reachable twice rather than hypothetically. Section 6.4 defaults
on_decision_failuretoproceed, "so that a slow, erroring, or unreachable Guardian does not halt production", which leaves an execution a harness cannot separate from a policy-evaluated allow. Section 4.1 does the same one level up: the startup posture defaults toproceed, and sinceon_decision_failureis negotiated in the handshake it cannot govern a handshake that never completed, so a failed hello starts the session unguarded.The two paths want different witnesses, which is why they are two members rather than one. Section 6.4's audit event has a Guardian to receive it. Section 4.1's does not, which is the MUST #37 says has no conformant shape to live in, so it belongs in the deployment's own log.
In the vocabulary already in use here, the discriminator is not whether the step was allowed but whether a decision record exists for it. A member that only asserts the allow scores the default posture as evidence of enforcement.
@astrogilda said on #19 they were building corpus members for both paths, so this may already be in hand.
astrogilda commentedon Oct 13, 2026
Thanks @Mayur021, both paths now have conformance test vectors. Section 6.4 was already family acs-f-5 (ACS-R-005), and section 4.1 is the new family acs-f-11 from agent-evidence-vectors#124.
Your test is in v0e1b9d0f75f54e9c, which rejects an unguarded start that left no record in the deployment's own log. v1ce4aeb611285368 rejects a session started after SESSION_REFUSED, and both ship in
agent-evidence-vectors==0.18.1on PyPI.Mayur021 commentedon Oct 13, 2026
Thanks @astrogilda. Turning the family on whether a record of the start exists, rather than on whether the session started, is the right axis to have picked.
One member worth naming alongside the two rejects: v58b380041d50cb9a, accepting on ACS-R-023 and ACS-R-025. No ServerHello, posture proceed, session started, a record in the deployment's own log, expected allow at coverage observed. That is the PASS with the control absent, which is the shape #19 closed without naming.
That it is in the family at all is the part I would not pass over. Rejects alone would also be satisfied by a verifier that denies every unguarded start, which fails the other way and still reads as enforcement. Having both directions present is what makes the family falsifiable, which is what #19 was opened about.
zhaoxinghua09-cell commentedon Oct 17, 2026
Evidence-based proposal: a "judgment semantics" dimension for the requirement ledger (#33), and the two-verdict gap #53 asks to write down
This is an independent, evidence-based contribution from the LGD-Eval evaluation-protocol project. It proposes nothing by way of ownership or priority — if the maintainers find the vectors useful, the normative wording and schema edits are theirs to shape.
The gap, in the project's own terms. #53 says it exactly: "A refusal because evidence was measured and found wrong differs from a refusal because evidence couldn't be measured, and the operator does something different in each case" — and names the job: "to write that down in normative text, where it costs nothing to maintain and survives every revision." In v0.1 the wire expresses the first case but not the second as a first-class category:
denyplus a free-vocabularyreason_codesentry, with §9.2 already setting the precedent (reason_codes: ["approver_unavailable"]for the no-recovery-path DENY).defer-details.json'sreasonis a closed enum of exactlyinsufficient_context | conflicting_policies | low_confidence | pending_dependency(spec §6 lists the same four), so an implementation that cannot measure the evidence must fold it intoinsufficient_contextand approximate. The distinction [Proposal] Ship negative conformance vectors in v0.2 #53 asks to write down currently survives only as an approximation inside the free-vocabularyreason_codes.Proposed normative additions (offered, not claimed).
defer-details.jsonreasonenum: addevidence_unmeasurable— "authoritative evidence could not be measured at decision time; absence of evidence is not confirmed invalidity." This keeps DEFER's recoverable semantics (resolution_method,resolution_timeout_ms,timeout_decision, defaultdeny) and stays consistent with the §9.2 boundary: potentially recoverable →DEFER; unconditionally outside reachable authority →DENY.confirmed_invalidas the DENY-side code for the measured-but-wrong case, explicitly distinct fromevidence_unmeasurable, so operators can switch on codes rather than parse reasoning text (§6.1: "UIs and meta-policies SHOULD switch on these rather than parsing reasoning text or rule IDs").Mapping for the #33 requirement ledger. Give each ledger row (requirement ID + exact normative text + positive/negative/integration tests + status) a judgment-semantics dimension: a four-state evidence classification of the row's conformance claim —
verified → ALLOW,unresolved → DEFER (evidence_unmeasurable),rejected → DENY (confirmed_invalid),quarantined → DEFER/ASK(pending human review) — each carrying the evidence grade and a payload digest, so a conformance claim is reproducible rather than self-attested. That is worth pinning while conformance.md still states "Who verifies a conformance claim: nobody, in v0.1.0" and the conformance suite is tracked in #19.Reproducible samples (schema-valid v0.1.0 wire shapes). Same step, same subject, two outcomes — B1-style (evidence unmeasurable) →
defer; B2-style (measured and found invalid) →deny:{ "jsonrpc": "2.0", "id": "acs-vector-b1-0001", "result": { "type": "final", "acs_version": "0.1.0", "request_id": "0a3b9f1e-4d2c-4e6b-9c7a-1c9d4b0b9a11", "decision": "defer", "reasoning": "Verdict not yet reachable: authoritative evidence for the requested capability (issuer-scope registry entry) could not be measured at decision time. Absence of measured evidence is not confirmed invalidity — the evidence-grader judgment class is unresolved, not rejected. The deferral fails closed on timeout.", "reason_codes": ["evidence_unmeasurable"], "defer_details": { "reason": "insufficient_context", "resolution_method": "additional_context", "resolution_timeout_ms": 30000, "timeout_decision": "deny", "required_context": ["authoritative_scope_registry_entry"] }, "policy_data": { "evidence-grader": { "authorization_result": "unresolved", "failure_class": "unresolved", "evidence_grade": "L", "payload_digest": "8d81c5d559914ec5", "payload_digest_method": "sha256(utf8)[:16] over canonical JSON of {domains, authorization} — LGD-Eval evidence-grader v1.2", "source": "https://github.com/LGD-Eval/evaluation-protocol/blob/main/worked-examples/cases/caseB1_unverifiable_issuer_scope_conclusion.json", "proposed_reason_code": "evidence_unmeasurable — DEFER; normative addition proposed for defer-details.json 'reason' enum (currently closed at 4 values)" } }, "cited_provenance_ids": [], "metadata": { "evaluator": "deterministic", "evaluator_version": "evidence-grader v1.2 (LGD-Eval evaluation-protocol)" } } }{ "jsonrpc": "2.0", "id": "acs-vector-b2-0001", "result": { "type": "final", "acs_version": "0.1.0", "request_id": "1b4c2a0e-5e3d-4f7c-8d0b-2d0e5c1c0b22", "decision": "deny", "reasoning": "Evidence was measured and found invalid: the authoritative scope registry confirms the subject lacks the required scope for this capability. This is confirmed invalidity with no recovery path — denied outright, not deferred (contrast: agent-control-standard#53 two-verdict, evidence_unmeasurable).", "reason_codes": ["confirmed_invalid"], "policy_data": { "evidence-grader": { "authorization_result": "rejected", "failure_class": "none", "evidence_grade": "V", "payload_digest": "c19e1781ebd4f690", "payload_digest_method": "sha256(utf8)[:16] over canonical JSON of {domains, authorization} — LGD-Eval evidence-grader v1.2", "source": "https://github.com/LGD-Eval/evaluation-protocol/blob/main/worked-examples/cases/caseB2_confirmed_out_of_scope_issuer_conclusion.json", "proposed_reason_code": "confirmed_invalid — DENY; distinct from evidence_unmeasurable (measured, authoritative, no recovery path)" } }, "cited_provenance_ids": ["prov-tb2b-scope-registry-entry"], "metadata": { "evaluator": "deterministic", "evaluator_version": "evidence-grader v1.2 (LGD-Eval evaluation-protocol)" } } }Both envelopes pass validation against the canonical
specification/v0.1.0/response-envelope.json(Draft 2020-12, whole schema package, 44 schemas) and use only §6.1-declared fields. The samples keepdefer_details.reasoninside the closed 4-value enum and carry the proposed codes inreason_codes+policy_data— which is how the distinction travels on the wire today. Thepolicy_data["evidence-grader"]block carries the evidence grade, the four-state class, and a payload digest computed with the exact method of the published grader (sha256 over canonical JSON of{domains, authorization}, first 16 hex chars). Both digests were independently recomputed from the published conclusion files and match the values embedded in the samples — B18d81c5d559914ec5, B2c19e1781ebd4f690— so these vectors are end-to-end reproducible from public artifacts:(Signing, mandatory under ACS-Core, is omitted from the sample envelopes as transport-level and deployment-specific.)
Offer. If this is useful to #33/#53, I can follow up per CONTRIBUTING.md: a Discussion proposing the
defer-details.jsonenum extension and the ledger column schema, then an accepted issue and a PR. Happy to adjust wording or add vectors (e.g., thequarantined → DEFER/ASKcase).zhaoxinghua09-cell commentedon Oct 17, 2026
Verdict evidence records + a quarantine category for the conformance suite
Building on what is already on the table — digest-named vectors (
negative-vectors-v0.2), thewitness_scope/verification_basiscolumns,classify_outcome— two additive pieces that the suite may still be missing. Offered as contribution method, not a standard claim.1. Make the suite's output an evidence record, not a test log
A conformance run currently produces a verdict per vector. For a claim anyone can independently re-check (conformance.md: "Test the deployment against the requirements above the way you would check any other vendor claim"), the per-vector output should be packaged as a verdict evidence record:
vector_id= SHA-256 of canonical input (same rule as the negative-vectors branch)verdict+witness_scope+verification_basis(reuse the columns already agreed)expectedvsobserved(decision + reason_codes)behavior_snapshot(chain_hash before/after, runner identity)payload_digestover the verdict fields, so a third party can verify "this record is what the runner actually observed" without trusting the runnerThis is deliberately shaped like an in-toto-style attestation: a conformance suite then emits records that can enter an audit/evidence chain (same family as the eval-result predicate discussed in in-toto #565). The suite stops being self-reported and becomes externally verifiable evidence.
2. A quarantine category for spec-undecided fields
Fields whose normative semantics are still moving (#21 MODIFY composition;
subagentStop.final_chain_hashoptionality) currently force a vector author to guess. Rather than folding them into pass/fail, route them to a quarantined status: excluded from the formal result until the spec settles, still recorded with their digest. This is orthogonal to the verdict/coverage discussion — it isolates spec uncertainty from implementation behavior.Reproducible sample (deterministic, offline)
{ "acs_conformance_verdict": { "vector_id": "70cbcb3d7d7aa44c", "profile": "acs-core", "anchor": "docs/spec/conformance.md#replay-protection", "clause": "CORE-008 replay protection", "verdict": "rejected", "witness_scope": "EXTERNAL", "verification_basis": "wire_transcript", "input_digest": "70cbcb3d7d7aa44cfcb0b319831cf4d5b77e51567dc9b40ecc32261be02bfa2a", "expected": {"decision": "DENY", "reason_codes": ["missing_request_id"]}, "observed": {"decision": "DENY", "reason_codes": ["missing_request_id"]}, "behavior_snapshot": {"chain_hash_before": "GENESIS", "chain_hash_after": "9f2c1b7e...", "runner": "acs-runner@0.1.0-demo"}, "payload_digest": "01e26fde3ca22e9ca209bf67c3ec4d40aae33579e23e285e660367124f7a7423" }, "quarantine_note": { "vector_id": "01410bfff073d383", "reason": "payload carries MODIFY redaction request; MODIFY composition still under discussion (#21)", "status": "quarantined" } }If useful, we can contribute a reference verifier that maps a suite's verdict records back to the evidence chain (recompute digests, cross-check observed vs expected, emit an eval-result-shaped summary). Happy to adapt to the suite's chosen format.
narko4u commentedon Oct 17, 2026
Direction is right, and thank you for building on
witness_scopeandverification_basisrather than inventing parallel fields. Three things before this shape hardens, all about keeping the record honest rather than making it bigger.1.
witness_scope: "EXTERNAL"in the sample is not derivable from the run described. The registry defines the axis as who the witness is relative to the party whose execution is described: SELF (the observed party itself), PEER (another participant in the same interaction), EXTERNAL (a party outside it that the observed party cannot reach). A conformance suite executed by the deployment's own operator, driven byacs-runner@0.1.0-demo, is SELF. Labelling it EXTERNAL is the one error this field exists to prevent, because it is the field that carries the independence claim. I would have the record state the derivation explicitly, the runner's relationship to the deployment operator, rather than assert the value.2.
behavior_snapshot.runneris self-asserted, so the record is SELF-attested however it is digested.payload_digestproves the record is intact. It does not prove the runner observed what it says it observed. A digest is integrity, not provenance. To reach anything above SELF the record needs something the runner cannot manufacture: a second party's signature over the same bytes, or an external anchor over the digest. Worth stating which of those lifts the scope, so a digest is not mistaken for independence.3. Quarantine needs an owner and an expiry. Agreed on separating spec uncertainty from implementation behaviour. Add
quarantine_refpointing at the open spec issue and areview_bydate, otherwise quarantined vectors become a place where unresolved claims persist without anyone accountable. The vocabulary already sets that precedent withreview_byon proposed terms.Happy to review a draft of the record schema against these three, or to map it onto an existing evidence-record shape so the fields do not diverge.
zhaoxinghua09-cell commentedon Oct 17, 2026
Thanks — all three are correct, and point 2 is the one we must not blur ourselves: a digest is integrity, not provenance. A revised draft is below; the sample now derives SELF and states what would actually lift the scope.
1. Derive
witness_scoperather than assert it. The asserted value is replaced withwitness_derivation: the runner identity, the deployment operator, the runner's relationship to that operator (self/peer/external), and the basis for that relationship.witness_scopeis then derived from the relationship, never asserted. A run where the deployment's own operator drivesacs-runner@0.1.0-demoagainst its own deployment derivesrelationship: self->SELF. The derivation rule is the registry's own assignment rule — "SELF unless a concrete artifact path exists that lets the named witness check the requirement without relying on the enforcement point's own account" — so no new vocabulary is introduced.2. Separate integrity from provenance. The record now carries two layers:
integrity.payload_digest: sha256 over the canonical bytes (sorted keys, compact separators, UTF-8) of an explicitly named field list (payload_digest_fields). It proves the record is intact — nothing more — and its ownstatementfield says so.provenance.lift: the only thing that can move scope above SELF, and the runner cannot manufacture it:counter-signature— a second party's DSSE signature over the same bytes; an in-agreement peer lifts toPEER, an out-of-domain party the observed party cannot reach lifts toEXTERNAL; keys are pinned out of band.external-anchor— an RFC 3161 timestamp or an append-only transparency-log entry overpayload_digest, lifting toEXTERNAL.With neither present,
provenance.liftisnulland the record is SELF however it is digested.3. Quarantine gets an owner and an expiry.
quarantine_refpoints at the open spec issue (the §6.3 invalid-modifications fail-closed gap is #270, now that #21 has closed),review_bycarries an ISO date — 30 days in the sample, with the cadence for the suite to set — andownernames the originator, with accountability transferring to the spec issue's assignee on suite merge. This follows thestatus: proposed/review_bypromotion rule already used on proposed terms in the evidence vocabulary.Mapping onto an existing evidence-record shape. Rather than invent a parallel envelope, the record is an in-toto Statement v1:
subjectis the digest-named vector (sha256 of canonical input),predicateTypeis the verdict-record predicate,predicateis the fields above, and the whole statement is signed as a DSSE v1 envelope. This is the same carriage theagent-change-admission-v1predicate already defines —application/vnd.in-toto+json, trusted keys pinned out of band, a co-signature a consumer may not know — and it already states the same boundary there: a valid signature proves the pinned key signed exactly these bytes, not that the evaluations ran or that the reports are true. (See its "What a valid decision does not establish" section.)Reproducible sample (deterministic, offline;
python acs_verdict_demo_v2.py).verdict record (SELF, derived):
{ "_type": "https://in-toto.io/Statement/v1", "subject": [ { "name": "acs-conformance-vector:70cbcb3d7d7aa44c", "digest": { "sha256": "70cbcb3d7d7aa44cfcb0b319831cf4d5b77e51567dc9b40ecc32261be02bfa2a" } } ], "predicateType": "https://github.com/GenAI-Security-Project/agent-control-standard/issues/33#verdict-evidence-record-v0", "predicate": { "vector_id": "70cbcb3d7d7aa44c", "profile": "acs-core", "anchor": "docs/spec/instrument/specification.md#103-replay-protection", "clause": "§10.3 Replay protection (ACS-Core)", "verdict": "rejected", "witness_derivation": { "runner": { "id": "acs-runner@0.1.0-demo", "hosted_by": "deployment-operator" }, "deployment_operator": { "id": "the deployment under test (same trust domain)" }, "relationship": "self", "basis_for_relationship": "runner executed by the deployment's own operator against its own deployment; no second party observed the run" }, "witness_scope": "SELF", "verification_basis": [ "wire_transcript" ], "expected": { "decision": "deny", "reason_codes": [ "missing_request_id" ] }, "observed": { "decision": "deny", "reason_codes": [ "missing_request_id" ] }, "behavior_snapshot": { "chain_hash_before": null, "chain_hash_after": null, "runner": "acs-runner@0.1.0-demo", "note": "offline deterministic demo: the runner populates the section 8 rolling chain hashes from the live on-wire responses; none were produced here" }, "integrity": { "payload_digest": "ff7ede5e3c336f97b0556667fa144ec21941b950e07209ce86e1c2b89d607da9", "payload_digest_method": "sha256 over canonical JSON (sorted keys, compact separators, UTF-8)", "payload_digest_fields": [ "anchor", "clause", "expected", "observed", "profile", "vector_id", "verdict", "verification_basis", "witness_derivation", "witness_scope" ], "statement": "integrity only: proves the record is intact; does not prove the runner observed what it reports, and is not provenance" }, "provenance": { "lift": null, "note": "no second-party DSSE co-signature and no external anchor over the digest; witness_scope stays SELF. lift=counter-signature by an in-agreement peer -> PEER; co-signature by an out-of-domain party, or an external anchor (RFC 3161 timestamp / append-only transparency log) over payload_digest -> EXTERNAL. Either must be something the runner cannot manufacture." } } }quarantine record (owner + ref + review_by):
{ "_type": "https://in-toto.io/Statement/v1", "subject": [ { "name": "acs-conformance-vector:01410bfff073d383", "digest": { "sha256": "01410bfff073d3835775373ec63be2418378de41aae00d5e6a508689079abb5f" } } ], "predicateType": "https://github.com/GenAI-Security-Project/agent-control-standard/issues/33#quarantine-record-v0", "predicate": { "vector_id": "01410bfff073d383", "status": "quarantined", "reason": "payload carries a MODIFY redaction request; the §6.3 invalid-modifications fail-closed gap is still open (#270)", "quarantine_ref": "https://github.com/GenAI-Security-Project/agent-control-standard/issues/270", "owner": "zhaoxinghua09-cell (originator; accountability transfers to #270 assignee on suite merge)", "review_by": "2026-11-16", "excluded_from": "formal result until #270 resolves" } }Re-running prints identical bytes:
We accept the offer to review a draft: does this Statement v1 / DSSE carriage — scope derived, SELF as the default, and integrity separated from provenance — match the shape you want before the ledger rows harden? If it does, the predicate can be drafted as a predicate document in the same family as
agent-change-admission-v1rather than living on this issue.narko4u commentedon Oct 17, 2026
The digest reproduces. I recomputed sha256 over the canonical bytes of the ten named fields taken from the JSON you posted and got
ff7ede5e3c336f97b0556667fa144ec21941b950e07209ce86e1c2b89d607da9, identical to your claim, with every declared field present. Determinism at the payload level is not something I have to take on trust. That is the right place to have spent it.On the three questions: the Statement v1 carriage is the right shape, scope derived with SELF as the default is right. Separating integrity from provenance is also right. Two sharpenings, both of which protect the design you already have.
The anchor does not lift
witness_scope. Your own derivation rule is the test: SELF unless a concrete artifact path exists that lets the named witness check the requirement without relying on the enforcement point's own account. An RFC 3161 timestamp and a transparency-log entry both answer a different question. They let any reader establish that this digest existed at a time and was not backdated afterwards. Neither of them checks the requirement. Neither becomes a party who can. Classingexternal-anchoras a lift to EXTERNAL therefore reintroduces one layer down exactly the conflation you just removed: as a digest is integrity rather than provenance, an anchor is persistence rather than verification. What it buys is durability, meaning the record cannot be quietly rewritten or predated. That is worth having. It is not independence. A SELF record carrying a timestamp is still SELF.counter-signatureis the only path in your list that can lift. It can only lift under the same test. A pinned key signing the same bytes establishes that the key signed those bytes, which is the boundaryagent-change-admission-v1already states for itself. It becomes a PEER witness when the signer can check the requirement without the enforcement point's account, which in practice means the signer either drove its own client against the same deployment or holds the wire transcript and the vector. Otherwise it is a co-signature over a SELF record, useful for non-repudiation and not for scope. The record should say which of the two it is, because in a ledger row they look identical.Derivation needs identifiers, not prose.
witness_derivationderives the scope fromrelationship. That field is justified inbasis_for_relationshipas free text, withdeployment_operator.idcarrying the sentence "(the deployment under test (same trust domain))". A consumer cannot computeselffrom that, so the value is still asserted, only one step further from the field name. Carry the two inputs as comparable identifiers, the runner's hosting operator and the deployment operator. Letselffall out of their equality rather than out of a description. The lift needs the same treatment: name the counter-signer's key. Name what that key checked.Two smaller things. The declared field list excludes
behavior_snapshot,integrityandprovenance. Excluding the last two is unavoidable, but the effect is that the run data is covered by the envelope signature alone. That signature is the runner's own. That is fine provided a reader is told, sopayload_digestis not read as binding the observed hashes.subjectalso sits outside the digest, so the record is tied to the vector by the envelope rather than bypayload_digest.Last, the quarantine ref. Quarantining the MODIFY vector is the right instinct and #270 is the wrong target as it stands. #270 asks what an implementation must do at the five hooks where DENY is not a legal disposition when it receives an invalid
modificationsobject. The uncertainty that makes the redaction vector unscoreable is a different defect, namely how an invalid MODIFY composition fails closed. If the quarantine ref does not name the issue whose closure actually resolves this vector's uncertainty,review_byexpires against a decision that never covered it. Point it at the composition gap. If that issue does not exist yet, the quarantine record is the evidence that it should.On the predicate document: yes. A
predicateTypeanchored to a section of a live issue is a moving target. The field list needs to travel with a version. Draft it in the family ofagent-change-admission-v1and I will review it against these points before the ledger rows depend on it.zhaoxinghua09-cell commentedon Oct 17, 2026
Thank you for recomputing it — and for the sharpening. All five points land. The draft below takes each of them; the sample is regenerated from it.
1. The anchor does not lift
witness_scope. Agreed: an RFC 3161 timestamp and a transparency-log entry answer persistence, not verification — they establish that this digest existed at a time and was not backdated, and let a record resist quiet rewrite. Removed as a lift entirely.provenance.anchoris now declared as persistence only; a SELF record carrying one is still SELF, and the doc says so.2.
counter-signatureis the only lift, and the record says which one it is. The counter-signature now carrieslift_state:peerwhen the pinned key independently checked the requirement — it drove its own client against this deployment, or holds the wire transcript and the vector — andco-signaturewhen it did not, in which case it is non-repudiation over a SELF record and lifts nothing. It also names the keyid and what the key checked, so a ledger row reads the same way a consumer does.3. Derivation from identifiers, not prose.
basis_for_relationshipis gone. The two inputs are now comparable identifiers —runner.hosting_operator_idanddeployment_operator_id— andSELFfalls out of their equality. The same treatment applies to the lift:counter_signature.keyidandcounter_signature.checkedname the key and the requirement it checked.4. The two smaller things.
integrity.does_not_covernow listsbehavior_snapshot,integrity,provenanceandsubject, andintegrity.bindingstates the consequence plainly: the observed hashes are bound only by the runner's own envelope signature, and the record is tied to the vector by the envelope rather than bypayload_digest. A reader cannot mistake the digest for either binding.5. The quarantine ref. You are right that #270 is the wrong target: it asks what an implementation must do at the hooks where DENY is not a legal disposition. The uncertainty that makes the redaction vector unscoreable is the normative one — how an invalid MODIFY composition fails closed. I searched the open issue list and nothing names it (#270 is the hook-implementation question, #264 is the trace event, #181 is a host-adapter bug on valid MODIFY).
quarantine_refis thereforenull, andquarantine_gapnames the gap instead; this quarantined vector is the evidence that the issue should be filed. Offered, not done: I can open it as a spec workstream issue if you would like the ledger row to point at it.Predicate document. Drafted in the
agent-change-admission-v1family — DSSE v1 envelope, in-toto Statement v1 carriage, the same field table / verification steps / "what a valid record does not establish" layout, versioned (acs-verdict-evidence-v1.0.0-draft) so the field list stops traveling on a live-issue anchor. The field list carries the ID-equality derivation table, the counter-signature two-state table, thepayload_digestscope declaration and the quarantine fields. I can push it to a branch asdocs/predicates/acs-verdict-evidence-v1.mdfor your review, or paste it inline here — whichever costs you less.Revised sample (deterministic, offline;
python acs_verdict_demo_v3.py).verdict record (SELF by identifier equality):
{ "_type": "https://in-toto.io/Statement/v1", "subject": [ { "name": "acs-conformance-vector:70cbcb3d7d7aa44c", "digest": { "sha256": "70cbcb3d7d7aa44cfcb0b319831cf4d5b77e51567dc9b40ecc32261be02bfa2a" } } ], "predicateType": "https://github.com/GenAI-Security-Project/agent-control-standard/blob/main/docs/predicates/acs-verdict-evidence-v1.md", "predicate": { "vector_id": "70cbcb3d7d7aa44c", "profile": "acs-core", "anchor": "docs/spec/instrument/specification.md#103-replay-protection", "clause": "§10.3 Replay protection (ACS-Core)", "verdict": "rejected", "witness_derivation": { "runner": { "id": "acs-runner@0.1.0-demo", "hosting_operator_id": "acme-demo:operator/deployment-01" }, "deployment_operator_id": "acme-demo:operator/deployment-01" }, "witness_scope": "SELF", "verification_basis": [ "wire_transcript" ], "expected": { "decision": "deny", "reason_codes": [ "missing_request_id" ] }, "observed": { "decision": "deny", "reason_codes": [ "missing_request_id" ] }, "behavior_snapshot": { "chain_hash_before": null, "chain_hash_after": null, "runner": "acs-runner@0.1.0-demo", "note": "offline deterministic demo: the runner populates the section 8 rolling chain hashes from the live on-wire responses; none were produced here" }, "integrity": { "payload_digest": "6954561a770fdfb969a12d8b7940de2511ce151ca1d95a6ef57b2ad7bec1e171", "payload_digest_method": "sha256 over canonical JSON (sorted keys, compact separators, UTF-8)", "payload_digest_fields": [ "anchor", "clause", "expected", "observed", "profile", "vector_id", "verdict", "verification_basis", "witness_derivation", "witness_scope" ], "does_not_cover": [ "behavior_snapshot", "integrity", "provenance", "subject" ], "binding": "observed hashes are bound only by the runner's own DSSE envelope signature; the subject (vector) is bound by the envelope, not by payload_digest", "statement": "integrity only: proves the record is intact; not provenance" }, "provenance": { "counter_signature": null, "anchor": null, "note": "counter_signature is the only scope lift: lift_state=peer when the pinned key independently checked this requirement (drove its own client against this deployment, or holds the wire transcript and the vector), signer_relationship=peer -> PEER / external -> EXTERNAL; otherwise lift_state=co-signature (non-repudiation only, no lift). anchor (RFC 3161 timestamp or transparency log) proves persistence, not verification; a SELF record carrying one remains SELF." } } }quarantine record (gap named, ref null until the issue exists):
{ "_type": "https://in-toto.io/Statement/v1", "subject": [ { "name": "acs-conformance-vector:01410bfff073d383", "digest": { "sha256": "01410bfff073d3835775373ec63be2418378de41aae00d5e6a508689079abb5f" } } ], "predicateType": "https://github.com/GenAI-Security-Project/agent-control-standard/blob/main/docs/predicates/acs-verdict-evidence-v1.md", "predicate": { "vector_id": "01410bfff073d383", "status": "quarantined", "reason": "payload carries a MODIFY redaction request; how an invalid MODIFY composition fails closed (§6.3 wire semantics) is not normatively specified, so this vector cannot be scored", "quarantine_ref": null, "quarantine_gap": "Normative disposition for an unscoreable MODIFY composition. Distinct from #270, which asks what implementations must do at hooks where DENY is not a legal disposition. No open issue names this gap; this quarantined vector is the evidence it should be filed.", "owner": "zhaoxinghua09-cell (originator; accountability transfers to the spec workstream when the gap issue is filed)", "review_by": "2026-11-16", "excluded_from": "formal result until the gap is normatively resolved" } }Re-running prints identical bytes:
One question to settle before the document is final: do you want it as a branch under
docs/predicates/for review, and should the opened MODIFY-composition gap issue be filed by us or by the spec workstream?zhaoxinghua09-cell commentedon Oct 18, 2026
Following up on the predicate draft: the reference implementation is now public at https://github.com/zhaoxinghua09-cell/agent-evidence-kit. It is standard-library-only and runs offline;
python run_demo.pyregenerates the verdict record and prints the same digests (input70cbcb3d…, payload6954561a…), writing machine-readable JSON evidence alongside a self-contained single-page HTML report. The predicate field table lives indocs/acs-verdict-evidence-v1.0.0-draft.md.Still your call on the two open points: branch under
docs/predicates/vs inline, and who opens the MODIFY-composition gap issue.narko4u commentedon Oct 18, 2026
The digest reproduces again, over a field list that is now exact: sha256 over the canonical bytes of the ten declared fields gives
6954561a770fdfb969a12d8b7940de2511ce151ca1d95a6ef57b2ad7bec1e171, identical to your claim, no declared field missing and exactlybehavior_snapshot,integrityandprovenanceoutside it asdoes_not_coverstates. Points 1 through 4 are taken as I meant them and point 5 correctly drops #270.Two things remain and they are the same pattern you have just finished removing: the lift is asserted by the party that benefits from it.
The scope lift is authored by the record's own subject.
lift_statesits inprovenance, whichdoes_not_coverexcludes frompayload_digest, inside an envelope signed by the runner. So the field that decides whether a record is SELF, PEER or EXTERNAL is written by the runner, outside the digest, under the runner's own signature. A co-signature relabelledpeeris indistinguishable in the record from a peer witness, which is the ambiguity the two-state table was introduced to remove. ID equality fixed theSELFcase because the scope fell out of data two parties can compare. The lift needs the same treatment. It must fall out of an artifact the peer authored: a peer statement, signed by the peer key, naming the requirement it checked and the artifact it used, either its own client run against this deployment or the wire transcript digest plus the vector. Thenlift_stateis derived rather than chosen. Absent that artifact,co-signatureis the only honest state and it should be the default rather than a branch.The quarantine record carries no integrity and no provenance. It holds
review_byand it is the evidence that a vector cannot be scored, which makes it the most valuable row to leave unbound. As posted it can be edited in silence, which is the failure the verdict record exists to prevent. It needs the same envelope treatment, at minimum anintegrity.payload_digestover its own declared fields and a defined disposition for whenreview_bypasses. Expiry currently has no outcome and a clock that changes nothing enforces nothing.One mechanical point:
predicateTyperesolves todocs/predicates/acs-verdict-evidence-v1.md, which returns 404 today. Publish the document before the sample that cites it or the sample is unresolvable at the moment it is being reviewed.On your two questions. Branch, as a PR under
docs/predicates/. An inline paste cannot be cited line by line, it drifts from whatever lands in main andpredicateTypeneeds a blob URL that exists. File the MODIFY-composition gap yourself. You hold the vector and the analysis, the quarantine ref then names an issue whose closure genuinely resolves this uncertainty and the spec leads triage rather than launder the provenance of the finding.zhaoxinghua09-cell commentedon Oct 18, 2026
Thank you — and for re-checking the ten fields. Both remaining problems are the same one, and both are real: the scope lift was still chosen by the party that benefits from it, and the quarantine row was left unbound. Taking both.
1. The lift is derived from a peer-authored artifact, not chosen in
provenance. Removinglift_stateas a field. The record instead carriesprovenance.peer_artifact: the SHA-256 of a statement the peer signed, which names (a) the requirement it checked and (b) the artifact it checked it with — its own client run against this deployment, or the wire transcript digest plus the vector. The verifier derives the scope from that statement: it must be signed by a peer key the consumer pins, name this vector and this clause, and name one of the two evidence artifacts. Only then does the record readPEER(orEXTERNALwhen the peer is out of domain). Absent that artifact there is no lift: a co-signature over the runner's envelope is the default state, non-repudiation only, and the record readsSELF. The runner cannot relabel it because the field it would have to write does not exist — the value comes from a statement it did not author.Illustrative shape (the digested sample lands with the PR; not a published digest yet):
(
witness_scopethen falls out of identifier equality —SELF— with no field in this envelope claiming otherwise.)2. The quarantine record gets its own envelope treatment. It now carries
integrity.payload_digestover its own declared fields (vector_id,status,reason,quarantine_ref,quarantine_gap,owner,review_by,excluded_from) — same canonical rule — so an edit in silence breaks the digest.review_byalso gets a defined expiry disposition rather than a clock that changes nothing: whenreview_bypasses with the gap still open, the vector reverts tounmeasurableand is excluded from formal results; the owner must either re-quarantine with a newreview_byor point at the gap issue's resolution. A quarantined vector never becomes a formal pass or reject while the gap is open.3. The 404. Acknowledged —
predicateTyperesolves to a document that does not exist yet. The document is drafted and pushed to branchdocs/acs-verdict-evidence-v1on my fork (zhaoxinghua09-cell/agent-control-standard), resolvable as a branch blob now; the PR against this repo lands it atdocs/predicates/acs-verdict-evidence-v1.mdfirst, and the sample that cites it is what goes with that PR. The v3 comment's citation is a forward reference in the meantime.4. Filing the gap. You are right that we hold the vector and the analysis. Opening the MODIFY-composition gap issue now (separately, linked below), and the quarantine record then points at it; the spec workstream triages it.
Determinism (reference sample, recomputed on a clean checkout). The kit runs as
python run_demo.py --selfteston a fresh copy: the two selftest suites pass (8 acs-verdict derivation/invariant cases; 15 evidence-grader invariants), and the digests are byte-identical on re-run — negative-vector input70cbcb3d…fa2a, verdict payload6954561a…e171(unchanged:provenancewas always outside its field list), quarantine payload (the new self-envelope)ff6ea9da…30c5d. The verdict digest itself did not move, because the change is exactly what you asked: nothing in the runner's field list asserts a lift anymore.narko4u commentedon Oct 18, 2026
Two of the three land, and the third is the same pattern one hop further in.
The lift is derived from a peer artifact, but not from the record.
peer_artifactsits inprovenance, anddoes_not_coverexcludesprovenancefrompayload_digest. The statement names this vector, the requirement and the evidence artifact. It does not name the outcome. So one genuine statement for vector V is reusable: mint a second record for V withverdict: passwhere the first saidrejected, point both at the samepeer_artifact, and both derive PEER. The peer attested that they checked R against V with artifact A, not that this record's result is what they observed, and the record's payload is not what the artifact binds. The requirement is that the lift binds the payload the peer witnessed, not just the vector. How the statement carries that is yours to design.expiry_dispositionis the only field on the quarantine record outside its digest. Row 153 lists the eight fields the digest covers andexpiry_dispositionis not among them, while row 152 makes it a field the runner writes. That field decides what happens whenreview_bypasses, which is the entire enforcement of the clock. Either it is normative (state the behaviour in the predicate, drop the field, have the verifier apply it) or it goes insidepayload_digest_fields. As drafted, the most consequential field on the record is the one that can be edited in silence.The quarantine digest is not reproducible from public code. I ran the public kit at
e2f85f62under a network-isolated sandbox: verdict digests reproduce exactly, input70cbcb3d…fa2aand payload6954561a…e171. But the generated evidence still emits a quarantine predicate with nointegrityblock andquarantine_ref: null, andff6ea9daappears nowhere in the repository. That digest is from unpublished code. Publish the branch that carries the change or drop the number, the same standard you accepted on the verdict record.Confirmed otherwise: doc before code is the right order, and the predicate being ahead of main is fine while it is labelled that way. #296 can now fill
quarantine_ref.One question rather than a finding. Row 111 rejects a record where
hosting_operator_iddiffers fromdeployment_operator_idand no peer artifact is present. That reads as verdict records only being authorable by the deployment operator, which means an external lab publishes a statement and never a record. If that is the intent, say it in the section, because the party that scores an implementation is not the party that shipped it, and that party needs a way to file its own record under its own name for the label to be worth anything to a buyer.Bind the peer statement to the verdict and test the digest rule
peer-reconciliation-ref-v1.2: close co_signer replay/reuse gap (signa…