Skip to content

[Bug] Guardian-side hook-coverage is unfalsifiable: methods_evaluated allows accept-and-ignore #31

Open

Description

@bar-capsule

Surfaced during Rock's review of PR #21 (item #6 of the review comment). Filing separately since the fix is a spec change beyond that PR's scope.

The gap

ACS spends substantial spec text constraining what the Observed Agent MUST emit, and never constrains what the Guardian MUST evaluate. The ServerHello's methods_evaluated says it plainly:

"Methods listed by the client but absent here are NOT evaluated ... Clients MAY still emit them for audit but MUST treat them as ALLOW-by-default."

So a Guardian can accept every subagent hook a conformant client emits and evaluate none of them, and both parties stay conformant. Every hook-coverage claim in ACS is unfalsifiable from the enforcement side.

Why this matters now

This is the first PR (#21) whose stated security rationale — mandating subagentStart to defend against the confused-deputy attack class — depends on the half that's missing. The client can be forced to emit the hook; nothing forces the Guardian to actually evaluate it against policy. A deployment can advertise ACS-Core conformance, emit every mandatory hook, and still have zero real enforcement, because methods_evaluated lets the Guardian opt out of evaluating what it accepts.

Distinct from #16, which is about the client omitting hooks. This is about the Guardian legitimately declaring it doesn't evaluate hooks it accepts.

Proposed directions (needs design work)

Why deferred from PR #21

Four-line prose relaxations to the ACS-Core disposition/hook sets cannot fix a spec-wide asymmetry between client-emit MUSTs and Guardian-evaluate SHOULDs. The mandate side of the subagent argument holds even without this issue closed; this issue is what makes the mandate checkable.

References

Activity

  1. added a commit that references this issue on Aug 18, 2026
  2. astrogilda commented on Aug 21, 2026

    @astrogilda

    A Guardian that answers -32004 is provably alive and provably refusing. A Guardian that lists a method in methods_evaluated and evaluates nothing is indistinguishable, on the wire and in the audit chain, from a Guardian enforcing a strict policy that nothing tripped. That is the whole problem, and it is not a gap in the prose. ACS already has a liveness method, and §13 is explicit that system/ping "carries no enforcement semantics and is not part of the audit chain", with hooks.md adding that it "always returns decision: allow regardless of policy, signature, or session state". So v0.1.0 has process liveness and no policy liveness, and the second one is the thing that has to be built.

    The sentence you quote lives in the schema, at specification/v0.1.0/handshake.json under $defs/ServerHello.properties.methods_evaluated.description, and specification.md §4 names the field with no semantics attached. That is a pointer for whoever writes the fix, since the normative content currently sits in a description string. Searching the specification, the conformance page and the hook catalog at the default branch head for "MUST evaluate" returns nothing, against on_decision_failure, system/ping and methods_evaluated all returning hits in the same command, so the asymmetry you describe is real and survives the way I searched for it.

    Your first proposed direction is the one I would change. "A Guardian that lists a method in methods_evaluated MUST evaluate it against non-null policy for the session" is a presence check, and presence is exactly the property that survives the failure you are describing. A policy object that is non-null and matches nothing is non-null. A rule that is loaded and never reached is loaded. I have measured this on my own corpus. I run single-site weakenings of the reference implementation and replay the entire vector suite against each one, and at the published baseline, twenty-seven of seven hundred and fifty-four weakenings are recorded as the corpus watching the rule change and staying green. Every one of those would satisfy a ledger row saying the requirement has a test. The instrument that catches them is breaking the requirement and seeing whether anything notices, and I go into that at length on #33.

    What I would build for ACS is a probe that fires inside the session being certified, where a test harness sits outside it. The construction I use is in a document called docs/DETECTOR-LIVENESS.md in the corpus, and it works like this. A corpus author commits in advance to what a given attack looks like on the wire. The enforcement substrate signs what it actually committed to during the run. A row asserts the two are comparable and declares the stronger attribution. A run-end seal, signed by a party that does not control the carried record set, names what was attributed. A channel counts as demonstrated only when all five line up, and the seal matters specifically because without it a party holding the enclosing envelope can delete the interception and recompute a self-consistent root over what remains. Translated to ACS: a Guardian that declares evaluation of steps/toolCallRequest should be provably unable to end a session cleanly without a session-end record, signed by something that does not control the decision log, naming at least one input it refused inside that session.

    Three details in that construction took me longest to get right, and each transfers directly. The verdict is computed per channel and is never rolled into one boolean for the run, because a probe caught on one boundary establishes nothing whatever about the boundary beside it. A corpus that plants one probe and reports a live detector has measured a sample and called it a census. For ACS the channel is the method, so this reads as: per entry of methods_evaluated, never as a single flag for the Guardian. The fixtures then need three channels, because a rail that decides on the first row and the last passes a two-channel statement while skipping everything between; my per-channel fixture carries three planted probes and the three refusals beside it each put the fault on a channel that is not the first. And there has to be an honest-negative that must be accepted, which is the detail I would insist on. Mine is a vector carrying three planted probes with the middle channel's row clean, its attribution at the honest floor, and its attack absent from the seal: a producer whose detector did not fire, saying so. It is an accept vector in the manifest and it exists so that liveness cannot quietly become a validity requirement by accident. Refusing it would refuse the honest report along with the dishonest one, and the dishonest report of that same run is a separate reject vector one mutation away. A method with no probe fired reports as unprobed, which is neither a pass nor a failure, and that is the correct answer.

    The probe has to be adversarially tested too, and I would put this in whatever conformance work follows. A liveness probe hard-wired to answer "demonstrated" clears every vector in the corpus and reports a live detector for a dead one, which is the identical defect one level up. Mine has a test that plants faults into the probe itself and asserts it catches them. It is the same shape as the finding I put on your PR #30, where an adapter that reads the answer key and enforces nothing scores full marks with all six positive controls green.

    If that is more construction than v0.2 wants, there is a cheap version that gets most of the falsifiability. It is the entry-point field I proposed on #30, applied to the Guardian side: require every conformance vector to record which production code path the decision was dispatched through, and make the runner refuse a suite whose positive controls resolved to a different path than the negative vectors did. That does not get you in-window liveness and it does stop the accept-and-ignore Guardian passing a suite, because a Guardian that never routes a method through its policy engine cannot name a path that a refusing vector also names.

    Your second and third proposed directions are both right and I would take them as written. Reconciling agbom/snapshot against methods_evaluated is a good cross-check, and the Guardian-side suite is the thing that makes any of this falsifiable by test. I would like to help build them.

    Sankalp Gilda

  3. narko4u commented on Aug 28, 2026

    @narko4u
    Contributor

    Strong agreement on the gap, and one additive proposal that makes it observable per-decision on the wire rather than only in a conformance suite.

    The asymmetry, restated: the wire today distinguishes what a Guardian accepts (methods_evaluated) from what it evaluates, but not how a given decision was produced. An ALLOW verdict from a Guardian that ran policy against the method is indistinguishable, in the response envelope, from an ALLOW that came from the default path for an unevaluated method. That is the accept-and-ignore hole: it hides not in the decision outcomes, but in the absence of any record of what produced them.

    Proposal (additive, non-breaking): a decision-basis field on the response envelope. When a Guardian returns a verdict for a method it lists in methods_evaluated, the envelope carries a small enum naming how the decision was produced: deterministic policy evaluation, delegated agent-layer reasoning, or default-ALLOW (method not evaluated). Default-ALLOW is the only value that may appear for methods absent from methods_evaluated.

    Three properties fall out:

    1. Declared coverage becomes checkable per decision. A session where a method in methods_evaluated only ever produced default-ALLOW basis records is a conformance failure on its face, no probe required. methods_evaluated and the decision stream reconcile mechanically: every declared method should eventually produce at least one non-default basis record.

    2. It complements the probe construction instead of replacing it. Per-channel probes make enforcement liveness falsifiable by adversarial test; the basis field makes declared evaluation falsifiable on every single decision. Probes answer "did the detector fire when it should have?" The basis field answers "when a verdict came back, what produced it?" Both belong in the same suite.

    3. It composes with the audit chain. A basis field survives into session-end records and audit entries, so a Guardian that routinely routes a declared method through the default path cannot rewrite that history without the discrepancy showing up in per-channel reconciliation.

    This is a vocabulary change, not an enforcement mechanism change, so it can land in v0.1.x without touching the deterministic layer. Happy to write the schema diff and the conformance wording if maintainers want it.

  4. moved this to Needs triage in ACS Project Trackeron Sep 10, 2026
  5. added
    type:bugSomething isn't working
    scope:in-focusFeeds the ninety-day committed outcome. Maintainers only
    on Sep 10, 2026
  6. moved this to Needs triage in ACS Project Trackeron Sep 10, 2026
  7. changed the title [-]Guardian-side hook-coverage is unfalsifiable: methods_evaluated allows accept-and-ignore[/-] [+][Bug] Guardian-side hook-coverage is unfalsifiable: methods_evaluated allows accept-and-ignore[/+] on Sep 10, 2026
  8. astrogilda commented on Sep 17, 2026

    @astrogilda

    I would take the field, and it does not close the hole. The Guardian writes that basis about itself, so one routing nothing through policy can still emit deterministic policy evaluation. That is an artifact-basis row at SELF scope, and reconciling it needs a record the Guardian does not control. One member at probityai/agent-evidence-vectors carries a shell action with hooks_fired empty while the registry lists the tool.

    Edited 2026-09-25: the link now points at probityai/agent-evidence-vectors, where the repository lives after its transfer.

  9. narko4u commented on Sep 18, 2026

    @narko4u
    Contributor

    The limit is real, and the field does not close the hole on its own. A basis value written by the Guardian about the Guardian is a self-attested row, and a Guardian that routes nothing through policy can emit the highest basis value indefinitely. The field survives as vocabulary, and it does not carry assurance on its own.

    That leaves one requirement worth stating on the issue. A basis row becomes checkable only when something outside the Guardian's control corroborates it. Whether that corroborating record exists, and which party holds it, is deployment and vendor territory: Principle 3 keeps it off the wire, and Principle 2 asks the spec to leave the value vendors should compete on uncaptured.

    So the field should ship with its own scope attached. A basis value that only the emitting Guardian can assert should be readable as such on its face, so no consumer mistakes a self-attested row for an evaluated one. The reconciliation then has a defined subject, and the conformance suite has something to assert: a session whose basis rows are all self-attested fails the evaluation claim it advertises.

    That keeps the field additive, and it keeps the reconciliation where Principle 3 already puts it.

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:P2Maintainers onlyscope:in-focusFeeds the ninety-day committed outcome. Maintainers onlystatus:acceptedIn the backlog. Maintainers onlytype:bugSomething isn't working

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions