Approval-gated change (Claude Agent SDK)
PremiumThe person's answer to the agent's permission request: { id, approved, reason? }.
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/approval-gated-change-claude
Brings in: approval-queue, approval-request, button, card-registry, chat, claude-agent-server, composer, entity-chip, message, prompt-suggestions, standard-cards, thread, use-claude-agent, work · 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.
<GET>
The approval queue for whoever is asking.
No props.
Also takes the props of Request.
<POST>
A decision on one request: { id, hash, approved }. The hash ties it to the exact change the approver saw.
No props.
Also takes the props of Request.
<POST>
The agent, on the Claude Agent SDK. Set ANTHROPIC_API_KEY. Runs on one Node server, not spread over instances.
No props.
Also takes the props of Request.
<Page>
No props.
<QueuePage>
The approver's side: requests the agent created, and a decision bound to each one's exact change.
No props.
<ApprovalGatedChange>
A chat with an agent that changes service settings. It can propose a change and show what the change touches, but a different person approves it in the queue, and it applies only the exact change they approved.
| Prop | Type | Default | Description |
|---|---|---|---|
| chatrequired | ChangeChat | — | The chat state from useCandorChat (AI SDK) or useClaudeAgent (Claude Agent SDK); the block works with either. |
| queueHrefrequired | string | — | Where the approval queue lives, linked from the chat so people can decide on what the agent asked for. |
Functions
connectChangeDb(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.
drizzleChangeRepo(db: ChangeDb)seedChangeSettings(db: ChangeDb)Load the demo's settings into the database, leaving any that already exist alone. Run it once after migrating, e.g. from a script: await seedChangeSettings(connectChangeDb(process.env.DATABASE_URL!)).
memoryChangeRepo()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.
setChangeRepo(repo: ChangeRepo)Use your own storage (or a database you opened yourself). Call it once, before the routes run.
changeRepo()The storage in use: whatever setChangeRepo() 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 queue page sets.
readConfig(environment: Environment)requestChange(input: { environment: Environment; key: string; value: string; reason: string }, requester: string)Create a request for a second person to approve. Nothing changes yet.
listRequests(viewer: string)The queue as one viewer sees it: they can't decide on their own requests.
decide(id: string, hash: string, approved: boolean, viewer: string)Approve or reject exactly the change the approver saw, identified by its hash.
applyChange(id: string, viewer: string)Apply an approved request, if the setting is still what the approver saw.
resetStore()For tests: start from the seed data again. On a database this deletes the block's tables' rows.
changeToolTitle(tool: string, input: unknown)Plain-words titles for the block's tools, the same for both agent SDKs.
Types
- ChangeDb
PgDatabase<PgQueryResultHKT, any>- ChangeRecord
- One change request, as stored. store.ts turns it into the ApprovalRequest the UI shows.
{ id: string environment: Environment key: string /** The value when the request was made; the change applies only if it's still this. */ from: string to: string reason: string requester: string /** sha256 of [environment, key, from, to]; the approver's decision is bound to it. */ hash: string state: ApprovalState createdAt: string expiresAt: string decidedBy?: string | undefined decidedAt?: strin- NewChangeRecord
Pick<ChangeRecord, "environment" | "key" | "from" | "to" | "reason" | "requester" | "hash" | "createdAt" | "expiresAt">- ChangePatch
Partial<Pick<ChangeRecord, "state" | "decidedBy" | "decidedAt" | "staleReason" | "appliedBy" | "appliedAt">>- AuditEvent
{ at: string; who: string; what: string }- Environment
(typeof environments)[number]- ChangeChat
- 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 }