Skip to content

es617/agent-airlock

Repository files navigation

agent-airlock

npm version MIT License Node.js 20+ Zero Dependencies

A tiny host-side credential-injection proxy for AI agents.

Not a gateway. Not a vault. Not a sandbox. Not a policy engine. Just a small reverse proxy that keeps your upstream API keys on the host and gives agents a lower-value, revocable proxy token instead.

It works with today's ugly reality: static API keys, bearer tokens, and arbitrary HTTP endpoints. No upstream changes required.

Why

Most real APIs aren't going to adopt a new auth model just for agents. Your agent needs to call GitHub, Slack, Fireflies, or some internal service — and all of them hand you a static API key or bearer token.

The problem: if your agent has that key, it can exfiltrate it. Whether it's running in a sandbox, as a local coding assistant, or in a CI pipeline — if the agent can read an env var, a config file, or its own prompt, the key is exposed.

agent-airlock sits between the agent and the upstream API. The agent sends requests to the proxy with a scoped proxy token. The proxy validates the token, checks the request against an explicit allowlist, injects the real credential, and forwards it. The agent never sees the upstream key.

Agent                              agent-airlock                         Upstream API
  │                                    │                                    │
  │  POST /github/repos/o/r/issues    │                                    │
  │  Authorization: Bearer agent-tok   │                                    │
  │ ──────────────────────────────────→│                                    │
  │                                    │  POST /repos/o/r/issues            │
  │                                    │  Authorization: Bearer ghp_real    │
  │                                    │ ──────────────────────────────────→│
  │                                    │                                    │
  │                                    │◀──────────────── 201 Created ──────│
  │◀──────────────── 201 Created ──────│                                    │

What makes this different

There is growing activity in this space — MCP Secret Wrapper, Microsoft MCP Gateway, AgentGate, and others. The problem is real and people care about it.

agent-airlock is deliberately small and simple:

  • ~300 lines of core logic, zero runtime dependencies — easy to audit, easy to understand
  • Not MCP-specific — works with any HTTP API (REST, GraphQL, remote MCP, anything)
  • Local-first — runs on your machine, talks over Unix socket or TCP, no cloud dependency
  • Explicit allowlists — you declare exactly what's allowed, no hidden magic or implicit permissions
  • No platform lock-in — a JSON config file and a Node.js process, nothing else

Quick Start

Install globally from npm:

npm install -g agent-airlock

Or run directly with npx (zero install):

npx agent-airlock --config agent-airlock.json

Create a config file (see Configuration), then:

agent-airlock --config agent-airlock.json

From source

git clone https://github.com/es617/agent-airlock.git
cd agent-airlock
npm install
npm run build
npm start -- --config agent-airlock.json

Configuration

{
  "listen": {
    "unix": "/tmp/agent-airlock.sock"
  },
  "log": "/var/log/agent-airlock.log",
  "tokens": [
    {
      "id": "my-agent",
      "secret": "change-me-to-a-random-string",
      "expires": "2026-04-01T00:00:00Z"
    }
  ],
  "upstreams": {
    "github": {
      "url": "https://api.github.com",
      "auth": {
        "header": "Authorization",
        "value": "Bearer ghp_YOUR_TOKEN"
      },
      "allowlist": [
        "GET /repos/**",
        "POST /repos/*/issues",
        "PATCH /repos/*/issues/*"
      ],
      "denylist": [
        "DELETE /**"
      ]
    }
  }
}

See config.example.json for a full example with multiple upstreams.

Listen

Field Description
unix Unix socket path (Windows: auto-converted to named pipe)
host TCP bind address (default: 127.0.0.1)
port TCP port

Specify either unix or port.

Tokens

Multiple tokens are supported — each agent gets its own token with independent expiry and revocation.

Field Description
id Identifier shown in audit logs
secret Bearer token value the agent uses
expires Optional ISO 8601 expiry

Generate tokens with:

agent-airlock --generate-token
# agk_a7f3b9c1e2d4f5a6b7c8d9e0f1a2b3c4...

agent-airlock --generate-token --id my-agent
# {
#   "id": "my-agent",
#   "secret": "agk_a7f3b9c1e2d4..."
# }

Upstreams

Each upstream is a named route prefix. A request to /github/repos/foo is forwarded to https://api.github.com/repos/foo.

Field Description
url Upstream base URL
auth.header Header name to inject (e.g. Authorization)
auth.value Header value to inject (e.g. Bearer ghp_xxx)
allowlist Allowed METHOD /path patterns. Default-deny if empty.
denylist Denied patterns. Checked before allowlist.
graphql Optional body-aware filtering for GraphQL (see below)
jsonrpc Optional body-aware filtering for JSON-RPC/MCP (see below)

