# Agent Auth Plugin

`@better-auth/agent-auth` is the official server implementation of the [Agent Auth Protocol](https://agentauthprotocol.com). When you enable it, your Quickback worker becomes an Agent Auth provider: AI agents can register, request scoped capability grants from a real user, get approved through device-authorization or CIBA, and then call your API with short-lived signed JWTs.

> The upstream `@better-auth/agent-auth` package is in heavy development and not yet stable. The Quickback integration pins a specific version and exposes a deliberately small surface for v1 — see "Limitations" at the bottom.


## What gets enabled

Adding `agentAuth` to your auth config force-enables two more plugins:

| Plugin | Why |
|--------|-----|
| `bearer` | Lets clients send Better Auth session tokens via `Authorization: Bearer …` instead of cookies. The agent surface is bearer-only. |
| `oauthProvider` | OAuth 2.1 + OIDC provider so MCP clients can register, get tokens, and call your API as a real user. Replaces the deprecated `mcp` plugin. |

There's no useful Agent Auth deployment without these — the compiler auto-adds them so you don't trip over a confusing runtime error.

The full Bearer matrix on the agent surface (everything reached through `/mcp` and the agent-auth REST projection):

| Bearer flavor | Resolved by | Identity |
|---------------|-------------|----------|
| BA session token | `bearer` plugin | The signed-in user |
| OAuth access token | `oauthProvider` plugin | The user who consented at OAuth time |
| Agent JWT | `@better-auth/agent-auth` | The agent (acting on behalf of its bound user) |

Cookie auth is **not** forwarded into `/mcp` — bearer is the only channel, and CSRF surface is zero.

## Configuration

```ts
import { defineAuth, defineConfig, defineDatabase, defineRuntime } from "@quickback/compiler";

export default defineConfig({
  name: "acme-api",
  providers: {
    runtime: defineRuntime("cloudflare"),
    database: defineDatabase("cloudflare-d1"),
    auth: defineAuth("better-auth", {
      emailAndPassword: { enabled: true },
      plugins: {
        agentAuth: {
          providerName: "Acme",
          providerDescription: "Acme APIs for AI agents.",
          modes: ["delegated", "autonomous"],
          defaultHostCapabilities: ["GET", "HEAD"],
        },
      },
    }),
  },
});
```

The plain-flag form is also valid — use it when you don't need to override any defaults:

```ts
auth: defineAuth("better-auth", {
  plugins: ["agentAuth"],
})
```

### Options

| Option | Type | Default | Notes |
|--------|------|---------|-------|
| `providerName` | `string` | `env.APP_NAME` | Shown in the discovery doc and in the approval UI. |
| `providerDescription` | `string` | `"Agent-callable API powered by Quickback."` | Free-form description in `/.well-known/agent-configuration`. |
| `modes` | `("delegated" \| "autonomous")[]` | `["delegated", "autonomous"]` | Supported agent modes. |
| `defaultHostCapabilities` | `string[]` \| `true` | `["GET", "HEAD"]` | Capabilities auto-granted to a newly registered host. |

Most of the underlying plugin's surface (capability list, `onExecute`, custom approval-method resolver, `onEvent` audit hook) isn't projected through the compiler in v1 — see "Limitations".

## Routes

When the plugin is enabled the compiler emits:

- `GET /.well-known/agent-configuration` — discovery document with issuer, endpoints, supported modes, default location.
- All of the plugin's own endpoints, mounted by Better Auth under `/auth/v1/agent/*`.

The Account SPA gets a new route at `/agents/approve` for device-code approval. The plugin's `deviceAuthorizationPage` is automatically pinned at `${ACCOUNT_URL}/agents/approve`.

## Approval flow

1. The agent calls `/auth/v1/agent/register` to create itself, then requests a capability grant.
2. The user enters the device code at `${ACCOUNT_URL}/agents/approve` (or follows the deep-link the agent showed them).
3. The Account SPA fetches the pending request, shows the agent name + provider name + the capabilities being requested, with a "Step-up" badge on any capability whose `approvalStrength` is `"webauthn"`.
4. User clicks **Authorize** (or **Deny**). Approval grants the capabilities; denial leaves the request pending.
5. The agent polls or receives the token via the configured method, then calls capabilities with `Authorization: Bearer <agent-jwt>`.

You revoke an agent at any time from your account settings (or directly via the BA admin API).

## Schema

The plugin adds tables for `agent`, `host`, `grant`, and `approval`. They're created automatically by `auth migrate` / `auth generate` — no manual Drizzle work needed.

## OpenAPI capability projection (default on)

When `agentAuth` is enabled, the compiler emits `src/lib/openapi-spec.ts` (the project's full OpenAPI spec inlined as a const) and wires `createFromOpenAPI(openapiSpec, { ... })` into the `agentAuth(...)` call. Every operation with an `operationId` becomes a capability. No manual capability list required.

**How requests flow:**

1. Agent calls `POST /capability/execute` with its agent JWT.
2. The plugin verifies JWT + grant + constraints, then runs `onExecute`.
3. `onExecute` (the plugin's built-in proxy) makes a self-fetch to the worker's own URL (`env.BETTER_AUTH_URL`).
4. `resolveHeaders` mints a Better Auth JWT with `sub: agentSession.user.id` so the inner request looks like an authenticated user call.
5. The worker's JWT fast-path picks it up, populates `ctx`, and the full firewall + access + masking pipeline runs.

**Default approval strength:** GET → `"session"`, POST/PUT/PATCH/DELETE → `"webauthn"`. Override with the `approvalStrength` option.

**To opt out** and wire capabilities by hand:

```ts
auth: defineAuth("better-auth", {
  plugins: {
    agentAuth: { fromOpenAPI: false },
  },
})
```

When `fromOpenAPI: false`, the emitted call has `capabilities: []` and no proxy — you write the capability list + `onExecute` in your own `lib/auth.ts` overrides.

## Limitations (v1)

- **WebAuthn step-up isn't wired in the approval UI yet.** Capabilities tagged `approvalStrength: "webauthn"` show a Step-up badge but the actual escalation call isn't made — the upstream API for that escalation is still settling. Until it lands, those capabilities will fail server-side regardless of approval.
- **No CLI helpers for listing / revoking agents.** Use the BA admin API directly.
- **Self-fetch on Cloudflare Workers** adds one subrequest per capability call (~50ms). For typical agent traffic this is fine; for very high-frequency agents consider pinning a custom `onExecute` that uses `app.request()` for in-process routing.
- **`@better-auth/agent-auth` 0.4.5 ships ESM-only exports** without a `require` condition. The compiler image patches the package's `package.json` at install time so the Better Auth CLI's `auth generate` step (jiti / CJS) can resolve it. Remove the patch in the Dockerfile when upstream ships proper exports.

## Why bearer-only?

The MCP and agent surfaces are server-to-server by nature. Cookies on those surfaces add CSRF surface for no benefit (no browser callers), and `x-api-key` was the legacy `apiKey`-plugin path that fragments the auth model. Bearer is the one true channel here. If your project still wants API-key auth, configure the `apiKey` plugin to accept its key via `Authorization: Bearer` so everything stays one channel.
