Skip to main content

Agent-Airlock

An in-process 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: 10,725 tests · Coverage: 91.57% · v0.10.33
Coverage runs 10,706 of 10,725; 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 a dict (AirlockResponse.to_dict()) 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. Validation, output sanitising and the audit log are on by default; the rest is opt-in.

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 (host and path-prefix allowlists; an allowlisted host that resolves to the metadata address is refused) · 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, code-sandbox host-bridge, WebSocket origin and task lifecycle — tracking MCP spec revision 2026-07-28

Irreversible actions

These stop a call that passes validation but should not run yet, or should not run twice.

  • SecurityPolicy(escalate_tools=..., approver=...) asks your approver before a matching tool runs, and the approval covers that call's exact arguments only. A denial, no answer within escalation_timeout_seconds, arguments changed while it waited or no approver refuses the call. @requires_human_oversight does the same for a plain function. Guide, tests, timeouts.
  • Approvals given ahead of time go in an InMemoryApprovalLedger or SQLiteApprovalLedger with an expiry, and one call claims each, across processes with SQLite. The LangGraph, OpenAI Agents and PydanticAI adapters record theirs there. Tests.
  • EffectGuard runs each tool_call_id at most once. A retry after a lost response gets the stored result, and it refuses a call whose outcome is unknown unless the tool is @reversibility("reversible"). Guide, tests.
  • ActionContradictionGate holds back privileged tools once an operator-defined signal says the agent saw contradicting evidence. Tests.

None of this undoes an action that already ran, and airlock ships no approval channel: you connect the approver to Slack, a ticket queue or a prompt.

Token audience and startup hooks

Two checks that run before any argument is read:

  • 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 call its config guards 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.

Shell and installer arguments

Nine MCP server CVEs published on 27 and 28 October 2026 share one shape: a string argument of an ordinary tool reaches a shell or a package installer on the server. CVE-2026-117558 (ai-sdlc, repo), CVE-2026-117559 (DroidMind), CVE-2026-117560 (ATXP), CVE-2026-117561 (sf-mcp), CVE-2026-117567 (esp-mcp, target, port, project_path), CVE-2026-117568 (mcp-ffmpeg), CVE-2026-117569 (Kali-GPT), CVE-2026-117573 (hagent, args) and CVE-2026-59176 (functype-mcp-server, version into pnpm add). ToolArgSinkGuard, off by default, reads those arguments on the call path, before the call reaches a server that builds a command line:

from agent_airlock import Airlock, SecurityPolicy, ToolArgSinkGuard

guard = ToolArgSinkGuard(shell_args={"repo"}, installer_args={"version"})


@Airlock(policy=SecurityPolicy(tool_arg_sink_guard=guard))
def check_issue(repo: str, version: str = "1.4.4") -> str:
    return f"checked {repo}"

repo="x; id" is refused (block_reason "tool_arg_sink"), and so is a version that is not a bare PEP 440 or npm semver version: a URL, a path, file: or npm:, pkg[extra]. screen_all_str_args=True screens every argument except free_text_args; policy_presets.tool_arg_sink_defaults() does that in warn mode first. Details: Shell and installer arguments.

