LangChain tool calls need a checkpoint. Gate is the checkpoint.
LangChain agents decide what to do. Gate decides whether they're allowed to do it. Every tool call goes through Gate first — evaluated against your YAML policy before the tool runs. No LLM judgment in the policy path. No exceptions.
How to add governance to LangChain tools
Wrap each tool with GateTool from @zehrava/langchain-gate. Gate intercepts the tool call before execution, submits it as a governed intent, evaluates it against a YAML policy, and returns one of three decisions:
- Approved — tool runs immediately, no latency added on the happy path
- Blocked —
GateBlockedErroris thrown with the policy reason - Pending — Gate polls for human approval; tool runs when approved
Policy evaluation is deterministic. The same input always produces the same output. Gate reads field values, checks rate limits, and applies environment-specific thresholds from a YAML file you control. It does not call an LLM to decide. Install: npm install @zehrava/langchain-gate zehrava-gate. Self-hosted. No data leaves your infrastructure.
Install and wrap a tool in 12 lines
npm install @zehrava/langchain-gate zehrava-gate
const { Gate } = require('zehrava-gate');
const { GateTool, GateBlockedError } = require('@zehrava/langchain-gate');
const gate = new Gate({ endpoint: 'http://localhost:3001', apiKey: process.env.GATE_API_KEY });
const governed = new GateTool({
tool: emailTool, // your existing LangChain Tool
gate,
policy: 'outbound-email',
destination: 'sendgrid.send',
});
// Drop-in replacement — same interface as the original tool
try {
const result = await governed._call(JSON.stringify({ to: 'user@co.com', subject: '...' }));
} catch (err) {
if (err instanceof GateBlockedError) {
console.error('Blocked:', err.intentId, err.blockReason);
}
}
The wrapped tool implements the same _call(input) interface. Pass governed to your agent exactly where you'd pass emailTool. No other changes.
What Gate evaluates
Gate evaluates YAML policies you define. Evaluation runs in this order:
- Destination allowlist — is this tool allowed to call this endpoint?
- Field checks —
required,max,min,max_length,patternon JSON fields - Environment overrides — different thresholds for production vs staging vs development
- Rate limits —
per_agent_per_hourandper_agent_per_dayvia SQLite counts - Sensitive term scan — normalized matching catches obfuscated variants
- Record count thresholds — auto-approve under N, require approval over N
# policies/outbound-email.yaml
id: outbound-email
destinations: [sendgrid.send, loops.send]
field_checks:
- path: "to"
required: true
pattern: "^[^@]+@[^@]+\\.[^@]+$"
rate_limits:
per_agent_per_hour: 50
per_agent_per_day: 200
environments:
production:
require_approval: always
development:
require_approval: never
Three decision paths
| Gate decision | What happens | Your code sees |
|---|---|---|
| approved | Tool executes immediately | Tool return value |
| blocked | Tool does not execute | GateBlockedError |
| pending | Gate polls for human approval; executes on approval | Tool return value (after delay) or GateTimeoutError |
Wrap multiple tools at once
const { GateToolkit } = require('@zehrava/langchain-gate');
const toolkit = new GateToolkit({
tools: [emailTool, crmTool, slackTool],
gate,
policies: {
'send-email': 'outbound-email',
'crm-update': 'crm-low-risk',
'__default__': 'crm-low-risk', // fallback for unmapped tools
},
destinations: {
'send-email': 'sendgrid.send',
'crm-update': 'salesforce.api',
'__default__': 'generic.http',
},
});
// Use governedTools exactly where you'd pass the originals
const governedTools = toolkit.getTools();
LangGraph: branch on Gate's decision
Gate provides two StateGraph primitives. Use gateNode to submit an intent as a node. Use gateRouteAfter to branch on the result.
const { gateNode, gateRouteAfter } = require('@zehrava/langchain-gate');
graph.addNode('check_gate', gateNode({
gate,
buildIntent: (state) => ({
payload: JSON.stringify(state.crmPayload),
destination: 'salesforce.api',
policy: 'crm-low-risk',
recordCount: state.records.length,
}),
}));
// gateNode writes: gateIntentId, gateStatus, gateBlockReason into state
graph.addConditionalEdges('check_gate', gateRouteAfter('gateStatus'), {
execute: 'run_tool', // approved
blocked: 'handle_blocked', // blocked | duplicate_blocked
pending: 'notify_human', // pending_approval
__end__: END,
});
What Gate does not do
Gate is a deterministic policy engine, not an LLM-based safety layer.
- Does not understand natural language in payloads — write explicit field checks for what matters
- Does not catch every bad action — it catches what your YAML policies describe
- Does not protect against a model that edits the policy files it's governed by
- Adds < 5ms latency on auto-approved intents (local SQLite, no network hop)
Gate protects against mistakes and unauthorized actions. It's a commit checkpoint, not a jailbreak defense.
Add Gate to your LangChain project
Self-hosted. MIT license. Running in 5 minutes.
View on GitHub Read the docs