# 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.** [![PyPI version](https://img.shields.io/pypi/v/agent-airlock?logo=pypi&logoColor=white&color=3775A9)](https://pypi.org/project/agent-airlock/) [![Downloads](https://img.shields.io/pypi/dm/agent-airlock?logo=python&logoColor=white&color=success)](https://pypistats.org/packages/agent-airlock) [![CI](https://img.shields.io/github/actions/workflow/status/sattyamjjain/agent-airlock/ci.yml?logo=github&label=CI&color=success)](https://github.com/sattyamjjain/agent-airlock/actions/workflows/ci.yml) [![Python 3.10+](https://img.shields.io/badge/python-3.10+-3776AB?logo=python&logoColor=white)](https://www.python.org/downloads/) [![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-green)](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)