# Managed Agents through Token Wallet

Token Wallet proxies Anthropic's [Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview) so app builders can run agents on behalf of a user **using that user's own Anthropic key**. The user pays Anthropic directly; Token Wallet meters tokens + session runtime against the budget the user granted the app on consent.

This doc is for app builders wiring managed agents through our proxy, and for anyone maintaining the agent plumbing inside the proxy itself.

---

## TL;DR

- Proxy forwards every `/v1/{agents,environments,sessions,vaults,files}/*` call to Anthropic with the caller's BYOK key.
- App must have the **`allowsManagedAgents` scope** when it was created. Without it the proxy returns `403 AGENT_SCOPE_MISSING`.
- Session rows are persisted in `agent_sessions` for attribution and budget finalisation.
- Session archive debits the access-token's budget by (tokens + `$0.08/hr` × total_running_ms).
- When the user revokes an access token, the proxy interrupts + archives every running session attached to it.
- Streaming (`/events/stream`) is relayed verbatim as SSE. No buffering.

---

## Lifecycle, end-to-end

### Provisioning (once per `(user, app)`)

App calls the proxy with the user's access token:

```http
POST /anthropic/v1/agents
Authorization: Bearer <aliceAccessToken>
Content-Type: application/json

{
  "name": "Life Coach",
  "model": "claude-haiku-4-5",
  "system": "You are a terse life coach.",
  "tools": [{"type": "agent_toolset_20260401"}]
}
```

The proxy:
1. Resolves Alice's token data.
2. Rejects if `apps.allows_managed_agents = false` on this app (→ `403 AGENT_SCOPE_MISSING`).
3. Enforces budget + model-allowlist.
4. Adds `anthropic-beta: managed-agents-2026-04-01`, forwards with Alice's Anthropic key.
5. Returns Anthropic's response untouched (`{ id: "agent_…", version: 1 }`).

Environments and vaults follow the same shape (`POST /anthropic/v1/environments`, `POST /anthropic/v1/vaults`).

### Running a session

```http
POST /anthropic/v1/sessions
Authorization: Bearer <aliceAccessToken>
Content-Type: application/json

{
  "agent": "agent_abc",
  "environment_id": "env_xyz",
  "title": "daily checkin 2026-04-20"
}
```

On success the proxy inserts a row into `agent_sessions`:

| column | value |
|---|---|
| `access_token_id` | Alice's token → used for attribution + revoke cleanup |
| `anthropic_session_id` | Anthropic's `sesn_…` id |
| `anthropic_agent_id` | denormalised from the request |
| `status` | `idle` |
| `title` | the session title, if any |

Sending events and streaming responses are verbatim passthroughs:

```http
POST /anthropic/v1/sessions/sesn_xyz/events
GET  /anthropic/v1/sessions/sesn_xyz/events/stream      # SSE
```

### Teardown

```http
POST /anthropic/v1/sessions/sesn_xyz/archive
```

After the forward succeeds, the proxy:
1. Flips the DB row to `status = 'archived'`, sets `archived_at`.
2. Fires a `GET /v1/sessions/sesn_xyz` on Anthropic to read cumulative token usage + `total_running_ms`.
3. Computes `cost = token_cost + runtime_cost` where:
   - `token_cost = input_tokens * pricing.inputPerToken + output_tokens * pricing.outputPerToken` (via catalog `pricingFor('anthropic', model)`)
   - `runtime_cost = (total_running_ms / 1000) * ($0.08 / 3600)`
4. Writes a `usage_logs` row with `model = "<vendor-model>+agent_runtime"` and debits the access token's `budget_spent`.

The debit step is idempotent — it skips if `cost_usd > 0` already.

### Revoke cleanup

When the user revokes a connection (either via dashboard or `DELETE /api/connections/:id`), the dashboard fires `cleanupSessionsForAccessToken`:

```
1. SELECT * FROM agent_sessions WHERE access_token_id = ? AND status ∈ {idle, running}
2. Decrypt the user's Anthropic key (dashboard already holds the masterSecret).
3. For each session:
     POST /v1/sessions/<id>/events   { "type": "interrupt" }
     POST /v1/sessions/<id>/archive
     flip DB row to 'archived'.
```

Without this, sessions keep ticking `$0.08/hr` on Anthropic even after the user pulled the plug.

---

## Consent scope

A brand-new app in the wallet **does not** get the managed-agents scope. The creator must pass `allowsManagedAgents: true`:

