Skip to content

item 8 (part): the ACS control hook - #14

Merged
arpanghoshal merged 2 commits into
mainfrom
item-8-acs-adapter
Sep 3, 2026
Merged

arpanghoshal merged 2 commits into
mainfrom
item-8-acs-adapter

Conversation

@arpanghoshal

Copy link
Copy Markdown
Member

The ACS half of item 8. This amends SPEC-v0.2 §9, which said "no code, no adapter, no claim" — flagged up front because it also narrows a line in the do-not-build list.

Why the amendment

§9's ruling rested on ACS having no stable interface to build against. Reading the v0.1.0 schemas settled that differently: the envelope, the hook payloads and the five decisions are specified precisely enough to write against, and steps/toolCallResult gives a post-execution checkpoint the earlier draft assumed did not exist.

The no-claim rule is untouched, and it is why the adapter is safe to ship. At the commit read there is no ACS reference implementation and no conformance suite, so there is nothing to be conformant with. "ACS-compatible" appears nowhere in the README, docstrings or CLI output. The do-not-build line narrows from "an ACS adapter or any ACS claim" to "any ACS compliance claim"; ROADMAP's v0.3 standards line records that the adapter landed early.

What was read

GenAI-Security-Project/agent-control-standard at commit c7ad162f69386daac94b89073e3b751e8cdf28b2 (2026-08-11), release v0.1.1, spec version v0.1.0 (decoupled at 1af1f92, same day). Apache-2.0 code, CC BY-SA 4.0 docs.

specification/ACS/acs_schema.json · v0.1.0/request-envelope.json · response-envelope.json · hooks/tool-call-request.json · hooks/tool-call-result.json · ask-details.json · CONTRIBUTING.md. All recorded file-by-file in docs/ACS.md.

Two absences shaped the work: no reference implementation (the README puts a Guardian sample and FastMCP instrumentation at v1), and no examples/ directory, so there is no house format for a community example — examples/acs/ follows CTRLRun's own convention and says so.

The design

ACS is advisory and CTRLRun is executing: a Guardian returns a decision and the platform runs the tool, the opposite way round from @protect. So one action splits across two hooks with the reservation held between them — exactly the shape Suspended and Control.resume were built for in §6.9.

steps/toolCallRequest  → Control.execute(…, executor=raise Suspended(request_id))
                         decided, reserved, held; no outcome, no receipt
                       → allow | deny | ask
steps/toolCallResult   → Control.resume(request_id_ref, executor=report(exit_status))
                       → allow

Control stays the only module that composes the others, and there is still one implementation of v0.1 §5.5's asymmetry.

exit_status is success | failure | timeout | blocked, and ACS says nothing about what any of them means for a side effect. The fail-closed reading is §6.8's: success committed, blocked failed (a control refused before dispatch), failure and timeout unknown — because a tool that failed after acting and one that failed before send the same string.

Of ACS's 22 hooks the adapter answers two and declines twenty. The rest are checkpoints about what the model is thinking, remembering or composed of; the product rule is that CTRLRun decides actions that affect the real world, so an unanswered method returns a JSON-RPC error in ACS's reserved range rather than an opinion.

ask_details.intent_extension is not used: it grants capabilities for a session, which is an authority model CTRLRun does not have until v0.3, and a grant it cannot represent is one it must not claim to honour.

Where ACS is silent

docs/ACS.md sets this out at length; the short version is five gaps — no effect identity, exit_status describing the call rather than the effect, no binding of an approval to a canonical action, no terminal unknown state, and a Guardian that cannot make reserve-and-execute atomic because it does not execute.

Mutation table — 30 mutations, all red

# Group Result
M1–M6 the exit_status mapping, incl. the inversion, and not_executed_on_error red
M7–M9 results with no ref, no match; the request hook suspends rather than completing red
M10–M17 the decision mapping, ask_details, approval re-presentation, mutation refusal red
M18–M25 argument unwrapping, principal, action naming, resource template red
M26–M30 envelope validation, reserved error range, echoed ids, the result hook's allow red

Seven started green, five of them genuinely untested paths — the most useful being that not_executed_on_error reached across to timeout. That flag is an operator's assertion about errors; a timeout is not an error the tool reported, it is the absence of any report, and no per-tool claim can speak for it. Nothing tested that boundary.

The other two are the familiar subsumed-guard shape and stay for their messages: a missing agent_id would be refused by Principal too, but this refusal names the ACS field a producer has to fix.

Not in this PR

