# Permissions, API keys, and the PAT

This page explains how AIronClaw decides who can do what, and how to mint client API keys with the right scope.

## Two kinds of credentials

| Credential | Issued by | Used by | Reaches | Lifetime |
|-----------|-----------|---------|---------|----------|
| **Personal Access Token (PAT)** | Dashboard → Profile → REST API Access | An agent calling `${BASE_URL}/api/*` on behalf of the user | The Next.js management API | Until rotated |
| **Client API key** | `POST /api/keys` (you, on behalf of the user) | An end-client (agent, app) calling an MCP tool or LLM completion | The gateway, then upstream MCP/LLM | Until deleted |

The PAT identifies the human user. Client keys identify an application using one or more of that user's proxies, with explicit scope tags.

The PAT is **never** valid against an MCP/LLM proxy URL (the gateway will return 401). A client key is **never** valid against `/api/*` management routes (the management routes only accept the PAT, marked by the gateway tag `aifw:personal:token` which a client key does not carry).

## How permissions are encoded

Every gateway key-auth credential carries a `tags` array. The plugin inspects these on each request to decide whether the call is authorised. The tags AIronClaw recognises:

| Tag                              | Meaning |
|---------------------------------|---------|
| `aifw:personal:token`            | This credential is the user's PAT (do not assign manually) |
| `name:<some-string>`             | Human label for the key |
| `private:permissions:user`       | Marks the consumer record itself (set on user creation, not on keys) |
| `private:permissions:mcp`        | Generic flag — the key can talk to MCP proxies (refined by `mcp:<id>:tool:*` tags) |
| `private:permissions:apikey:limit:N`   | Per-user quota: max keys the user can hold |
| `private:permissions:mcp:limit:N`      | Per-user quota: max MCP servers |
| `private:permissions:resource:limit:N` | Per-user quota: max resources in the resource repository |
| `private:permissions:mcp:max_resp_body_kb:N` | Per-user MCP response-body buffer cap in KB (default 512). Drives the static cache and any response rewrite — too small a cap on a tool that returns large payloads (e.g. fetch/scrape) yields a "Response too large" JSON-RPC error. |
| `private:permissions:llm:max_resp_body_kb:N` | Per-user LLM upstream-body buffer cap in KB (default 2048). Truncation is benign for LLM (just downgrades token-usage to the char/4 estimator), so raise only if accurate billing on very large completions matters. |
| `mcp:<mcp-uuid>:tool:<tool-name>` | Allow this key to call the named tool on the named MCP |
| `mcp:<mcp-uuid>:tool:*`            | Allow all tools on the named MCP |
| `mcp:<mcp-uuid>:resource:<rid>`    | Allow access to a specific MCP resource |
| `mcp:<mcp-uuid>:resource:*`        | Allow all resources on the MCP |
| `llm:<llm-uuid>:model:<model-id>`  | Allow this model on the named LLM proxy |
| `llm:<llm-uuid>:model:*`           | Allow any model on the named LLM proxy |

Two important properties:

1. **Tags are additive.** A key with no `mcp:<id>:tool:*` tag has no access to any tool on `<id>` — even if it has access to other MCPs. Default-deny per resource.
2. **Tags are forge-resistant via the tag-builder.** The `POST /api/keys` and `PATCH /api/keys/:id` endpoints accept structured input (`mcpPermissions`, `llmPermissions`) and *build* the tags server-side, cross-checking every UUID against the caller's owned set. Trying to inject a raw `mcp:<other-user-uuid>:tool:*` via `customTags` is rejected.

## Minting a client key — recipes

### A key for a single MCP, all tools

```json
POST /api/keys
{ "name": "stripe-prod", "mcpPermissions": [{ "id": "<mcp-id>", "tools": ["*"] }] }
```

### A key for a specific tool on a specific MCP

```json
{ "name": "stripe-readonly", "mcpPermissions": [{ "id": "<mcp-id>", "tools": ["search_customer", "get_invoice"] }] }
```

### A key spanning two MCPs

```json
{
  "name": "ops-bot",
  "mcpPermissions": [
    { "id": "<stripe-id>",  "tools": ["*"] },
    { "id": "<linear-id>",  "tools": ["search_issue", "create_issue"] }
  ]
}
```

### A key for an LLM proxy with a model whitelist

```json
{
  "name": "frontend-app",
  "llmPermissions": [{ "id": "<llm-id>", "models": ["gpt-4o-mini"] }]
}
```

Pair `mcpPermissions` and `llmPermissions` freely on the same key.

## Rotating a client key

There is no rotate endpoint — delete and recreate:

```bash
curl -fsS -X DELETE -H "Authorization: Bearer $T" "$URL/api/keys/$ID"
curl -fsS -X POST   -H "Authorization: Bearer $T" -H "Content-Type: application/json" \
     "$URL/api/keys" -d '{ "name": "<same-name>", "mcpPermissions": [...] }'
```

## Tightening permissions on an existing key

PATCH replaces the entire tag set computed from the new structured input. You cannot append or remove a single tag; always send the full intended state.

```bash
curl -fsS -X PATCH -H "Authorization: Bearer $T" -H "Content-Type: application/json" \
  "$URL/api/keys/$ID" \
  -d '{ "mcpPermissions": [{ "id": "<mcp-id>", "tools": ["search"] }] }'
```

(After this, the key only has `search` on that MCP — everything else previously granted is dropped.)

## Listing keys and their scopes

```bash
curl -fsS -H "Authorization: Bearer $T" "$URL/api/keys" | \
  jq '.keys[] | { id, name: (.tags[] | select(startswith("name:"))[5:]), tags }'
```

The PAT itself is filtered out of this listing; only client keys appear.

## Quotas

Defaults (overridable per account by an administrator):

- 10 MCP servers per user
- 40 client API keys per user

Quota breaches return 403 with a message like `MCP limit reached (10/10). Contact an administrator to raise your quota.`. Don't retry without first asking the user to clean up.

## Cascading deletes

When you `DELETE /api/mcp/:id`, every client key on that user has its `mcp:<id>:tool:*` and `mcp:<id>:resource:*` tags stripped automatically. Same for LLM proxies (`llm:<id>:model:*`). After the cascade, a key may end up with zero permissions — it remains valid as a credential but cannot reach anything until you PATCH new permissions in.
