Skip to main content
Back to projects

Agent IAP

The agent makes the call. The credential never leaves the proxy.

An identity-aware proxy for LLM agents. Give an agent an API or an MCP server without giving it the key — an ACL decides what may go out, a human answers what the ACL cannot, and every call lands in a hash-chained log naming the agent and the rule that let it through.

iap.toml pick one

what the agent gets back

GET /demo/v1/models → 200
{
  "path": "/v1/models",
  "x-api-key seen by the upstream": "sk-the-real-api-key",
  "authorization seen by the upstream": null
}

rust ·0 credentials in the agent·~60 service profiles·HTTP and MCP·hash-chained audit log

Run it

Homebrew on macOS or Linux, or the static binary, or a distroless image. The tap is the repository itself, so there is no second homebrew- repo to keep in step.

brew tap vpetersson/agent-iap https://github.com/vpetersson/agent-iap
brew trust vpetersson/agent-iap
brew install agent-iap

Then five commands and a proxy. There is no editor step: every one of them validates the edit before it writes, and a rejected flag leaves the file exactly as it was.

agent-iap init
agent-iap upstream add anthropic \
    --base-url https://api.anthropic.com \
    --auth header --header x-api-key --secret op://Private/Anthropic/credential
agent-iap acl add --target anthropic --methods POST --paths /v1/messages --action allow
agent-iap agent add claude-code --target anthropic
agent-iap run

agent add prints the agent’s token once and copies it to your clipboard. Point the agent at http://127.0.0.1:8080/anthropic with that token where the API key used to go, and nothing else about it changes.

What the agent finds

An agent handed a token and an address has everything it needs and no idea what to do with it. So the root of the proxy answers that question, generated from the policy that is actually running, for the agent that asked:

$ curl -sH "Authorization: Bearer $IAP_TOKEN" http://127.0.0.1:8080/
# agent-iap

You have reached agent-iap at `http://127.0.0.1:8080`, as `claude-code`.
…
## What you can reach

### `demo`

- proxied at `http://127.0.0.1:8080/demo/<path>`
- the proxy attaches the credential — `header x-api-key` — and drops any you send

#### Rules that apply to you, in order

- **allow** `acl[0]` — GET `/v1/models`
- **ask a human** `acl[1]` — POST `/v1/messages`

Two agents on one proxy are told two different things, and neither is told anything it could not have learned by making one refused call.

MCP, both directions

The proxy is itself an MCP server: add http://127.0.0.1:8080/_iap/mcp to a client and the APIs behind it arrive as tools, with skills built from the running policy rather than a README that goes stale the first time a rule changes.

It also fronts somebody else’s MCP server — remote ones as an ordinary upstream, stdio ones through a bridge that spawns the real server with the credential in its environment and asks the daemon to rule on every JSON-RPC message. A tools/call shows up in the same audit log and the same approval queue as an HTTP request.

On a laptop, or on a box

agent-iap run on a terminal is the approval console: the queue, the rule list, the enrolment forms and a live tail of the audit log. Under systemd or in a container there is no terminal to draw on, so it is the log stream it always was, and an ask is answered over the control plane or denied. Either way the policy file is watched — enrolling an agent, revoking one, rotating a leaked token or renewing a certificate all land in the running proxy without a restart.

The reference documentation lives with the code: the policy file, the service profiles, TLS, deployment, and the security model with the list of what it deliberately does not give you.

What you get

The key stays in the proxy

The agent holds a token this proxy minted. It authenticates nowhere else, revoking it rotates nothing, and only its sha256 is in the file. The real credential is read from 1Password — or the environment, or a file — inside the proxy and attached on the way out.

Little Snitch, for credentialed calls

A rule can park a call and raise a dialogue on your terminal — allow it once, for five minutes, for an hour, or from now on. Anything but once writes the rule, and writes it in front of the rule that asked.

A default never widens access

Adding a rule defaults to asking rather than allowing, and a grant has to name the service it is about. Enrolling a service grants nothing on its own — the first call stops at the console, and the rule comes out of the answer.

Every call is attributable

One JSON line per request — the agent, the rule that decided, the status, the duration — each committing to the line before it. Bodies are not logged, and credential headers are replaced before anything is written.

Tokens that expire with the job

An agent can trade its standing token for one scoped to the run in front of it, valid for an hour at most. Renewing rotates it, and a second holder presenting the superseded token kills the lineage.

About sixty services already worked out

Google service accounts, Cloudflare, Sentry, PostHog, Semrush, Linear, Slack, Stripe and the rest arrive with their base URL, credential scheme and scopes — and a note saying what that profile cannot reach.

MIT licensed, built in Rust. Source, issues, and images at ghcr.io/vpetersson/agent-iap.