```ts
await admin.apps.create({
  name: 'Life Coach',
  callbackUrl: 'https://lifecoach.ai/callback',
  requiredProviders: ['anthropic'],
  allowsManagedAgents: true,      // ← required
});
```

The flag is persisted on `apps.allows_managed_agents`, cached on the proxy's `TokenData`, and checked on every agent-surface request. A missing scope produces:

```json
HTTP/1.1 403 Forbidden
{ "error": { "code": "AGENT_SCOPE_MISSING",
             "message": "This app does not have the managed-agents scope. …",
             "source": "proxy" } }
```

Why it's separate from the existing `requiredProviders` consent: managed agents run **long-lived, billable server-side work** (minutes to hours). The default OAuth consent screen contemplated one-shot inference requests; agents deserve an explicit yes.

---

## Billing

Two meters apply per session:

| meter | rate |
|---|---|
| tokens (input, output, cache-read) | same as Messages API — per the [catalog pricing](../packages/shared/src/catalog/bindings/anthropic.ts) |
| session runtime | `$0.08/hr`, per millisecond, only while Anthropic reports `status = running` |

Tool pass-throughs (web search `$10/1k`, etc.) are **not** meterred today — Anthropic rolls them into the session's cumulative usage but the proxy doesn't break them out yet.

Both meters are settled **at archive time** from the single `GET /v1/sessions/:id` poll. There's no real-time interrupt when budget is exceeded; callers may overshoot mid-session. Design choice, documented.

---

## Known limitations

1. **Overshoot accepted.** Budget is checked pre-call. Once Anthropic starts running a session, there's no hard cut-off before archive. A $1 budget can run a $1.50 session. Revoke is the user's lever.
2. **Suspend does not stop Anthropic-side work.** Suspend blocks new calls through our surface; existing sessions keep ticking. Only revoke triggers the interrupt+archive cascade.
3. **Model allowlist is enforced on `POST /agents` only.** A session inherits the model from the agent; we don't re-enforce at event time.
4. **App-level cost attribution via `session.title`** is not auto-injected yet. If users query Anthropic's Admin API directly, they won't see "which TW app ran this session" unless the app wrote its own breadcrumb.
5. **Thinking signatures** that Anthropic adds to thinking blocks **are not preserved** when a session's transcript is replayed across vendor tools. Multi-turn tool flows should stay on the same vendor.
6. **Tool pass-throughs (`web_search_requests`, `bash` etc.)** are not separately metered today; they're folded into the token bucket at archive.

---

## Testing

- `packages/proxy/src/routes/__tests__/agent-paths.test.ts` — pure path/beta-header logic (37 checks).
- `packages/proxy/src/services/__tests__/agent-session-cost.test.ts` — cost math (11 checks).
- `integration-tests/managed-agents.ts` — live end-to-end against the dev stack:
  1. Scope missing → 403.
  2. Scope granted → agent + environment + session create, DB row persisted.
  3. Archive → DB row flipped + cost finalised.
  4. Revoke on a running session → DB row auto-archived via cleanup path.

Run all:

```bash
pnpm --filter @token-wallet/proxy exec tsx src/routes/__tests__/agent-paths.test.ts
pnpm --filter @token-wallet/proxy exec tsx src/services/__tests__/agent-session-cost.test.ts
pnpm --filter @token-wallet/integration-tests managed-agents
```

The integration test pulls Alice's encrypted Anthropic key from `packages/shared/src/config/broder.ts`. It creates a real (cheap — metadata only) agent, environment and session in that account; it does **not** send billable events.

---

## Code map

| file | purpose |
|---|---|
| `packages/proxy/src/routes/agent-handler.ts` | HTTP pipeline (gates → vendor request → relay + tap) |
| `packages/proxy/src/routes/agent-paths.ts` | Pure path matchers + beta-header merge |
| `packages/proxy/src/services/agent-session-registry.ts` | Persist-on-create, flip-on-archive, finalize (tokens + runtime debit) |
| `packages/dashboard/src/services/agent-session-cleanup.ts` | Revoke → interrupt + archive cascade on Anthropic |
| `packages/shared/src/db/schema.ts` | `apps.allows_managed_agents` column + `agent_sessions` table |

---

## What the proxy does NOT do

- No SDK wrapper. App builders use the Anthropic SDK directly with `baseURL = <proxy>/anthropic`.
- No cross-flavor translation. Managed Agents is Anthropic-specific; `/tw/*` is the right entry point only for Messages-style calls that could route across vendors.
- No Admin API proxying. Cost reports require a per-user `sk-ant-admin-…` key; not something we hold.