The OTel sink (T29) and T30's named test — item 8's other half. docs/ACS.md explains why the OTel attribute alignment is deliberately not written: it would be a compliance claim with nothing behind it, and receipts already written carry the current names.

Drafted, not posted

Three documents in internal/owasp/ (git-excluded, as internal/ always is):

  • acs-issue.md — the execution-outcome gap in ACS's own terms, with the concrete duplicate-refund trace and a proposed extension shape (optional effect_id, a three-state effect_outcome, an action_digest on ask_details). CTRLRun is named once as one reference implementation alongside Stripe/Adyen idempotency keys and Temporal. CONTRIBUTING.md says schema changes open a Discussion first, not an issue, so it is written to be pasted there.
  • slack-intro.md — four sentences.
  • top10-contribution.md — mitigation paragraphs for ASI02 Tool Misuse, ASI03 Identity & Privilege Abuse and ASI09 Human-Agent Trust Exploitation, in the Top 10's register, each with a reference scenario from the demos. It flags that section numbers and file paths must be checked against the live document before filing, and that ASI08 is a plausible fourth home that would weaken both if duplicated.

Nothing has been posted anywhere.

Verification

1241 tests pass (1207 → 1241), mypy --strict src/, ruff check and ruff format --check clean. import ctrlrun imports neither ctrlrun.acs, httpx, ssl nor ctrlrun.gateway. examples/acs/main.py runs with no network.

SPEC-v0.2 §9 amended, acceptance tests T51-T55. §9 said "no code, no adapter,
no claim" on the reading that ACS had no stable interface to build against.
Reading the v0.1.0 schemas settled that differently, so the adapter ships and
the no-claim rule stays exactly where it was.

Read against GenAI-Security-Project/agent-control-standard at commit c7ad162
(2026-08-11), spec v0.1.0: request-envelope.json, response-envelope.json,
hooks/tool-call-request.json, hooks/tool-call-result.json, ask-details.json,
CONTRIBUTING.md. docs/ACS.md records what was read, file by file.

ACS is advisory and CTRLRun is executing: a Guardian returns a decision and
the platform runs the tool, which is the opposite way round from @Protect. So
one action splits across two hooks and the reservation is held between them —
exactly the shape Suspended and Control.resume were built for in §6.9. The
adapter reuses them rather than reaching for the store, so Control stays the
only module that composes the others and there is still one implementation of
v0.1 §5.5's asymmetry.

exit_status is success|failure|timeout|blocked, and ACS says nothing about
what any of them means for a side effect. The fail-closed reading is the same
one §6.8 applies to MCP: success committed, blocked failed (a control refused
before dispatch), failure and timeout unknown — because a tool that failed
after acting and one that failed before send the same string.

Of ACS's 22 hooks the adapter answers two and declines twenty. The rest are
checkpoints about what the model is thinking, remembering or composed of; the
product rule is that CTRLRun decides actions that affect the real world and
nothing else, so an unanswered method returns a JSON-RPC error rather than an
opinion.

No compliance claim, and that is not a formality: at that commit ACS has no
reference implementation and no conformance suite, so there is nothing to be
conformant with. "ACS-compatible" appears nowhere.

examples/acs/ runs the double-refund scenario through the hooks with no
network. ACS has no examples/ directory of its own, so it follows CTRLRun's
convention and says so.

The build list and ROADMAP.md move with §9: the do-not-build line narrows from
"an ACS adapter or any ACS claim" to "any ACS compliance claim", and ROADMAP's
v0.3 standards line records that the adapter landed early.
Mutation-testing §9 found seven, of which five were untested rather than
subsumed:

- not_executed_on_error reaching across to `timeout`. The operator asserted
  something about *errors*; a timeout is not an error the tool reported, it is
  the absence of any report, and no per-tool claim can speak for it.
- a toolCallResult with no request_id_ref at all — the earlier test passed a
  UUID that matched nothing, which is a different case.
- an argument that is a mapping with provenance but no `value`, which is the
  shape a producer gets wrong; the earlier test sent a bare string, which the
  isinstance check catches either way.
- an envelope whose only fault is `jsonrpc: "1.0"`; every other malformed case
  was also caught by a later guard.
- the operation in the action name, asserted on the receipt rather than on an
  event merely existing.

Two are subsumed and stay for their messages: a missing agent_id would be
refused by Principal too, but this one names the ACS field a producer has to
fix; the tests now assert which refusal they got.
@arpanghoshal
arpanghoshal merged commit 876af37 into main Sep 3, 2026
3 checks passed
@arpanghoshal
arpanghoshal deleted the item-8-acs-adapter branch September 3, 2026 21:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant