# Agent admin

An admin page for one agent: which tools it may use and which ask first, its model and effort, versioned instructions, and a monthly spend limit. The agent routes enforce every setting.

- Registry name: `agent-admin` (`@candor/agent-admin`)
- Tier: Premium. Installing it needs a Premium or Team licence key: the person runs `npx candor-ui key <key>` themselves; never ask for the key in chat or write it to a file.
- Docs: https://candor.themethodic.com/docs/agent-admin

## Install

```bash
npx candor-ui add agent-admin
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/agent-admin`.
It brings in: button, card-registry, chat, composer, entity-chip, input, instructions-editor, know, locked, message, model-settings, ops, prompt-suggestions, relative-time, standard-cards, thread, tool-permissions, usage-meters, use-candor-chat.
npm packages: @ai-sdk/anthropic@^4.0.96, @ai-sdk/react@^4.0.166, ai@^7.0.162, drizzle-orm@^0.45.4, lucide-react@^1.67.0, postgres@^4.0.0, zod@^4.6.7.

It installs under your app's `app/` folder and runs with a server and an agent: set `ANTHROPIC_API_KEY`, and read the comment at the top of its `store.ts` for storage and auth. Its files:

- `app/agent-admin/agent.ts`
- `app/agent-admin/api/route.ts`
- `app/agent-admin/api/settings/route.ts`
- `app/agent-admin/db/connect.ts`
- `app/agent-admin/db/drizzle-repo.ts`
- `app/agent-admin/db/schema.ts`
- `app/agent-admin/db/seed.ts`
- `app/agent-admin/page.tsx`
- `app/agent-admin/repo.ts`
- `app/agent-admin/settings/page.tsx`
- `app/agent-admin/store.ts`
- `app/agent-admin/ui.tsx`

## <Page>

No props.

## <SettingsPage>

The admin's side: what the agent may do, how it thinks, what it's told, and how much it may spend.

No props.

## <AgentChat>

A chat with the release agent, which runs with whatever its admin set on the settings page: the tools it may use, which of them ask you first, its model and effort, and the month's spend limit.

Props:

- `chat: ReleaseChat`: The chat state from useCandorChat (AI SDK) or useClaudeAgent (Claude Agent SDK); the block works with either.
- `settingsHref: string`: Where the settings page lives, linked from the chat so an admin can change what the agent may do.

## Functions

### releaseToolTitle

```ts
releaseToolTitle(tool: string, input: unknown)
```

Plain-words titles for the agent's tools, the same for both agent SDKs.

### connectAgentAdminDb

```ts
connectAgentAdminDb(url: string)
```

Open the block's database from a Postgres URL with the postgres-js driver. Swap the driver here (node-postgres, Neon's websocket Pool, …) if your app already uses another; it must support interactive transactions.

### drizzleAgentAdminRepo

```ts
drizzleAgentAdminRepo(db: AgentAdminDb)
```

### seedAgentAdminData

```ts
seedAgentAdminData(db: AgentAdminDb)
```

Load the demo's settings, tools, first instructions, deploys and flags, leaving any rows that already exist alone. Run it once after migrating, e.g. from a script: `await seedAgentAdminData(connectAgentAdminDb(process.env.DATABASE_URL!))`.

### demoData

```ts
demoData()
```

The demo's data, for the in-memory store and for seeding a database (./db/seed.ts).

### memoryAgentAdminRepo

```ts
memoryAgentAdminRepo()
```

In this server process's memory: gone on restart and not shared between instances. Transactions run one at a time, and a transaction that throws is rolled back to the state it started from.

### setAgentAdminRepo

```ts
setAgentAdminRepo(repo: AgentAdminRepo)
```

Use your own storage (or a database you opened yourself). Call it once, before the routes run.

### agentAdminRepo

```ts
agentAdminRepo()
```

The storage in use: whatever setAgentAdminRepo() was given, else Postgres when DATABASE_URL is set, else memory.

### viewerOf

```ts
viewerOf(request: Request)
```

Who is asking. Replace with your auth; the demo reads a cookie the settings page sets.

### saveInstructions

```ts
saveInstructions(text: string, viewer: string)
```

Save the text as a new version. Every earlier version is kept and can be restored.

### restoreInstructions

```ts
restoreInstructions(id: string, viewer: string)
```

Bring back an earlier version by saving its text as the newest one, so the restore can be undone too.

### setToolPermission

```ts
setToolPermission(id: string, permission: ToolPermission, viewer: string)
```

### setModelSettings

```ts
setModelSettings(value: ModelSettingsValue, viewer: string)
```

### costOf

```ts
costOf(model: string, usage: { inputTokens?: number | undefined; outputTokens?: number | undefined })
```

What one model call cost, or null when that can't be known (no price, or the provider didn't report usage).

### recordSpend

```ts
recordSpend(usd: number | null)
```

Add one run's cost, as one increment. null means it couldn't be priced: counted as such, never guessed.

### setSpendLimit

```ts
setSpendLimit(usd: number, viewer: string)
```

### spendStop

```ts
spendStop()
```

Why a run can't start, or null when it can.

### runConfig

```ts
runConfig(viewer: string)
```

Everything a route needs to run the agent exactly as configured.

### settingsFor

```ts
settingsFor(viewer: string)
```

The settings page's view, for one viewer. Non-admins see every control locked, with why.

### listDeploys

```ts
listDeploys(service?: string)
```

### setFlag

```ts
setFlag(input: { environment: Environment; flag: string; on: boolean })
```

### startDeploy

```ts
startDeploy(input: { service: string; environment: Environment }, viewer: string)
```

### postUpdate

```ts
postUpdate(text: string)
```

### resetStore

```ts
resetStore()
```

For tests: start from the seed data again. On a database this deletes the block's tables' rows.

## Types

### AgentAdminDb

```ts
PgDatabase<PgQueryResultHKT, any>
```

### Version

One saved version of the instructions. Versions are only ever added.

```ts
{ id: string; text: string; by: string; at: string }
```

### Deploy

```ts
{ id: string; service: string; environment: Environment; commit: string; by: string; at: string; status: "succeeded" | "failed" | "running" }
```

### Spend

The month's spend against the limit, as stored. `month` is YYYY-MM; a stored month that isn't this one counts as nothing spent.

```ts
{ limitUsd: number; usedUsd: number; unpriced: number; month: string }
```

### Environment

```ts
(typeof environments)[number]
```

### ToolId

```ts
(typeof toolIds)[number]
```

### ReleaseChat

What either adapter hook returns (useCandorChat or useClaudeAgent): the block works with both.

```ts
{ messages: MessageData[] composer: React.ComponentProps<typeof Composer> approvals: { id: string; tool: string; input?: unknown; title?: string | undefined; reason?: string | undefined }[] respond: (id: string, approved: boolean, reason?: string) => unknown error?: string | undefined }
```

## Rules for using Candor UI

- Components take plain typed data. Never pass model output as HTML or render it as a link; messages are typed blocks.
- A control that can't be used says why: wrap it in `<Locked reason="…">`, don't just disable it.
- Don't show a value you don't have: leave it out (it renders as —) rather than guess. An estimate is labelled as one.
- Nothing is pre-selected in a decision a person makes, and the agent is named from your app's data, never from model output.
- Your app's words go in `LocaleProvider` (fixed strings) and `TermsProvider` (nouns); entity kinds and agent roles in `EntityKindsProvider` and `AgentRolesProvider`.
