# Strands Agents + ACP — Policy & Audit Install Guide

Add per-user policy, audit logging, and PII detection to AWS Strands Agents. One pip install, one HookProvider, every tool call checked and audited.

# Strands Agents + ACP — Policy & Audit Install Guide

[Strands Agents](https://strandsagents.com) 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 one `HookProvider`, bind the user's JWT per request. See [the runnable starter](https://github.com/agentic-control-plane/acp-governance-sdks/tree/main/examples/starters/strands) or the [frameworks index](/frameworks).

## Install

```bash
pip install acp-governance strands-agents
```

## Minimal governed agent

```python
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

1. `BeforeToolCallEvent` fires; the provider POSTs to ACP's `/govern/tool-use` with the tool name, input, and the user JWT bound by `set_context`.
2. **Deny** → `event.cancel_tool` is set. Strands skips the tool function entirely and synthesizes an error `ToolResult` carrying 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.
3. **Allow** → your function runs.
4. `AfterToolCallEvent` fires; the output is reported to `/govern/tool-output` for audit + PII scan. If policy says `redact` or `block`, the provider rewrites `event.result` before 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`, and `tool_use` on the before event; `result` and `retry` on the after event. The integration uses exactly those.
- **Denied calls still fire `AfterToolCallEvent`** with `cancel_message` set — 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 own `hooks=[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-governance` is on 0.x. A framework-specific `acp-strands` package with a packaged `ACPHookProvider` is planned.

## Related

- [Strands Agents docs — hooks](https://strandsagents.com/latest/documentation/docs/user-guide/concepts/agents/hooks/)
- [Strands Agents SDK (GitHub)](https://github.com/strands-agents/sdk-python)
- [`acp-governance` (core SDK)](https://pypi.org/project/acp-governance)
- [Pydantic AI integration](/integrations/pydantic-ai)
- [LangChain / LangGraph integration](/integrations/langgraph)

<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "HowTo",
  "name": "Add Agentic Control Plane policy and audit to Strands Agents",
  "totalTime": "PT5M",
  "step": [
    {"@type": "HowToStep", "name": "Install acp-governance", "text": "pip install acp-governance strands-agents"},
    {"@type": "HowToStep", "name": "Add ACPHookProvider to the agent", "text": "Agent(tools=[...], hooks=[ACPHookProvider()]) — one provider covers every tool."},
    {"@type": "HowToStep", "name": "Bind the user's JWT per request", "text": "Call set_context(user_token=...) at the start of each request."}
  ]
}
</script>
