brainstate

sattyamjjain/agent-airlock · source

Snapshot 0 · 2026-11-04T12:47:28Z · Python · Apache-2.0

Content fingerprint57415f704347047a92d9f2adee3132fc5fd1cd7ce9c331986499b4248ae5fcdd

GitHub description at capture

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.

README at capture

<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](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-BADGE-START -->
<!-- Auto-generated by scripts/update_test_badge.py. Do not edit by hand. -->
**Test suite:** 9,440 tests · **Coverage:** 91.35% · **v0.10.31**<br>
<sub>Coverage runs 9,421 of 9,440; 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](#quickstart) · [Benchmarks](#benchmarks) · [Frameworks](#framework-support) · [Docs](https://sattyamjjain.github.io/agent-airlock/) · [How numbers are reported](#how-numbers-are-reported-here)

</div>

---

## 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, code-sandbox host-bridge, 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](https://github.com/awslabs/mcp/security/advisories/GHSA-4prq-p2x5-w9fh).
- **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](docs/guide/token-audience-and-startup-hooks.md).

### 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](https://nvd.nist.gov/vuln/detail/CVE-2026-117558) (ai-sdlc, `repo`),
[CVE-2026-117559](https://nvd.nist.gov/vuln/detail/CVE-2026-117559) (DroidMind),
[CVE-2026-117560](https://nvd.nist.gov/vuln/detail/CVE-2026-117560) (ATXP),
[CVE-2026-117561](https://nvd.nist.gov/vuln/detail/CVE-2026-117561) (sf-mcp),
[CVE-2026-117567](https://nvd.nist.gov/vuln/detail/CVE-2026-117567) (esp-mcp, `target`, `port`, `project_path`),
[CVE-2026-117568](https://nvd.nist.gov/vuln/detail/CVE-2026-117568) (mcp-ffmpeg),
[CVE-2026-117569](https://nvd.nist.gov/vuln/detail/CVE-2026-117569) (Kali-GPT),
[CVE-2026-117573](https://nvd.nist.gov/vuln/detail/CVE-2026-117573) (hagent, `args`) and
[CVE-2026-59176](https://nvd.nist.gov/vuln/detail/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:

```python
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](docs/guide/shell-and-installer-arguments.md).

`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](https://nvd.nist.gov/vuln/detail/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()`.
Details: [Dispatch arguments](docs/guide/dispatch-arguments.md).

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**<sup>1</sup> | `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**<sup>2</sup> | `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)<sup>3</sup> | `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**<sup>4</sup> | `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**

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 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 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. 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 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 (13):** LangChain (`integrations/langchain.py`),
LangGraph (`integrations/langgraph.py` at the ToolNode, `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.

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

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

</details>

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, `CodeSandboxBridgeGuard` for code-tool sandbox escapes, 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 <command>` (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).

---

<div align="center">

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)

</div>