Skip to content

[Proposal] Milestone: full ACS-Core conformance (requirement ledger + behavioral tests, beyond adapter emission) #33

Open

Description

@bar-capsule

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.md and 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.md lists 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 honor timeout_config (incl. per-method), methods_evaluated, selected_transport, negotiated signature algorithms, skew_window_ms, accepted profiles. Reconcile the stdio advertisement in acs_common with 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 generic steps/toolCallRequest/Result collapsing 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/changed on 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.

Activity

  1. astrogilda commented on Aug 21, 2026

    @astrogilda

    "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 --check flag 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_evaluated semantics, 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.md listing 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 in conformance.md and 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.

  2. ryjen commented on Aug 25, 2026

    @ryjen

    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:

    authorize A
    -> dispatch A
    -> downstream effect is B
    -> independently observed execution or terminal state confirms B
    -> correspondence FAIL
    

    Positive control:

    authorize A
    -> dispatch A
    -> downstream effect is A
    -> independently observed terminal state confirms A
    -> correspondence PASS
    

    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:

    authorization evidence
      != dispatch evidence
      != provider result
      != independently observed terminal state
    

    A valid authorization for A establishes that A was permitted under the modeled policy inputs. It does not establish that the framework, tool adapter, or downstream provider actually performed A, and it should not retroactively authorize a materially different B.

    ACS already has useful request/result correlation through toolCallRequest, toolCallResult, and request_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:

    write "approved-content" to workspace/allowed.txt
    

    but the downstream effect instead changes:

    workspace/protected.txt
    

    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.

  3. narko4u commented on Aug 28, 2026

    @narko4u
    Contributor

    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:

    1. 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.

    2. 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.

    3. 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.

  4. narko4u commented on Aug 28, 2026

    @narko4u
    Contributor

    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)

    Scope Who can observe satisfaction Evidence comes from
    SELF The deployment's own reports; the enforcement point is the only party able to produce the evidence The deployment's own account, no external path
    PEER Another party within an agreement: a counterparty, a peer Guardian mediating the same interaction, or a tenant Artifacts exchanged within the agreement
    EXTERNAL A party outside the trust domain: an auditor, a regulator, or any non-trusting third party Artifacts obtainable and checkable without cooperation from the enforcement point

    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)

    Basis What the witness checks
    wire_transcript Recorded request/response traffic between the parties
    signed_decision_record Signed decision artifacts the Guardian emits
    policy_state The enforced policy configuration and its version at decision time
    terminal_state_evidence Observable system state after the action
    anchor_commitment External commitments such as signed chain-head checkpoints or an append-only log

    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

    Claim form Meaning Scope
    "emission-conformant" Emitted artifacts validate against the canonical schema (no operational scope asserted) None asserted
    "operationally conformant, self-attested" Operational conformance attested by the deployment itself SELF
    "operationally conformant, peer-verifiable" Operational conformance checkable by an in-agreement party PEER
    "operationally conformant, externally verifiable" Operational conformance checkable by an out-of-domain party EXTERNAL

    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.

  5. giskard09 commented on Aug 28, 2026

    @giskard09

    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 of wire_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.

  6. narko4u commented on Aug 28, 2026

    @narko4u
    Contributor

    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, basis anchor_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.

  7. giskard09 commented on Aug 28, 2026

    @giskard09

    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.

  8. astrogilda commented on Aug 29, 2026

    @astrogilda

    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.

  9. narko4u commented on Aug 29, 2026

    @narko4u
    Contributor

    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.

  10. giskard09 commented on Aug 29, 2026

    @giskard09

    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.

  11. narko4u commented on Aug 29, 2026

    @narko4u
    Contributor

    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.

  12. giskard09 commented on Aug 29, 2026

    @giskard09

    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.

  13. astrogilda commented on Aug 30, 2026

    @astrogilda

    Co-build works. The half you named as ours is built, so here is where the seam falls.

    conformance/basis/ on branch basis-method-conformance at f451b00. 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-v1 is 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 carries witness_scope as 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.

  14. giskard09 commented on Aug 30, 2026

    @giskard09

    Contract for cross-system-verification-v1, two rows (mapped from the four canonical steps in docs/spec/cross-system-verification.md):

    wire_transcript — covers steps 1-2 (recompute_action_ref, match_trail_record). basis: artifact — both steps operate on the trail_record, which the emitter produces and hands over. What it asserts: the four preimage fields hash to the stated action_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 fake trail_record end 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 exact action_ref exists as on-chain payload at the stated tx_hash. What it does NOT assert: that the preimage fields hashed into that action_ref are 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_transcript and anchor_commitment should be two separate corpus rows or one row with an internal two-step basis split, given they're both required for the same verification_ref to be conformant (spec invariant 3 — all four steps mandatory, none optional). Your call on which shape fits the runner better.

  15. 28 remaining items

  16. Mayur021 commented on Oct 5, 2026

    @Mayur021

    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_failure to proceed, "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 to proceed, and since on_decision_failure is 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.

  17. astrogilda commented on Oct 13, 2026

    @astrogilda

    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.1 on PyPI.

  18. Mayur021 commented on Oct 13, 2026

    @Mayur021

    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.

  19. zhaoxinghua09-cell commented on Oct 17, 2026

    @zhaoxinghua09-cell

    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:

    • "Measured and found wrong" is expressible today: deny plus a free-vocabulary reason_codes entry, with §9.2 already setting the precedent (reason_codes: ["approver_unavailable"] for the no-recovery-path DENY).
    • "Evidence couldn't be measured" has no first-class slot: defer-details.json's reason is a closed enum of exactly insufficient_context | conflicting_policies | low_confidence | pending_dependency (spec §6 lists the same four), so an implementation that cannot measure the evidence must fold it into insufficient_context and 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-vocabulary reason_codes.

    Proposed normative additions (offered, not claimed).

    1. §6 DEFER reasons + defer-details.json reason enum: add evidence_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, default deny) and stays consistent with the §9.2 boundary: potentially recoverable → DEFER; unconditionally outside reachable authority → DENY.
    2. confirmed_invalid as the DENY-side code for the measured-but-wrong case, explicitly distinct from evidence_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 keep defer_details.reason inside the closed 4-value enum and carry the proposed codes in reason_codes + policy_data — which is how the distinction travels on the wire today. The policy_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 — B1 8d81c5d559914ec5, B2 c19e1781ebd4f690 — 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.json enum extension and the ledger column schema, then an accepted issue and a PR. Happy to adjust wording or add vectors (e.g., the quarantined → DEFER/ASK case).

  20. zhaoxinghua09-cell commented on Oct 17, 2026

    @zhaoxinghua09-cell

    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), the witness_scope/verification_basis columns, 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)
    • expected vs observed (decision + reason_codes)
    • behavior_snapshot (chain_hash before/after, runner identity)
    • payload_digest over the verdict fields, so a third party can verify "this record is what the runner actually observed" without trusting the runner

    This 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_hash optionality) 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)

    python acs_verdict_demo.py
    {
      "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.

  21. narko4u commented on Oct 17, 2026

    @narko4u
    Contributor

    Direction is right, and thank you for building on witness_scope and verification_basis rather 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 by acs-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.runner is self-asserted, so the record is SELF-attested however it is digested. payload_digest proves 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_ref pointing at the open spec issue and a review_by date, otherwise quarantined vectors become a place where unresolved claims persist without anyone accountable. The vocabulary already sets that precedent with review_by on 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.

  22. zhaoxinghua09-cell commented on Oct 17, 2026

    @zhaoxinghua09-cell

    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_scope rather than assert it. The asserted value is replaced with witness_derivation: the runner identity, the deployment operator, the runner's relationship to that operator (self / peer / external), and the basis for that relationship. witness_scope is then derived from the relationship, never asserted. A run where the deployment's own operator drives acs-runner@0.1.0-demo against its own deployment derives relationship: 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 own statement field 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 to PEER, an out-of-domain party the observed party cannot reach lifts to EXTERNAL; keys are pinned out of band.
      • external-anchor — an RFC 3161 timestamp or an append-only transparency-log entry over payload_digest, lifting to EXTERNAL.

    With neither present, provenance.lift is null and the record is SELF however it is digested.

    3. Quarantine gets an owner and an expiry. quarantine_ref points at the open spec issue (the §6.3 invalid-modifications fail-closed gap is #270, now that #21 has closed), review_by carries an ISO date — 30 days in the sample, with the cadence for the suite to set — and owner names the originator, with accountability transferring to the spec issue's assignee on suite merge. This follows the status: proposed / review_by promotion 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: subject is the digest-named vector (sha256 of canonical input), predicateType is the verdict-record predicate, predicate is the fields above, and the whole statement is signed as a DSSE v1 envelope. This is the same carriage the agent-change-admission-v1 predicate 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:

    negative vector input_digest : 70cbcb3d7d7aa44cfcb0b319831cf4d5b77e51567dc9b40ecc32261be02bfa2a
    quarantine vector digest    : 01410bfff073d3835775373ec63be2418378de41aae00d5e6a508689079abb5f
    verdict payload_digest      : ff7ede5e3c336f97b0556667fa144ec21941b950e07209ce86e1c2b89d607da9
    derived witness_scope       : self -> SELF
    

    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-v1 rather than living on this issue.

  23. narko4u commented on Oct 17, 2026

    @narko4u
    Contributor

    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. Classing external-anchor as 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-signature is 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 boundary agent-change-admission-v1 already 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_derivation derives the scope from relationship. That field is justified in basis_for_relationship as free text, with deployment_operator.id carrying the sentence "(the deployment under test (same trust domain))". A consumer cannot compute self from 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. Let self fall 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, integrity and provenance. 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, so payload_digest is not read as binding the observed hashes. subject also sits outside the digest, so the record is tied to the vector by the envelope rather than by payload_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 modifications object. 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_by expires 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 predicateType anchored 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 of agent-change-admission-v1 and I will review it against these points before the ledger rows depend on it.

  24. zhaoxinghua09-cell commented on Oct 17, 2026

    @zhaoxinghua09-cell

    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.anchor is now declared as persistence only; a SELF record carrying one is still SELF, and the doc says so.

    2. counter-signature is the only lift, and the record says which one it is. The counter-signature now carries lift_state: peer when the pinned key independently checked the requirement — it drove its own client against this deployment, or holds the wire transcript and the vector — and co-signature when 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_relationship is gone. The two inputs are now comparable identifiers — runner.hosting_operator_id and deployment_operator_id — and SELF falls out of their equality. The same treatment applies to the lift: counter_signature.keyid and counter_signature.checked name the key and the requirement it checked.

    4. The two smaller things. integrity.does_not_cover now lists behavior_snapshot, integrity, provenance and subject, and integrity.binding states 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 by payload_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_ref is therefore null, and quarantine_gap names 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-v1 family — 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, the payload_digest scope declaration and the quarantine fields. I can push it to a branch as docs/predicates/acs-verdict-evidence-v1.md for 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:

    negative vector input_digest : 70cbcb3d7d7aa44cfcb0b319831cf4d5b77e51567dc9b40ecc32261be02bfa2a
    quarantine vector digest    : 01410bfff073d3835775373ec63be2418378de41aae00d5e6a508689079abb5f
    verdict payload_digest      : 6954561a770fdfb969a12d8b7940de2511ce151ca1d95a6ef57b2ad7bec1e171
    derivation                 : hosting=deployment -> SELF
    

    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?

  25. zhaoxinghua09-cell commented on Oct 18, 2026

    @zhaoxinghua09-cell

    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.py regenerates the verdict record and prints the same digests (input 70cbcb3d…, payload 6954561a…), writing machine-readable JSON evidence alongside a self-contained single-page HTML report. The predicate field table lives in docs/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.

  26. narko4u commented on Oct 18, 2026

    @narko4u
    Contributor

    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 exactly behavior_snapshot, integrity and provenance outside it as does_not_cover states. 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_state sits in provenance, which does_not_cover excludes from payload_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 relabelled peer is indistinguishable in the record from a peer witness, which is the ambiguity the two-state table was introduced to remove. ID equality fixed the SELF case 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. Then lift_state is derived rather than chosen. Absent that artifact, co-signature is 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_by and 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 an integrity.payload_digest over its own declared fields and a defined disposition for when review_by passes. Expiry currently has no outcome and a clock that changes nothing enforces nothing.

    One mechanical point: predicateType resolves to docs/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 and predicateType needs 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.

  27. zhaoxinghua09-cell commented on Oct 18, 2026

    @zhaoxinghua09-cell

    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. Removing lift_state as a field. The record instead carries provenance.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 read PEER (or EXTERNAL when 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 reads SELF. 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):

    provenance: {
      peer_artifact: null
    }

    (witness_scope then falls out of identifier equality — SELF — with no field in this envelope claiming otherwise.)

    // the peer-authored statement, signed by the peer key (not the runner's),
    // referenced from the verdict record as provenance.peer_artifact = sha256 of its canonical bytes
    {
      _type: https://in-toto.io/Statement/v1,
      subject: [{name: acs-conformance-vector:70cbcb3d7d7aa44c, digest: {sha256: 70cbcb3d…fa2a}}],
      predicate: {requirement: docs/spec/instrument/specification.md#103-replay-protection,
                    evidence_artifact: {kind: wire_transcript, transcript_digest: <sha256>, vector: acs-conformance-vector:70cbcb3d7d7aa44c}}
    }

    2. The quarantine record gets its own envelope treatment. It now carries integrity.payload_digest over 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_by also gets a defined expiry disposition rather than a clock that changes nothing: when review_by passes with the gap still open, the vector reverts to unmeasurable and is excluded from formal results; the owner must either re-quarantine with a new review_by or 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 — predicateType resolves to a document that does not exist yet. The document is drafted and pushed to branch docs/acs-verdict-evidence-v1 on my fork (zhaoxinghua09-cell/agent-control-standard), resolvable as a branch blob now; the PR against this repo lands it at docs/predicates/acs-verdict-evidence-v1.md first, 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 --selftest on 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 input 70cbcb3d…fa2a, verdict payload 6954561a…e171 (unchanged: provenance was 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.

  28. narko4u commented on Oct 18, 2026

    @narko4u
    Contributor

    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_artifact sits in provenance, and does_not_cover excludes provenance from payload_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 with verdict: pass where the first said rejected, point both at the same peer_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_disposition is the only field on the quarantine record outside its digest. Row 153 lists the eight fields the digest covers and expiry_disposition is not among them, while row 152 makes it a field the runner writes. That field decides what happens when review_by passes, 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 inside payload_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 e2f85f62 under a network-isolated sandbox: verdict digests reproduce exactly, input 70cbcb3d…fa2a and payload 6954561a…e171. But the generated evidence still emits a quarantine predicate with no integrity block and quarantine_ref: null, and ff6ea9da appears 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_id differs from deployment_operator_id and 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    priority:P1Maintainers onlyscope:in-focusFeeds the ninety-day committed outcome. Maintainers onlystatus:acceptedIn the backlog. Maintainers onlytype:proposalNew feature or request

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions