Slim ACS-Core: relax MODIFY, system/ping, and wrapped MCP to SHOULD - #21
Slim ACS-Core: relax MODIFY, system/ping, and wrapped MCP to SHOULD#21bar-capsule wants to merge 3 commits into
Conversation
Lowers the adoption floor for frameworks claiming ACS-Core conformance by relaxing three items within Core to conditional requirements, without removing them from Core or introducing new profiles. The hook taxonomy minimum, dispositions ALLOW/DENY/ASK/DEFER, SessionContext with chain hash and Intent, replay protection, baseline HMAC-SHA256 integrity, and decision-honoring behavior remain MUST. Changes: - Dispositions: ALLOW/DENY/ASK/DEFER stay MUST. MODIFY moves to SHOULD, with composition rules ([..]/specification.md#63-modify-composition-normative) still normative for implementers. Deployments that do not implement MODIFY declare so in the handshake; Guardians MUST NOT return MODIFY to such deployments (DENY with audit substitution). - Liveness `system/ping`: SHOULD-implement. MAY be omitted by deployments where both parties co-locate or rely on transport-level liveness. - Wrapped MCP `protocols/MCP/*`: SHOULD-implement by deployments that govern MCP tool calls. MAY be omitted by deployments that do not use MCP. Support is declared in the handshake. What stays mandatory in Core (the differentiation from a stateless single-call evaluator): SessionContext + chain_hash + Intent option, the 6-hook lifecycle minimum, four dispositions, replay protection, HMAC baseline integrity, and decision honoring. These preserve the runtime governance properties that distinguish ACS from a stateless policy evaluator. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds subagent lifecycle hooks to the Core minimum (8 hooks instead of 6). Rationale (Ariel): a sub-agent is itself an Observed Agent under delegated authority. Without subagentStart/subagentStop on the Core floor, a Guardian is blind to cross-agent propagation and to the confused-deputy attack class that delegation enables. ACS already has the schemas, the AgBOM models subagent components, and the wire shape is identical to other steps -- the cost of mandating these is marginal, the cost of leaving them optional is a structural gap in runtime governance. Frameworks without a sub-agent abstraction satisfy this requirement vacuously: the hooks never fire, the framework still declares them as implemented in the handshake. Net Core hook count: 6 -> 8. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PR GenAI-Security-Project#21 promotes subagentStart and subagentStop from SHOULD to MUST in the ACS-Core hook minimum (8 hooks instead of 6). Update the FAQ to match: - "How do I make my framework ACS-conformant?" lists subagent hooks in the mandatory floor with a short justification. - "What if my framework does not have a particular hook surface?" no longer lists subagent hooks under "Additional hooks (SHOULD)"; they are now part of Core. Frameworks without a sub-agent abstraction satisfy this requirement vacuously, which is the same shape as the merged spec's "implement when observable" pattern. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Update: subagent hooks promoted to mandatory (per Ariel)Added Net Core after both relaxations and this addition:
Rationale for the subagent promotion (Ariel's reasoning): a sub-agent is itself an Observed Agent operating under delegated authority. Without Frameworks without a sub-agent abstraction satisfy this vacuously (the hooks never fire). Same shape as the existing "implement when observable" pattern. Shape of the slim: dropping the three things that add infrastructure burden (MODIFY composition rules, system/ping plumbing, MCP awareness for non-MCP deployments), adding the two things that close a structural governance gap. The PR's adoption-floor goal still holds — a framework can implement the 8 hooks and the rest of Core in one focused session — and the security floor is materially better. FAQ updated in parallel (PR #20, commit |
|
Merge-order note (for whoever lands these): this one sets the ACS-Core floor, so it should go in before #20 and #22. It promotes the subagent hooks to MUST and relaxes MODIFY,
No git conflict with either (this only touches |
|
this work should actually also bump the patch version @bar-capsule |
|
I spent a while on this one because four lines that change what a MUST means are worth more scrutiny than four lines usually get. The direction is defensible and I'll say why below, but there are two places where the text as written can't be implemented, and one that I think gets the security trade backwards. Taking them in order of how much they block. The MODIFY declaration has nowhere to liveLine 20 says deployments that don't implement MODIFY "MUST advertise this in the handshake." I went to check what field that would be, and there isn't one. Here's the full ClientHello property set from Nothing about dispositions, on either message shape. And a vendor can't just invent one, because the schema description says "Unknown fields MUST be ignored at every level," so a conformant peer is required to throw the extension away. The part that made me think this was an oversight rather than a decision: §9.2 already solved this exact problem for ASK, and it went the other way. I think Wrapped MCP's demotion is what made this feel safe, because that one does land on a real field ( Either add Nothing defines what an agent does with a MODIFY it can't applyThis is the one I'd fix even if you disagree with everything else here. Once MODIFY is optional, some agents will receive one anyway, because the declaration above doesn't work yet and handshakes are the first thing an integration gets wrong. I went looking for the defined behavior and couldn't find it. So an implementer writes the obvious three lines: unknown or unapplicable disposition, proceed with the original payload. Under a house style where Before this PR that was flatly non-conformant and a test could assert it. After, it's an unspecified path inside a negotiated capability, and nothing can call it a violation. One sentence fixes it, mirroring §6.3: an Observed Agent that receives a disposition it cannot apply MUST treat the decision as DENY and MUST record an audit event. While you're in there, the substitution on line 20 mandates an audit event with no The subagent clause has two readings and both are defensibleI like the reasoning here more than the other three changes, and I think the confused-deputy argument is right. The clause still needs work, because I read it twice and got two different answers. Reading one: "Frameworks that do not have a sub-agent abstraction" means the framework genuinely has no such concept. Under that reading Claude Code and Cursor both have subagents, so neither gets the exemption. Reading two: the parenthetical "(the hooks never fire)" is the operative test, so a framework that emits no subagent event satisfies it vacuously regardless of whether the concept exists. Those give opposite answers for the same deployment, which means the clause can't settle a conformance dispute. Worse under reading two, any deployment can claim vacuity by declining to implement the emitter, which makes the promotion from SHOULD to MUST a no-op. The deeper problem is that "has no sub-agent abstraction" is a fact about a framework's internals and not a fact on the wire. A deployment that spawns subagents and doesn't instrument them sends a ClientHello that's byte-identical to one from a framework that genuinely has none. No Guardian, auditor, or test can tell them apart. Every other omission in this PR got a declaration mechanism ("Support is declared in the handshake" on line 26, "MUST advertise this in the handshake" on line 20), and this one didn't. Two more things I'd want you to look at before settling this:
And My suggestion: make the vacuity claim a wire fact (a ClientHello boolean a Guardian can refuse on and an auditor can hold a vendor to), or split the pair so One thing this PR can't fix but should probably acknowledgeWhile tracing the subagent change I hit something bigger. ACS spends 428 lines constraining what the Observed Agent MUST emit and never constrains what the Guardian MUST evaluate. The ServerHello's 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. That's not yours to solve in a four-line PR. But this is the first change whose stated security rationale depends on the half that's missing, so it's worth a sentence in the PR body at least, and probably an issue. Wrapped MCP: the fallback doesn't cover what you'd want it toLine 26 lets deployments "that do not use MCP" omit the namespace. The problem is what happens to deployments that do.
There's a timing problem underneath it. "Deployments that do not use MCP" is assessed once, at handshake, and MCP servers get added at runtime. Suggestion: condition the requirement on The ping justification points at something that isn't definedLine 25 lets deployments omit I'll grant the strongest form of your case here, because The trouble is the recommended alternative. Decision-failure monitoring depends on an audit stream, and in #22 that stream is I'd either strike the unanchored justification, or define what a transport-level liveness signal is per transport, or require ACS-Core deployments to declare a liveness mechanism rather than merely permit omitting one. Small stuff, quick to fixThe quick-reference table at line 83 still says ping is mandatory, in this same file, 58 lines below the bullet that makes it optional: That row also doesn't mention the subagent hooks you just promoted, says "dispositions" unqualified, and has no Wrapped MCP entry. Since it's the row people paste into comparison matrices, I'd update it in this PR. Line 30's guarantee paragraph is also untouched and still says the agent "honors the Guardian's decisions," which is now true for four of five. One clause would fix it, something like noting a minimum-conformant deployment can permit or refuse an action but cannot alter it, and redaction requires MODIFY support. Version
The practical effect is that two peers who disagree about whether MODIFY is required both send I know the project is pre-1.0 and SemVer says you owe nobody compatibility below 1.0.0. That argument works right up until #20's FAQ starts telling compliance teams to rely on this, which it does. A SequencingThese three PRs aren't textually conflicting (I checked, the file sets are disjoint), but #20 and #22 both restate what this PR changes. #20's FAQ already carries your subagent language nearly verbatim at line 124 and hasn't picked up any of the three relaxations, so it'll contradict this PR on three of four items the moment both land. #22's conformance suite cites This one should go first, with #20 rebased against the final text and #22 updating its citations. I've left the details on both of those PRs. Last thing, and it's procedural rather than technical: |
|
Rock's broader item #6 ("ACS spends 428 lines constraining what the Observed Agent MUST emit and never constrains what the Guardian MUST evaluate") is out of scope for this PR but tracked at #31 for follow-up. That issue names the exact Addressing the four blocking / important items Rock raised on this PR directly in the next push. |
…n, subagent split, MCP fallback, liveness, harmonization Blocking / important items from Rock's review: - MODIFY declaration moves off-wire. New §6.5 (MODIFY-incapable clients) mirrors the §9.2 ASK precedent: Guardian determines client capability by deployment-defined means, substitutes DENY with reason_codes: ["modify_unsupported"] and an audit event. A client that receives an unapplicable MODIFY MUST treat it as DENY + audit event (Rock's item 1 and 2). - Subagent taxonomy split: subagentStart MUST-emit for subagent-capable frameworks (real gate, decision-eligible); subagentStop SHOULD-emit (audit-only). Vacuity determined by deployment-defined means, mirroring §9.2 (Rock's item 3, first half). - subagent-stop.json: final_chain_hash promoted to optional. Frameworks that maintain no session-chain MAY omit rather than fabricate; Guardian MUST treat omission as "chain not maintained" not integrity failure (Rock's item 3, second half). - Wrapped MCP MUST-implement for deployments whose sessions involve MCP at any point (closes the resources/read prompt-injection ingress gap). Mid-session MCP addition in a deployment that declared none is non-conformant until v0.2 ships renegotiation (Rock's item 4). - Liveness requires a declared mechanism: system/ping or a named transport-level alternative (TCP keepalive, HTTP/2 PING, process supervision, continuous observed hook traffic). Omitting system/ping without a declared alternative is non-conformant, so the startup_posture / on_decision_failure / no-liveness chain of defaults cannot produce silent fail-open (Rock's item 5). Cross-spec harmonization (called out during review): - §6.3 (malformed MODIFY): SHOULD record an audit event → MUST record an audit event. - §9.2 (approver-incapable): adds MUST record substitution as audit event, so substitution rate is machine-detectable rather than only reachable by grepping log prose. - All fallback/substitution rules now consistent across §6.3, §6.4, §6.5, and §9.2: MUST-DENY + MUST-audit. Cosmetic sweep: - conformance.md quick-ref table row for acs-core updated to reflect new baseline (subagentStart in taxonomy, four MUST-support dispositions + MODIFY SHOULD, declared liveness, Wrapped MCP conditional). - docs/acs.md ACS-Core description rewritten to match. - conformance.md guarantee paragraph updated: minimum-conformant deployment can permit or refuse but cannot alter; redaction and other content rewrites require MODIFY support. Versioning: - version.txt and pyproject.toml bumped 0.1.0 → 0.1.1 (Ariel + Rock both requested). - CHANGELOG.md created with the 0.1.1 entry. Related: Rock's item 6 (Guardian-side hook-coverage unfalsifiability) is out of scope for this PR; filed as GenAI-Security-Project#31 and linked from PR GenAI-Security-Project#21. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: Bar Kaduri <bar@capsule.security>
|
@rocklambros pushed Blocking / important items:
Cross-spec harmonization (called out in your MODIFY note):
Cosmetic sweep (your items 7–9):
Versioning (your item 10, Ariel also asked):
Procedural (item 11):
Ready for another pass when you have the cycles. |
…apter gaps
One batch covering the five review findings from running the branch, the
adversarial/spec/host-contract audits they triggered, and the fixes those
audits surfaced. Every behavioral change ships with a regression test
that was verified to FAIL against the pre-change code (red-checked).
Review findings (all five fixed, with the reviewer's suggested tests):
- Unusable disposition fails closed: an arrived verdict that cannot be
interpreted (padding, unknown value, non-string type, non-object
modifications) is DENY on every adapter, never the fail posture; a
never-raising normalize_decision() makes verdict reading total, and a
translate-layer guard turns any future translation bug fail-closed.
- Stop/SubagentStop are audit-only: {"decision":"block"} there means
"keep going" in Claude Code, so deny/decision-failure on those hooks
records unenforceable_decision and emits nothing.
- settings.json.example regenerated from wire.py itself: all settings
ride inline in the command string (Claude Code has no per-hook env
field); the config test asserts the command string and pins
example == wire.py output so they cannot drift again.
- Degraded bootstrap: a missing rfc8785 no longer dies as a silent
exit-1 no-op — both shell adapters audit adapter_unavailable and honor
ACS_DEFAULT_DENY with stdlib only; per-adapter requirements.txt pins
rfc8785; NAT fails LOUDLY (UNGOVERNED banner + re-raise) since its
in-process middleware cannot degrade per-hook.
- Handshake cache holds the whole signed ServerHello envelope and
re-verifies the signature on every read; tampering is audited
(handshake_cache_signature_invalid) and falls to re-handshake; the
negative-cache branch is audited per §4.1.
Spec-conformance fixes (from re-deriving the normative requirements):
- Negotiated timeout honored: the ServerHello's timeout_config.default_ms
replaces the hardcoded 5s deadline on all three adapters; a decision
timeout is audited distinctly (decision_timeout) from an unreachable
Guardian.
- Prompt gates fail closed: modify/ask/defer on a prompt (which the
hosts cannot apply there) block instead of silently proceeding.
- MODIFY merges: parameter_overrides are per-argument edits, so they are
merged onto the original input before updatedInput/updated_input
(which replace wholesale); a modify carrying redactions the host
cannot express is refused rather than half-applied, and NAT's output
gate redacts rather than leaking unmodified output on structured
modify.
- §6.3 composition check implemented: a modifications object combining
wholesale + structured shapes, or with overlapping JSON-Pointer
targets, is refused as DENY (shared checker with proper pointer-prefix
comparison and a false-positive guard test).
- Honesty: profiles_supported advertises acs-core only when signing is
configured; ACS_HANDSHAKE=0 is audited as non-conformant;
ACS_DISABLED writes a structured audit event to the durable sink;
ahead-of-spec behaviors (refusal fail-closed, posture merge,
ask/defer substitutions) are labeled as deliberate hardening or
deployment behavior pending the open spec decisions, not cited as
mandated.
Turn tracking (Claude Code + Cursor):
- A prompt opens a turn with an explicit decision-eligible
steps/turnStart (a Guardian deny blocks the prompt), the userMessage
and every in-turn step carry metadata.turn_id, and Stop/stop closes it
with steps/turnEnd — replacing the wrong Stop->sessionEnd mapping that
sealed the audit chain after every reply. A stop with no open turn is
skipped and audited, never a fabricated turn_id. Guardian round-trip
handling is factored into one shared helper so both POSTs get
identical binding/signature/refusal treatment.
Cursor host-contract fixes (verified against the official hooks docs):
- Payload builders now read only fields Cursor actually sends:
workspace_roots (not workspace_path), tool_name/tool_input(+url|command)
for MCP (not mcp_server/mcp_tool), result_json, text (not
response/thought), error_message/failure_type (mapped onto the ACS
exit_status enum incl. timeout and blocked), command/output/duration/
sandbox for shell results. JSON-stringified inputs are parsed, never
crashed on. postToolUse's embedded exitCode drives an honest failure
status; sessionEnd reasons and stop status map onto the ACS enums;
subagentStart lineage derives from the real tool_call_id;
beforeSubmitPrompt emits the documented {"continue": false,
"user_message"} block alongside exit 2; output fields are emitted only
on postToolUse (the one event that documents them). Emission tests
feed the documented shapes and assert the content lands in the ACS
payloads, so schema-valid-but-empty can no longer pass. Claude Code's
subagent gate matches the current `Agent` tool name (legacy Task kept);
duration_ms is coerced to the schema's integer.
Docs: mapping.md/README rewritten to match the code (block-shape hooks,
turn model, per-event field sources, correlation gaps stated instead of
papered over); PR GenAI-Security-Project#21 is referenced only as an open proposal not in this
branch; the three normative schema edits carried here are flagged for
explicit spec-owner approval.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Bar Kaduri <bar@capsule.security>
Summary
Lowers the ACS-Core adoption floor by relaxing three items to conditional requirements within Core, without removing anything from Core or introducing new profiles. The behavior contract that makes ACS distinct (SessionContext + Intent, the hook lifecycle minimum, decision honoring) stays mandatory.
Three changes
system/pingprotocols/MCP/*What stays mandatory in ACS-Core
The seven things that make ACS a meaningful runtime governance contract, not a stateless single-call policy evaluator:
request_id,timestamp,acs_version,metadata)sessionStart/agentTrigger,userMessage,toolCallRequest,toolCallResult,agentResponse,sessionEndchain_hashand the Intent invariant (the runtime context that makes governance meaningful, not just per-call yes/no)request_idUUID +timestamp+ Guardian replay rejection)Why these three specifically
All three add real deployment friction without being load-bearing for the spec's core property (preventing the agent from acting on injected reasoning):
MODIFY requires the agent to rewrite its own outbound payload according to Guardian-provided modifications. That's qualitatively different from ALLOW/DENY/ASK/DEFER, which are state-machine outcomes the agent doesn't have to construct. A framework that doesn't implement MODIFY can still ALLOW/DENY/ASK/DEFER cleanly.
system/ping is useful for managed-Guardian deployments where the agent needs to verify reachability before sending real traffic. Co-located deployments don't need it; transport-level liveness covers the same property.
Wrapped MCP forces every conformant framework to be MCP-aware, including frameworks whose deployments don't use MCP at all. Declaring MCP support in the handshake is the right cut.
ACS picks "stateful and meaningful on hooks." SessionContext with a hash-chained audit and an immutable Intent are required in Core; the dispositions you actually implement track the policy outcomes a framework can produce. Compositional risk is governable because the Guardian sees the running session, not just one call. The cost is that ACS-Core is a slightly higher implementation bar than "wire up one hook and return a verdict."
This PR reduces the gap on the implementation-bar axis without giving up the statefulness or the meaningful hook minimum that distinguish ACS.
What this is NOT changing
on_decision_failure: proceed): unchangedOpen questions for working-group review
Handshake field for MODIFY-incapable clients. The PR says deployments not implementing MODIFY "declare so in the handshake," but doesn't pin the field name. Suggest a follow-up that adds a
dispositions_supportedfield to ClientHello, parallel to the existingapprover_types_supported. Could ride this PR or land separately.FIDES interaction. FIDES-style enforcement uses MODIFY for redaction (
modifications.redactions). A FIDES Guardian targeting a MODIFY-incapable client would need to substitute DENY for actions that would otherwise be redaction-rewritten. Worth flagging in the FIDES paradigm-binding docs.Wrapped MCP signalling. The PR says MCP support "is declared in the handshake." Today the handshake's
wrapped_protocolsfield carries this implicitly. Worth making explicit in a follow-up that absence of MCP fromwrapped_protocolsmeans the deployment is MCP-incapable.Test plan
specification/v0.1.0/remain valid (no schema changes in this PR)docs/spec/conformance.md🤖 Generated with Claude Code