Skip to content
Agentic Control Plane

Per-user auth, policy & audit for LangChain & LangGraph tools

TL;DR. pip install acp-langchain, wrap your tools with @governed, bind the end user’s JWT per request — and every LangChain/LangGraph tool call gets per-user identity, policy enforcement (allow / deny / redact), rate limits, PII detection, and an audit trail. One decorator, same governance model as Claude Code.

LangChain and LangGraph are the most widely deployed agent frameworks in Python. Out of the box, a production deployment shares one backend API key across every end user’s request — no per-user policy enforcement, no per-user audit trail, no way to tell downstream systems which human triggered which action.

acp-langchain closes that gap. Wrap tools with @governed; before each runs, ACP decides allow / deny / redact based on your workspace’s policy, the end user’s identity, rate limits, and PII detection. Same governance model as Claude Code — same /govern/tool-use endpoint, same workspace policies.

Works with any LangChain agent (ReAct, tool-calling, custom) and any LangGraph pattern (prebuilt create_react_agent, custom StateGraph, supervisor-worker).

Starter · 5-minute install. pip install acp-langchain, wrap tools with @governed, bind the end user’s JWT per request. See the governance model for the shared concepts across every framework, or the frameworks index for other options.

Install

pip install acp-langchain

Minimal governed agent (langchain.agents.create_agent)

from fastapi import FastAPI, Header
from langchain.agents import create_agent
from langchain.tools import tool
from acp_langchain import governed, set_context

app = FastAPI()

# Stack @governed under @tool — the governance check runs inside the
# tool's dispatch.
@tool
@governed("web_search")
def web_search(query: str) -> str:
    """Search the web."""
    return my_search(query)   # your code, your credentials

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

@app.post("/run")
def run(prompt: str, authorization: str = Header(...)):
    set_context(user_token=authorization.removeprefix("Bearer ").strip())

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

create_agent is the 2026 idiom that replaces the legacy langgraph.prebuilt.create_react_agent. The decorator pattern is identical between the two — only the agent constructor changes.

What happens on every tool call

  1. @governed POSTs { tool_name, tool_input, session_id } + Authorization: Bearer <user-jwt> to ACP’s /govern/tool-use.
  2. ACP evaluates workspace policy, user scopes, rate limits, and PII.
  3. Deny → the wrapped function returns "tool_error: <reason>". LangChain treats this as the tool’s output; the model sees it and adapts.
  4. Allow → your tool runs.
  5. Post-audit: ACP scans the output for PII. If policy says redact, the redacted output replaces the original. Audit row written, rooted in the end user’s identity.

Works with plain LangChain too

@governed is framework-agnostic — it’s just a Python decorator. Works with any LangChain agent executor:

from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.tools import tool
from acp_langchain import governed

@tool
@governed("my_tool")
def my_tool(arg: str) -> str: ...

agent = create_tool_calling_agent(model, tools=[my_tool], prompt=prompt)
executor = AgentExecutor(agent=agent, tools=[my_tool])

Custom LangGraph StateGraph

Same pattern — the tool decoration is what matters, not the graph shape:

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: ...

# Tools go into ToolNode — ToolNode invokes them, @governed runs on every invoke.
tools_node = ToolNode([query_db])

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

Fail-open

If /govern/tool-use times out (5s default) or is unreachable, the tool proceeds. Matches Claude Code hook behavior. Governance is never a single point of failure for the agent.

Configure your ACP workspace

Before the graph can usefully call tools, your ACP workspace needs:

  1. An IdP configured — ACP verifies the end user’s JWT against your identity provider (Firebase, Auth0, any OIDC). Dashboard → Settings → Identity Provider.
  2. Tools listed — names in @governed("...") must match tools enabled in your workspace. Dashboard → Policies → Tools.
  3. Policy — set allow/deny, rate limits, PII mode per tool. Dashboard → Policies.

What shows up in the dashboard

Every governed call appears in cloud.agenticcontrolplane.com/activity with:

  • Actor — the end user’s sub
  • Tool name — whatever you passed to @governed(...)
  • Decision — allow / deny / redact, with reason
  • Session — groups all tool calls from one request
  • Findings — PII detected in input or output, if any

LangChain/LangGraph tool calls sit alongside Claude Code, Cursor, and CrewAI calls from the same user. One audit log across every agent surface.

Add or replace tools

from langchain_core.tools import tool
from acp_langchain import governed

@tool
@governed("my_tool")
def my_tool(arg: str) -> str:
    """Description the LLM sees."""
    return "..."

@tool (LangChain) and @governed (ACP) stack — @governed sits closer to the function so the governance check runs inside the tool’s dispatch. Tools outside this pattern are not governed.

Limitations

  • Only tools routed through @governed are covered. Plain functions used as tools bypass governance. Intentional (explicit opt-in) but worth flagging.
  • LLM calls go direct to your provider. ACP governs tools and actions, not tokens. For per-user LLM cost attribution, pair with Portkey or LiteLLM virtual keys.
  • Async tools are supported. @governed detects coroutine functions and dispatches accordingly.
  • Pre-release. acp-langchain@0.1.x is the initial release. Pin exact versions.

Troubleshooting

Graph runs but nothing appears in the dashboard. Confirm the end user’s JWT is sent as Authorization: Bearer ... and that ACP has the IdP configured for its issuer.

401 from /govern/tool-use. The JWT is invalid, expired, or from an untrusted IdP. Check Settings → Identity Provider.

Tools run but decisions always show allow with reason fail-open. The gateway request is erroring. Check network reachability. Raise timeout via configure(timeout_s=10) if needed.

Policy says deny, but the tool still runs. Verify @governed is actually on the function. Inspect decorator order — @governed must sit closer to the function than @tool.