Agent Airlock MCP Server

by sattyamjjain• 17•• Scanned•Claim this listing•Request removal
Coding Agentsai agentsai securitycrewaifirewalllangchain

A type-checker and contract layer for AI agent tool calls: in-process, Pydantic-only. Strict argument validation, ghost-argument stripping, and self-healing retries for MCP servers and agent frameworks.

Agent Airlock is an open-source mcp server by sattyamjjain, written mainly in Python, with 17 GitHub stars and the Apache-2.0 license. Its code was last updated Nov 1, 2026.

<div align="center">

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-BADGE-START --> <!-- Auto-generated by scripts/update_test_badge.py. Do not edit by hand. -->

Test suite: 10,969 tests · Coverage: 91.67% · v0.10.35<br> <sub>Coverage runs 10,950 of 10,969; 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).</sub>

<!-- TEST-BADGE-END -->

Quickstart · Benchmarks · Frameworks · Docs · How numbers are reported

</div>

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 sendsWhat happens
amount="500"Blocked — Pydantic V2 strict mode, no coercion. Returns a structured error carrying fix_hints the model can retry against
force=TrueStripped — 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.

LayerWhat it covers
ValidatePydantic V2 strict mode · ghost-argument stripping · self-healing fix_hints
AuthorizeRBAC · token-bucket rate limits · time windows · per-model-tier cost budgets · capability gating
IsolateFilesystem 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)
SanitizePII and secret masking, including opt-in Indic PII · output truncation · JSON-Lines audit log
MCP guardsPer-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.

BenchmarkResultReproduce
Cross-tool block-rate · 210 tool calls100% blocked (106/106) · 0% false-positive (0/104) · p50 ~3µs<sup>1</sup>python -m benchmarks.blockrate · re-run 2026-11-10 · results
sandbox=True dispatch · contract parity4/4 annotated-contract probes refused on the sandbox path · 204/204 verdicts agree with the local path<sup>2</sup>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 suites86.0% of injection→target tool-calls blocked (deterministic bound)<sup>3</sup>python -m benchmarks.agentdojo.run · re-run 2026-11-10 · results
Tool poisoning · MCPTox, 1,348 casesDirect execution of the poisoned tool refused 100% under the G (tool allowlist) and LP tiers<sup>4</sup>python -m benchmarks.mcptox replay · re-run 2026-11-10 · results
vs. native MCP gateway · 12 malformed payloadsairlock 12/12 blocked · Docker MCP Gateway 0/12 · 0% false-positive on bothpython -m benchmarks.vs_gateway · re-measured live 2026-11-03 · results
Prompt injection across agent harnessesA 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 detectionpython -m benchmarks.harness_injection · re-run 2026-11-03 · results
MCP spec conformance · @modelcontextprotocol/conformanceWire-path validators, outcome published in full. Not a full MCP server/client conformance pass — airlock is a request validator, not a serverpython -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.

<details> <summary><b>Every framework: adapter, doc and runnable example</b></summary>

Complete Examples

FrameworkPathSurface
LangChainadapter · example@tool, AgentExecutor
LangGraphadapter · exampleStateGraph, ToolNode
OpenAI Agentsadapter · exampleHandoffs, manager pattern
Anthropic APIadapter · exampleDirect Messages API
Claude Agent SDKadapter · docwrap_tools(tools, policy=...) for @tools, wrap_agent(agent, policy=...)
smolagentsadapter · exampleCodeAgent, E2B
Gemini 3adapterfunction_call carrier + thought_signature redaction
GPT-5.5adaptergpt_5_5_spud_agent_defaults preset
FastMCPadapter · example@secure_tool decorator
PydanticAIadapter · doc · examplewrap_agent(agent, policy=...) + output_validate hook
CrewAIadapter · doc · examplewrap_crew(crew, policy=...) + task-level tool overrides
Google ADKadapter · doc · examplewrap_agent(agent, policy) + tool_context relaxation
LlamaIndexexample onlyReActAgent
AutoGenexample onlyConversableAgent
</details>

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.