Path Patterns

  • * matches a single path segment: GET /repos/*/issues matches /repos/foo/issues
  • ** matches zero or more segments: GET /repos/** matches /repos, /repos/foo, /repos/foo/bar/baz

Body-Aware Filtering

For APIs where a single endpoint handles many operations (GraphQL, JSON-RPC/MCP), the proxy can inspect the request body to filter at the operation level. This is a coarse safety net, not fine-grained authorization — see Limitations.

GraphQL

{
  "allowlist": ["POST /graphql"],
  "graphql": {
    "allow": ["query *", "mutation createItem", "mutation updateItem"],
    "deny": ["mutation delete*"]
  }
}

query * allows any query. mutation delete* matches any mutation whose name starts with "delete". Operations not matching any allow rule are denied.

JSON-RPC / Remote MCP

{
  "allowlist": ["POST /mcp"],
  "jsonrpc": {
    "allow": ["tools/list", "tools/call:get_transcripts", "tools/call:search_*"],
    "deny": ["tools/call:delete_*"]
  }
}

Rules use the format method:tool_name. tools/call:get_* allows any tool call whose name starts with "get_".

Usage

From a sandboxed agent (OpenClaw)

  1. Start agent-airlock on the host with a Unix socket:
{ "listen": { "unix": "/tmp/agent-airlock.sock" } }
  1. Bind-mount the socket into the sandbox. A bind-mount makes a host file available inside the container. The agent inside the sandbox connects to the socket as if it were a local file — no network needed. In your OpenClaw gateway config, add the socket to the binds array:
{
  "agents": {
    "defaults": {
      "sandbox": {
        "docker": {
          "binds": [
            "/tmp/agent-airlock.sock:/tmp/agent-airlock.sock:rw"
          ]
        }
      }
    }
  }
}

This works even with "network": "none" — the sandbox stays fully network-isolated while the agent communicates with the proxy through the filesystem.

Important: After adding the bind-mount, restart the gateway (openclaw gateway restart) so new containers pick up the change. The socket is automatically created with world-readable permissions (chmod 777) so containers running as non-root users can connect.

  1. Tell the agent to use the socket. In your skill/tool description:
To call the GitHub API, make HTTP requests to the Unix socket at /tmp/agent-airlock.sock.
Use the Authorization header with "Bearer <your-agent-token>".
Example: curl --unix-socket /tmp/agent-airlock.sock -H "Authorization: Bearer agent-token" http://localhost/github/repos/owner/repo

From a local agent (no sandbox)

For agents running directly on the host (Claude Code, Cursor, etc.), use TCP or a Unix socket:

curl -H "Authorization: Bearer agent-token" \
  http://localhost:3100/github/repos/owner/repo

Note on HTTP security: All requests require a valid bearer token. However, plain HTTP on localhost means the token travels in cleartext. For stronger isolation, use a Unix socket — filesystem permissions ensure only your user can connect:

{ "listen": { "unix": "/tmp/agent-airlock.sock" } }

Error Responses

Errors generated by agent-airlock include an X-Agent-Airlock: error header and a "source": "agent-airlock" field, so agents can distinguish proxy errors from upstream errors:

{"error": "Forbidden: DELETE /repos/owner/repo", "source": "agent-airlock"}

Audit Log

All requests are logged as JSON lines to stdout. Optionally also to a file:

{ "log": "/var/log/agent-airlock.log" }

Each entry includes the token ID, so you can tell which agent made which request:

{"ts":"2026-03-09T12:00:00Z","token":"my-agent","method":"POST","upstream":"github","path":"/repos/owner/repo/issues","status":201,"ms":142}
{"ts":"2026-03-09T12:01:00Z","token":"ci-agent","method":"POST","upstream":"myapi","path":"/graphql","status":403,"ms":0,"graphql":"mutation deleteItem"}

Security Model

What agent-airlock does

It narrows the blast radius without requiring upstreams to change.

  • Credential isolation: Upstream API keys stay on the host. The agent only gets a revocable proxy token — lower value, independently rotatable, scoped to what you allow.
  • Default-deny: Requests not matching an allowlist rule are rejected.
  • Denylist priority: Deny rules are checked before allow rules.
  • Token scoping: Each agent gets its own revocable token with optional expiry.
  • No credential escalation: A compromised agent can't pivot to services it wasn't granted access to.

What agent-airlock does NOT do

  • Confused deputy: A prompt-injected agent can still misuse any operation it has legitimate access to. This is inherent to giving agents tools with side effects — no proxy, MCP server, or tool framework solves this.

  • Proxy token theft: The agent does hold a proxy token. If compromised, that token can be exfiltrated and replayed. The damage is scoped and revocable, but not zero.

  • Host compromise: If an attacker reaches the host, they can read the config with upstream credentials. The proxy protects agents from credentials, not the host from attackers.

  • Bad allowlist design: "POST /**" defeats the purpose. The proxy is only as good as your rules.

  • GraphQL filtering limits: Operation-level filtering by name is a coarse safety net. GraphQL requests can be unnamed, aliased, use persisted queries, or bundle fields under one operation in ways that exceed intent. Not a substitute for authorization.

  • Upstream tool trust: JSON-RPC/MCP filtering relies on upstream naming conventions being stable and honest. If a remote server hides a destructive operation behind an innocuous tool name, the proxy can't catch it.

Development

npm install
npm run dev -- --config config.example.json
npm test

License

MIT — see LICENSE

About

Credential-isolating reverse proxy for AI agents. Lets agents call APIs without seeing the keys.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors