Strands Agents + ACP — Policy & Audit Install Guide
Strands Agents is AWS’s open-source Python SDK for building agents — the framework behind Amazon Q Developer. It ships the best-documented hook seam in the framework category: typed lifecycle events around every tool call, including a hard cancel (cancel_tool) that AWS describes as rules the LLM cannot bypass.
That cancel is the hands. acp-governance is the brain: workspace policy, per-user identity, human approvals, and an off-machine audit trail — the same rules your Claude Code, LangChain, and CrewAI agents already follow, now enforced through Strands’ own hooks.
Starter · 5-minute install.
pip install acp-governance, add oneHookProvider, bind the user’s JWT per request. See the runnable starter or the frameworks index.
Install
pip install acp-governance strands-agents
Minimal governed agent
from strands import Agent, tool
from strands.hooks import AfterToolCallEvent, BeforeToolCallEvent, HookProvider, HookRegistry
from acp_governance import configure, pre_tool_use, post_tool_output, set_context
configure(base_url="https://api.agenticcontrolplane.com")
class ACPHookProvider(HookProvider):
def register_hooks(self, registry: HookRegistry, **kwargs) -> None:
registry.add_callback(BeforeToolCallEvent, self.before_tool)
registry.add_callback(AfterToolCallEvent, self.after_tool)
def before_tool(self, event: BeforeToolCallEvent) -> None:
allowed, reason = pre_tool_use(event.tool_use["name"], event.tool_use.get("input") or {})
if not allowed:
event.cancel_tool = f"[ACP] Denied: {reason}"
def after_tool(self, event: AfterToolCallEvent) -> None:
if event.cancel_message is not None:
return # cancelled pre-execution; nothing ran
verdict = post_tool_output(
event.tool_use["name"], event.tool_use.get("input") or {},
"".join(b.get("text", "") for b in event.result["content"]),
)
if verdict and verdict.get("action") == "redact":
event.result = {"toolUseId": event.result["toolUseId"], "status": "success",
"content": [{"text": verdict["modified_output"]}]}
elif verdict and verdict.get("action") == "block":
event.result = {"toolUseId": event.result["toolUseId"], "status": "error",
"content": [{"text": f"[ACP] Blocked: {verdict.get('reason', '')}"}]}
@tool
def lookup_record(id: str) -> str:
"""Look up a record by ID."""
return db.records.find_one({"id": id})
agent = Agent(tools=[lookup_record], hooks=[ACPHookProvider()])
set_context(user_token=user_jwt, agent_name="my-strands-agent", agent_tier="interactive")
agent("Look up record abc-123")
One provider covers every tool registered on the agent — no per-tool decorators, no wrapper functions.
What happens on every tool call
BeforeToolCallEventfires; the provider POSTs to ACP’s/govern/tool-usewith the tool name, input, and the user JWT bound byset_context.- Deny →
event.cancel_toolis set. Strands skips the tool function entirely and synthesizes an errorToolResultcarrying the reason. The model sees the denial and adapts. This is enforced in the executor, not the prompt — the model cannot argue its way past it. - Allow → your function runs.
AfterToolCallEventfires; the output is reported to/govern/tool-outputfor audit + PII scan. If policy saysredactorblock, the provider rewritesevent.resultbefore it reaches the conversation. Audit row written, rooted in the end user’s identity.
Per-tier policy
set_context(agent_tier="...") selects the policy tier:
interactive— human at the keyboard, permissive default.subagent— invoked by another agent, no human in the immediate loop.background— autonomous, most restrictive.api— programmatic call from your backend.
Strands-specific notes
- Frozen events, writable seams. Strands hook events reject attribute writes except for the fields designed for intervention:
cancel_tool,selected_tool, andtool_useon the before event;resultandretryon the after event. The integration uses exactly those. - Denied calls still fire
AfterToolCallEventwithcancel_messageset — the provider skips output processing for them since nothing executed. - Model-agnostic.
BedrockModel(default),AnthropicModel,OpenAIModel,OllamaModel, and the rest all run the same tool executor, so the same hooks cover all of them. - Multi-agent orchestration. Graph/swarm nodes have their own
BeforeNodeCallEvent/AfterNodeCallEvent; each member agent carries its ownhooks=[ACPHookProvider()], so tool coverage follows the agent, not the orchestrator.
Limitations
- Tool-layer, not token-layer. LLM calls go direct to your provider with your own key; ACP checks and records tool calls.
- Pre-release.
acp-governanceis on 0.x. A framework-specificacp-strandspackage with a packagedACPHookProvideris planned.