# Add Per-User Permissions and Audit to LangGraph in 3 Minutes

Wrap your LangGraph tools with one decorator and bind the user's JWT — every tool call across every node is permission-checked, identity-attributed, and logged. Three minutes from `pip install` to running.

LangGraph's most popular pattern is a supervisor with workers: the supervisor node routes to a researcher, a coder, or a reviewer based on state, then loops until done. It's the reference architecture for every serious multi-agent system built on LangChain.

It's also a delegation chain. Nobody calls it that, but that's what it is — one agent decides, hands work to another, and expects a result back. The supervisor is accountable for what the workers do.

Which is a problem, because LangGraph gives you zero tools for enforcing that accountability. The workers share credentials with the supervisor. Their tool access is whatever you happened to put in the `tools` list. And when something goes wrong, the trace shows *which node ran* but not *what permissions that node actually had at the time*.

[Agentic Control Plane](/agent-to-agent) fixes this at the tool layer — so you don't have to rewrite your graph.

## The 3-minute setup

`pip install acp-langchain`. Stack `@governed` under `@tool`. Bind the user's JWT. Done.

```python
import os
from acp_langchain import configure, governed, set_context
from langchain.agents import create_agent
from langchain.tools import tool

configure(base_url="https://api.agenticcontrolplane.com")


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


@tool
@governed("send_email")
def send_email(to: str, subject: str, body: str) -> str:
    """Send an email."""
    return sendmail(to, subject, body)


def run(prompt: str, user_jwt: str):
    set_context(
        user_token=user_jwt,
        agent_name="my-langgraph-agent",
        agent_tier="background",
    )

    agent = create_agent(
        model="openai:gpt-4o-mini",
        tools=[web_search, send_email],
    )
    result = agent.invoke({"messages": [{"role": "user", "content": prompt}]})
    return result["messages"][-1].content
```

Every tool call your graph makes — supervisor's, researcher's, every worker's — flows through ACP's `/govern/tool-use` endpoint before the tool runs, and through `/govern/tool-output` after. Every call is identity-attributed, audited, and policy-checked.

`create_agent` is the 2026 idiom (it replaces the legacy `langgraph.prebuilt.create_react_agent`). For custom `StateGraph` builds, the decorator pattern is identical — what matters is the tool decoration, not the graph shape.

## What you get for free

Open the [Activity dashboard](https://cloud.agenticcontrolplane.com/activity). One row per tool call, each with:

- **Actor** — the user from `set_context(user_token=...)`
- **Tool** — `web_search`, `send_email`, whatever you passed to `@governed("...")`
- **Decision** — allow / deny / redact / fail-open + reason
- **Session** — groups all tool calls from one `set_context` scope
- **Findings** — PII or secret patterns detected

LangGraph tool calls sit alongside Claude Code, Cursor, CrewAI, and every other framework wired into ACP — one audit log across every agent surface in your stack.

## What changes when policy enforces

**Per-tool denials become tool errors.** When ACP denies a call (rate limit, scope violation, PII detection), `@governed` returns the string `"tool_error: <reason>"`. LangChain treats this as the tool's output; the model sees it and adapts. Your graph keeps running.

**Output redaction is automatic.** If your policy redacts PII or secrets in tool outputs, the redacted version replaces the original before the model sees it. The model never sees `[redacted:credit_card]`'s original value, even when the model itself called the tool that returned it.

**Identity flows through every hop.** `set_context` uses Python's contextvars — every governed call inside the request scope reads the bound user identity automatically. You don't thread the JWT through nodes manually.

## Custom StateGraph

The decorator pattern works regardless of how you build the graph. For a custom supervisor-worker graph:

```python
from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
from acp_langchain import governed

@tool
@governed("query_db")
def query_db(sql: str) -> str:
    """Run a SQL query."""
    return db.execute(sql)

# ToolNode dispatches tool calls — @governed runs on every dispatch.
tools_node = ToolNode([query_db])

graph = StateGraph(MyState)
graph.add_node("tools", tools_node)
# rest of your graph as you'd normally build it
```

The tool decoration is what matters. ACP doesn't care about your graph shape — it cares about your tool boundaries.

## Per-tier policy

`set_context(agent_tier="...")` controls which policy tier evaluates the call:

- **`interactive`** — a real user is at the keyboard. Permissive default.
- **`subagent`** — invoked by another agent, no human in the immediate loop.
- **`background`** — autonomous, no human anywhere. Most restrictive.
- **`api`** — programmatic call from your backend.

A destructive tool (`Bash.kubectl`, `db.delete`, `wire_transfer`) typically gets `deny` in `background`, `ask` in `subagent`, `allow` in `interactive`. Match the tier to actual deployment reality, not what's permissive.

## Alternative: LLM-layer governance

If you want LLM-call audit, per-agent cost attribution, or budget enforcement at the model layer in addition to (or instead of) tool-layer governance, point your model at ACP's OpenAI-compatible proxy:

```python
from langchain_openai import ChatOpenAI

governed_llm = ChatOpenAI(
    base_url="https://api.agenticcontrolplane.com/v1",
    api_key=os.environ["ACP_API_KEY"],
    model="claude-sonnet-4-6",
)
```

Both patterns are valid and composable. Tool-layer (the `@governed` decorator) is the primary recommendation because it gives you per-tool-call interception, output redaction, and per-user identity attribution. LLM-layer (OAI proxy) adds budget enforcement and per-agent cost rollups. Use one, the other, or both.

## What this unlocks

LangGraph is the most widely deployed Python agent framework. Three lines of integration — `configure`, `@governed`, `set_context` — and your graph becomes auditable, identity-attributed, and policy-bound without rewriting a single node.

Your graph stays a graph. Your governance becomes a control plane. They stay out of each other's way.

---

[Install ACP free](/getting-started) · [LangGraph integration guide](/integrations/langgraph) · [LangGraph governance scorecard](/blog/langgraph-governance-scorecard) · [Delegation chains deep dive](/agent-to-agent)
