Guardrail Python SDK

Policy enforcement layer for programmatic tool execution. Works with AI agents, CI pipelines, automation frameworks, cron jobs, and interactive shells.

pip install ./sdks/python

The SDK ships with the licensed source and is not yet published to PyPI — install it from the shipped sdks/python directory. It talks to a running guardrail service (see the deployment guidedocker compose up -d is the fastest path).

5-Second Start

from ellm_guardrail import GuardrailClient

# Assumes guardrail is running: docker compose up -d
guard = GuardrailClient()

result = guard.check("Bash", "rm -rf /")
print(result.verdict)  # "deny"
print(result.reason)   # "rm with destructive flags"

if result.is_allowed:
    execute_tool()
elif result.is_denied:
    print(f"BLOCKED: {result.reason}")
else:
    escalate_to_human()

Integration

Any Python automation

from ellm_guardrail import GuardrailClient

guard = GuardrailClient()

def safe_execute(tool_name, target, params=None):
    """Execute a tool call, gated by the guardrail."""
    result = guard.check(tool_name, target, params)

    if result.is_denied:
        raise PermissionError(f"Guardrail blocked {tool_name}: {result.reason}")

    if result.is_escalated:
        # Requires human review
        if not ask_user(f"Escalated: {result.reason}. Proceed?"):
            raise PermissionError("User denied escalated action")

    return actual_execute(tool_name, target, params)

Claude Code via MCP

# Start the guardrail
docker compose up -d

# Register as an MCP server
claude mcp add guardrail -- python -m ellm_guardrail.mcp_server

# Now Claude Code can call guardrail_check before every tool execution

Inline proxy (framework-agnostic)

# Start the proxy (guardrail must be running)
python -m ellm_guardrail.proxy --port 9091 --upstream-url http://tool-executor:8080/execute

# Check a tool call (verdict-only)
curl -X POST http://localhost:9091/check \
  -H 'Content-Type: application/json' \
  -d '{"tool_name":"Bash","target":"rm -rf /","params":{}}'
# → 403 Deny

# Forward a tool call (check + execute)
curl -X POST http://localhost:9091/forward \
  -H 'Content-Type: application/json' \
  -d '{"tool_name":"Bash","target":"cargo test","params":{}}'
# → 200 + upstream response body + X-Guardrail-Verdict: allow headers

The proxy checks the guardrail first. On ALLOW it forwards to the upstream executor and passes through the response. On DENY it returns 403 without forwarding. On ESCALATE it returns the verdict by default (configurable via GUARDRAIL_AUTO_FORWARD_ESCALATED).

Use /check when your caller handles execution itself and just wants a verdict. Use /forward when you want the proxy to sit transparently between the caller and the executor — one HTTP call does the check and the forward.

Context manager

with GuardrailClient() as guard:
    for tool_call in agent_tool_calls:
        result = guard.check(tool_call.name, tool_call.target)
        if result.is_allowed:
            tool_call.execute()

Configuration

VariableDefaultDescription
GUARDRAIL_URLhttp://localhost:9090Guardrail service URL
GUARDRAIL_TIMEOUT2.0Per-request timeout in seconds (client, MCP server, proxy)
GUARDRAIL_FAIL_MODEescalateFallback verdict when the guardrail is unreachable: escalate (ask a human) or deny (fail-closed). MCP server and proxy only
GUARDRAIL_PROXY_MAX_BODY1048576Max request body the proxy accepts (→ 413 beyond)
GUARDRAIL_UPSTREAM_URLUpstream tool-executor URL for /forward (no default — must be set)
GUARDRAIL_FORWARD_TIMEOUT30.0Timeout for upstream forwarding in seconds
GUARDRAIL_AUTO_FORWARD_ESCALATEDfalseForward ESCALATE verdicts to upstream (default: safe, return verdict only)
guard = GuardrailClient(
    base_url="http://guardrail.internal:9090",
    timeout=5.0,  # seconds
    retries=2,    # transport-error retries (default 1)
)

Failure modes

The guardrail is a security boundary, so its behavior when it fails is explicit and configurable.

Client (GuardrailClient)

  • timeout defaults to GUARDRAIL_TIMEOUT (2.0s); invalid values fall back to 2.0.
  • Automatic retries with exponential backoff (0.2s, 0.4s, …) apply only to transport errors (ConnectionError, Timeout). HTTP responses — including 429 and 5xx — are never retried.
  • After retries are exhausted the exception propagates; wrap check() and apply your own policy, or use the MCP server / proxy, which encode one.

MCP server (ellm_guardrail.mcp_server)

  • Unreachable guardrail → tool result with guardrail_reachable: false, an error string, and isError: true (an operational failure is not a verdict).
  • The fallback verdict comes from GUARDRAIL_FAIL_MODE: escalate (default — a human is asked) or deny (fail-closed).

Proxy (ellm_guardrail.proxy)

  • Unreachable guardrail → GUARDRAIL_FAIL_MODE verdict with guardrail_reachable: false in the body. On /forward the proxy never forwards without a guardrail verdict.
  • Unreachable upstream (on /forward) → 502 Bad Gateway with error details and guardrail verdict info.
  • Upstream timeout (on /forward) → 502 with error: "timeout".
  • Body beyond GUARDRAIL_PROXY_MAX_BODY → 413; missing/invalid Content-Length → 400.
  • No upstream configured + /forward → 502 with guidance to set GUARDRAIL_UPSTREAM_URL.

Server

  • Overloaded → 429 (rate limit) or 503 (connection cap); the JSON body carries retry_after_ms. GET /health is never rate limited.
  • An internal error returns 500 with a fail-safe escalate verdict and never wedges the pipeline.

Requirements

  • Python 3.10+
  • Running guardrail service (docker compose up -d)
  • mcp extra for MCP server: pip install ./sdks/python[mcp]