Agent admin (Claude Agent SDK)

Premium

The person's answer to the agent's permission request: { id, approved, reason? }.

Markdown

Preview

It runs with a server and an agent, so there’s no preview here: install it to run it in your app.

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/agent-admin-claude

Brings in: button, card-registry, chat, claude-agent-server, composer, entity-chip, input, instructions-editor, know, locked, message, model-settings, ops, prompt-suggestions, relative-time, standard-cards, thread, tool-permissions, usage-meters, use-claude-agent · npm: @anthropic-ai/claude-agent-sdk@^0.3.318, 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.

<POST>

The agent, on the Claude Agent SDK, run with the settings an admin saved: only the tools not set to "never", "ask" tools left out of allowedTools so the person is asked, the chosen model, effort and fallback, the step limit, and what's left of the month's spend limit. Set ANTHROPIC_API_KEY. Runs on one Node server.

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.

PropTypeDefaultDescription
chatrequiredReleaseChat—The chat state from useCandorChat (AI SDK) or useClaudeAgent (Claude Agent SDK); the block works with either.
settingsHrefrequiredstring—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 }