dispatch_args covers an argument that names a function the tool then calls. Agno's PandasTools passed read_pickle to getattr(pandas, ...) that way (CVE-2026-119050): no shell syntax for the check above to see. ToolArgSinkGuard(dispatch_args={"operation": ("head", "describe")}) admits those names only; an empty allowlist refuses private names, *pickle*, read_*, to_*, eval, exec and their like. Preset: policy_presets.reflection_sink_defaults(dispatch_args=...), warn mode first. Details: Dispatch arguments.

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 ~3µs1 python -m benchmarks.blockrate · re-run 2026-11-10 · 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-11-10 · results
Least-privilege · ToolPrivBench, 100 scenarios (subset) 100% over-privileged blocked · 100% low-privileged allowed · OPUR 100% → 0% python -m benchmarks.toolprivbench · re-run 2026-11-10 · 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-11-10 · 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-11-10 · 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-11-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 too (claude-code 0/36, codex 0/5 before its plan's usage limit stopped it), so this is indifference to the channel, not detection python -m benchmarks.harness_injection · re-run 2026-11-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 python -m benchmarks.mcp_conformance.run · re-run 2026-11-10 · results

Table notes

Every airlock arm in rows 1 to 6 runs a least-privilege SecurityPolicy(default_deny=True) with only the tools the task needs on its allowlist (row 1's argument items go to the guard chain, row 5's G0 tier is contract-only), not the library defaults, under which an empty allowlist allows every tool.

  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 200/200 policy parity was vacuous: every policy item was refused on both paths (corrected in the results). Isolation-backend execution not run here: no E2B in the runner.
  3. AgentDojo v1.2.1 (its CLI defaults to v1.2.2): 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 (2026-10-16) 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

The decorator is in-process: not a proxy, a gateway or a sidecar. It runs inside the process that executes the tool, the only place the real Python arguments exist. A gateway authenticates identity and transport and sandboxes the server, but it does not check a tool call's argument contract: live, a Docker MCP Gateway forwarded all 12 malformed payloads that airlock blocks (row 6 above).

Framework support

Both paths use the same @Airlock() decorator. Adapter-shipped means a module in the package handles the framework's glue (signature preservation, tool-registry rewrites, request shapes, approvals); example-only means the plain decorator works.

Adapter-shipped (16) modules for 12 frameworks: LangChain (integrations/langchain.py), LangGraph (integrations/langgraph.py at the ToolNode, integrations/langgraph_toolnode_compat.py), OpenAI Agents SDK (integrations/openai_guardrails.py, integrations/openai_agents.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, integrations/pydantic_ai_approvals.py, v0.7.1+), CrewAI (integrations/crewai.py, v0.7.2+), Google ADK (integrations/google_adk.py, integrations/google_adk_plugin.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_spud_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. Each row's evidence (guard module, preset, test, measured rate) is 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, source-to-sink data-flow 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 (list_all_tools_pinned judges a paginated listing as one), attested admission, the malware-advisory blast list, and 73 CVE/advisory regression tests in tests/cves/ (the 73 distinct CVEs the suite replays are published in the generated catalog: 65 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 (an empty or path-like tenant id is refused), conversation tracking Partial
ASI07 Insecure Inter-Agent Communication A2A guard, MCP proxy guard, transport validation Partial
ASI08 Cascading Failures RunGuard loop detection and run halt, amplification budget, circuit breaker, retry policy Full
ASI09 Human-Agent Trust Exploitation Human-oversight decorator, elicitation guard and request window (JSON-RPC 7 and "7" are two requests), 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. Each row's evidence (modules, presets, tests, where each control runs) is in docs/owasp-mcp-top-10-coverage.md, generated from YAML and byte-diffed in CI.

Risk Primary controls Coverage
MCP01 Token Mismanagement & Secret Exposure MCPProxyGuard rejects passthrough headers and enforces audience; ArgumentSecretGuard refuses secrets in arguments; sanitizer masks secrets in tool output Partial
MCP02 Privilege Escalation via Scope Creep SecurityPolicy(require_agent_id=True), capability gating, CredentialScope, runtime capability-union deny at grant time Partial
MCP03 Tool Poisoning Ghost-arg rejection, SafePath / SafeURL, mcp_description_manifest_guard_defaults(), install_* deny Partial
MCP04 Software Supply Chain Attacks & Dependency Tampering stdio_guard_ox_defaults() (Ox 2026-04-16 advisory), download_plugin_* / fetch_plugin_* deny Partial
MCP05 Command Injection & Execution stdio_guard shell-metachar and deny-pattern rules, ToolArgSinkGuard and ShellCommandPolicy on arguments, exec_* / run_* / system_* deny, CodeSandboxBridgeGuard for code-tool sandbox escapes, E2B sandbox (sandbox=True) 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 (a redirect URI with a fragment is refused, RFC 6749 section 3.1.2), iss mix-up validation, issuer and resource in discovered metadata bound to the URLs the client asked for (RFC 8414 and RFC 9728 section 3.3), 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, hash-chained decision log, OpenTelemetry export, spans and metrics Partial
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 Partial

CLI

One CLI: airlock <command> (since v0.8.56)

airlock scan-tools ./tools.json     # static contract check on tool declarations
airlock doctor                      # scan code for unprotected tools, unsafe patterns
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 pages: docs/cli/.

Performance

Metric Value Detail
Policy decision ~3µs p50 the policy or guard check alone (block-rate run), not a whole @Airlock call
Framework integrations 14 see above
Core dependencies 0 beyond Pydantic (and tomli on Python 3.10); 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.
  • 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.33

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.33
File Size Uploaded Provenance Actions
agent_airlock-0.10.33.tar.gz 1.0 MB With provenance Details for agent_airlock-0.10.33.tar.gz

Built distribution (wheel)

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

Total release size: 2.2 MB

Release files / agent_airlock-0.10.33.tar.gz

Download URL agent_airlock-0.10.33.tar.gz
Size 1.0 MB
Tags Source
SHA-256 checksum
How to use checksums
fd1831534140822def2d8a9fa620ff8a94cd49c43ee4fadcf4dd3895040f36a9
BLAKE2b-256 checksum
How to use checksums
de46e321cea3b1524b5a5286154e8219d2e4adf6ba4538474aaa10473f050487
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 Nov 10, 2026.

Transparency log

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

Download URL agent_airlock-0.10.33-py3-none-any.whl
Size 1.2 MB
Tags Python 3
SHA-256 checksum
How to use checksums
ccccd1864a39a1f5de27e2cc2cd736bac82a6b0ffee5fb7c3b350d4097074906
BLAKE2b-256 checksum
How to use checksums
aef4f8b447414ae05103db4d6c07d2e7c595d95d5463128c7f3f78af1860f80d
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 Nov 10, 2026.

Transparency log

Security for agent-airlock 0.10.33

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.33.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 Nov 10, 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.33 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