Metadata-Version: 2.4
Name: llm-leash
Version: 2.0.0
Summary: Cost ceiling, audit log, and kill switch for LLM agents.
Project-URL: Homepage, https://github.com/avelikiy/llm-leash
Project-URL: Issues, https://github.com/avelikiy/llm-leash/issues
Project-URL: Source, https://github.com/avelikiy/llm-leash
Author: Alexander Velikiy
License: MIT
License-File: LICENSE
Keywords: agent,anthropic,audit,budget,compliance,cost,crewai,firewall,langgraph,llm,mcp,openai,safety
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries
Requires-Python: >=3.11
Provides-Extra: all
Requires-Dist: anthropic>=0.40; extra == 'all'
Requires-Dist: crewai>=0.80; extra == 'all'
Requires-Dist: httpx>=0.27; extra == 'all'
Requires-Dist: langgraph>=0.2; extra == 'all'
Requires-Dist: llama-firewall; extra == 'all'
Requires-Dist: mcp>=1.0; extra == 'all'
Requires-Dist: openai>=1.50; extra == 'all'
Requires-Dist: openhands-ai>=0.20; extra == 'all'
Requires-Dist: presidio-analyzer; extra == 'all'
Requires-Dist: pydantic-ai>=0.0.20; extra == 'all'
Requires-Dist: redis>=5.0; extra == 'all'
Requires-Dist: starlette>=0.37; extra == 'all'
Requires-Dist: uvicorn>=0.30; extra == 'all'
Provides-Extra: anthropic
Requires-Dist: anthropic>=0.40; extra == 'anthropic'
Provides-Extra: crewai
Requires-Dist: crewai>=0.80; extra == 'crewai'
Provides-Extra: dev
Requires-Dist: coverage>=7.6; extra == 'dev'
Requires-Dist: httpx>=0.27; extra == 'dev'
Requires-Dist: hypothesis>=6.100; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: starlette>=0.37; extra == 'dev'
Requires-Dist: uvicorn>=0.30; extra == 'dev'
Requires-Dist: vcrpy>=6.0; extra == 'dev'
Provides-Extra: external-scanners
Requires-Dist: llama-firewall; extra == 'external-scanners'
Requires-Dist: presidio-analyzer; extra == 'external-scanners'
Provides-Extra: langgraph
Requires-Dist: langgraph>=0.2; extra == 'langgraph'
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == 'mcp'
Provides-Extra: openai
Requires-Dist: openai>=1.50; extra == 'openai'
Provides-Extra: openhands
Requires-Dist: openhands-ai>=0.20; extra == 'openhands'
Provides-Extra: proxy
Requires-Dist: httpx>=0.27; extra == 'proxy'
Requires-Dist: starlette>=0.37; extra == 'proxy'
Requires-Dist: uvicorn>=0.30; extra == 'proxy'
Provides-Extra: pydantic-ai
Requires-Dist: pydantic-ai>=0.0.20; extra == 'pydantic-ai'
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == 'redis'
Description-Content-Type: text/markdown

# llm-leash

> The cost ceiling, audit log, and kill switch your LLM agent should never run without.

