# How a call is decided — one model, every framework

How the @governed decorator, /govern/tool-use endpoint, fail-open semantics, session IDs, and end-user JWT binding fit together. Framework-agnostic reference.

# How a call is decided

Every ACP integration &mdash; Claude Code hook, CrewAI decorator, OpenAI-compatible proxy &mdash; lands at the same decision pipeline. Learn the model here; each framework guide then reduces to "how to wire this framework&rsquo;s tool dispatch into the pipeline."

## The shape of a policy-checked call

Every tool invocation in a policy-enforced agent takes the same six steps:

1. **Identity binds to the request.** The end user's JWT is attached via `set_context(user_token=...)` (Python) or `withContext(...)` (TS). Not your service key &mdash; the human&rsquo;s token.
2. **Pre-tool check.** Before the tool runs, the SDK POSTs `{ tool_name, tool_input, session_id }` plus `Authorization: Bearer <user-jwt>` to `/govern/tool-use`.
3. **ACP evaluates.** Server verifies the JWT against the configured IdP, then runs the control pipeline: immutable rules, scope intersection, ABAC, rate limits, plan limits, PII detection.
4. **Decision returns.** One of `allow`, `flag`, `ask`, or `deny`, with a human-readable reason.
5. **Tool runs (or doesn&rsquo;t).** Allow runs your function; flag runs it and marks the row for review. Ask holds the call for a human &mdash; the agent sees `[ACP] Approval required: <reason>` (see [Approvals](/docs/approvals)). Deny returns `[ACP] Denied by policy: <reason>`, which the LLM sees as the tool&rsquo;s output.
6. **Post-tool scan.** The output is POSTed to `/govern/tool-output` for PII, prompt-injection, and secret detection. Findings write to the audit log. If policy says `redact`, the redacted string replaces the output.

This is the whole model. Everything else is framework-specific plumbing.

## Two install patterns

ACP enters the loop in one of two places depending on the framework:

### Pattern A — wrap the tool (`@governed`)

For frameworks where you define tools as functions or classes, you stack a policy decorator on each tool. The decorator is synchronous with the tool dispatch &mdash; the check runs before your function body.

```python
from acp_langchain import governed, set_context

@tool
@governed("web_search")
def web_search(query: str) -> str:
    """Search the web."""
    return my_search(query)
```

Used by **CrewAI**, **LangChain / LangGraph / Deep Agents**, **Anthropic Agent SDK**. The LLM call itself goes direct to your model provider.

### Pattern B — proxy the LLM (OpenAI-compatible)

For frameworks that talk to an OpenAI-compatible client, you point the `base_url` at ACP. Every LLM call &mdash; and the tool calls it emits &mdash; flows through the proxy. Per-agent attribution via an `x-acp-agent-name` header.

```python
from openai import AsyncOpenAI

client = AsyncOpenAI(
    base_url="https://api.agenticcontrolplane.com/v1",
    api_key=os.environ["ACP_API_KEY"],
    default_headers={"x-acp-agent-name": "researcher"},
)
```

Used by the **OpenAI Agents SDK** and anything that speaks the OpenAI chat-completions API.

The two patterns are not mutually exclusive &mdash; you can wrap tools *and* proxy the LLM. Both land at the same audit log.

## Four building blocks

Every framework starter teaches the same four primitives.

### 1. End-user JWT binding

Your service is the one holding the service account. ACP needs the *end user's* token &mdash; the human who triggered the run &mdash; to attribute the call correctly.

```python
@app.post("/run")
def run(payload: Payload, authorization: str = Header(...)):
    set_context(user_token=authorization.removeprefix("Bearer ").strip())
    # ...kickoff the agent
```

ACP verifies this JWT on every `/govern/tool-use` call against the IdP you configured in **Settings &rarr; Identity Provider**. Firebase, Auth0, Okta, any OIDC.

### 2. The `@governed` decorator

Marks a tool as policy-enforced. Stack it *under* the framework&rsquo;s own tool decorator so the check runs inside the tool&rsquo;s dispatch:

```python
@tool                          # framework (CrewAI / LangChain) decorator — outer
@governed("send_email")        # ACP decorator — inner
def send_email(to, subject, body):
    return sendmail(to, subject, body)
```

Tools without `@governed` are *not* controlled. This is intentional &mdash; the decorator makes control an explicit choice, visible in diffs.

### 3. Session IDs

