Metadata-Version: 2.4
Name: ellm-guardrail
Version: 0.1.0
Summary: Python SDK for the ELLM Guardrail — deterministic policy firewall for AI agent tool calls
Author-email: "El AI Intelligence, LLC" <Elai-intelligence@pm.me>
Project-URL: Homepage, https://github.com/El-AI-Intelligence/ellm-guardrail
Project-URL: Documentation, https://github.com/El-AI-Intelligence/ellm-guardrail/tree/main/docs/guardrail
Project-URL: Repository, https://github.com/El-AI-Intelligence/ellm-guardrail
Keywords: ai-safety,guardrail,agent,policy,deterministic
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: requests>=2.28
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == "mcp"
Provides-Extra: proxy

# ELLM Guardrail Python SDK

```bash
pip install ellm-guardrail
```

## 5-Second Start

```python
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 agent

```python
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 approval
        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

```bash
# 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
```

### Verdict gateway (framework-agnostic)

```bash
# Start the gateway (guardrail must be running)
python -m ellm_guardrail.proxy --port 9091

# Point any agent framework at it:
# POST tool calls to http://localhost:9091/check
# → 200 Allow / 403 Deny (JSON body carries verdict, reason, guardrail_reachable)
```

This is a verdict gateway, not an inline proxy: it tells the agent whether a tool call is allowed — it does not execute or forward the call itself. A true inline proxy is on the roadmap.

### Context manager

```python
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) |

```python
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.
- Body beyond `GUARDRAIL_PROXY_MAX_BODY` → 413; missing/invalid `Content-Length` → 400.

**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 ellm-guardrail[mcp]`
