Approval-gated change

Premium

The approval queue for whoever is asking.

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/approval-gated-change

Brings in: approval-queue, approval-request, button, card-registry, chat, composer, entity-chip, message, prompt-suggestions, standard-cards, thread, use-candor-chat, work · 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

<GET>

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 Vercel AI SDK. Set ANTHROPIC_API_KEY, and CANDOR_APPROVAL_SECRET so approvals can't be forged.

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.

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