LangChain + LangGraph + ACP — Governance Install Guide
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
@governedPOSTs{ tool_name, tool_input, session_id }+Authorization: Bearer <user-jwt>to ACP’s/govern/tool-use.- ACP evaluates workspace policy, user scopes, rate limits, and PII.
- Deny → the wrapped function returns
"tool_error: <reason>". LangChain treats this as the tool’s output; the model sees it and adapts. - Allow → your tool runs.
- 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:
- An IdP configured — ACP verifies the end user’s JWT against your identity provider (Firebase, Auth0, any OIDC). Dashboard → Settings → Identity Provider.
- Tools listed — names in
@governed("...")must match tools enabled in your workspace. Dashboard → Policies → Tools. - 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
@governedare 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.
@governeddetects coroutine functions and dispatches accordingly. - Pre-release.
acp-langchain@0.1.xis 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.