[![PyPI](https://img.shields.io/pypi/v/llm-leash)](https://pypi.org/project/llm-leash/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
[![Tests](https://img.shields.io/badge/tests-709%20passing-brightgreen.svg)](.)
[![Coverage](https://img.shields.io/badge/coverage-93%25-brightgreen.svg)](.)

`llm-leash` is a **5-line runtime firewall** for LLM agents. It enforces hard USD budgets, writes an immutable audit log, and gives you a kill switch and human-in-the-loop hook — without locking you into any agent framework.

**Status:** **v2.0.0 — Production / Stable.** Both the in-process middleware (v1.x surface, unchanged) and the HTTP proxy (v2.x surface) are part of the semver-stable public API. See [CHANGELOG.md](./CHANGELOG.md).

## Why

You shipped an agent. Then:

- A retry loop spent **$2,387 in 14 minutes**.
- A vague user message coaxed it into **`DROP TABLE users`**.
- Compliance asked **"show me every action this agent took for customer X last month"** — you can't.

`llm-leash` solves the boring B2B half of agent safety that nobody else owns: **money, paperwork, panic button.** It works alongside the content-safety scanners ([LlamaFirewall](https://github.com/meta-llama/PurpleLlama), [Invariant](https://github.com/invariantlabs-ai/invariant), [Prompt-Guard](https://huggingface.co/meta-llama/Llama-Prompt-Guard-2-86M)) — not against them.

## Two ways to install the leash

| Mode | When | Code change required |
|---|---|---|
| **In-process middleware** (v1.x surface, stable) | Python agents you control the source of | 1 line: `fw.wrap(client)` |
| **HTTP proxy** (v2.0 GA, stable) | Any language, vendor agents, multi-app compliance | 0 lines — one env var: `OPENAI_BASE_URL=http://localhost:8000` |

Both modes share the same audit format, policy engine, secrets detection, HITL queue, semantic detection (`LLMGuardRule`), behavioral baseline (`BehavioralBaselineRule`), and Prometheus metrics. Pick by code-access constraints, not by features.

## Quickstart

```python
from llm_leash import Firewall, LeashKilled
from anthropic import Anthropic

fw = Firewall(budget_usd=10.00, audit_log="audit.jsonl")
client = fw.wrap(Anthropic())

try:
    while True:
        client.messages.create(model="claude-opus-4-7", max_tokens=200,
                               messages=[{"role": "user", "content": "..."}])
except LeashKilled as e:
    print(f"Saved you the rest. Reason: {e.reason}")
```

Three things happen on every call:

1. **Budget tracked** — cumulative cost per session, raises `LeashKilled` when the cap is hit.
2. **Audit logged** — every model call appends one hash-chained JSONL line. Tamper-evident: `llm-leash verify audit.jsonl`.
3. **Kill switch** — `await fw.kill("reason")` stops the session immediately; the next call raises `LeashKilled`.

Run the offline demo (no API key needed):

```bash
python demo.py
llm-leash verify audit.jsonl
```

## Proxy mode (v2.0 GA)

For agents you can't (or don't want to) modify — change one env var, get the firewall:

```bash
# Install + start
pip install "llm-leash[proxy]"
llm-leash-proxy --listen 127.0.0.1:8000 --audit-log audit.jsonl --budget-usd 50

# Point any agent at it (no source changes)
export ANTHROPIC_BASE_URL=http://localhost:8000
export OPENAI_BASE_URL=http://localhost:8000
python my_agent.py
```

Works with any client speaking OpenAI / Anthropic on-wire protocol: the OpenAI / Anthropic SDKs, OpenRouter, LangChain.js, Vercel AI SDK, CrewAI via LiteLLM, OpenHands, custom Rust / Go / TS agents. Identifies sessions via headers (`X-LLM-Leash-Session-Id`, `X-LLM-Leash-Tenant-Id`, `X-LLM-Leash-Agent-Name`), with auto-fallback to a hash of the bearer token when no headers are set.

**What v2.0 GA gives you over v1.x:**

- **Streaming SSE** with end-of-stream accounting **and mid-stream cancellation** — if a runaway generation would blow the hard cap mid-flight, the proxy aborts the upstream connection and emits a synthetic `event: error` frame to the client.
- **Multi-replica state sharing** via Redis backend (`budget.backend = "redis"`, `kill.backend = "redis"`) — run the proxy behind a load balancer with consistent budget cumulatives + kill state across pods.
- **Per-agent budget caps** with `[budget.per_agent_caps]` TOML — agent-level ceilings override per-tenant, which override the default cap.
- **Operator console** — `llm-leash-console` ships a read-only Web UI (live sessions, top spend, kill button, HITL queue, audit tail).
- **Alerts sidecar** — `llm-leash-alerts` watches the audit stream and fans budget-breach / kill / HITL events out to Slack + PagerDuty.
- **Docker + Helm + k8s manifests** for fleet deployment.

**Operator workflow:**

```bash
# Real-time stats from the CLI
llm-leash status --proxy http://localhost:8000

# Stop a runaway session
llm-leash kill <session_id> --proxy http://localhost:8000 --reason "blew budget"

# Approve a pending HITL request
llm-leash hitl list --proxy http://localhost:8000
llm-leash hitl approve <request_id> --proxy http://localhost:8000

# Prometheus scrape
curl http://localhost:8000/metrics

# Operator Web UI (separate binary, separate port so a UI crash never
# takes down agent traffic)
llm-leash-console --proxy http://localhost:8000 --listen 127.0.0.1:8001
```

See [docs/PROXY.md](./docs/PROXY.md) for deployment recipes (Docker, k8s, Helm), Redis tuning, and the full TOML reference.

## Adapters — one wrap, every framework (in-process, stable)

```python
from llm_leash import Firewall
fw = Firewall(budget_usd=10.00, audit_log="audit.jsonl")
```

| Framework | Client class | Example |
|---|---|---|
| **Anthropic** | `anthropic.Anthropic` | `fw.wrap(Anthropic()).messages.create(...)` |
| **OpenAI** | `openai.OpenAI` | `fw.wrap(OpenAI()).chat.completions.create(...)` |
| **LangChain / LangGraph** | `ChatAnthropic`, `ChatOpenAI` | `fw.wrap(ChatAnthropic(model="…")).invoke([…])` |
| **CrewAI** | `crewai.LLM` | `fw.wrap(LLM(model="openai/gpt-4o")).call([…])` |
| **OpenHands** | `openhands.llm.LLM` | `fw.wrap(LLM(config)).completion(messages=[…])` |
| **Pydantic-AI** | `pydantic_ai.models.*` | `await fw.wrap(OpenAIModel(...)).request([…])` |
| **MCP** | `mcp.ClientSession` | `await fw.wrap(session).call_tool("read_file", {…})` |

All adapters are **duck-typed** — no SDK imports at module level, no version
pinning. The wrapped client preserves every attribute of the original; only
the call surface is intercepted.

```python
# LangGraph example: drop the firewall into your existing graph
from langchain_anthropic import ChatAnthropic
from langgraph.graph import StateGraph

chat = fw.wrap(ChatAnthropic(model="claude-haiku-4-5"))
graph = StateGraph(MyState)
graph.add_node("llm", lambda state: {"reply": chat.invoke(state["messages"])})
```

```python
# CrewAI example: pass the wrapped LLM to your Agent
from crewai import Agent, Crew, Task, LLM
llm = fw.wrap(LLM(model="anthropic/claude-haiku-4-5"))
agent = Agent(role="researcher", llm=llm, goal="...")
result = Crew(agents=[agent], tasks=[Task(...)]).kickoff()
```

```python
# MCP example: every tool call is audited; dangerous tools can require HITL
async with ClientSession(read, write) as session:
    wrapped = fw.wrap(session)
    await wrapped.call_tool("read_file", {"path": "/etc/hosts"})
```

## SOC 2 evidence pack

Generate a complete SOC 2 evidence package from any `audit.jsonl` log:

```bash
llm-leash soc2 /var/log/agent-audit.jsonl \
  --out ./evidence-2026-Q2/ \
  --period-start 2026-04-01T00:00:00Z \
  --period-end   2026-06-30T23:59:59Z \
  --org "Acme Inc"
```

Produces six artefacts an auditor can attach to their evidence binder
directly: `executive-summary.html`, `cc6_access_control.csv`,
`cc7_monitoring.csv`, `cc7_integrity.json`, `anomalies.csv`, and
`bom.json`. Each file is sha256-hashed and listed in the bill of
materials. See [docs/SOC2.md](./docs/SOC2.md) for the Trust Service
Criteria mapping.

## Persistent state for multi-worker prod

```python
from llm_leash import Firewall, SQLiteBudgetStore, SQLiteKillRegistry

fw = Firewall(
    budget_usd=100.0,
    audit_log="/var/log/agent-audit.jsonl",
    kill_registry=SQLiteKillRegistry("/var/lib/myapp/kill.db"),
)
fw._budget._store = SQLiteBudgetStore("/var/lib/myapp/budget.db")
```

Redis variants (`RedisBudgetStore` / `RedisKillRegistry`) accept any
duck-typed client.

For **proxy mode**, the same backends are config-driven — no code:

```toml
# proxy.toml
[budget]
backend = "redis"
redis_url = "redis://redis.internal:6379/0"
default_cap_usd = 50.0
per_tenant_caps = { acme = 500.0, beta = 25.0 }
per_agent_caps  = { "writer-prod" = 100.0, "researcher-prod" = 10.0 }

[kill]
backend = "redis"
redis_url = "redis://redis.internal:6379/0"
```

## What it does

- **Hard USD budget per session.** Soft cap warns. Hard cap kills.
- **Append-only JSONL audit log**, hash-chained, optionally HMAC-signed. `jq`-able. SOC 2 / EU AI Act Article 12 evidence-shaped.
- **Kill switch.** Stop a runaway agent from CLI, HTTP, or Redis. Sub-300ms propagation.
- **Human-in-the-loop** webhook for high-stakes tool calls. Default-deny on timeout.
- **Tool ACL** with regex / SQL-AST / shell-AST patterns.
- **PII redaction** before tool dispatch and before audit write.
- **Adapters** for Anthropic, OpenAI, LangGraph, CrewAI, OpenHands, Pydantic-AI, MCP.
- **Semantic threat detection** (`LLMGuardRule`) — cheap-LLM classifier flags prompt injection / jailbreak / data exfiltration; sampling-rate aware so high-QPS is affordable.
- **Behavioral baseline** (`BehavioralBaselineRule`) — Welford-online statistics per `(tenant, agent)`; flags token spikes, new models, off-hours, tool-spam. Pluggable `InMemoryBaselineStore` / `SQLiteBaselineStore`.
- **Streaming + mid-stream cancel** (proxy) for both Anthropic and OpenAI SSE.

## What it does NOT do

| You want | Use this instead |
|---|---|
| Prompt-injection classifier | [Prompt-Guard](https://huggingface.co/meta-llama/Llama-Prompt-Guard-2-86M) (call from a rule) |
| Content guardrails (DSL) | [NeMo Guardrails](https://github.com/NVIDIA/NeMo-Guardrails) / [Guardrails AI](https://github.com/guardrails-ai/guardrails) |
| Tool-arg pattern catalog | [Invariant Labs](https://github.com/invariantlabs-ai/invariant) (import their .rules from a policy) |
| Eval framework | [PromptFoo](https://github.com/promptfoo/promptfoo) / [DeepEval](https://github.com/confident-ai/deepeval) |
| Observability dashboard | [Langfuse](https://github.com/langfuse/langfuse) / [LangSmith](https://smith.langchain.com) (ship JSONL into them) |
| Model router | [LiteLLM](https://github.com/BerriAI/litellm) / [OpenRouter](https://openrouter.ai) |

`llm-leash` is the layer beneath all of them. It does enforcement and evidence. Everything else is a rule you can plug in.

## Documents

- [PRODUCT.md](./PRODUCT.md) — what this is, who buys it, what it is not.
- [ARCHITECTURE.md](./ARCHITECTURE.md) — modules, data flow, performance budget.
- [API.md](./API.md) — public surface, CLI, JSONL schema, custom rules.
- [docs/PROXY.md](./docs/PROXY.md) — proxy mode operator guide (Docker, k8s, Redis, TOML).
- [docs/SOC2.md](./docs/SOC2.md) — SOC 2 Trust Service Criteria mapping.
- [docs/adr/](./docs/adr/) — architecture decisions (in progress).

## Install

```bash
pip install llm-leash                  # core, zero runtime deps
pip install "llm-leash[anthropic]"     # + Anthropic adapter
pip install "llm-leash[proxy]"         # + HTTP proxy mode (starlette/uvicorn/httpx)
pip install "llm-leash[redis]"         # + Redis backend for proxy state
pip install "llm-leash[all]"           # everything
```

## Roadmap

| Version | Status | What |
|---|---|---|
| v0.1 | ✓ | Core firewall + Anthropic adapter + audit chain + CLI `verify` |
| v0.2 | ✓ | PolicyEngine + PII redactor |
| v0.3 | ✓ | BlockedSql + BlockedShell rules |
| v0.4 | ✓ | Redis transports for budget + kill |
| v0.5 | ✓ | HITL gates (InMemory + Webhook) + `HitlThreshold` |
| v0.6 | ✓ | LangGraph + CrewAI + MCP adapters + acceptance gate |
| v0.7 | ✓ | `audit replay/export` + SQLite stores + extended CLI |
| **v1.0** | ✓ | Stable public API · semver lock · PyPI release · per-adapter examples |
| **v1.1** | ✓ | OpenHands + Pydantic-AI adapters · LlamaFirewall / Presidio rule wrappers |
| **v1.2** | ✓ | Durable HITL queue (SQLite/InMemory) · HTTP kill transport · CLI `hitl` ops |
| **v1.3** | ✓ | SOC 2 evidence pack generator · CLI `soc2` · TSC mapping |
| **v2.0** | ✓ | HTTP proxy mode · SSE streaming + mid-stream cancel · Redis/SQLite backends · per-agent caps · operator console (`llm-leash-console`) · alerts sidecar (`llm-leash-alerts`) · `LLMGuardRule` (semantic) · `BehavioralBaselineRule` · Docker / k8s / Helm |
| v2.1 | planned | TypeScript port of the core |
| v2.2 | planned | OPA / Rego policy backend |

## License

MIT — see [LICENSE](./LICENSE).

The OSS firewall is and always will be free. The hosted audit-log service (forthcoming) is the only thing that costs money — and you never need it. JSONL is yours.
