# Agent-Airlock
### A deny-by-default contract layer for AI agent tool calls
**Strict argument validation, ghost-argument stripping, and self-healing retries — one decorator, any agent or MCP server. Pydantic-only core.**
[](https://pypi.org/project/agent-airlock/)
[](https://pypistats.org/packages/agent-airlock)
[](https://github.com/sattyamjjain/agent-airlock/actions/workflows/ci.yml)
[](https://www.python.org/downloads/)
[](https://opensource.org/licenses/Apache-2.0)
**Test suite:** 6,584 tests · **Coverage:** 89.20% · **v0.10.26**
Coverage runs 6,565 of 6,584; the 19 excluded are 8 benchmark tests (not correctness tests), 5 docker-marked (need a daemon — they run in CI's `docker-sandbox` job, against the image built from the Dockerfile at the repo root) and 6 live API contract tests (need an Anthropic API key and spend API credit; `make test-live`).
[Quickstart](#quickstart) · [Benchmarks](#benchmarks) · [Frameworks](#framework-support) · [Docs](https://sattyamjjain.github.io/agent-airlock/) · [How numbers are reported](#how-numbers-are-reported-here)
---
## The problem
An LLM decides which tool to call and what to pass it. Nothing checks the arguments before
they reach your code.
```python
# Your tool expects an int. The model sends a string, and invents a parameter.
transfer_funds(amount="500", to="alice", force=True)
# ^^^^^ ^^^^^^^^^^
# wrong type never in your signature
```
Type coercion turns `"500"` into `500` and moves on. The invented `force=True` lands in
`**kwargs` and silently changes behaviour. Neither is a model failure you can prompt away —
it is a missing contract at the call boundary.
## Quickstart
```bash
pip install "agent-airlock>=0.10.3"
```
```python
from agent_airlock import Airlock
@Airlock()
def transfer_funds(amount: int, to: str) -> str:
return f"Sent ${amount} to {to}"
```
That is the whole integration. Now:
| The model sends | What happens |
|---|---|
| `amount="500"` | **Blocked** — Pydantic V2 strict mode, no coercion. Returns a structured error carrying `fix_hints` the model can retry against |
| `force=True` | **Stripped** — a parameter that is not in your signature never reaches your function |
| `amount=500, to="alice"` | **Executes** |
A blocked call returns an `AirlockResponse` rather than raising, so the model gets a
refusal it can act on instead of a traceback it cannot.
## What it does
The decorator runs a fixed sequence of gates before your function and sanitises after it.
Everything except validation is off by default.
| Layer | What it covers |
|---|---|
| **Validate** | Pydantic V2 strict mode · ghost-argument stripping · self-healing `fix_hints` |
| **Authorize** | RBAC · token-bucket rate limits · time windows · per-model-tier cost budgets · capability gating |
| **Isolate** | Filesystem path validation · network egress control · sandboxed execution (`sandbox=True` runs on E2B; Docker, Modal and local backends are called directly) |
| **Sanitize** | PII and secret masking, including opt-in Indic PII · output truncation · JSON-Lines audit log |
| **MCP guards** | Per-CVE guards for stdio injection, OAuth, DNS rebinding, SSRF, eval-RCE, WebSocket origin and task lifecycle — tracking MCP spec revision `2026-07-28` |
Full reference on the [documentation site](https://sattyamjjain.github.io/agent-airlock/).
## Benchmarks
Every row is one command you can run. Dates are when the number was last actually produced,
not when the row was last edited — [`check_benchmark_freshness.py`](scripts/check_benchmark_freshness.py)
fails the release if any row drifts past 30 days.
| Benchmark | Result | Reproduce |
|---|---|---|
| **Cross-tool block-rate** · 210 tool calls | **100% blocked (106/106) · 0% false-positive (0/104) · p50 ~2µs**1 | `python -m benchmarks.blockrate` · _re-run 2026-10-03_ · [results](benchmarks/blockrate/RESULTS.md) |
| **`sandbox=True` dispatch** · contract parity | **4/4 annotated-contract probes refused on the sandbox path · 204/204 verdicts agree with the local path**2 | `python -m benchmarks.blockrate` · _re-run 2026-10-03_ · [results](benchmarks/blockrate/RESULTS.md#sandboxtrue-dispatch-arm) |
| **Least-privilege** · ToolPrivBench, 100 scenarios | **100% over-privileged blocked · 100% low-privileged allowed** · OPUR 100% → 0% | `python -m benchmarks.toolprivbench` · _re-run 2026-10-03_ · [results](benchmarks/toolprivbench/RESULTS.md) |
| **Indirect prompt injection** · AgentDojo, all 4 suites | **86.0% of injection→target tool-calls blocked** (deterministic bound)3 | `python -m benchmarks.agentdojo.run` · _re-run 2026-10-12_ · [results](benchmarks/agentdojo/RESULTS.md) |
| **Tool poisoning** · MCPTox, 1,348 cases | **Direct execution of the poisoned tool refused 100% under the G (tool allowlist) and LP tiers**4 | `python -m benchmarks.mcptox replay` · _re-run 2026-10-17_ · [results](benchmarks/mcptox/RESULTS.md) |
| **vs. native MCP gateway** · 12 malformed payloads | **airlock 12/12 blocked · Docker MCP Gateway 0/12** · 0% false-positive on both | `python -m benchmarks.vs_gateway` · _re-measured live 2026-10-03_ · [results](benchmarks/vs_gateway/RESULTS.md) |
| **Prompt injection across agent harnesses** | **A null result, published as one.** `claude-code` and `codex` each acted on the planted script **0/36** — and ignored the benign twin just as completely, so this is indifference to the channel, not detection | `python -m benchmarks.harness_injection` · _re-run 2026-10-03_ · [results](benchmarks/harness_injection/RESULTS.md) |
| **MCP spec conformance** · `@modelcontextprotocol/conformance` | Wire-path validators, outcome published in full. **Not a full MCP server/client conformance pass** — airlock is a request validator, not a server | [`benchmarks/mcp_conformance/RESULTS.md`](benchmarks/mcp_conformance/RESULTS.md) · _re-run 2026-10-03_ |
**Table notes**
1. 95% CI 96.5–100% (blocked) and 0–3.6% (false-positive); 6 of 10 OWASP ASI slots measured, ASI07–ASI10 are n=0.
2. 0/4 before v0.10.6; 100 policy items admitted and 100 refused on both paths. The 2026-09-16 run's 204/204 was vacuous — every policy item was refused on both paths (corrected in the results). Isolation-backend execution _not run_ here: no E2B or Docker in the runner.
3. 524/609 pairs that have a tool-call target, of 949 attacked. Model-in-the-loop ASR 45% → 10% (gpt-4o-mini, 2026-07-31) is **one model family on a 60-pair subset**, under AgentDojo's fixed-template attack, not an adaptive one. Claude 5.5 (168 pairs/arm, 2026-10-16) is a near-null: Haiku and Sonnet resist unaided (undefended ASR 5-8%), so airlock's realised reduction is +0-1% at a +14% benign-refusal cost; against models this robust the 86% deterministic bound is the informative measure.
4. 1650/1650 API-model calls; G0 contract-only 2.4%; benign-proxy false-refusal 2.4% (G) / 4.1% (LP), over a free judge-free replay of 27 models' recorded calls. Fresh Claude 5.5 two-arm run confirms it: Haiku 74/74, Sonnet 42/42, Opus 26/26 direct poisoned calls refused, 0-6% benign cost.
Summary and method: [`BENCHMARK.md`](BENCHMARK.md). Every row, with the full results for
each: [Benchmarks](https://sattyamjjain.github.io/agent-airlock/benchmarks/).
## Where this sits
Airlock is **in-process**. It is not a proxy, a gateway or a sidecar — it runs inside the
process that executes the tool, which is the only place the real Python arguments exist.
That makes it complementary to the layers around it, not a replacement for them. A gateway
authenticates identity and transport and sandboxes the server; it does not validate the
tool call's argument contract. Measured live against a Docker MCP Gateway on 12 malformed
payloads, the gateway forwarded **12/12** that airlock blocks.
## Framework support
Both paths use the same `@Airlock()` decorator. **Adapter-shipped** means a dedicated module
under `src/agent_airlock/integrations/` handles framework-specific glue — signature
preservation, tool-registry rewrites, request-shape adapters. **Example-only** means the
decorator works out of the box with no adapter.
**Adapter-shipped (12):** LangChain (`integrations/langchain.py`),
LangGraph (`integrations/langgraph_toolnode_compat.py`),
OpenAI Agents SDK (`integrations/openai_guardrails.py`),
Anthropic Messages API (`integrations/anthropic.py`),
Anthropic Claude Agent SDK (`integrations/anthropic_claude_agent_sdk.py`, v0.6.1+),
smolagents (`integrations/smolagents_wrapper.py`),
Gemini 3 Agent Mode (`integrations/gemini3_tool_shape_adapter.py`),
GPT-5.5 (`integrations/gpt5_5_tool_shape_adapter.py`),
PydanticAI (`integrations/pydantic_ai.py`, v0.7.1+),
CrewAI (`integrations/crewai.py`, v0.7.2+),
Google ADK (`integrations/google_adk.py`, v0.9.0+),
FastMCP (`agent_airlock/mcp/`).
**Example-only (2):** AutoGen, LlamaIndex — decorator-compatible without an adapter.
Every framework: adapter, doc and runnable example
### Complete Examples
| Framework | Path | Surface |
|-----------|------|---------|
| LangChain | [adapter](./src/agent_airlock/integrations/langchain.py) · [example](./examples/langchain_integration.py) | @tool, AgentExecutor |
| LangGraph | [adapter](./src/agent_airlock/integrations/langgraph_toolnode_compat.py) · [example](./examples/langgraph_integration.py) | StateGraph, ToolNode |
| OpenAI Agents | [adapter](./src/agent_airlock/integrations/openai_guardrails.py) · [example](./examples/openai_agents_sdk_integration.py) | Handoffs, manager pattern |
| Anthropic API | [adapter](./src/agent_airlock/integrations/anthropic.py) · [example](./examples/anthropic_integration.py) | Direct Messages API |
| Claude Agent SDK | [adapter](./src/agent_airlock/integrations/anthropic_claude_agent_sdk.py) · [doc](./docs/integrations/anthropic-claude-agent-sdk.md) | `wrap_tools(tools, policy=...)` for `@tool`s, `wrap_agent(agent, policy=...)` |
| smolagents | [adapter](./src/agent_airlock/integrations/smolagents_wrapper.py) · [example](./examples/smolagents_integration.py) | CodeAgent, E2B |
| Gemini 3 | [adapter](./src/agent_airlock/integrations/gemini3_tool_shape_adapter.py) | `function_call` carrier + `thought_signature` redaction |
| GPT-5.5 | [adapter](./src/agent_airlock/integrations/gpt5_5_tool_shape_adapter.py) | `gpt_5_5_agent_defaults` preset |
| FastMCP | [adapter](./src/agent_airlock/mcp/__init__.py) · [example](./examples/fastmcp_integration.py) | `@secure_tool` decorator |
| PydanticAI | [adapter](./src/agent_airlock/integrations/pydantic_ai.py) · [doc](./docs/integrations/pydantic-ai.md) · [example](./examples/pydanticai_integration.py) | `wrap_agent(agent, policy=...)` + output_validate hook |
| CrewAI | [adapter](./src/agent_airlock/integrations/crewai.py) · [doc](./docs/integrations/crewai.md) · [example](./examples/crewai_integration.py) | `wrap_crew(crew, policy=...)` + task-level tool overrides |
| Google ADK | [adapter](./src/agent_airlock/integrations/google_adk.py) · [doc](./docs/integrations/google-adk.md) · [example](./examples/google_adk_integration.py) | `wrap_agent(agent, policy)` + `tool_context` relaxation |
| LlamaIndex | [example only](./examples/llamaindex_integration.py) | ReActAgent |
| AutoGen | [example only](./examples/autogen_integration.py) | ConversableAgent |
Decorator-ordering rules per framework: [`docs/COMPATIBILITY.md`](docs/COMPATIBILITY.md).
## Security coverage
Mapped against the **OWASP Agentic Top-10 (2026)**. `Partial` means airlock covers the
runtime leg and something upstream is out of scope. Every label is justified per row in
[`docs/owasp-agentic-2026-coverage.md`](docs/owasp-agentic-2026-coverage.md), which is
generated from a YAML source and byte-diffed in CI.
| Risk | Primary controls | Coverage |
|---|---|---|
| **ASI01 Agent Goal Hijack** | Sequence guard, action-contradiction gate, tool-output trust guard | Partial |
| **ASI02 Tool Misuse and Exploitation** | Strict validation, ghost-arg stripping, `SafePath` / `SafeURL`, capability gating | Full |
| **ASI03 Identity and Privilege Abuse** | RBAC, signed agent identity, capability-union boundary, privilege right-sizing | Partial |
| **ASI04 Agentic Supply Chain Vulnerabilities** | Description and manifest pinning, schema `$ref` guard, tool-definition pin, attested admission, and 58 CVE/advisory regression tests in `tests/cves/` (the 58 distinct CVEs they replay are published in [the generated catalog](docs/cves/index.md): 50 with a row of their own, 8 inside umbrella modules) | Partial |
| **ASI05 Unexpected Code Execution (RCE)** | Eval-RCE guards, sandboxed execution, stdio command-injection guards | Full |
| **ASI06 Memory & Context Poisoning** | Auto-memory provenance, cross-tenant isolation, conversation tracking | Partial |
| **ASI07 Insecure Inter-Agent Communication** | A2A guard, MCP proxy guard, transport validation | Partial |
| **ASI08 Cascading Failures** | Circuit breaker, retry policy, amplification budget | Full |
| **ASI09 Human-Agent Trust Exploitation** | Human-oversight decorator, elicitation guard, honeypot deception | Partial |
| **ASI10 Rogue Agents** | Audit telemetry, anomaly detector, kill switch — **no quarantine primitive** | Monitor-only |
### MCP-specific mapping
Against the **MCP Top-10**. Evidence is compressed here; each row's full justification is in
[`docs/owasp-agentic-2026-coverage.md`](docs/owasp-agentic-2026-coverage.md).
| Risk | Primary controls | Coverage |
|---|---|---|
| **MCP01 Token Mismanagement & Secret Exposure** | `MCPProxyGuard` rejects passthrough headers and enforces audience; sanitizer masks secrets in tool output | Partial |
| **MCP02 Privilege Escalation via Scope Creep** | `require_agent_id` preset, capability gating, `CredentialScope`, runtime capability-union deny at grant time | Full |
| **MCP03 Tool Poisoning** | Ghost-arg rejection, `SafePath` / `SafeURL`, `mcp_description_manifest_guard`, `install_*` deny | Full |
| **MCP04 Software Supply Chain Attacks & Dependency Tampering** | `stdio_guard_ox_defaults()` (Ox 2026-04-16 advisory), `download_plugin_*` / `fetch_plugin_*` deny | Full |
| **MCP05 Command Injection & Execution** | `stdio_guard` shell-metachar and deny-pattern rules, `exec_*` / `run_*` / `system_*` deny, sandbox for `DANGEROUS` | Full |
| **MCP06 Intent Flow Subversion** | `ToolOutputTrustGuard` envelopes injected-instruction output; `@requires_human_oversight` gates high-value actions | Partial |
| **MCP07 Insufficient Authentication & Authorization** | OAuth 2.1 + PKCE S256 helpers, `iss` mix-up validation, header/body routing integrity, unsigned-`_meta` trust boundary, step-up scope guard, Tasks lifecycle and admission guards. Transport-level auth remains server-side | Partial |
| **MCP08 Lack of Audit and Telemetry** | JSON-Lines audit log, OpenTelemetry export, spans and metrics | Full |
| **MCP09 Shadow MCP Servers** | Attested tool-server admission, LAN unauthenticated-MCP guard | Partial |
| **MCP10 Context Injection & Over-Sharing** | PII and secret sanitizer, workspace-scoped config, `ToolOutputTrustGuard` untrusted-data envelope | Full |
Use it directly via the presets named above, or the per-CVE catalogue at
[`docs/cves/index.md`](docs/cves/index.md).
## CLI
### One CLI: `airlock ` (unified dispatcher, full command set since v0.8.56)
```bash
airlock scan-tools ./tools.json # static contract check on tool declarations
airlock doctor # environment and config diagnosis
airlock explain --unused-scopes # diff granted privilege against used
airlock attest receipt emit ... # signed evidence that a run was gated
airlock policy compile "..." # English to typed policy (needs an LLM backend)
airlock kill-switch trigger # HMAC-signed fleet freeze
```
Per-command documentation: [CLI reference](https://sattyamjjain.github.io/agent-airlock/).
## Performance
| Metric | Value | | Detail |
|---|---|---|---|
| **Validation overhead** | ~2µs p50 per decision | | deterministic, in-process |
| **Framework integrations** | 14 | | [see above](#framework-support) |
| **Core dependencies** | 0 | | beyond the Pydantic foundation; everything else is an opt-in extra |
## How numbers are reported here
This project holds itself to a rule worth stating plainly: **a claim in this README is
gated by a test, or it is not made.**
- Counts — tests, CVEs, adapters — are cross-checked against the tree by
[`tests/test_numeric_claim_parity.py`](tests/test_numeric_claim_parity.py) and siblings.
A number that drifts fails the build.
- Benchmark dates are enforced. A row older than 30 days blocks a release.
- **Null results are published.** The prompt-injection row above found nothing and says so,
in the same table as the wins.
- Competitor numbers are never fabricated. Where an incumbent is cited it is from their
published scope and marked *not re-run*; where a comparison **was** run live, it says so.
- Where no head-to-head exists, that is recorded rather than implied — see
[**Prior art**](./PRIOR_ART.md), including arXiv:2608.18351, where the honest claim is
complementarity and not superiority.
Known gaps and their status: [`ROADMAP.md`](ROADMAP.md).
## Documentation
| | |
|---|---|
| [Documentation site](https://sattyamjjain.github.io/agent-airlock/) | Guides, API reference, CLI, integrations |
| [`BENCHMARK.md`](BENCHMARK.md) | Benchmark summary and method |
| [`docs/cves/index.md`](docs/cves/index.md) | Generated CVE catalogue |
| [`ROADMAP.md`](ROADMAP.md) | What is not done, and why |
| [`PRIOR_ART.md`](./PRIOR_ART.md) | External research this rests on |
| [`CHANGELOG.md`](CHANGELOG.md) | Release history |
| [`AGENTS.md`](AGENTS.md) | Contributor contract |
## Contributing
Read [`AGENTS.md`](AGENTS.md) first — it is the load-bearing contributor contract — then
[`CONTRIBUTING.md`](CONTRIBUTING.md).
```bash
pip install -e ".[dev]"
make test lint
```
Every `feat:` needs a regression test. Every CVE fixture needs a primary-source URL in the
commit message. Claims need gates.
## Security
Report vulnerabilities via [`SECURITY.md`](SECURITY.md). Please do not open a public issue
for an unpatched vulnerability.
## License
[Apache-2.0](LICENSE). Relicensed from MIT on 2026-09-06 to add an explicit patent grant.
Citation metadata: [`CITATION.cff`](CITATION.cff).
---
If this is useful, a ⭐ helps other people find it.
[Report a bug](https://github.com/sattyamjjain/agent-airlock/issues) ·
[Request a feature](https://github.com/sattyamjjain/agent-airlock/issues) ·
[Discussions](https://github.com/sattyamjjain/agent-airlock/discussions)