Agent admin
PremiumThe agent, on the Vercel AI SDK, run with the settings an admin saved: only the tools not set to "never", "ask" tools behind the SDK's approval, the chosen model, effort and fallback, the step limit, and the month's spend limit. Set ANTHROPIC_API_KEY, and CANDOR_APPROVAL_SECRET so approvals can't be forged.
Preview
It runs with a server and an agent, so there’s no preview here: install it to run it in your app.
Install
First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/agent-admin
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: @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
<POST>
No props.
Also takes the props of Request.
<GET>
The agent's settings, as this viewer may see and change them.
No props.
Also takes the props of Request.
<POST>
One change, by an admin. The store checks the viewer's role and the value; the answer is the settings after it.
No props.
Also takes the props of Request.
<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.
| Prop | Type | Default | Description |
|---|---|---|---|
| chatrequired | ReleaseChat | — | The chat state from useCandorChat (AI SDK) or useClaudeAgent (Claude Agent SDK); the block works with either. |
| settingsHrefrequired | string | — | Where the settings page lives, linked from the chat so an admin can change what the agent may do. |
Functions
releaseToolTitle(tool: string, input: unknown)Plain-words titles for the agent's tools, the same for both agent SDKs.
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(db: AgentAdminDb)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()The demo's data, for the in-memory store and for seeding a database (./db/seed.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(repo: AgentAdminRepo)Use your own storage (or a database you opened yourself). Call it once, before the routes run.
agentAdminRepo()The storage in use: whatever setAgentAdminRepo() was given, else Postgres when DATABASE_URL is set, else memory.
viewerOf(request: Request)Who is asking. Replace with your auth; the demo reads a cookie the settings page sets.
saveInstructions(text: string, viewer: string)Save the text as a new version. Every earlier version is kept and can be restored.
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(id: string, permission: ToolPermission, viewer: string)setModelSettings(value: ModelSettingsValue, viewer: string)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(usd: number | null)Add one run's cost, as one increment. null means it couldn't be priced: counted as such, never guessed.
setSpendLimit(usd: number, viewer: string)spendStop()Why a run can't start, or null when it can.
runConfig(viewer: string)Everything a route needs to run the agent exactly as configured.
settingsFor(viewer: string)The settings page's view, for one viewer. Non-admins see every control locked, with why.
listDeploys(service?: string)setFlag(input: { environment: Environment; flag: string; on: boolean })startDeploy(input: { service: string; environment: Environment }, viewer: string)postUpdate(text: string)resetStore()For tests: start from the seed data again. On a database this deletes the block's tables' rows.
Types
- AgentAdminDb
PgDatabase<PgQueryResultHKT, any>- Version
- One saved version of the instructions. Versions are only ever added.
{ id: string; text: string; by: string; at: string }- Deploy
{ 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.
{ limitUsd: number; usedUsd: number; unpriced: number; month: string }- Environment
(typeof environments)[number]- ToolId
(typeof toolIds)[number]- ReleaseChat
- What either adapter hook returns (useCandorChat or useClaudeAgent): the block works with both.
{ 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 }