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"
  }
}
FieldTypeRequiredDescription
tool_namestringYesAgent framework tool name (Read, Bash, Edit, Write, Glob, Grep, etc.)
targetstringYesPrimary target: file path for Read/Write, shell command for Bash, URL for WebFetch
paramsobject (string→string)NoKey-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
}
FieldTypeDescription
verdictstring"allow", "deny", or "escalate"
tierstring"deterministic", "semantic", or "fallback_escalate"
actionstringThe guardrail action the call was classified as
confidencestring"high", "medium", "low", or "unknown"
strategystringHow the mapping was produced (see below)
reasonstringHuman-readable explanation
ellm_tracearray of stringsAudit trace of which rules fired
llm_outcomeobject or nullLLM judge outcome (null if LLM was not called)
latenciesobjectPer-tier latency breakdown in microseconds
service_wall_usnumberTotal wall-clock time including HTTP overhead

Error Response (400 Bad Request):

{
  "error": "invalid JSON: missing field `tool_name`"
}

Strategies:

StrategyDescription
direct_tool_nameTool name matched a direct mapping
exact_commandBash command matched an exact rule
command_prefixBash command matched a prefix rule
command_containsBash command matched a substring rule
param_key_dangerCapability-granting param key detected
target_heuristicTarget extension/path heuristic
fallbackNo 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):

EndpointMethodPurpose
/check/explainPOSTCheck plus a rule-by-rule trace of how the verdict was reached
/metrics/prometheusGETPrometheus text exposition of the metrics snapshot
/api/eventsGETAudit event stream
/api/keysGETList configured API keys
/api/approvalsGET / POSTList / act on escalation approvals
/api/approvals/revokePOSTRevoke an approval
/api/pendingGETPending escalations awaiting review
/api/pending/resolvePOSTResolve a pending escalation with a verdict
/api/systemGETSystem status and configuration summary
/api/sessionsGETSession-tracking state
/audit/exportGETExport the audit log
/audit/verifyGETVerify audit-log integrity
/dashboardGETHuman-readable status page
/admin/sync-keysPOSTSync 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 StatusErrorMeaningRecovery
400invalid JSON: ...Request body is not valid JSON or missing required fieldsFix request format
404not foundPath does not match any endpointCheck request URL
405method not allowedHTTP method not supported for this pathUse the correct method
413(reverse proxy)Request body too largeReduce parameter size
500(server crash)Unhandled panic or fatal errorCheck server logs, restart
503(no response)Server overloaded or downCheck health, retry with backoff

These are returned as part of a successful POST /check response:

VerdictMeaningTypical Action
allowCall is safe — execute itProceed with execution
denyCall is dangerous — do NOT executeReturn error to caller, log the block
escalateGuardrail cannot determine safetyEscalate to human or LLM judge

Rate Limiting Behavior

The guardrail server implements built-in per-IP rate limiting (token bucket). In high load scenarios:

ScenarioBehaviorMitigation
Per-IP floodHTTP 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 probesGET /health is exempt from rate limitingSafe to poll from orchestrators
Too many connectionsHTTP 503 beyond GUARDRAIL_MAX_CONNS (default 128)Raise the cap or deploy multiple instances behind a LB
Oversized requestsHTTP 413 beyond GUARDRAIL_MAX_BODY (default 64 KiB); HTTP 431 beyond the 16 KiB header capKeep tool-call payloads small
Slow clientsConnections idle past the 10 s request deadline are droppedSend complete requests promptly
Concurrent requestsChecks are serialized by a mutexDeploy multiple instances behind LB
LLM judge burstEach escalate call blocks the pipeline for 1–5 sUse deterministic-only mode for high throughput
  • 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/Timeout once with backoff; it never retries HTTP responses. On a 429, wait retry_after_ms before retrying yourself
  • Decide your fail mode up front (see the Python SDK docs): the MCP server and proxy return guardrail_reachable: false and a deny/escalate verdict per GUARDRAIL_FAIL_MODE when 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