RiskPrimary controlsCoverage
ASI01 Agent Goal HijackSequence guard, action-contradiction gate, source-to-sink data-flow gate, tool-output trust guardPartial
ASI02 Tool Misuse and ExploitationStrict validation, ghost-arg stripping, SafePath / SafeURL, capability gatingFull
ASI03 Identity and Privilege AbuseRBAC, signed agent identity, capability-union boundary, privilege right-sizingPartial
ASI04 Agentic Supply Chain VulnerabilitiesDescription 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 guardsFull
ASI06 Memory & Context PoisoningAuto-memory provenance, cross-tenant isolation (an empty or path-like tenant id is refused), conversation trackingPartial
ASI07 Insecure Inter-Agent CommunicationA2A guard, MCP proxy guard, transport validationPartial
ASI08 Cascading FailuresRunGuard loop detection and run halt, amplification budget, circuit breaker, retry policyFull
ASI09 Human-Agent Trust ExploitationHuman-oversight decorator, elicitation guard and request window (JSON-RPC 7 and "7" are two requests), honeypot deceptionPartial
ASI10 Rogue AgentsAudit telemetry, anomaly detector, kill switch — no quarantine primitiveMonitor-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.

RiskPrimary controlsCoverage
MCP01 Token Mismanagement & Secret ExposureMCPProxyGuard rejects passthrough headers and enforces audience; ArgumentSecretGuard refuses secrets in arguments; sanitizer masks secrets in tool outputPartial
MCP02 Privilege Escalation via Scope CreepSecurityPolicy(require_agent_id=True), capability gating, CredentialScope, runtime capability-union deny at grant timePartial
MCP03 Tool PoisoningGhost-arg rejection, SafePath / SafeURL, mcp_description_manifest_guard_defaults(), install_* denyPartial
MCP04 Software Supply Chain Attacks & Dependency Tamperingstdio_guard_ox_defaults() (Ox 2026-04-16 advisory), download_plugin_* / fetch_plugin_* denyPartial
MCP05 Command Injection & Executionstdio_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 SubversionToolOutputTrustGuard envelopes injected-instruction output; @requires_human_oversight gates high-value actionsPartial
MCP07 Insufficient Authentication & AuthorizationOAuth 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-sidePartial
MCP08 Lack of Audit and TelemetryJSON-Lines audit log, hash-chained decision log, OpenTelemetry export, spans and metricsPartial
MCP09 Shadow MCP ServersAttested tool-server admission, LAN unauthenticated-MCP guardPartial
MCP10 Context Injection & Over-SharingPII and secret sanitizer, workspace-scoped config, ToolOutputTrustGuard untrusted-data envelopePartial

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

MetricValueDetail
Policy decision~3µs p50the policy or guard check alone (block-rate run), not a whole @Airlock call
Framework integrations14see above
Core dependencies0beyond 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 siteGuides, API reference, CLI, integrations
BENCHMARK.mdBenchmark summary and method
docs/cves/index.mdGenerated CVE catalogue
ROADMAP.mdWhat is not done, and why
PRIOR_ART.mdExternal research this rests on
CHANGELOG.mdRelease history
AGENTS.mdContributor 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.


<div align="center">

If this is useful, a ⭐ helps other people find it.

Report a bug · Request a feature · Discussions

</div>

Agent Airlock alternatives

View more →

Similar mcp servers in Coding Agents, ranked by GitHub stars. Compare licence, recent activity and scan status before you choose.

NameStarsLicenceLast updatedScan
Agent Airlock (this page)
sattyamjjain
17Apache-2.0Nov 1, 2026Scanned, no flagged issues
Context Mode
mksglu
33kNone foundNov 1, 2026Not scanned yet
Serena
oraios
31kNone foundOct 28, 2026Not scanned yet
Fastmcp
PrefectHQ
28kApache-2.0Nov 1, 2026Scanned, no flagged issues
Nuclear
nukeop
19kAGPL-3.0Nov 1, 2026Scanned, no flagged issues
MCP For Beginners
microsoft
18kMITOct 30, 2026Scanned, no flagged issues