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.
Test suite: 4,539 tests · Coverage: 87.44% · v0.10.7
Coverage runs 4,527 of 4,539; the 12 excluded are 8 benchmark tests (not correctness tests) and 4 docker-marked (need a daemon — they run in CI's docker-sandbox job, against the image built from the Dockerfile at the repo root).
Quickstart · Benchmarks · Frameworks · Docs · How numbers are reported
The problem
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 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
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.
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 (E2B, Modal, Docker, local) |
| 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.
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
fails the release if any row drifts past 30 days.
| Benchmark | Result | Reproduce |
|---|---|---|
| Cross-tool block-rate · 210 tool calls | 100% blocked · 0% false-positive · p50 ~2µs · 6 of 10 OWASP ASI slots measured; ASI07–ASI10 are n=0 | python -m benchmarks.blockrate · re-run 2026-09-16 · results |
sandbox=True dispatch · contract parity |
4/4 annotated-contract probes refused on the sandbox path (0/4 before v0.10.6) · 204/204 verdicts agree with the local path. Isolation-backend execution not run here: no E2B or Docker in the runner, reported as not-run rather than folded in | python -m benchmarks.blockrate · re-run 2026-09-16 · results |
| Least-privilege · ToolPrivBench, 100 scenarios | 100% over-privileged blocked · 100% low-privileged allowed · OPUR 100% → 0% | python -m benchmarks.toolprivbench · re-run 2026-09-08 · results |
| Adaptive attacker · AgentDojo, all 4 suites | 86.0% of injection→target tool-calls blocked (524/609, deterministic bound). Model-in-the-loop ASR 45% → 10%, but that is one model family on a 60-pair subset | python -m benchmarks.agentdojo.run · re-run 2026-09-08 · 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-09-12 · results |
| Prompt injection across agent harnesses | A null result, published as one. claude-code and codex each acted on the planted script 0/36 — but they ignored the benign twin just as completely, so this is indifference to the channel, not detection |
python -m benchmarks.harness_injection · last verified 2026-08-26 · results |
MCP spec conformance · @modelcontextprotocol/conformance |
Run against the wire-path validators, outcome published in full. Not a full MCP server/client conformance pass — airlock is a request validator, not a server, so it does not claim one | benchmarks/mcp_conformance/RESULTS.md · re-run 2026-09-08 |
Summary and method: BENCHMARK.md.
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 · 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_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.
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, 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 46 CVE/advisory regression tests in tests/cves/ (the 37 distinct CVEs they cover are published in the generated catalog) |
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.
| 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.
CLI
One CLI: airlock <command> (unified dispatcher, full command set since v0.8.56)
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.
Performance
| 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 |
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.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
| 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 |
Contributing
Read AGENTS.md first — it is the load-bearing contributor contract — then
CONTRIBUTING.md.
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. Please do not open a public issue
for an unpatched vulnerability.
License
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.
Release files for agent-airlock 0.10.7
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| agent_airlock-0.10.7.tar.gz | 654.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| agent_airlock-0.10.7-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 1.4 MB
Release files / agent_airlock-0.10.7.tar.gz
| Download URL | agent_airlock-0.10.7.tar.gz |
|---|---|
| Size | 654.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
d99b9947fe27c798de505cdc4eeca201da162826efcbfafd95ceedb97573c33a
|
|
BLAKE2b-256 checksum How to use checksums |
32536e179973d8788b3d6f05ff4b734d9451c2d03e8f236645ecc0629570b395
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Release files / agent_airlock-0.10.7-py3-none-any.whl
| Download URL | agent_airlock-0.10.7-py3-none-any.whl |
|---|---|
| Size | 782.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
65e57b8befcd0f8dfee71899fcc018abb559c69f74702fef75f6e1c4b66ad42c
|
|
BLAKE2b-256 checksum How to use checksums |
6854b4333bbb9605246cc728273bf0f309f97a73e3825e3bf0bdfab508a31ddb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|