Strict argument validation, ghost-argument stripping, and self-healing retries — one decorator, any agent or MCP server. Pydantic-only core.
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 · Benchmarks · Frameworks · Docs · How numbers are reported
An LLM decides which tool to call and what to pass it. Nothing checks the arguments before they reach your code.
# 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 signatureType 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.
pip install "agent-airlock>=0.10.3"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.
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.
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
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µs1 | python -m benchmarks.blockrate · re-run 2026-10-03 · results |
sandbox=True dispatch · contract parity |
4/4 annotated-contract probes refused on the sandbox path · 204/204 verdicts agree with the local path2 | python -m benchmarks.blockrate · re-run 2026-10-03 · results |
| 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 |
| 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 |
| Tool poisoning · MCPTox, 1,348 cases | Direct execution of the poisoned tool refused 100% under the G (tool allowlist) and LP tiers4 | python -m benchmarks.mcptox replay · re-run 2026-10-17 · results |
| 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 |
| 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 |
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 · re-run 2026-10-03 |
Table notes
- 95% CI 96.5–100% (blocked) and 0–3.6% (false-positive); 6 of 10 OWASP ASI slots measured, ASI07–ASI10 are n=0.
- 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.
- 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.
- 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. Every row, with the full results for
each: Benchmarks.
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.
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
| Framework | Path | Surface |
|---|---|---|
| LangChain | adapter · example | @tool, AgentExecutor |
| LangGraph | adapter · example | StateGraph, ToolNode |
| OpenAI Agents | adapter · example | Handoffs, manager pattern |
| Anthropic API | adapter · example | Direct Messages API |
| Claude Agent SDK | adapter · doc | wrap_tools(tools, policy=...) for @tools, wrap_agent(agent, policy=...) |
| smolagents | adapter · example | CodeAgent, E2B |
| Gemini 3 | adapter | function_call carrier + thought_signature redaction |
| GPT-5.5 | adapter | gpt_5_5_agent_defaults preset |
| FastMCP | adapter · example | @secure_tool decorator |
| PydanticAI | adapter · doc · example | wrap_agent(agent, policy=...) + output_validate hook |
| CrewAI | adapter · doc · example | wrap_crew(crew, policy=...) + task-level tool overrides |
| Google ADK | adapter · doc · example | wrap_agent(agent, policy) + tool_context relaxation |
| LlamaIndex | example only | ReActAgent |
| AutoGen | example only | ConversableAgent |
Decorator-ordering rules per framework: docs/COMPATIBILITY.md.
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, 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 57 CVE/advisory regression tests in tests/cves/ (the 57 distinct CVEs they replay are published in the generated catalog: 49 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 |
Against the MCP Top-10. Evidence is compressed here; each row's full justification is in
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.
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 freezePer-command documentation: CLI reference.
| Metric | Value | Detail | |
|---|---|---|---|
| Validation overhead | ~2µs p50 per decision | deterministic, in-process | |
| Framework integrations | 14 | see above | |
| Core dependencies | 0 | beyond the Pydantic foundation; everything else is an opt-in extra |
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.pyand 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, including arXiv:2608.18351, where the honest claim is complementarity and not superiority.
Known gaps and their status: ROADMAP.md.
| Documentation site | Guides, API reference, CLI, integrations |
BENCHMARK.md |
Benchmark summary and method |
docs/cves/index.md |
Generated CVE catalogue |
ROADMAP.md |
What is not done, and why |
PRIOR_ART.md |
External research this rests on |
CHANGELOG.md |
Release history |
AGENTS.md |
Contributor contract |
Read AGENTS.md first — it is the load-bearing contributor contract — then
CONTRIBUTING.md.
pip install -e ".[dev]"
make test lintEvery feat: needs a regression test. Every CVE fixture needs a primary-source URL in the
commit message. Claims need gates.
Report vulnerabilities via SECURITY.md. Please do not open a public issue
for an unpatched vulnerability.
Apache-2.0. Relicensed from MIT on 2026-09-06 to add an explicit patent grant.
Citation metadata: CITATION.cff.
If this is useful, a ⭐ helps other people find it.