Aira
Integrations

LangChain

Gate LangChain tool calls through Aira's policy engine. Denied tools never run; chain and LLM steps are audit-only because LangChain has no pre-chain abort hook.

Kind: gate (for tools) + audit (for chains and LLM calls) · Pre-execution gate: yes, on tools · Peer dep: @langchain/core

What this integration actually does

LangChain fires callbacks before and after each tool, chain, and LLM step. The on_tool_start hook is a genuine pre-execution boundary — if it throws, LangChain surfaces the error as a tool error and the tool body never runs.

  • on_tool_startaira.authorize(). If the policy engine denies, we raise AiraToolDenied and LangChain aborts the tool call. This is a real gate.
  • on_tool_endaira.notarize(outcome="completed").
  • on_tool_erroraira.notarize(outcome="failed").
  • on_chain_end / on_llm_end → audit-only authorize + notarize back-to-back. LangChain does not expose a pre-chain hook that can abort across all chain types, so these produce post-hoc receipts rather than gates.

For a full gate over every LangChain operation (not just tool calls), put aira.authorize() at the top of your tool bodies and check the status yourself. The callback handler is the convenient path; the inline pattern is the absolute path.

Install

pip install aira-sdk[langchain]

This pulls in langchain-core as a peer dependency. If you already have LangChain installed you're covered.

Full example — gated tool calls

gated-langchain-agent.py
from aira import Aira
from aira.extras.langchain import AiraCallbackHandler
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from langgraph.prebuilt import create_react_agent

aira = Aira(api_key="aira_live_...")

@tool
def send_wire_transfer(amount: float, to: str) -> str:
    """Send a wire transfer. This is the side-effect we want to gate."""
    # Your real transfer code — Stripe, banking API, whatever
    return f"Wire sent: €{amount} to {to}"

# The callback handler is what actually runs authorize() before the tool body
handler = AiraCallbackHandler(
    client=aira,
    agent_id="payments-agent",
    model_id="gpt-4o",
)

agent = create_react_agent(
    ChatOpenAI(model="gpt-4o"),
    tools=[send_wire_transfer],
)

# Pass the handler via config — LangChain will call it around every tool
result = agent.invoke(
    {"messages": [("user", "Send €75,000 to vendor-x")]},
    config={"callbacks": [handler]},
)

What happens when a policy denies

  1. The LLM decides to call send_wire_transfer.
  2. LangChain fires on_tool_start with the serialized tool name and input.
  3. The handler calls aira.authorize(action_type="tool_call", details=...).
  4. The policy engine runs (rules → AI → consensus → content scan).
  5. If denied, the handler raises AiraToolDenied("send_wire_transfer", "POLICY_DENIED", "...").
  6. LangChain catches the exception, treats the tool call as errored, and typically surfaces it to the LLM so the agent can react.
  7. The real transfer function never runs. The signed PolicyEvaluation row is persisted regardless so the denial is auditable.

When to use this vs inline aira.authorize()

Use the callback handler when...Call aira.authorize() inline when...
You want every tool in your LangChain agent gated without touching tool codeYou want to gate a specific branch inside a tool body
You're using create_react_agent or similar prebuilt agent loopsYou need custom details strings per call
You're OK with chains and LLM calls being audit-onlyYou want a pre-execution gate on chains or LLM completions

The two patterns compose fine — you can use the handler for tool-level gating and drop inline authorize() calls inside specific tools for extra business-logic gates.

Known limits

  • Chain and LLM hooks are audit-only. LangChain has no reliable pre-execution hook for chains or LLM calls that can abort across every chain type. The _audit() helper in AiraCallbackHandler runs authorize + notarize back-to-back for these so you still get a receipt, but the call has already happened by then.
  • run_uuid must be unique per call. The handler keeps an in-memory run_uuid → action_uuid map so on_tool_end can notarize the correct action. LangChain guarantees this within a single invocation, but if you're doing something unusual with run ids, check the source.
  • Non-blocking notarize. If the notarize call fails (network flake, 5xx), the handler logs a warning and lets the agent continue. Your receipt is missing but the agent is not wedged.

Proof it works

The integration is pinned by a regression test that imports the real AiraCallbackHandler, constructs it against a mocked Aira client, and asserts on_tool_start raises on POLICY_DENIED + on_tool_end calls notarize with the right action id:

  • Python: tests/test_extras_langchain.py (196 lines, 18 tests)
  • The SDK's INTEGRATIONS registry (aira/extras/__init__.py) declares this integration as kind="gate" with pre_execution_gate=True. Tests pin the registry so if the code ever stops being a real gate, CI fails.

On this page