Metadata-Version: 2.4
Name: llm-leash
Version: 2.3.0a1
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 :: 4 - Beta
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-229%20passing-brightgreen.svg)](.)
[![Coverage](https://img.shields.io/badge/coverage-96%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.0a1 — alpha** (proxy mode preview). v1.x in-process middleware remains stable and is the recommended production path. 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, stable) | Python agents you control the source of | 1 line: `fw.wrap(client)` |
| **HTTP proxy** (v2.0.0a1, alpha) | 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, 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 (new in v2.0.0a1, alpha)

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]==2.0.0a1"
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: 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.

**Operator workflow:**

```bash
# Real-time stats
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
```

**Streaming SSE and Docker images ship in 2.0.0a2 / 2.0.0.** For streaming today, use the in-process middleware path below.

## 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.

## 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.

## 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/adr/](./docs/adr/) — architecture decisions (in progress).

## Install (when published)

```bash
pip install llm-leash                  # core, zero deps
pip install llm-leash[anthropic]       # + Anthropic adapter
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 |
| v1.4 | planned | TypeScript port of the core |
| v1.5 | 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.