Every tool call for one request shares a `session_id`, so the audit log groups them into a single logical trace. SDKs generate this automatically per `set_context` call; you rarely set it yourself.

Session IDs are how the dashboard&rsquo;s Activity view shows "these five tool calls were part of the same user request."

### 4. Fail-open

If `/govern/tool-use` times out (5s default) or is unreachable, the SDK returns `allow` with reason `fail-open`. The tool proceeds. This is deliberate: control is never a single point of failure for your agent.

Fail-open is opinionated. It means a downed control plane doesn&rsquo;t break user-facing functionality. If you need fail-closed semantics for specific tools, set policy server-side &mdash; the Claude Code hook is fail-closed by default because its ACP connection is a hard dependency, but framework SDKs lean fail-open for availability.

## Decisions and their semantics

| Decision | What happens | LLM-visible |
|---|---|---|
| `allow` | Tool runs. Output passes through post-scan. | Yes &mdash; real tool output. |
| `flag` | Tool runs. Row is marked for review. | Yes &mdash; real tool output. |
| `ask` | Tool is held until a human answers on the Approvals page. Approval grants one retry within the grant window; no answer counts as deny. | Yes &mdash; `[ACP] Approval required: <reason>`. |
| `deny` | Tool does not run. Agent sees `[ACP] Denied by policy: <reason>`. | Yes &mdash; the model sees the error string and adapts. |
| `redact` | A transform, not a decision: the tool runs, and the post-scan rewrites the output per policy. | Yes &mdash; redacted output. |
| `fail-open` | Control plane unreachable. Tool runs. Reason annotated in audit log. | Same as `allow`. |

All of these write structured rows to the audit log, viewable at [cloud.agenticcontrolplane.com/activity](https://cloud.agenticcontrolplane.com/activity).

## What&rsquo;s audited

Every decision &mdash; allow, flag, ask, deny, redact, fail-open &mdash; emits one row with:

- **Actor.** The end user&rsquo;s `sub` (from JWT), not your service key.
- **Tool name.** Whatever string you passed to `@governed("...")` or the framework&rsquo;s tool name.
- **Decision and reason.** Human-readable, machine-parseable.
- **Session ID.** Groups all tool calls from one request.
- **Findings.** PII detected in input or output, if any.
- **Latency, cost, depth.** Metrics for dashboards and budgets.

Claude Code, Cursor, CrewAI, LangGraph &mdash; all write to the same audit log, keyed by the same end-user identity. One log per human across every agent surface.

## Inter-agent handoffs

Some frameworks delegate work between agents without a tool boundary:

- **CrewAI** has sequential task handoffs and hierarchical "delegate to coworker" tools.
- **LangGraph** has supervisor-worker patterns.
- **OpenAI Agents SDK** has first-class `handoffs`.

These don&rsquo;t hit `/govern/tool-use` directly (no tool was called). Each SDK adapter provides a hook &mdash; e.g. `install_crew_hooks(crew)` &mdash; that emits synthetic `Agent.Handoff` audit events for these transitions. PII scanning applies; existing callbacks chain, not overwrite.

## Framework coverage today

| Framework | Pattern | Controls tool calls | Controls handoffs | SDK |
|---|---|---|---|---|
| [CrewAI](/integrations/crewai) | A | via `@governed` | via `install_crew_hooks` | `acp-crewai` |
| [LangChain / LangGraph](/integrations/langgraph) | A | via `@governed` | via graph callbacks | `acp-langchain` |
| [Anthropic Agent SDK](/integrations/anthropic-agent-sdk) | A | via `governHandlers` | n/a (single-agent loop) | `@agenticcontrolplane/governance-anthropic` |
| [OpenAI Agents SDK](/integrations/openai-agents-sdk) | B | via proxy | per-agent via header | No install &mdash; `base_url` change |
| [Claude Code](/integrations/claude-code) | A | via `PreToolUse` hook | via delegation chain | `install.sh` |

## Related

- [Frameworks index](/frameworks) &mdash; starter code for every framework.
- [Integrations index](/integrations) &mdash; off-the-shelf AI clients.
- [Policies &amp; scopes](/docs/policies) &mdash; how allow/deny rules are configured.
- [Agent identity](/agent-identity) &mdash; deeper dive on how JWTs flow through the LLM.
- [Agent-to-agent governance](/agent-to-agent) &mdash; delegation chain semantics.
