Adding commercial guardrails to LangChain agents
You add commercial guardrails to LangChain agents by wrapping the tools that act on the outside world in a tool-call middleware: before the handler executes send_email or issue_refund, the wrapper posts the intended content to EvalLayer's /authorize endpoint and switches on the decision, executing, substituting the compliant rewrite, queueing for a human, or refusing with a reason the model can read. One wrapper, declared once on create_agent, covers every consequential tool.
Why LangChain agents create commercial risk
LangChain's 1.0 reset consolidated agent-building into create_agent with LangGraph as the runtime underneath, and the ecosystem's enormous tool surface is the draw: agents wired to email, CRMs, payment APIs, and support desks in an afternoon. Every one of those tools converts model text into a business action. The model does not need to be unsafe to be expensive; it needs to be helpful to a customer asking for 25 percent off, or confident about a ship date, in a channel where its word binds the company that deployed it.
The 1.0 middleware system is the right substrate for a fix. It gives you named hooks at each stage of the agent loop, including a wrapper around every tool invocation, so interception is now a supported pattern rather than a monkey-patch. What middleware does not include is the judgment: nothing in the framework knows your discount cap, your signing authority, or which claims legal approved. That is the piece you attach.
The interception point: a tool-call wrapper
LangChain middleware exposes a hook that wraps tool execution: it receives the pending tool call and a handler that will run it, and it decides what actually happens. That is the exact seam for pre-action authorization. The shape below follows the documented wrapper pattern; treat it as a pattern to adapt to your middleware setup rather than copy-paste, since request object fields vary by version.
import httpx
from langchain.agents.middleware import wrap_tool_call
GUARDED = {"send_email", "send_quote", "issue_refund"}
POLICY = {
"max_discount_pct": 10,
"commitment_authority_usd": 1000,
"forbidden_commitments": ["delivery_date", "refund_promise"],
"approved_claims": ["soc2_certified"],
"required_disclaimers": ["pricing_subject_to_change"],
"blocked_topics": [],
}
@wrap_tool_call
def commercial_guardrail(request, handler):
name = request.tool_call["name"]
if name not in GUARDED:
return handler(request)
draft = request.tool_call["args"].get("body", "")
r = httpx.post(
"https://api.evallayer.ai/authorize",
headers={"Authorization": "Bearer " + EVALLAYER_KEY},
json={"action_type": name, "content": draft,
"context": {"tool": name}, "policy": POLICY},
timeout=10,
)
data = r.json()
if data["decision"] == "allow":
return handler(request)
if data["decision"] == "rewrite":
request.tool_call["args"]["body"] = data["approved_rewrite"]
return handler(request)
if data["decision"] == "require_approval":
queue_for_human(draft, data["authorization_id"])
return tool_message(request,
"Held for human approval (ref " + data["authorization_id"] + ").")
return tool_message(request,
"Blocked by commercial policy: " + data["violations"][0]["detail"])
agent = create_agent(model, tools=tools, middleware=[commercial_guardrail])
Returning a message instead of calling the handler is the important move on the two failing branches: the send never happens, but the agent learns why in-band and can continue the conversation gracefully, telling the customer the offer is being confirmed rather than going silent.
Decision handling
- allow: call the handler; the tool runs with the original arguments.
- rewrite: swap the outbound text for
approved_rewrite, then call the handler. The customer receives the compliant version; the loop never stalls. - require_approval: skip the handler, queue the draft with its
authorization_id, and return a tool message saying so. A human approves or rejects out-of-band. - block: skip the handler and return the violation reason. Log the
authorization_id; the model gets enough context to take a different path.
Latency: wrap sends, not thinking
The GUARDED set is the control. Retrieval, search, and calculator tools should not pass through authorization; only tools with external side effects should. On those, deterministic-only policies cost milliseconds and semantic checks one to two seconds, which is a rounding error next to the model calls already in the loop. Set a client timeout and decide failure modes per action type: fail closed on refunds, fail open with logging on low-stakes replies if availability matters more.
What to log
Record the authorization_id in the tool message and in your tracing (it slots naturally into LangSmith metadata). Every consequential send in a trace then carries its policy verdict, which turns incident review from transcript forensics into a lookup: draft, decision, violations, and what actually went out.
Try it on one tool
Wrap your single riskiest tool first and point it at /demo/authorize, which allows three free checks per day without a key, or preview the decision card in the interactive demo. The full decision semantics live in the authorization guide, and policy design covers picking caps and forbidden commitments that mirror real authority.