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 guide — docker 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
| Variable | Default | Description |
|---|---|---|
GUARDRAIL_URL | http://localhost:9090 | Guardrail service URL |
GUARDRAIL_TIMEOUT | 2.0 | Per-request timeout in seconds (client, MCP server, proxy) |
GUARDRAIL_FAIL_MODE | escalate | Fallback verdict when the guardrail is unreachable: escalate (ask a human) or deny (fail-closed). MCP server and proxy only |
GUARDRAIL_PROXY_MAX_BODY | 1048576 | Max request body the proxy accepts (→ 413 beyond) |
GUARDRAIL_UPSTREAM_URL | — | Upstream tool-executor URL for /forward (no default — must be set) |
GUARDRAIL_FORWARD_TIMEOUT | 30.0 | Timeout for upstream forwarding in seconds |
GUARDRAIL_AUTO_FORWARD_ESCALATED | false | Forward 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)
timeoutdefaults toGUARDRAIL_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, anerrorstring, andisError: true(an operational failure is not a verdict). - The fallback verdict comes from
GUARDRAIL_FAIL_MODE:escalate(default — a human is asked) ordeny(fail-closed).
Proxy (ellm_guardrail.proxy)
- Unreachable guardrail →
GUARDRAIL_FAIL_MODEverdict withguardrail_reachable: falsein the body. On/forwardthe 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 witherror: "timeout". - Body beyond
GUARDRAIL_PROXY_MAX_BODY→ 413; missing/invalidContent-Length→ 400. - No upstream configured +
/forward→ 502 with guidance to setGUARDRAIL_UPSTREAM_URL.
Server
- Overloaded → 429 (rate limit) or 503 (connection cap); the JSON body carries
retry_after_ms.GET /healthis never rate limited. - An internal error returns 500 with a fail-safe
escalateverdict and never wedges the pipeline.
Requirements
- Python 3.10+
- Running guardrail service (
docker compose up -d) mcpextra for MCP server:pip install ./sdks/python[mcp]