Integration Guide
Integration with Agent Frameworks
Overview
The guardrail sits between your agent framework and the tool execution environment. Every tool call goes through the guardrail before execution. The guardrail returns a verdict (Allow, Deny, or Escalate), and the agent framework honors the verdict.
Agent Framework ──tool call──► Guardrail ──verdict──► Agent Framework
│
Allow ──────► execute tool
Deny ──────► block, report reason
Escalate ──► escalate to human / LLM
Integration with Claude Code / Anthropic Tool Use
To integrate with Claude Code or any Anthropic SDK tool-use application, wrap tool calls with the guardrail:
# Example: Python integration with Anthropic SDK
import anthropic
import requests
client = anthropic.Anthropic()
GUARDRAIL_URL = "http://localhost:9090/check"
def check_with_guardrail(tool_name, target, params=None):
"""Send a tool call to the guardrail and return verdict."""
response = requests.post(GUARDRAIL_URL, json={
"tool_name": tool_name,
"target": target,
"params": params or {}
})
response.raise_for_status()
return response.json()
# In your tool use loop:
def handle_tool_call(tool_name, tool_input):
target = tool_input.get("command", "") if tool_name == "Bash" else str(tool_input)
result = check_with_guardrail(tool_name, target, tool_input)
if result["verdict"] == "allow":
# Execute the tool call normally
return execute_tool(tool_name, tool_input)
elif result["verdict"] == "deny":
return f"Guardrail blocked this operation: {result['reason']}"
else: # escalate
return f"Guardrail escalated for review: {result['reason']}"
Integration with OpenAI Function Calling
# Example: Integration with OpenAI function calling
from openai import OpenAI
import requests
client = OpenAI()
GUARDRAIL_URL = "http://localhost:9090/check"
def guardrail_check(tool_name, target, params=None):
resp = requests.post(GUARDRAIL_URL, json={
"tool_name": tool_name,
"target": target,
"params": params or {}
})
return resp.json()
# Wrapper around tool execution:
def guarded_function_call(name, arguments):
# Map OpenAI tool name to guardrail tool name
guardrail_name = name
# Determine target based on tool
if "command" in arguments:
target = arguments["command"]
elif "path" in arguments:
target = arguments["path"]
elif "url" in arguments:
target = arguments["url"]
else:
target = json.dumps(arguments)
result = guardrail_check(guardrail_name, target, arguments)
if result["verdict"] == "allow":
return execute_function(name, arguments)
elif result["verdict"] == "deny":
return json.dumps({"error": "blocked", "reason": result["reason"]})
else:
return json.dumps({"error": "escalated", "reason": result["reason"]})
Integration with LangChain
# Example: Custom LangChain callback that checks with guardrail
from langchain.tools import BaseTool
from langchain.callbacks.base import BaseCallbackHandler
import requests
GUARDRAIL_URL = "http://localhost:9090/check"
class GuardrailCallback(BaseCallbackHandler):
"""LangChain callback handler that checks tool calls against guardrail."""
def on_tool_start(self, tool_name, tool_input, **kwargs):
# Determine target
if isinstance(tool_input, dict):
target = tool_input.get("command") or tool_input.get("path") or str(tool_input)
params = tool_input
else:
target = str(tool_input)
params = {"input": tool_input}
response = requests.post(GUARDRAIL_URL, json={
"tool_name": tool_name,
"target": target,
"params": params
})
result = response.json()
if result["verdict"] == "deny":
raise ValueError(f"Guardrail denied: {result['reason']}")
elif result["verdict"] == "escalate":
# Log escalation — in production, escalate to human
print(f"Guardrail escalated: {result['reason']}")
return result["verdict"] == "allow"
# Usage:
# agent = create_react_agent(llm, tools, callbacks=[GuardrailCallback()])
Integration with MCP Servers
For MCP (Model Context Protocol) servers, the guardrail checks each tool call before it reaches the MCP server. MCP tool name patterns (mcp__filesystem__read_file, mcp__slack__post_message, etc.) are mapped by the guardrail. There is also a ready-made Python MCP server you can register with claude mcp add.
# Example: MCP server with guardrail middleware
from mcp.server.fastmcp import FastMCP
import requests
GUARDRAIL_URL = "http://localhost:9090/check"
mcp = FastMCP("MyServer")
# Guardrail middleware for MCP tools
def guardrail_wrapper(func):
async def wrapper(name, arguments):
result = requests.post(GUARDRAIL_URL, json={
"tool_name": f"mcp__{name}",
"target": arguments.get("path") or arguments.get("url") or str(arguments),
"params": arguments
}).json()
if result["verdict"] == "deny":
raise PermissionError(f"Guardrail blocked: {result['reason']}")
elif result["verdict"] == "escalate":
print(f"Guardrail escalated: {result['reason']}")
return await func(name, arguments)
return wrapper
# Decorate tools with guardrail
@mcp.tool()
@guardrail_wrapper
async def read_file(name, arguments):
# Original tool implementation
pass
Embedded Library Usage (Rust)
The guardrail ships as a Rust library you can embed directly — no HTTP round trip. Basic usage:
use ellm_guardrail::pipeline::GuardrailPipeline;
let mut pipeline = GuardrailPipeline::deterministic_only();
// Check a Read tool call
let result = pipeline.check("Read", "src/main.rs", &[]);
assert_eq!(result.verdict, Verdict::Allow);
// Check a Bash tool call
let result = pipeline.check("Bash", "rm -rf /", &[]);
assert_eq!(result.verdict, Verdict::Deny);
// Check with parameters — param-trap triggered
let result = pipeline.check("Read", "id_rsa", &[
("cmd".into(), "rm -rf /".into()),
]);
assert_eq!(result.verdict, Verdict::Deny);
With LLM judge: construct the pipeline from environment variables (GUARDRAIL_LLM_URL, GUARDRAIL_LLM_MODEL); escalated calls are then sent to the LLM and the outcome is returned on the result.
With session tracking: enable session tracking to catch multi-turn recon → exploit chains. The first recon_threshold consecutive safe calls build the window; a destructive call after that is denied by chain detection.
With privacy redaction: enable the privacy translator and calls escalated to the LLM will have PII redacted before the LLM call.
HTTP API Reference
Endpoints
POST /check
Check a tool call against the guardrail policy.
Request:
{
"tool_name": "Bash",
"target": "rm -rf /",
"params": {
"timeout": "60000",
"description": "cleanup temp files"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
tool_name | string | Yes | Agent framework tool name (Read, Bash, Edit, Write, Glob, Grep, etc.) |
target | string | Yes | Primary target: file path for Read/Write, shell command for Bash, URL for WebFetch |
params | object (string→string) | No | Key-value parameters from the tool call |
Response (200 OK):
{
"verdict": "deny",
"tier": "deterministic",
"action": "rm_rf",
"confidence": "high",
"strategy": "exact_command",
"reason": "rm with destructive flags",
"ellm_trace": ["rm_rf --rel1--> destructive_op", "destructive_op --rel225--> rm_rf"],
"llm_outcome": null,
"latencies": {
"mapper_us": 6,
"ellm_us": 9,
"llm_us": 0,
"total_us": 15
},
"service_wall_us": 45
}
| Field | Type | Description |
|---|---|---|
verdict | string | "allow", "deny", or "escalate" |
tier | string | "deterministic", "semantic", or "fallback_escalate" |
action | string | The guardrail action the call was classified as |
confidence | string | "high", "medium", "low", or "unknown" |
strategy | string | How the mapping was produced (see below) |
reason | string | Human-readable explanation |
ellm_trace | array of strings | Audit trace of which rules fired |
llm_outcome | object or null | LLM judge outcome (null if LLM was not called) |
latencies | object | Per-tier latency breakdown in microseconds |
service_wall_us | number | Total wall-clock time including HTTP overhead |
Error Response (400 Bad Request):
{
"error": "invalid JSON: missing field `tool_name`"
}
Strategies:
| Strategy | Description |
|---|---|
direct_tool_name | Tool name matched a direct mapping |
exact_command | Bash command matched an exact rule |
command_prefix | Bash command matched a prefix rule |
command_contains | Bash command matched a substring rule |
param_key_danger | Capability-granting param key detected |
target_heuristic | Target extension/path heuristic |
fallback | No rule matched; raw tool name passed through |
GET /health
Liveness check endpoint.
Response (200 OK):
{
"status": "ok"
}
GET /metrics
Counter snapshot for monitoring.
Response (200 OK):
{
"total_checks": 12345,
"allow_count": 11000,
"deny_count": 1000,
"escalate_count": 345,
"avg_latency_us": 19
}
GET /
Service information.
Response (200 OK):
{
"service": "ellm-guardrail",
"version": "0.1.0",
"endpoints": ["POST /check", "GET /health", "GET /metrics"]
}
Additional Endpoints
Beyond the core four, the server exposes an authenticated management surface (requires a valid API key):
| Endpoint | Method | Purpose |
|---|---|---|
/check/explain | POST | Check plus a rule-by-rule trace of how the verdict was reached |
/metrics/prometheus | GET | Prometheus text exposition of the metrics snapshot |
/api/events | GET | Audit event stream |
/api/keys | GET | List configured API keys |
/api/approvals | GET / POST | List / act on escalation approvals |
/api/approvals/revoke | POST | Revoke an approval |
/api/pending | GET | Pending escalations awaiting review |
/api/pending/resolve | POST | Resolve a pending escalation with a verdict |
/api/system | GET | System status and configuration summary |
/api/sessions | GET | Session-tracking state |
/audit/export | GET | Export the audit log |
/audit/verify | GET | Verify audit-log integrity |
/dashboard | GET | Human-readable status page |
/admin/sync-keys | POST | Sync API keys with the console |
The core four (/check, /health, /metrics, /) cover day-to-day integration; the rest support human review workflows, audit, and administration.
Error Codes
| HTTP Status | Error | Meaning | Recovery |
|---|---|---|---|
| 400 | invalid JSON: ... | Request body is not valid JSON or missing required fields | Fix request format |
| 404 | not found | Path does not match any endpoint | Check request URL |
| 405 | method not allowed | HTTP method not supported for this path | Use the correct method |
| 413 | (reverse proxy) | Request body too large | Reduce parameter size |
| 500 | (server crash) | Unhandled panic or fatal error | Check server logs, restart |
| 503 | (no response) | Server overloaded or down | Check health, retry with backoff |
Verdict-Related “Errors” (Not HTTP Errors)
These are returned as part of a successful POST /check response:
| Verdict | Meaning | Typical Action |
|---|---|---|
allow | Call is safe — execute it | Proceed with execution |
deny | Call is dangerous — do NOT execute | Return error to caller, log the block |
escalate | Guardrail cannot determine safety | Escalate to human or LLM judge |
Rate Limiting Behavior
The guardrail server implements built-in per-IP rate limiting (token bucket). In high load scenarios:
| Scenario | Behavior | Mitigation |
|---|---|---|
| Per-IP flood | HTTP 429 once the bucket empties; body: {"error": "rate limit exceeded", "retry_after_ms": 1000} | Tune GUARDRAIL_RATE_LIMIT (default 100 req/s); honor retry_after_ms |
| Liveness probes | GET /health is exempt from rate limiting | Safe to poll from orchestrators |
| Too many connections | HTTP 503 beyond GUARDRAIL_MAX_CONNS (default 128) | Raise the cap or deploy multiple instances behind a LB |
| Oversized requests | HTTP 413 beyond GUARDRAIL_MAX_BODY (default 64 KiB); HTTP 431 beyond the 16 KiB header cap | Keep tool-call payloads small |
| Slow clients | Connections idle past the 10 s request deadline are dropped | Send complete requests promptly |
| Concurrent requests | Checks are serialized by a mutex | Deploy multiple instances behind LB |
| LLM judge burst | Each escalate call blocks the pipeline for 1–5 s | Use deterministic-only mode for high throughput |
Recommended Client Behavior
- Set a client timeout. The Python SDK reads
GUARDRAIL_TIMEOUT(default 2.0s); raise it for LLM-tier deployments where escalations take seconds - Retry only transport failures. The Python SDK retries
ConnectionError/Timeoutonce with backoff; it never retries HTTP responses. On a 429, waitretry_after_msbefore retrying yourself - Decide your fail mode up front (see the Python SDK docs): the MCP server and proxy return
guardrail_reachable: falseand a deny/escalate verdict perGUARDRAIL_FAIL_MODEwhen the server is unreachable - Keep-alive is not supported (HTTP/1.1
connection: close) — the SDK opens a connection per request - If integrating at high throughput, embed the library directly rather than using the HTTP API