Skip to main content

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 Downloads CI Python 3.10+ License: Apache 2.0

Test suite: 6,729 tests · Coverage: 89.33% · v0.10.27
Coverage runs 6,710 of 6,729; 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


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 (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

Token audience and startup hooks

Two checks that run before any argument is read, both inert until you set them:

  • Token audience. AirlockConfig(expected_token_audience="https://mcp.example.test") checks the aud claim of the OAuth token behind every guarded call, read from the claims your server verified and put on AirlockContext(metadata={"access_token_claims": ...}), never from tool arguments. It warns by default; token_audience_mode="deny" refuses a token issued to another application, the bug in CVE-2026-115413.
  • Startup hooks. startup_hook_check="warn" (or "deny") lists the executable .pth lines and sitecustomize / usercustomize of the running interpreter, once per process; deny refuses every guarded call while one is not on startup_hook_allowlist. Those hooks ran before your code did, so this reports them; it cannot stop them.

Details: Token audience and startup hooks.

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 (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

  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. Every row, with the full results for each: 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 · 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.

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 58 CVE/advisory regression tests in tests/cves/ (the 58 distinct CVEs they replay are published in the generated catalog: 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.

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

Report a bug · Request a feature · Discussions

Metadata

Release files for agent-airlock 0.10.27

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for agent-airlock 0.10.27
File Size Uploaded Provenance Actions
agent_airlock-0.10.27.tar.gz 742.0 kB With provenance Details for agent_airlock-0.10.27.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for agent-airlock 0.10.27
File Interpreter ABI Platform Actions
agent_airlock-0.10.27-py3-none-any.whl Python 3 none any Details for agent_airlock-0.10.27-py3-none-any.whl

Total release size: 1.6 MB

Release files / agent_airlock-0.10.27.tar.gz

Download URL agent_airlock-0.10.27.tar.gz
Size 742.0 kB
Tags Source
SHA-256 checksum
How to use checksums
46fec5b8ca1529f14615be11283620857da4cf4e46b2a45193f3f810c4536d38
BLAKE2b-256 checksum
How to use checksums
7797e79d52229f2319a536c9c8a2daafb8bf57549fdb3d52cbc05d20b20db44b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.

Transparency log

Release files / agent_airlock-0.10.27-py3-none-any.whl

Download URL agent_airlock-0.10.27-py3-none-any.whl
Size 874.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
58ca7398559ce03c93339243fb64f48554ce3e830ddb9617f43b6199b765e746
BLAKE2b-256 checksum
How to use checksums
799a87f6b555b43d28ac414e664c6befd29c79b21cd8451e7293954afddd87e3
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.16

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.

Transparency log

Security for agent-airlock 0.10.27

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for agent_airlock-0.10.27.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 28, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.10.27 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page