*Published: April 2026 | Tags: LangChain agent security, LangChain governance, AI agent audit trail, LangChain tool permissions*
LangChain is the most popular framework for building AI agents. But LangChain has no built-in governance. Any agent can call any tool, spend any amount, and access any credential. There is no session scoping, no budget enforcement, and no audit trail. When your LangChain agent goes to production, this becomes a liability.
Haldir adds the missing layer. Wrap your LangChain tool calls with Haldir sessions, permission checks, and immutable audit logging. Every tool invocation is authorized, budgeted, and recorded. Here is how.
A typical LangChain agent with tools looks like this:
from langchain.agents import initialize_agent, Tool
tools = [
Tool(name="search", func=search_api, description="Search the web"),
Tool(name="send_email", func=send_email, description="Send an email"),
Tool(name="charge_card", func=charge_card, description="Charge a credit card"),
]
agent = initialize_agent(tools, llm, agent="zero-shot-react-description")
agent.run("Refund the customer $500 and email them confirmation")
Nothing stops the agent from calling charge_card with the wrong amount. Nothing logs what happened. Nothing enforces a budget. If the LLM hallucinates a $5,000 charge, it goes through.
Create a wrapper that routes every tool call through Haldir. Each call is checked against session scopes, logged to the audit trail, and tracked for spend.
from haldir import HaldirClient, HaldirPermissionError
from langchain.agents import initialize_agent, Tool
h = HaldirClient(api_key="hld_xxx")
# Create a scoped session for this agent run
session = h.create_session(
agent_id="customer-service-bot",
scopes=["read", "send_email", "spend:100"],
spend_limit=100.00,
ttl=1800
)
sid = session["session_id"]
def governed_tool(name, func, scope, cost_usd=0.0):
"""Wrap a LangChain tool function with Haldir governance."""
def wrapper(*args, **kwargs):
# Check permission before executing
check = h.check_permission(sid, scope)
if not check["allowed"]:
return f"BLOCKED: Agent does not have '{scope}' permission."
# If this costs money, authorize the spend first
if cost_usd > 0:
h.authorize_payment(sid, amount=cost_usd, description=f"{name}: {args}")
# Execute the actual tool
result = func(*args, **kwargs)
# Log the action to the immutable audit trail
h.log_action(sid, tool=name, action="execute", cost_usd=cost_usd,
details={"args": str(args), "result_preview": str(result)[:200]})
return result
return wrapper
# Wrap each tool with governance
tools = [
Tool(
name="search",
func=governed_tool("search", search_api, scope="read"),
description="Search the web"
),
Tool(
name="send_email",
func=governed_tool("send_email", send_email, scope="send_email"),
description="Send an email to a customer"
),
Tool(
name="charge_card",
func=governed_tool("charge_card", charge_card, scope="spend", cost_usd=50.0),
description="Charge a credit card"
),
]
agent = initialize_agent(tools, llm, agent="zero-shot-react-description")
Now every tool call goes through Haldir. The agent can search freely (it has the read scope), send emails (it has send_email), and charge up to $100 total (its spend limit). If the LLM tries to call a tool outside its scopes, the call is blocked before it executes.
When a tool is blocked, the agent receives the denial as a tool response. LangChain agents handle this naturally by trying a different approach or reporting the limitation to the user:
def governed_tool(name, func, scope, cost_usd=0.0):
def wrapper(*args, **kwargs):
try:
check = h.check_permission(sid, scope)
if not check["allowed"]:
return f"Permission denied: '{scope}' not in session scopes."
if cost_usd > 0:
h.authorize_payment(sid, amount=cost_usd, description=name)
result = func(*args, **kwargs)
h.log_action(sid, tool=name, action="execute", cost_usd=cost_usd)
return result
except HaldirPermissionError as e:
h.log_action(sid, tool=name, action="blocked", details={"reason": str(e)})
return f"Budget exceeded or permission denied: {e}"
return wrapper
The agent receives "Budget exceeded" as a tool output and can inform the user instead of failing silently.
After the agent run completes, pull the full audit trail:
# Get everything this session did
trail = h.get_audit_trail(session_id=sid)
for entry in trail["entries"]:
print(f"{entry['timestamp']} | {entry['tool']} | {entry['action']} | ${entry['cost_usd']}")
# Check total spend
spend = h.get_spend(session_id=sid)
print(f"Total spend: ${spend['total_usd']:.2f}")
# Revoke the session when the agent is done
h.revoke_session(sid)
Output:
2026-04-05T14:30:01Z | search | execute | $0.00 2026-04-05T14:30:03Z | send_email | execute | $0.00 2026-04-05T14:30:05Z | charge_card | execute | $50.00 Total spend: $50.00
Every action is recorded with the session ID, agent ID, timestamp, tool name, and cost. This is the compliance record that enterprise teams require before deploying agents.
Store your API keys in Haldir Vault instead of environment variables. The agent only accesses credentials through scoped sessions:
# Store secrets once (admin setup)
h.store_secret("stripe_key", "sk_live_xxx", scope_required="spend")
h.store_secret("sendgrid_key", "SG.xxx", scope_required="send_email")
# Agent retrieves secrets through its session — scope is enforced
stripe_key = h.get_secret("stripe_key", session_id=sid)
If the session does not have the spend scope, the secret retrieval is denied. No more plaintext keys in .env files.
pip install haldir langchain
Create a session, wrap your tools, and run your agent. Five lines of code between "no governance" and "full audit trail with budget enforcement."
Docs: haldir.xyz/docs | Source: GitHub
---
*Haldir is the governance layer for AI agents. Session scoping, spend limits, encrypted secrets, and immutable audit trails. Works with LangChain, CrewAI, AutoGen, or any framework. Start free at haldir.xyz.*
© 2026 Haldir · haldir.xyz