Last reviewed 2026-06-10 - available
LangChain Integration
Add Qortara dispatch-path policy enforcement to LangChain and LangGraph tool calls, closing the native-tool-calling bypass.
Verified against: Awaiting app-dev integration validation
LangChain Integration
Qortara Governance evaluates an agent's actions against policy before those actions run. For LangChain and LangGraph agents, the integration is a sidecar: a small layer that hooks into tool dispatch, checks each call against your policy pack, and stops anything that policy denies before the tool executes.
This is the open source path. The integration ships as the `qortara-governance-langchain` package under the Apache-2.0 license, it is available today, and you can run it self-serve. You do not need a hosted account to use it.
> LangChain and LangGraph are trademarks of LangChain, Inc. Qortara is an independent project and is not affiliated with or endorsed by LangChain, Inc.
What the sidecar does
A LangChain agent reasons in the LLM and then calls tools. The tools are where real-world effects happen: reading customer records, sending email, writing to a database, calling another API. Governance belongs at that boundary, so the sidecar wraps tool dispatch rather than the agent's reasoning.
Concretely, the sidecar intercepts:
- `BaseTool.invoke` and `BaseTool.ainvoke`, which is the dispatch path for every standard LangChain tool (sync and async).
- LangGraph `ToolNode.invoke`, which is how a LangGraph state machine fans out tool calls inside a graph.
Because it hooks the framework's own dispatch methods, it governs any tool the agent can reach: custom tools, built-in tools, and third-party tools. You do not edit individual tools, and you do not change the agent's prompt or model. Governance is transparent to the agent's reasoning loop.
For each intercepted call the sidecar resolves the tool name and arguments, builds an evaluation context, asks the policy engine for a decision, and then either lets the call proceed or blocks it. A blocked call never reaches the underlying tool.
Install
The base package installs the sidecar for standard LangChain agents:
pip install qortara-governance-langchainIf you run LangGraph, install the extra so the sidecar can also hook `ToolNode`:
pip install "qortara-governance-langchain[langgraph]"The package targets Python 3.10 and newer. It depends on a recent `langchain-core`; the LangGraph hook additionally requires `langgraph` and is only activated when you install the extra.
Quick start
Install the sidecar, point it at a policy pack, and attach it to your agent. The example below shows a normal LangChain agent and the three lines that bring it under governance.
Before: a LangChain agent without governance
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
@tool
def read_customer_data(customer_id: str) -> dict:
"""Read customer data from the CRM."""
return crm_client.get_customer(customer_id)
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email to a customer."""
return email_client.send(to, subject, body)
tools = [read_customer_data, send_email]
llm = ChatOpenAI(model="gpt-4")
agent = create_openai_tools_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools)
result = executor.invoke(
{"input": "Look up customer 12345 and email them a receipt"}
)Any tool the model decides to call runs. There is nothing between the agent's decision and the side effect.
After: the same agent governed by Qortara
from langchain.agents import AgentExecutor, create_openai_tools_agent
from langchain_openai import ChatOpenAI
from langchain_core.tools import tool
from qortara_governance_langchain import QortaraGovernance, PolicyPack
@tool
def read_customer_data(customer_id: str) -> dict:
"""Read customer data from the CRM."""
return crm_client.get_customer(customer_id)
@tool
def send_email(to: str, subject: str, body: str) -> str:
"""Send an email to a customer."""
return email_client.send(to, subject, body)
tools = [read_customer_data, send_email]
llm = ChatOpenAI(model="gpt-4")
agent = create_openai_tools_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools)
# Load a policy pack and install the sidecar.
governance = QortaraGovernance(
agent_id="my-langchain-agent",
policy_pack=PolicyPack.from_file("policies/customer-agent.yaml"),
fail_closed=True,
)
governance.install(executor)
# Same invocation. Now every tool call is evaluated before it runs.
result = executor.invoke(
{"input": "Look up customer 12345 and email them a receipt"}
)The agent logic, prompt, and model are unchanged. The only additions are loading a policy pack and calling `governance.install(executor)`, which patches the tool dispatch path for the executor and its tools.
What happens at runtime
When the model calls `send_email`, the sidecar intercepts the `BaseTool.invoke` call, evaluates `send_email` with its arguments against the policy pack, and acts on the decision:
- On `allow`, the original tool runs and returns its result to the agent as normal.
- On `deny`, the sidecar raises a `QortaraPolicyDenied` error. The tool never runs. The error message carries the policy reason so the agent can surface it or recover.
- On `needs_review`, the sidecar blocks by default and treats the call the same as a deny, because no human is in the loop inside a synchronous agent run. You can change this behavior in configuration if you route review requests elsewhere.
If you prefer the agent to keep reasoning instead of raising, catch the error in your orchestration and feed the reason back as a tool result.
Policy pack wiring
The sidecar evaluates against a policy pack: a set of rules that decide which actions an agent may take against which resources. You can load a pack from a file or build it inline.
from qortara_governance_langchain import PolicyPack
# From a YAML or JSON file on disk.
pack = PolicyPack.from_file("policies/customer-agent.yaml")
# Or define rules inline.
pack = PolicyPack(
rules=[
{
"tool": "read_customer_data",
"effect": "allow",
"when": {"resource_prefix": "customers/"},
},
{
"tool": "send_email",
"effect": "needs_review",
"when": {"recipient_domain_not_in": ["yourcompany.com"]},
},
{
"tool": "*",
"effect": "deny",
},
]
)A few notes on how rules resolve:
- Rules are evaluated in order; the first matching rule wins. A trailing `"tool": "*"` rule lets you make the pack deny-by-default, which is the safer posture for an agent that touches sensitive data.
- The sidecar derives the action and resource from the tool name and its arguments. You can override that mapping per tool if your resource identifiers do not fall out of the arguments cleanly.
- Keep the pack in source control next to the agent. Treat a policy change like a code change: review it, version it, and ship it through the same pipeline.
LangGraph
For LangGraph agents, install the `[langgraph]` extra and install the sidecar against your compiled graph. The sidecar hooks `ToolNode.invoke`, so tool calls dispatched from inside the graph are governed the same way as tool calls from a standard executor.
from langgraph.prebuilt import ToolNode
from qortara_governance_langchain import QortaraGovernance, PolicyPack
tool_node = ToolNode(tools)
governance = QortaraGovernance(
agent_id="my-langgraph-agent",
policy_pack=PolicyPack.from_file("policies/customer-agent.yaml"),
fail_closed=True,
)
governance.install(tool_node)
# Build your graph with the governed tool node as usual.Decision semantics are identical to the standard agent: allowed calls run, denied calls raise before the tool executes.
Async agents
If your agent uses LangChain's async interface, the sidecar hooks `BaseTool.ainvoke` as well, so no separate setup is required. Install the sidecar the same way and run your agent with `ainvoke`:
governance = QortaraGovernance(
agent_id="my-langchain-agent",
policy_pack=PolicyPack.from_file("policies/customer-agent.yaml"),
fail_closed=True,
)
governance.install(executor)
result = await executor.ainvoke(
{"input": "Look up customer 12345 and email them a receipt"}
)Policy evaluation runs without blocking the event loop, so a high-concurrency async agent stays async end to end.
Gotchas
- **Latency.** Policy evaluation adds a small amount of time per tool call (single-digit to low double-digit milliseconds in typical local evaluation). That is usually negligible next to LLM round trips, which run in the hundreds to thousands of milliseconds. If an agent fires many tool calls per turn, the sidecar can cache decisions for identical (tool, arguments) pairs within a run; enable caching in configuration when call volume is high.
- **Fail-closed behavior.** `fail_closed=True` means that if the policy engine cannot reach a decision (for example, a misconfigured pack or an unavailable backing service in a hosted setup), the sidecar denies the call rather than letting it through. For agents that handle sensitive data, keep this on. If you set `fail_closed=False`, an evaluation failure allows the call, which trades safety for availability; choose deliberately.
- **Async and sync paths are separate hooks.** The sidecar hooks both `invoke` and `ainvoke`. If you mix sync and async tool calls in the same agent, both are covered, but make sure you call `install` once on the executor (or graph) rather than per tool, so you do not double-wrap a dispatch path.
- **Denials raise.** A denied call raises `QortaraPolicyDenied` by default. Decide where you want to catch it: at the executor boundary to fail the run, or inside an orchestration layer to convert the denial into a tool result the agent can reason about. Do not swallow it silently; a denial is a signal you usually want to record.
- **Policy pack is the source of truth.** An empty or overly permissive pack governs nothing. Start deny-by-default and open up specific tools and resources as you confirm the agent needs them.
What you get
Once the sidecar is installed:
- Every tool call is evaluated against policy, and unauthorized actions are blocked before they execute rather than cleaned up afterward.
- Each decision is recorded as a structured, tamper-evident entry you can keep alongside your application logs for audit and incident review.
- Decisions carry enough context (agent, tool, action, resource, reason) to correlate agent behavior across systems and to map actions to compliance frameworks such as SOC 2, GDPR, the EU AI Act, and NIST AI RMF.
- The same decision stream can feed your SIEM. See the [Splunk integration](/docs/integrations/splunk) for streaming governance events into Splunk HEC.