Skip to content

[Bug] No on-wire signal lets a Guardian detect missing hook coverage from an Observed Agent #16

Open

Description

@rocklambros

Surfaced during the v0.1.0 review (PR #2). Related to the Core-baseline conversation. Filing separately so we can track the detection design.

hooks.md says frameworks MUST fire toolCallRequest for every action that escapes the agent's reasoning context. The handshake's methods_evaluated is client-declared. The Guardian can refuse a session, but cannot detect what's missing once the session is underway. A compromised or malicious Observed Agent can declare methods_implemented: ["steps/sessionStart", "steps/userMessage", "steps/agentResponse", "steps/sessionEnd"] and omit toolCallRequest, memoryStore, subagentStart. The Guardian sees a chatty conversation. The agent runs tools, writes memory, spawns subagents, all of it invisible.

The Guardian has no way to tell "no toolCallRequest fired this turn" apart from "agent took no actions this turn." The spec frames non-enforcement as the client's loss (their actions get ALLOW by default) when really it is the Guardian's blindness.

Proposed direction:

  • Cross-check agbom/snapshot (which lists tools, models, subagents the agent is capable of using) against the methods declared in the handshake. If the agent declares it has tools but didn't declare toolCallRequest in methods_implemented, the Guardian should refuse the session.
  • Periodic agbom/snapshot reconciliation against observed hook traffic. If the snapshot shows tools that have never fired toolCallRequest, surface as an audit signal.
  • Optional expected_hooks_per_turn field with Guardian-side anomaly detection

Why deferred from v0.1.0: depends on the Core-baseline conversation in PR #2. If we add a minimum hook set to Core and tie it to AgBOM coverage, this issue is partially addressed at v0.1.0. The full detection mechanism (anomaly thresholds, AgBOM reconciliation logic) is v0.2 work.

References: hooks.md:172, handshake.json methods_evaluated, AgBOM spec

Activity

  1. rocklambros commented on Jun 16, 2026

    @rocklambros
    CollaboratorAuthor

    Concrete instance from the adapters in #22, worth tracking here.

    The Claude Code reference adapter doesn't wire SubagentStart into HOOK_MAP, and Claude Code can't block on it even if it did. The bundled example_guardian.py also returns allow for Task by default. So a subagent spawn runs without the Guardian ever evaluating it, and that's with a cooperative agent, not a malicious one omitting hooks. The blindness this issue describes shows up in the reference implementation itself.

    The AgBOM-reconciliation direction you proposed would catch it cleanly: an adapter whose AgBOM lists a Task / subagent capability but never fires subagentStart is exactly the mismatch the cross-check flags. The reference adapter is the first thing people copy, so it'd be good for it to demonstrate the coverage we want the Guardian to verify, not the gap.

    Cross-linked from #22.

  2. vgerardo2 commented on Jul 1, 2026

    @vgerardo2

    Good morning everyone, I hope you're all doing well! I've been working on ACS issue #16: a design note with a threat model plus a staged reconciliation approach for detecting missing hook coverage. Sharing for critique, not as a finished answer. The figure below summarizes the fuller design note.

    Image

    Short version of the argument: the in-band checks in the proposed direction (agbom/snapshot ↔ methods_implemented ↔ observed hook traffic) catch misconfiguration and inconsistent omissions, but they can't catch an Observed Agent that lies consistently, declare a minimal AgBOM, declare a minimal method set, fire the matching hooks, and every in-band cross-check agrees. Since ACS telemetry originates inside the environment being observed, the only signal that breaks that case is one sourced outside it: independent runtime / network / identity / tool evidence reconciled against the audit chain. The note unifies both under a coverage-status vocabulary (declared → observed → corroborated, plus degraded / inconsistent / bypass_suspected) so a Guardian's confidence is explicit rather than implied.

    It stays layered in line with the Core-baseline direction in #2: Phase A uses existing AgBOM + methods_implemented with no wire-format change; profile or spec changes only where prototype testing shows a conflict. It includes an effect-based capability-to-hook matrix (mapped to the existing hooks, including skillRegister / skillLoad / skillUnload), misuse cases, deterministic reconciliation rules, and test vectors.

    A concrete instance already surfaced in the #22 adapter work: a subagent/Task spawn that ran before the Guardian evaluated it (subagentStart not wired, and the framework unable to block on it anyway). @rocklambros cross-linked it to #16, and it's the adapter-level version of exactly this blindness, showing up with a cooperative agent, not even a malicious one.

    My model sits on top of the fix already landing there rather than duplicating it.

    Two things it adds: the AgBOM-vs-declared cross-check flags this class in-band and cheaply (a capability declared but never hooked is the mismatch, Phase A, no out-of-band evidence needed), and the coverage-status vocabulary forces an honest label, a spawn that runs unevaluated isn't "allow," it's an enforcement failure. The natural complement is the Core conformance suite already in #22: that suite proves a hook that fires is well-formed and honored; a coverage matrix + test vectors would prove the hook that should fire actually did; different axis, so a reference adapter can't silently regress the gap back open.

    Full note is drafted; but before I post it, the places I'd most value pushback:

    1. Out-of-band evidence is the load-bearing piece and also the weakest, in many deployments it's unavailable, delayed, or itself corruptible, and a compromised runtime plus a compromised evidence source can collude. Does corroboration earn its keep, or is the honest ceiling "declared vs observed" plus fail-closed on high-risk actions?
    2. Core vs profile boundary: which capability→hook requirements (if any) belong in Core versus an optional coverage profile? I've tried to leave Core untouched, but the AgBOM-vs-declared session refusal arguably wants a Core anchor.
    3. Whether a coverage-status value should ever travel on the wire, or stay a purely Guardian-side derivation; I'm wary of adding another self-reported field that a malicious agent just sets to "corroborated."

    Figure attached. It's a one-page summary of the fuller design note. If there's interest, I can post the full draft here, or share it in Slack for review, whichever you prefer. Thanks to @rocklambros for the original framing and the #22 cross-link, and to @bar-capsule for the adapter work this builds on.

  3. avp9-nexus commented on Sep 2, 2026

    @avp9-nexus

    Taking your three questions in the order they are tractable, with a count behind the first one rather than an opinion.

    On (3), the self-reported field. Your instinct is right and the spec already contains the remedy, applied elsewhere. specification.md does not let the LLM produce provenance: "Provenance MUST be populated by deterministic code outside the LLM's output path", "The framework, not the LLM, MUST attach the label, deterministically, based on which channel data crossed", and for cross-boundary receipt, "MUST treat the field as a hint and re-derive trust against local policy keyed off origin + source_id rather than honor a remote-asserted label at face value". That is the pattern your coverage_status needs: not a value on the wire, but a value each Guardian derives from what it observed, with the remote assertion demoted to a hint. A corroborated that travels is a digest_verified boolean one level up, which the skill-load design already rejects for the same reason.

    Why the pattern is worth naming explicitly. I classified every RFC 2119 requirement in the spec at c7ad162 - 213 of them across 67 files, schemas and prose - by who the requirement is addressed to and whether it governs data or execution. Six requirements remove the untrusted component from the path rather than ask it for something: all six are the provenance and trust rules above. All six govern data. None govern execution. On the execution side, twenty-eight requirements tell a Guardian to deny, and they rest on five that tell the Observed Agent to submit its steps and honor the decision. The spec is candid about the consequence - "Every step that proceeds without a decision MUST be recorded as an audit event, so the bypass is visible rather than silent" - which reads to me as a deliberate choice rather than an oversight, but it does bound what the deny requirements can promise.

    The classification is mechanical on the addressee and the plane, then read back by hand for the small classes - the six and the five are quoted from the text, the twenty-eight is a machine count I have not read line by line, and about a fifth of the 213 have no addressee my rule can identify and are excluded from all of the above. Happy to share the breakdown if anyone wants to argue with a specific line.

    So the asymmetry is not that ACS lacks the structural instinct. It has it, states it five times, and applies it only to fields. Your note is the first place I have seen the execution side pushed on directly.

    On (1), whether corroboration earns its keep. Your figure answers this more sharply than the prose around it, and I would lean on it harder. The fork between the governed path and the uninstrumented one sits inside TB-1, upstream of ACS instrumentation: the observed runtime picks the branch. Everything downstream of that fork, TB-4 reconciliation included, can establish which branch was taken and cannot constrain the choice - and since both branches land on the same external action, that limit is structural rather than a gap in the reconciliation design. So the ceiling is not set by how good the evidence is; it is set by where the fork is.

    Which is why I would not put the honest ceiling at "declared vs observed" either. Out-of-band evidence moves the trust rather than removing it: a second source can be unavailable, stale, or colluding, and you end up reasoning about two environments instead of one. What does not move is a value computed by code the agent never runs, at a boundary it cannot choose to skip - which is to say, at the fork rather than after it. Provenance already works that way. Coverage could: a capability present in the AgBOM with no corresponding hook traffic is a mismatch the Guardian derives from two things it already holds, needing no third party. That is your Phase A, and I think it is stronger than the corroboration layer rather than a cheaper approximation of it. Corroboration then earns its keep only for the residue Phase A cannot reach - which is worth stating as its scope, so it is not asked to carry the whole argument.

    A worked instance of the same move, in case it is useful. In OWASP/Agent-Security-Regression-Harness#175 an assertion accepted an approval the target could fabricate: the harness passed input.context to the target, trust: trusted labels included, and the target could echo a matching name back in an event it invented. The fix was not better validation - it was moving the trusted-source declaration onto the assertion, which never leaves the harness. Same shape as the provenance rule: the value the check depends on stops traveling through the component the check is about. Diff and tests are on that PR if the detail is useful; the reason it is relevant here is that the failure looked exactly like a coverage question until we tried to write the negative test, and then it turned into a data-path question.

    On (2), Core versus profile. #21 makes this answerable, and I think it argues for your Core anchor. Its list of what stays mandatory keeps decision honoring and describes it exactly right: it "is what distinguishes ACS from a hook surface a framework can ignore". That is the same clause my count lands on - the twenty-eight deny requirements are downstream of it. But a behavior contract binds whoever honors it, and nothing in Core currently protects it from an agent that declares a method set narrow enough to have nothing to honor. The AgBOM-vs-declared refusal is the one check that closes that specific hole, it needs nothing new on the wire and no third party, and it defends a property #21 has already argued is irreducible. On that reading it belongs next to the clause it protects rather than in an optional profile - and the rest of the reconciliation work, which does need new fields or outside evidence, is exactly what a profile is for.

    Thanks for posting the note rather than only the conclusion - the three questions are what made it answerable.

    Avp9.

  4. vgerardo2 commented on Sep 2, 2026

    @vgerardo2

    Thank you @avp9-nexus , for working through each question with concrete examples from the specification. Your response clarified several parts of the design that need to be revised.

    From your response, I can see that I need to state explicitly that coverage_status is derived locally by the Guardian or verifier, not trusted as a self-reported wire value. I also need to sharpen Phase A as the primary deterministic consistency check, limit corroboration to the cases Phase A cannot reach, and turn the AgBOM-to-declared-method comparison into a clear admission requirement rather than leaving the Core-versus-profile boundary unresolved.

    Your explanation also helped me recognize that the capability-to-hook rules must be controlled outside the Observed Agent, and that observation, corroboration, and actual enforcement assurance should not be collapsed into a single claim.

    Although I only shared the trust-boundary figure to focus the discussion on the design’s weaker assumptions, it was intended as an excerpt rather than the draft’s complete conclusion. I value your input and would be interested in your perspective on the full draft. The full draft includes the trust-boundary model, capability-to-hook matrix, reconciliation rules, misuse cases, staged implementation approach, and test vectors.

    Would you be interested in reviewing it? I can either revise these areas first or share the previous draft for initial critique before preparing the next revision.

  5. avp9-nexus commented on Sep 2, 2026

    @avp9-nexus

    Yes, and I would take the previous draft first, before you revise it.

    Three reasons. A critique of the version the figure came from lets both of us see what the revision changes and why, rather than reading conclusions that have already absorbed this thread. Reviewing after the revision would mean reviewing my own comments reflected back, which is the least useful pass either of us could do. And if the test vectors are in the draft, I would rather run them than read them - a vector that executes against a reference adapter tells more in ten minutes than a page describing it, and it is the part where a design note usually turns out to be further along or less far along than its prose suggests.

    Here is easier to work from than Slack, if that is open to you: line references survive, and anyone who lands on #16 later gets the whole exchange. A gist or a draft PR against the repo both work.

    What I would look at first, so you can decide whether that is the review you want: the capability-to-hook matrix against the six provenance and trust rules already in the spec, to see where the matrix asks the Observed Agent for something and where it removes it from the path; the misuse cases against the fork position from your figure, since any case downstream of the fork is detection and any case at it is enforcement, and the note should not let the two share a verdict; and the reconciliation rules, for whether degraded, inconsistent and bypass_suspected stay separate outcomes all the way into the record, because a fleet that cannot tell nothing-observed from observed-and-contradicting is the one that looks healthy.

    One precedent, for when you write the admission requirement. OWASP's AISVS, in the same house as this project, already carries the "cannot" form rather than the "must be detected" form: 9.3.5 reads "components processing untrusted data are isolated from tool-calling capabilities, ensuring that compromised data processing cannot trigger unauthorized tool invocations", and 9.3.1 puts each tool in a least-privilege sandbox or otherwise isolated from model operations. Across 262 AISVS requirements those are the two that constrain execution structurally; ACS at c7ad162 has none, which is the gap your note is working on. So the form exists, it is normative, and it has been through that project's review - worth citing rather than reinventing when the capability-to-hook rules are written down.

    Avp9. 🫡

  6. RemanenetSpy commented on Sep 2, 2026

    @RemanenetSpy

    Relying on Observed Agents to self-report toolCallRequest creates a fundamental security flaw: a compromised runtime simply suppresses the hook emission while calling tools directly.

    Detection cannot be wire-reported by the claimant. It must be enforced via Fail-Closed Execution Tokens at the Tool Dispatch Boundary:

    [Observed Agent] 
           │ 1. Request Permit (Canonical Request)
           ▼
    [Guardian PEP] 
           │ 2. Evaluate Policy & Mint Single-Use Decision Token:
           │    decision_token = HMAC(decision_id, canonical_args_digest, expiry)
           ▼
    [Tool Executor / Network Boundary]
           │ 3. Dispatch Tool Call WITH decision_token
           │    Asserts token validity & consumes atomically
           ▼
    [External Environment]
    
    def authorize_tool_dispatch(tool_name: str, args: dict, decision_token: str) -> bool:
        # Tool executor requires verified PEP token; fails closed if omitted
        if not decision_token:
            raise SecurityException("UNAUTHORIZED_TOOL_INVOCATION: Missing PEP decision token")
            
        is_valid = verify_and_consume_token(decision_token, tool_name, args)
        if not is_valid:
            raise SecurityException("REPLAY_OR_FORGERY_DETECTED: Invalid or expired token")
            
        return True

    By decoupling the evaluation gate from agent honesty, un-hooked tool calls are rejected at the execution perimeter.

  7. vgerardo2 commented on Sep 3, 2026

    @vgerardo2

    Thank you @avp9-nexus and @RemanenetSpy .

    @avp9-nexus , I agree that reviewing the previous draft before I revise it will provide the most useful critique. I’m sharing the original PDF together with a Markdown version published as a GitHub Gist. It preserves the draft’s content while making line-by-line references easier:

    ACS_Issue_16_Missing_Hook_Coverage_Gist.md
    VG_ACS_16_Missing_Hook_Coverage_.pdf

    This version predates the feedback in this thread, so I have not incorporated what needs to be added or removed from either response. I would love your review of the three areas you identified: the capability-to-hook matrix against the existing provenance and trust rules, the separation of detection from enforcement across the misuse cases, and whether DEGRADED, INCONSISTENT, and BYPASS_SUSPECTED remain distinct throughout reconciliation.

    One clarification: the test vectors in this draft are currently expressed as conditions and expected outcomes rather than implemented in an executable harness. I initially prepared this as a design note for feedback, so your preference to run the vectors gives me a clear next step: refine them based on the review, then translate them into executable tests.

    @RemanenetSpy , thank you for the fail-closed execution-token proposal. It addresses the enforcement side of the problem at the tool-dispatch boundary, where an unauthorized action can be prevented rather than detected afterward. Because the attached files are the previous draft, I have not added that mechanism to them. I will treat it as feedback for the next revision and evaluate it alongside the draft’s distinction between observation, corroboration, and enforcement assurance.

    I appreciate you both taking the time to review this.

  8. RemanenetSpy commented on Sep 3, 2026

    @RemanenetSpy

    @vgerardo2 Read through the draft Gist — the structure and threat modeling across Sections 8–11 are exceptionally clear. The distinction in §10.1 ("policy attestation does not prove every action reached that policy") isolates the core problem.

    Three specific observations on the areas you highlighted:

    1. Capability-to-Hook Matrix (§8.1 — Persistent Memory):
      When an un-hooked write or memory bypass is detected (CH-02), marking the session INCONSISTENT is necessary, but the storage layer may already contain the record. The matrix should explicitly specify render-time quarantine: unverified memory records must render strictly inside an inert <untrusted_evidence> fence, preventing injected text from regaining instruction authority even if storage-level write detection was delayed.

    2. Phase A Deterministic Check (§11.1 — Dynamic Skills/Tools):
      Static checking at session establishment works cleanly for pre-declared tools. However, modern agent runtimes frequently discover tools dynamically mid-session (e.g. lazy-loaded MCP servers or skillLoad). Phase A should include an explicit Dynamic Registration Invariant: any tool invocation whose capability was absent from the initial AgBOM snapshot must fail closed until an atomic agbom/changed event reconciles the baseline.

    3. Guardian-Derived Status vs Wire Values (§10):
      The taxonomy (DECLARED -> OBSERVED -> CORROBORATED -> BYPASS_SUSPECTED) correctly treats client assertions as unverified claims. To enforce this mechanically: coverage_status must exist purely in the Guardian's local state machine. The wire protocol should reject any incoming client payload that attempts to supply a self-asserted coverage_status attribute, treating it as an immediate CH-15 violation.

  9. avp9-nexus commented on Sep 3, 2026

    @avp9-nexus

    Thanks @vgerardo2, I’ll sort it out in the next few hours, I’d be happy to 🫴🏼.

  10. avp9-nexus commented on Sep 4, 2026

    @avp9-nexus

    Read the Gist end to end, against the spec at c7ad162 and against the two comments above. Taking your three areas in order, then the test plan, with section references so each point can be argued on its line.

    Where the note is already right, so it does not get lost under the rest. §3.5's second non-goal - not claiming completeness from agent-supplied telemetry - is the honest boundary and most designs in this space do not draw it. MC-33 and CH-13, a hook that fires after the action can no longer be blocked, is the fork position from your figure turned into a rule, and it is the single most useful rule in §10. T-08, declared but unused stays DECLARED, is a real positive control. And §14's second-to-last bullet says the load-bearing thing plainly: complete proof of coverage needs an out-of-process reference monitor. My main suggestion is structural rather than local - promote that bullet from a limitation to the spine, and let the rest of the note be what it actually is: a detection design plus a small set of admission rules. Said that way, the note stops carrying the false-assurance risk it names in §1.

    1. The matrix against the six provenance and trust rules (§8)

    The six rules in the spec share one shape: the value the Guardian depends on is computed by framework code at a channel boundary, and the LLM is named only to be kept out of the path. Reading §8.2 with that lens: every row puts an agent-emitted hook in "Expected ACS evidence". The matrix asks the Observed Agent for something in every row and removes it from the path in none - which is correct for a matrix of ACS evidence, since ACS evidence is by definition agent-emitted, but it means the column that carries the structural analogue is "Corroborating evidence", and that column does not yet distinguish two very different things. A tool or MCP gateway record (row: tool invocation) and a dispatcher-issued execution ID (row: Guardian decision enforcement) are produced at the fork, by a component the agent does not write; a proxy or DNS log, a database write log, an application audit record are produced downstream, after the effect. The first kind can refuse; the second can only report. One more column - evidence position: at-fork or downstream - would make §8.2 say which rows a deployment can turn into enforcement and which stay detection, and it would give @RemanenetSpy's execution token its exact place in the table: it is at-fork corroboration for the tool-invocation row, and the only kind that makes "un-hooked calls rejected" a true sentence.

    §8.1's effect-based classification is the right principle and it already has a home in the spec: hooks.md, "Frameworks MUST fire toolCallRequest for every action that escapes the agent's reasoning context, regardless of whether the framework's tool registry models it as a tool". Worth citing there, since it turns 8.1 from a proposal into an application of an existing MUST. And there is a live instance of 8.1 doing exactly that in #22: Claude Code has no SubagentStart event at all - the adapter's HOOK_MAP at the current head maps SubagentStop only - so the subagent row of the matrix cannot be covered by its named hook on that framework. What #22 does instead is gate the Task tool at PreToolUse by default (subagent_gated, opt-in to allow), which is the effect-based reading: the delegation is caught as a tool invocation, at the fork, and it blocks. That is worth recording in the matrix as the shape corroboration takes when the lifecycle hook does not exist on a given runtime, because it is also the shape @RemanenetSpy's token takes.

    2. The misuse cases against the fork (§9, §10)

    The distinction the note needs is between what reconciliation can establish and what a deployment does about it, and §8.2's "Response" column currently mixes the two: BYPASS_SUSPECTED and INCONSISTENT are findings, "Freeze memory writes", "Suspend browser capability", "Deny registration" are actions. Splitting them into Finding and Response would let each CH rule be classified by where it sits relative to the fork. By my reading: CH-01, 02, 03, 11, 12, 16, 17, 19 fire at admission or registration, before any action - those are the rules that can refuse, and they are the ones with a case for sitting next to decision honoring in Core. CH-04, 05, 10, 18 need execution evidence to fire, so they are post-hoc by construction - detection, however fast. CH-13 is a third kind: it detects a design defect in the instrumentation, not an attack, and giving it the same CRITICAL_ENFORCEMENT_FAILURE as CH-05 (DENY followed by execution) puts a misconfigured adapter and an adversary in the same bucket. Two names, or a severity field, would keep them apart in the record.

    TA-07 in §5 - Guardian decisions are enforced, weak without external enforcement - is the root, and it lines up with the spec's own posture: the five requirements that make the Observed Agent wait for and apply the decision are addressed to the agent, and the spec says of the failure mode "so the bypass is visible rather than silent". CH-05 is that sentence turned into a rule. Naming the lineage would make clear the note is not contradicting the spec's choice, it is making the choice measurable.

    Open question 15.2 - which hooks are observational and which must be blocking - is answerable from the spec text rather than left open: hooks whose response is disposition-eligible and that the agent must wait on (toolCallRequest, skillRegister, skillLoad, preCompact) against hooks the spec marks audit-only (postCompact, which "MUST NOT return deny" because compaction has already happened; skillUnload, which §8.2 already calls audit-only; trace events, which "MUST NOT block enforcement"). That partition exists; importing it as a column in §8.2 would answer 15.2 and 15.13 at once.

    3. Whether DEGRADED, INCONSISTENT and BYPASS_SUSPECTED stay separate into the record (§10, §12)

    Two things, one of them a count. The count: the figure in §4 lists six coverage states; the status table in §10 defines eight; CH-08 and CH-09 return two more - BUSINESS_INTEGRITY_FAILURE and UNKNOWN_EXECUTION_STATE - that the status table does not define. So the note carries ten distinct status values, defines eight, and draws six. That is exactly the kind of drift a reader will use to dismiss the taxonomy, and it is a ten-minute fix. Related, and found by checking the PDF against the Gist rather than assuming they match: the trust-boundary legend on the figure (TB-1 Observed environment through TB-4 Evidence and reconciliation) exists only in the image. It is in neither the Gist text nor the PDF text layer, and the §4 mermaid source has no boundary boxes. Since the fork argument above hangs on those boundaries, they deserve to be in the text - a four-row table under the figure would do it.

    The structural point: §12 specifies coverage_status as "Overall state and per-capability state". An overall state is a collapse by construction - it is the gate outcome, not the record. The record should be the per-rule findings (which §12 already has as findings), and the overall value should be derived from them by a stated precedence, so that two implementations facing the same findings produce the same overall. Right now no precedence is stated, and T-09's expected outcome "DEGRADED or INCONSISTENT" is the missing precedence showing up in the test plan as an "or". Same for T-16 and T-17. A test whose expected outcome is a disjunction cannot be executed, which connects to the next section.

    The test plan (§13) - what would make it executable

    Since the vectors are conditions rather than a harness, two things to build in before translating them, both learned the hard way on the #30 suite. First, every T-case is a case that must fire; only T-07 and T-08 are cases that must stay silent, and between them they cover two rules (CH-01 and CH-04) out of twenty. A validator that returns INCONSISTENT for every session passes T-02, T-09, T-16, T-17 and T-22 without reading anything - the suite needs, for each rule, the consistent input on which that rule MUST NOT fire, or it measures nothing about the rule. Second, pin the disjunctions: T-09, T-16, T-17 each need one expected value and the rule that selects it. Once those two are in, translating the table to executable vectors is mechanical, and I would be glad to run them against a reference reconciler when they exist - that offer stands.

    One precedent for the "cannot" form when you write the admission requirements, already on this thread: AISVS 9.3.5 and 9.3.1. And one note on @RemanenetSpy's second observation: the dynamic-registration invariant closes the gap between Phase A's check at session establishment (§11.1) and Phase B's tracking (§11.2) - CH-06 and CH-27 describe the failure, the invariant is the rule that prevents it, and it belongs in Phase A rather than B, since it needs nothing beyond the AgBOM the Guardian already holds.

    One small thing on the references, checked rather than assumed. §1 and the References cite "maintainer discussion around ACS PR #2" for seven items. All seven are in #2 - as the PR that landed them: at its merge commit the spec text already carries policy_version, the harness-primitive MUST, the pillar structure, the wire-contract framing and provenance. But several of them never appear in #2's discussion threads (body, comments, 90 review comments): "wire contract", "harness primitive" and policy_version are absent there and live only in the merged files. So the citation is right about the PR and loose about "discussion"; a reader who checks it in the threads finds three items missing, a reader who checks the diff finds all seven. Pointing at the files (conformance.md, hooks.md, response-envelope.json) instead of the discussion costs nothing and makes the claim checkable in one click.

    Happy to go a level deeper on any of these once the revision lands.

    Avp9.

  11. ryjen commented on Sep 9, 2026

    @ryjen

    One refinement I would make to the fail-closed execution-token proposal above: requiring a permit at tool dispatch only proves complete mediation if that dispatch point dominates every path capable of producing the governed effect.

    If the agent can produce the same external effect through a raw service credential, direct network/API path, alternate tool registry, or another unmediated client, the token can be perfectly bound and consumed while the effect still bypasses ACS entirely.

    So I think the security invariant is about effect-path closure, not the token mechanism itself.

                             Guardian
                                |
                         exact authorization
                                |
                                v
    Agent --------> Governed tool path ------> Effect
      |                    |
      |                    | permit checked
      |                    v
      |             Enforcement boundary
      |
      +----------> Raw API -----------------> Effect
      |
      +----------> Direct credential -------> Effect
      |
      +----------> Alternate tool ----------> Effect
    
                     ^ bypass problem
    
    Complete mediation requires either:
    
          every equivalent path
                  |
                  v
          [unavoidable gate]
                  |
                  v
                Effect
    
    or the alternate paths must be structurally unreachable.
    

    For each governed effect, a deployment claiming strong enforcement should be able to answer:

    1. What equivalent paths can produce this effect?
    2. Which enforcement boundary is unavoidable across those paths?
    3. Does every path require an exact-action authorization, or is that path structurally unreachable to the agent?
    4. Which paths, if any, are explicitly outside the claimed assurance boundary?

    A useful negative vector would pair the normal mediated case with an equivalent bypass attempt:

    • authorize and execute the governed path;
    • then attempt the same external effect through a raw endpoint, direct credential, alternate tool, or equivalent unmediated path;
    • require no externally observable effect from the bypass attempt.

    The authorization should still bind the exact target, policy-relevant parameters, expiry, executor/provider identity, and single-use semantics. But the placement of enforcement is what establishes complete mediation.

    This seems complementary to the detection/enforcement distinction already emerging in this thread. #31 separates declared Guardian coverage from demonstrated evaluation, while #32 distinguishes refusal from decision failure. #38 applies a related principle to tool declarations: the security decision should bind to something independently observed rather than merely self-reported by the component being checked.

    AgBOM/hook reconciliation can therefore help detect inconsistent coverage, while an unavoidable effect boundary is what prevents a consistently lying or bypassing runtime from acting.

    imagine a room with four doors. Putting a security guard on one door does not make the room secure if someone can enter through the other three. Either every door needs the same check, or the other doors need to be locked. Securing the normal tool-call path is not enough if the agent can reach the same external effect another way.

  12. moved this to Needs triage in ACS Project Trackeron Sep 10, 2026
  13. added
    type:bugSomething isn't working
    scope:in-focusFeeds the ninety-day committed outcome. Maintainers only
    on Sep 10, 2026
  14. 1 remaining item

  15. moved this to Needs triage in ACS Project Trackeron Sep 10, 2026
  16. changed the title [-]No on-wire signal lets a Guardian detect missing hook coverage from an Observed Agent[/-] [+][Bug] No on-wire signal lets a Guardian detect missing hook coverage from an Observed Agent[/+] on Sep 10, 2026
  17. avp9-nexus commented on Sep 10, 2026

    @avp9-nexus

    @ryjen This lands, and it moves the question to the right place: from what the token proves to where the check sits.

    The refinement matters most for what it rules out. A permit bound to the exact target, with expiry and single-use semantics, can be perfectly implemented and still govern nothing, if the same external effect is reachable through a raw credential or an alternate client. That is not a weakness in the token, it is a scope claim that was never stated. Your four questions are the missing statement, and the fourth is the one deployments will resist answering, because "explicitly outside the claimed assurance boundary" is the honest name for what most designs leave silent.

    Reading it against what I filed on this thread on September 4, reviewing the draft note: I argued that the note's own §14 bullet, complete proof of coverage needs an out-of-process reference monitor, should be promoted from a limitation to the spine, so the rest reads as a detection design plus admission rules rather than as coverage proof. Your effect-path closure is the same conclusion reached from the enforcement side, and it is the sharper formulation, because it names the property a deployment must demonstrate instead of the component it must install. Complete mediation is a property of the path set, not of any one gate.

    One consequence worth stating for the specification, given that the policy engine, transport, and authentication are out of scope by design. Effect-path closure cannot be specified as a mechanism without crossing that line, but it can be specified as an obligation: for each governed effect, the implementer enumerates the equivalent paths, names the boundary that is unavoidable across them, and declares which paths sit outside the assurance boundary. That is answerable at the contract level and it stays permissive on implementation, which is the position the specification already takes.

    On the negative vector you sketch, pairing the mediated case with an equivalent bypass attempt and requiring no externally observable effect: it belongs to the class that triage just deferred to v0.2.0 in #53, so this is a note for that window rather than a request to write it now. Worth recording there while the reasoning is fresh, since the vector only has to be written once the MUST floor stops moving in #21.

    The room-with-four-doors framing is the part I would keep verbatim if any of this reaches normative text.

    Avp9.

  18. vgerardo2 commented on Sep 10, 2026

    @vgerardo2

    Thank you, @avp9-nexus , @RemanenetSpy , and @ryjen , for reviewing the draft, and @rocklambros for the original framing and adapter example.

    I began this note in early June. The original PDF and Markdown will remain the review baseline. For the revision, I will identify the specification commit being evaluated and distinguish changes in ACS from the weak points in my draft.

    The central change is to make the assurance boundary explicit: reconciliation detects inconsistencies, admission checks can refuse access, and prevention requires authorization controls that cannot be bypassed through an equivalent path to the governed effect.

    I also understand the timing clarified in #53. Negative conformance work is deferred to v0.2 while the normative requirements settle. The attached executable package is an experimental translation of the draft for feedback, not a proposed conformance gate or a request to bring that work forward.

    What I will add or clarify

    1. Evidence position and enforcement authority

    Following @avp9-nexus's review, the capability matrix will distinguish evidence produced before execution from evidence produced after an effect. It will separately identify the component that can block the action and whether the agent can bypass or modify it.

    A gateway record or execution ID is evidence, not proof of enforcement by itself. The deployment must establish that the associated authorization check is mandatory.

    1. Effect path closure

    Following @ryjen’s refinement and @avp9-nexus’s latest comment, the revision will describe complete mediation as a property of the reachable paths, rather than a property established merely by installing a gate or reference monitor.

    For each governed effect, the matrix will identify equivalent paths, the authorization boundary those paths cannot bypass, structurally inaccessible alternatives, and paths excluded from the assurance claim. Changes to credentials, tools, or network access will require reassessment.

    I will frame this as a proposed obligation to substantiate the assurance claim without prescribing a particular token, gateway, sandbox, or policy engine.

    1. Initial admission and dynamic registration

    Phase A will define the initial comparison between declared capabilities, required hooks, and implemented methods. The mapping rules must be controlled outside the Observed Agent. Agreement between declarations will not be treated as proof that the inventory is complete.

    Following @RemanenetSpy’s recommendation, Phase A will also include a proposed requirement to hold a newly introduced capability until its inventory and hook coverage have been reconciled.

    Receiving agbom/changed, completing reconciliation, and approving use are separate events. The revision will identify what prevents execution during that interval. CH 06 and CH 20 describe reconciliation failures; they do not alone establish prevention.

    1. Findings, responses, and status

    I will separate findings from deployment responses. INCONSISTENT describes an assessment; restricting access or quarantining a record describes an action.

    coverage_status will be derived locally by the Guardian or verifier. Client assertions will not establish that status. Whether a supplied attribute is rejected or ignored will be specified against the applicable schema and extension rules.

    The figure, definitions, rules, and outputs will use a consistent vocabulary. Detailed findings will remain in the record, with an explicit aggregation policy for any overall status. BYPASS_SUSPECTED will not be presented as a maturity level above CORROBORATED.

    Coverage status will summarize evidence. It will not grant execution authority or override an explicit denial.

    1. Distinct enforcement failures

    I will distinguish execution after denial from a hook that arrives too late to block. CH 05 and CH 13 describe different conditions, even if both warrant critical severity. Neither finding will assume malicious intent without supporting evidence.

    I will also connect TA 07 and CH 05 to the specification’s decision honoring requirements and documented bypass visibility boundary.

    1. Memory handling and execution tokens

    Unverified memory quarantine and execution tokens will be evaluated as deployment controls, not presented as features already established by the original draft.

    An <untrusted_evidence> wrapper alone does not establish memory isolation. The revision will distinguish labeling content from preventing it from gaining instruction authority.

    For tokens, exact action binding and single use remain useful, but the prevention claim depends on effect path closure. Protecting only the normal dispatcher is insufficient.

    1. Specification alignment and references

    I will use the canonical hook catalog to distinguish decision eligibility, timing, and audit behavior. For the Task adapter example, I will distinguish blocking delegation through a tool gate from providing the full evidence associated with a native subagent lifecycle hook.

    I will add the figure’s trust boundary labels to the text and replace broad discussion references with links to supporting files at a pinned commit. AISVS 9.3.1 and 9.3.5 will be evaluated as precedents, not treated as existing ACS requirements.

    Core versus profile placement remains open for review. I will cross reference #31, #32, #38, and #53 without treating unresolved proposals as accepted requirements.

    What I will remove or replace

    1. Any implication that valid hooks, matching logs, or a protected dispatcher alone establish complete mediation.

    2. The mixed Finding and Response column.

    3. Undefined status values, unexplained aggregation, and ambiguous expected outcomes.

    4. Questions already answered by the pinned specification, replacing them with the remaining implementation or assurance question.

    5. Broad claims that every difference is a missing ACS requirement. Some are documentation corrections, some are deployment assumptions, and some require specification discussion.

    I will retain stable rule identifiers where possible and document changed meanings. The current draft ends at CH 20, so the CH 27 reference will be clarified rather than carried forward.

    Executable vectors now available

    The original table contained conditions and expected outcomes. The package contains 47 reconciliation fixtures incorporating selected reviewer feedback, plus 18 checks of the suite and runner. The latest update corrected runner defects and added regression checks; it did not add deployment enforcement vectors. Dynamic registration blocking, alternate execution paths, token enforcement, memory quarantine, and Guardian refusal handling remain planned work.

    The package includes:

    1. A triggering input and valid control for every original CH rule, including the declared but unused capability case.

    2. Specific interpretations for T09, T16, and T17, with exact expected findings documented in the README.

    3. The proposed LATE_CONTROL_POINT finding for CH 13, distinct from CH 05.

    4. Preservation of overlapping findings, proposed overall status precedence, and checks that client supplied status cannot elevate the result.

    5. Synthetic memory, subagent, and native effect records for testing bypass classification.

    The README’s “Original test traceability” section maps the PDF cases to fixtures. It does not mean all reviewer proposals have been implemented.

    Runner corrections and validation limits

    After reviewing #53, I identified and corrected three false success paths in the initial runner: adapter termination with exit code zero, an empty fixture suite, and comparisons disabled by Python optimization.

    The updated runner uses explicit comparisons and one adapter child process per complete run, preserving state across fixtures. The parent retains expected outputs, checks completion, and evaluates results. Added regression checks cover those defects, timeouts, duplicate fixture IDs, exceptions, and adapter output handling.

    All 47 fixtures and 18 checks pass locally. These results establish internal consistency with the included evaluator, not independent validation. The published corpus can still be memorized, and process separation is not a sandbox for untrusted code.

    The evaluator does not yet implement healthy OBSERVED or CORROBORATED aggregation, per capability status aggregation, or a production evidence adapter. Its correlation inputs are trusted adapter assumptions, not facts established by the suite.

    Enforcement vectors still planned

    Consistent with #53, the following remain documented future work rather than completed conformance tests:

    1. Dynamic capability use before reconciliation, while pending, after approval, and after rejection.

    2. Authorized execution paired with unauthorized equivalent attempts through raw endpoints, direct credentials, alternate tools, or other reachable routes.

    3. Missing, expired, replayed, or incorrectly bound permits, including concurrent consumption that permits at most one authorization.

    4. Late hooks, execution after denial, Guardian refusal, and unavailable decisions.

    5. Memory quarantine, background worker termination, and malformed envelope handling.

    Where prevention is claimed, these tests must check authoritative target state or sufficiently reliable independent evidence over a defined observation window, including delayed effects. Missing or insufficient observations must produce an inconclusive result, not a pass.

    Passing would support the tested paths and configuration, not prove that every possible route has been discovered.

    The immediate review question is whether the normalized evidence assumptions, exact expected findings, and proposed status precedence are appropriate. I will keep reconciler testing separate from deployment enforcement testing and record accepted feedback, open proposals, and remaining evidence points in the next design revision.

    Starter_#16_Executable_Vectors.zip

  19. RemanenetSpy commented on Sep 11, 2026

    @RemanenetSpy

    @vgerardo2 On the proposed status precedence and evidence assumptions for the planned memory quarantine vectors:

    The separation between reconciler testing and deployment enforcement testing is clean. For the planned memory quarantine vectors, status precedence should follow a fail-closed hierarchy:

    1. Status Precedence Order (Strict Monotonic Fallback):
      DENIED > LATE_CONTROL_POINT > INCONSISTENT > QUARANTINED > CORROBORATED > OBSERVED
      Any active conflict or late hook immediately suppresses CORROBORATED standing.

    2. Storage-Layer Retrieval Partitioning:
      To prevent prompt-level injection bypasses (which break <untrusted_evidence> delimiters), quarantine should be asserted at the retrieval query layer prior to context compilation:

      SELECT memory_id, content, confidence
      FROM agent_memory
      WHERE coverage_status = 'CORROBORATED'
        AND tenant_id = :current_tenant;

      Records marked QUARANTINED or INCONSISTENT are physically excluded from the prompt token stream during privileged turns, eliminating instruction authority entirely.

    3. Expected Finding for Unverified Dynamic Memory:
      If an agent attempts privileged tool execution using facts sourced exclusively from unverified memory, the evaluator should emit FINDING: UNVERIFIED_MEMORY_AUTHORITY_ESCALATION rather than a generic execution failure.

    Does this status precedence match the hierarchy you have structured for the 47 runner checks?

  20. vgerardo2 commented on Sep 11, 2026

    @vgerardo2

    Thank you, @avp9-nexus , @RemanenetSpy , and @ryjen . I have updated the experimental package to distinguish the original reconciliation fixtures from the reviewer revision tests and later additions. The original PDF and Markdown remain the review baseline.

    Reviewer revision suite now executable

    The package now includes 24 executable cases across RV 01 through RV 12, using a local reference controller:

    1. RV 01: Local status derivation. Client supplied assurance cannot hide findings or grant authority.

    2. RV 02: Initial admission. Incomplete capability declarations cause refusal; complete declarations still require authorization before execution.

    3. RV 03: Protected mappings. Agent requests to weaken mapping rules are rejected, and modifying the original configuration object cannot change the controller’s retained rules.

    4. RV 04: Dynamic registration. Unregistered, pending, and rejected capabilities cannot execute. Approval permits normal authorization, while configuration changes invalidate earlier permits.

    5. RV 05: Declaration limits. Matching declarations do not establish inventory completeness. Contradictory external evidence remains visible.

    6. RV 06: Corroboration boundaries. Missing, stale, unreliable, and contradictory evidence cannot establish corroboration. Matching evidence cannot erase an active declaration mismatch.

    7. RV 07: Findings and aggregation. Overlapping findings remain distinct and produce the same result regardless of input order.

    8. RV 08: Assessment versus authority. Evidence assurance cannot override denial. Authorized execution remains a separate decision.

    9. RV 09: Late controls versus violated denial. A late control point and execution after denial produce different findings. Timely decisions have valid controls.

    10. RV 10: Refusal versus unavailable decisions. Refusal, timeout, and transport error remain distinct and produce no permit.

    11. RV 11: Permit binding and single use. Missing, malformed, forged, expired, replayed, and incorrectly bound permits are rejected. Eight concurrent attempts to consume one permit produce exactly one local effect.

    12. RV 12: Equivalent paths. Every enumerated local entry point requires authorization. An intentionally unguarded database write demonstrates that protecting the normal interfaces does not establish complete mediation.

    The admission and permit tests attempt actual local operations and inspect SQLite target state through a separate connection. They do not rely only on a returned “blocked” result.

    The limits remain important: evidence classification, timestamps, transport outcomes, and alternate route identities are modeled. These tests do not establish process isolation, real network behavior, or production effect path closure. The controller and expected results were developed together, so passing demonstrates local consistency rather than independent validation.

    Tokens are one mechanism evaluated by the reference implementation. Following @ryjen’s refinement, the underlying requirement is effect path closure: every reachable path to the governed effect must require equivalent authorization or be structurally inaccessible. RV 11 tests token binding and single use. RV 12 tests the enumerated local entry points and demonstrates how an unguarded path bypasses them. Neither establishes complete mediation in a real deployment; that requires testing the actual paths and explicitly identifying exclusions from the assurance claim.

    16_Revisions_from_Original.zip

    What the original 47 fixtures cover

    The original suite remains unchanged:

    1. Forty fixtures cover the 20 CH rules, with a triggering input and a valid control for each.
    2. Seven additional fixtures cover no activity, client status spoofing, spoofing alongside execution after denial, overlapping findings, and synthetic memory, subagent, and primitive bypass records.
    3. Eighteen separate checks validate that suite and its runner.

    Those fixtures primarily answer: given this evidence, does the evaluator report the expected finding?

    For example, CH 06 classifies an unreconciled inventory change. The new RV 04 cases additionally attempt execution while a capability is pending or rejected and inspect whether a local effect occurs.

    This separation makes clearer which parts of the feedback were already represented and which required new implementation.

    Status precedence clarification

    @RemanenetSpy , the proposed hierarchy does not match the original evaluator, and I would retain the distinction between:

    1. Evidence assurance, such as OBSERVED or CORROBORATED.
    2. Findings, such as INCONSISTENT or LATE_CONTROL_POINT.
    3. Authorization decisions, such as DENY.
    4. Record dispositions, such as QUARANTINED.
    5. Execution outcomes.

    I do not think these should all be placed in one precedence chain. For example, if the Guardian returns DENY but execution still occurs, reporting only DENIED would hide the actual CRITICAL_ENFORCEMENT_FAILURE. Similarly, the proposed hierarchy has no place for the existing BYPASS_SUSPECTED, AUTHORITY_GAP, or BUSINESS_INTEGRITY_FAILURE findings.

    I think the better direction is to retain the findings while representing the other dimensions separately. A result could contain a finding, evidence assurance, authorization decision, record disposition, and execution outcome without forcing them into one status value.

    The planned retrieval enforcement tests would place unique canary values in memory records and verify:

    An unverified record is absent from the final compiled model context.
    An equivalent locally corroborated record is included as a positive control.
    A client supplied CORROBORATED claim cannot make an unverified record eligible.
    Semantic search, direct lookup, caches, summaries, and linked records apply the same eligibility decision.
    Tenant boundaries are enforced.
    A quarantined record is removed from cached retrieval results.
    Failure of the trusted eligibility component does not expose all records.
    A privileged request relying only on quarantined memory produces no governed effect.

    For the proposed finding, I would use UNVERIFIED_MEMORY_AUTHORITY_USE when unverified memory influences a privileged request. If the action executes despite denial or mandatory quarantine, the evaluator would separately retain CRITICAL_ENFORCEMENT_FAILURE.

    The RV reference separates evidence assurance from findings and execution authority. It does not impose a combined precedence chain across those dimensions. The original CH evaluator retains its documented experimental summary policy for baseline comparison.

    Planned memory retrieval vectors

    MR 01 through MR 08 are documented separately:

    1. MR 01: Exclude locally unverified memory from privileged context.
    2. MR 02: Include an eligible record as the positive control.
    3. MR 03: Prevent client supplied corroboration from creating eligibility.
    4. MR 04: Apply eligibility checks across alternate retrieval paths.
    5. MR 05: Enforce tenant isolation.
    6. MR 06: Prevent cached or derived copies from reintroducing quarantined records.
    7. MR 07: Handle eligibility changes during retrieval and context compilation.
    8. MR 08: Handle eligibility service unavailability without treating records as eligible.

    These remain planned integration specifications. The SQLite target in the RV suite records action effects; it is not a memory retrieval implementation.

    I agree with enforcing quarantine before context compilation. Eligibility must be determined by trusted local policy, and CORROBORATED alone does not establish permission or content safety.

    Future retrieval tests must inspect compiled context and record lineage, including summaries and cached derivatives. Absence of a canary string alone would not exclude transformed content. The proposed memory authority finding also needs an explicit evidence rule distinguishing attempted use from demonstrated escalation.

    16_Memory.zip

    Supplemental hooks.security coverage

    Following the meeting from yesterday, I reviewed hooks.security to identify improvements to the capability matrix.

    The supplemental suite contains 27 executable synthetic fixtures covering:

    1. Supported but inactive controls.
    2. Matcher or scope exclusions.
    3. Observation being mistaken for prevention.
    4. Missing required evidence fields.
    5. Hook errors, invalid responses, unavailability, and timeouts.
    6. Equivalent paths without mandatory exact authorization.
    7. Configuration changes requiring reassessment.
    8. Platform version changes requiring reassessment.
    9. Positive, negative, and inconclusive effect observations.

    These assess supplied evidence. They do not install hooks, verify live platform behavior, or independently discover and test external paths. Registry mappings will still need verification against official documentation and the selected platform version.

    16_Hook_Security.zip

    Current package and review scope

    The package now contains:

    1. 47 original reconciliation fixtures.
    2. 27 supplemental synthetic coverage fixtures.
    3. 25 checks for those suites and runners.
    4. 24 separate RV cases.
    5. Eight planned MR specifications, with no completed retrieval enforcement integration tests.

    All executable cases pass locally. The combined unittest run contains 49 tests: the 25 suite and runner checks plus the 24 RV cases. The fixture counts are reported separately.

    FEEDBACK_TRACEABILITY.md maps the commitments to existing coverage and remaining gaps. RV_VECTORS.md explains the reference behavior and limitations, and rv_results.json lists the individual RV results.

    I would love review of the admission transitions, evidence assumptions, separation of findings from authority, and permit validity rules. Consistent with the timing discussed in #53, this remains an experimental review package rather than a proposed ACS conformance gate.

  21. avp9-nexus commented on Sep 11, 2026

    @avp9-nexus

    You're welcome, I'm glad to be able to help.
    @vgerardo2 I read the three packages and ran the RV one on a second machine, in a sandbox with networking removed (Python 3.14.4, Linux). Reproduced: the 49 tests pass, and run_rv.py --report writes a rv_results.json identical to the shipped one field for field (24 of 24, same case ids, same not_tested list). One condition: the RV archive as attached does not contain adapter_worker.py. test_runner.py copies it for every case, so without it 11 tests error and one fails with RUN_INVALID: adapter process failed. The file is in the two other archives (823 bytes, same content); dropping it into the RV folder is all it took. Worth adding it to that archive, or one line in RV_VECTORS.md.

    On the four points you asked to have reviewed. To move each from read to executed I ran a small probe of my own against rv_reference.py as shipped, without touching your files.

    Permit validity, and what an effect row proves. The effects table holds target, parameters, executor and nothing else; the permit that was consumed is not written with the effect. Executed: one authorized execute() plus one direct insert into the table give two identical rows, _spent holds one nonce, and nothing in the data says which row had a permit. RV 12's bypass detector works because the test knows which row it wrote itself, and RV 09's injected defect is reported through an EXECUTED_AFTER_DENY event the test supplies, not from the data. Suggestion: write the nonce into the effect row in the same transaction, and add one reconciliation invariant, every effect row maps to exactly one consumed permit and every consumed permit to exactly one effect row. Then an unguarded write is visible from the data alone, which is what a deployment would need, since it will not know in advance where the bypass came from.

    Separation of findings from authority. issue() returns {'decision': 'DENY', 'permit': None} in three situations: the Guardian said DENY; the Guardian said ALLOW but the capability is not APPROVED; the Guardian said ALLOW but expires <= now. Executed: the three return values are identical. RV 10 keeps REFUSE, TIMEOUT and TRANSPORT_ERROR distinct from DENY, which is right; the controller's own refusal is a fourth source, and it is folded into the Guardian's word. A record built on that field would say the Guardian denied a step it had allowed. Suggestion: a separate value for a controller refusal, carrying its cause, and DENY reserved for the Guardian's decision. It is the same property I measured on the host side in #109: a refusal has to say who refused, or the record is wrong about the one thing it exists to say.

    Evidence assumptions, one question. In assess(), evidence_assurance becomes OBSERVED as soon as events is non-empty (line 34), while CORROBORATED requires the literal OBSERVED event (line 35). Executed: ['LATE'] alone yields assurance OBSERVED with finding LATE_CONTROL_POINT. Is a late hook meant to count as observation for assurance? No test pins that case in either direction; one test would settle it.

    Admission transitions, one question. admit() rebuilds _states from its argument. Executed: after register('child') and a reconciliation that returns APPROVED, a second admit(['tool'], ...) returns ADMITTED and child is gone from the states; a permit for it is then refused. That is fail-closed, so nothing runs that should not, but it is neither tested nor stated. Is initial admission single-shot, in which case a second call should be refused, or repeatable, in which case the reference should say it resets dynamic registrations?

    On the hooks.security supplement. HC-01 to HC-09 each assess one hook. None covers two controls registered on the same event, which is the ordinary situation once a Guardian's hook is installed next to a project's own hook. On Claude Code I measured it (#109): both hooks run, the block is the union, but only one reason reaches the model, and a hook that exits 2 displaces a JSON deny's reason entirely. A tenth case would fit the supplement's shape: trigger, two registered controls and one reason rendered; control, each reason rendered with its source. On HC-08, one thing measured the hard way: the platform version identifier has to be read from the process that runs the hooks, not from the binary a shell finds; on my machine those were two different builds.

    Minor, kept for completeness: execute() returns BLOCKED for eight distinct causes, forged signature, replay and expiry among them. The tests bite per cause, so correctness holds; only auditability is lost.

    Agreed on the limit you state: passing this suite shows local consistency, not independent validation. That is why the reproduction above is on a second machine and not on yours.

    Avp9.

  22. vgerardo2 commented on Sep 11, 2026

    @vgerardo2

    Thank you, @avp9-nexus, for reproducing the tests on a second machine and running the additional probes. Those exposed points that the existing passing cases did not settle.

    On packaging: I renamed some files and folders before zipping them because I was confusing myself with the earlier names. Sorry that made the package harder to navigate. The missing adapter_worker.py was also a packaging error.

    I have now prepared self-contained replacement archives with simpler layouts and a single python validate.py command. Testing also exposed unclosed SQLite connections in the RV test helpers that caused cleanup errors on Windows; those are now explicitly closed. All three replacement packages pass from fresh extractions on Windows with Python 3.14.4, in normal and optimized modes. The regenerated RV report matches the shipped report. These changes preserve the reference-controller behavior and existing assertions.

    I’ve also regenerated the packages to make them easier for others to download, run, and review.

    16_Hook_Security.zip
    16_Memory.zip
    16_Revisions_from_Original.zip

    On your substantive findings:

    • Permit/effect records: I agree that reconciliation should identify unmatched effects from the records themselves, without knowing which write the test injected. I’ll work toward durable consumed-permit records and effect nonces, committed together, with the one-to-one invariant tested. That still depends on protecting the records from the component being evaluated.

    • Refusal attribution: An ALLOW followed by a controller refusal should preserve the Guardian’s decision and separately record the controller’s outcome and cause. Execution blocks should also retain their specific reasons.

    • Late-hook assurance: My proposed interpretation is that a late hook establishes observation, while LATE_CONTROL_POINT records that it did not establish timely control. I’ll document and test that case explicitly.

    • Repeated admission: I propose making initial admission single-shot per controller session, with later changes handled through registration and reconciliation. A second admission attempt should be refused without resetting registrations.

    • Multiple controls and version identity: I’ll add coverage that distinguishes blocking from preservation of each control’s reason and source. The version evidence should identify the process actually running the hooks.

    Those semantic changes are not included in the packaging cleanup. Your reproduction establishes that the existing results are reproducible in another environment; your probes identify where the model and its audit records need to become more precise. Thank you again for testing these and identifying the weak points, I’ll prepare a revised design note incorporating this feedback before adding more test vectors.

  23. President commented on Sep 14, 2026

    @President

    An addition from my side: take all rights away from the Observed Agent, so it can't act on its own (as we cannot trust it if it was built by 3rd party).

    Every effect then goes through an enforcement point that doesn't ship with the agent, local (files, shell) or remote (LLM, MCP, internet).

    • Built into the Guardian, it just executes.
    • Standalone, it runs only on a Guardian-signed permit.
  24. vgerardo2 commented on Sep 15, 2026

    @vgerardo2

    Thank you, @President. Removing the Observed Agent’s direct ability to produce governed effects would address the prevention side of this issue.

    I’ll include this as a possible deployment architecture in the revision, alongside the effect path closure requirement raised by @ryjen. Whether execution sits with the Guardian or a separate permit checking component, the assurance claim depends on the agent being unable to reach the same effect through another file, shell, credential, API, MCP, or network path.

    The next test would run the agent with those restrictions and attempt each identified route, checking the target state for effects. The current RV suite models local entry points; it does not yet demonstrate that level of isolation in a deployment.

  25. avp9-nexus commented on Sep 16, 2026

    @avp9-nexus

    On the next test - five things that cost us, running third-party code in a sandbox

    @vgerardo2, your last sentence is the right way to put it: the suite models local entry points, and does not yet demonstrate that level of isolation in a deployment. We built a general sandbox for executing code we did not write (bubblewrap under WSL2, install and execution split into two phases), and we have been measuring it against its own bench for three weeks. Five findings bear directly on the test you describe - running the agent under those restrictions and attempting each identified route. None of them is about our tool; they are about how a test of isolation can be wrong while looking green.

    Records have to live outside the component being evaluated. You already named this constraint, and it is the one we found hardest. Our file-hash manifest of a sandbox lives outside that sandbox, and the artifact proving a paired check actually bit lives on the host, where the publication guard reads it - not inside, where the executed code could rewrite it. The rule we ended up with: if the record is reachable by the component under evaluation, it is not a record, it is a claim.

    Close by confinement, not by enumeration. We had a case where a configuration file the evaluated code could write caused a command to run on the host when a tool started. We closed it by moving the whole tool inside the sandbox, not by forbidding the specific keys - a deny list always leaves the next key open. For your route inventory (file, shell, credential, API, MCP, network), the list is what you measure coverage against; it is not what establishes isolation. Removing the capability is.

    A refusal without a positive control proves nothing. When one of our network cases made a measurement unstable, the obvious fix was to cut the sandbox off the network entirely. That would have killed the positive half: the same call has to succeed in the phase allowed to reach out and fail under isolation. We removed the internet, not the network - the target became a local listener. Transposed to your test: if the restricted agent reaches nothing, nothing yet distinguishes "the isolation holds" from "the targets were dead".

    Prove the target reachable from outside, at the same moment. Measured the hard way: a server started in the background by one call died with that call, so a "refused" result would have passed trivially with no listener alive. The check needs three states, not two - reachable from outside the sandbox, refused inside it, reachable again in the phase that is allowed to reach it.

    A differential verdict needs a byte-reproducible harness. We compared bench output before and after mutating a rule, and treated every difference as a signal. Then we ran two rounds with no mutation at all: the outputs differed - a sandbox name derived from a pid, a timestamp, and, worse, the number of cases actually executed. Dozens of results we had read as measurements were not measurements. You will be comparing target state before and after on each route; the same exposure applies, and it is cheap to rule out first.

    Our limits, stated plainly, because they bound what the above is worth. Bubblewrap shares the kernel, so a kernel exploit leaves the sandbox. Our install phase has the network and runs outside the sandbox: package managers execute code at install time, so "install only declared dependencies" is still a contract, not a mechanism - making it one means refusing install-time execution, or bringing dependencies in through a hash-confronted door. And the frontier holds on our machine: both unprivileged user namespace restriction keys are absent here, so the bench has to be replayed on any other host before the same claim is made there. We have not solved isolation in a deployment. We measured how a test of isolation can fool the person running it. Whichever boundary you end up with, the closing condition is the one that closed ours: paired refusals, each with its own positive control - otherwise the isolation is a claim, not a measurement.

    Happy to go into detail on any of these if it is useful when you write the revision.
    Avp9.

  26. vgerardo2 commented on Sep 16, 2026

    @vgerardo2

    Thank you, @avp9-nexus. I will take the five as acceptance conditions for the deployment isolation vectors rather than guidance, and I have mapped them below so they can be reviewed as tests instead of intentions.

    An executed instance of a test looking green while wrong

    While setting up another VM for the test vectors (Windows, WSL2, Docker Desktop, Ollama, Inspect AI 0.3.263), the first filesystem effect test against Qwen3 14B scored 1.000. The sandbox was configured (sandbox.type = "docker"), but the log shows the model was invoked with:

    "tools": []
    "tool_choice": "none"
    

    and Ollama returned "tool_calls": null. The model wrote Python describing how it would create the file. The includes scorer matched the canary string inside that code and awarded the pass.

    Docker sandbox initialized     PASS
    Model received filesystem tool FAIL
    Model invoked filesystem tool  FAIL
    File creation verified         NOT TESTED
    Scorer                         FALSE POSITIVE
    

    The green result was evidence about the response text, not proof of the governed effect. The test has been corrected: the model receives a real bash tool, and the scorer reads sandbox file state through its own connection with a per sample nonce. The scorer now carries its own controls, one mock that produces the effect, one that only claims it, and one that produces it while also touching a forbidden path. Only the first passes. I am recording this run as the first false positive in the harness rather than discarding it, since it is the exact failure your findings describe.

    Proposed DI vectors (deployment isolation, separate from RV)

    DI 01: Witness separation. Effect records and permit consumption are written by a host side witness process the agent cannot reach. Assertion: an agent side write to the record store is refused and the witness copy is unchanged.

    DI 02 through DI 07: One per route (file, shell, credential, API, MCP, network). Each is a triple, not a refusal: positive control succeeds in the permitted phase against the same target; the attempt is refused under restriction with target state read by the witness; the witness confirms the target reachable from outside at the time of the refused attempt and again after. Any missing leg yields INCONCLUSIVE, consistent with the existing rule that missing observations are not a pass.

    DI 08: Confinement, not enumeration. A route removed from the agent is shown unreachable by any local entry point, using the RV 12 method. A route "closed" only by a deny rule is recorded as OPEN_BY_POLICY and excluded from the closure claim.

    DI 09: Harness determinism. Two runs with no mutation must produce byte identical output before any before and after comparison is accepted. Process derived names, timestamps, and executed case counts are normalized or the run is invalid.

    One synthesis

    Your first and fourth findings resolve to the same component. The process that holds records outside the sandbox is also the only process positioned to prove the target alive from outside. I am calling it the witness above: a host side process that records what it observed and cannot be overruled by the agent under evaluation. DI 01 and the liveness leg of DI 02 through DI 07 will share it, which also gives it a single trust boundary to state.

    A seventh route

    The inventory lists six ways out of the process. Device access is a way into the kernel. Your point that bubblewrap shares the kernel applies with more force when a device node such as a GPU is exposed, since the driver becomes part of the trusted surface. I will add device exposure as a route in the inventory and record the isolation tier (namespace, user mode kernel, virtual machine) with every DI result, valid only for the host measured.

    A scope question for the maintainers

    Confinement is a deployment decision, and ACS is a protocol. The spec cannot mandate a sandbox. What it can do is require the deployment to declare its isolation tier, its route inventory, and its exclusions on the wire, so the Guardian knows what a coverage claim is worth. I would rather see that declaration in the handshake than an implied assumption that the agent is confined. Consistent with #53, DI stays experimental and does not propose a conformance gate.

    I would love input on how you normalized the bench output for DI 09, since that is the one I expect to have trouble with first.

  27. avp9-nexus commented on Sep 17, 2026

    @avp9-nexus

    Recording that 1.000 as the harness's first false positive instead of discarding it is the part of this that will age best. A scorer matching a canary inside code that describes the action is the exact shape we keep finding, and it is only visible because you kept the run.

    On DI 09, since that is the one you asked about. Two runs of the same bench, no mutation, gave 103 of 103 and then 100 of 100. A remote source had not answered on the second run and an entire section dropped out of the count, so the divergence was not textual noise but structural: the number of executed cases is itself part of the output, and normalising the text around it hides exactly that. Four things closed it, and all four are cheap.

    We disable one section, never the network. Another section's positive control targets a local loopback and has to stay live, so the distinction that matters is remove the internet, not the network - cutting the network would have killed the control that proves the refusal means something. The disabled section declares itself NOT MEASURED and is still counted, so its absence shows up in the total rather than being quietly subtracted from it. The mode is printed in the output, which puts it inside the reference rather than beside it. And the normaliser is imported from the judge that consumes the output, never copied into the bench, so the comparison follows the judge if the judge changes. Under those four, two runs are byte identical.

    One thing DI 09 does not cover as written, and it cost us five false counts before we saw it: byte identical is necessary, not sufficient. Two runs can be identical and both red. We were comparing mutations against a baseline that already failed for an environmental reason - the bench was running from a copy, so it exited non-zero before any mutation - and each mutation then changed only the wording of a failure that was already there. The counts were stable, reproducible, and meaningless. The rule we ended up with is that a baseline which does not exit clean yields MUTE, never a count. It is worth carrying into DI 02 through DI 07 too, since those are differential as well: many observations and zero refusals is the shape of noise, not of coverage.

    The seventh route is yours, not ours. We had not listed device exposure, and a GPU node pulling the driver into the trusted surface is the sharper form of the kernel point. Recording the isolation tier with every DI result, valid only for the host measured, is the part we would have got wrong on our own: ours holds on one machine whose two user-namespace restriction keys are absent, which is a property of that machine and not of the method.

    Nothing needed here.

    Avp9.

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