# Approval-gated change (Claude Agent SDK)

An agent proposes a settings change and shows what it touches; a different person approves that exact change in a queue before it applies.

- Registry name: `approval-gated-change-claude` (`@candor/approval-gated-change-claude`)
- 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/approval-gated-change-claude

## Install

```bash
npx candor-ui add approval-gated-change-claude
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/approval-gated-change-claude`.
It 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 packages: @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.

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/approval-gated-change/api/approve/route.ts`
- `app/approval-gated-change/api/requests/route.ts`
- `app/approval-gated-change/api/route.ts`
- `app/approval-gated-change/db/connect.ts`
- `app/approval-gated-change/db/drizzle-repo.ts`
- `app/approval-gated-change/db/schema.ts`
- `app/approval-gated-change/db/seed.ts`
- `app/approval-gated-change/page.tsx`
- `app/approval-gated-change/queue/page.tsx`
- `app/approval-gated-change/repo.ts`
- `app/approval-gated-change/store.ts`
- `app/approval-gated-change/ui.tsx`

## <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.

Props:

- `chat: ChangeChat`: The chat state from useCandorChat (AI SDK) or useClaudeAgent (Claude Agent SDK); the block works with either.
- `queueHref: string`: Where the approval queue lives, linked from the chat so people can decide on what the agent asked for.

## Functions

### connectChangeDb

```ts
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

```ts
drizzleChangeRepo(db: ChangeDb)
```

### seedChangeSettings

```ts
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

```ts
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

```ts
setChangeRepo(repo: ChangeRepo)
```

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

### changeRepo

```ts
changeRepo()
```

The storage in use: whatever setChangeRepo() 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 queue page sets.

### readConfig

```ts
readConfig(environment: Environment)
```

### requestChange

```ts
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

```ts
listRequests(viewer: string)
```

The queue as one viewer sees it: they can't decide on their own requests.

### decide

```ts
decide(id: string, hash: string, approved: boolean, viewer: string)
```

Approve or reject exactly the change the approver saw, identified by its hash.

### applyChange

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

Apply an approved request, if the setting is still what the approver saw.

### resetStore

```ts
resetStore()
```

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

### changeToolTitle

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

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

## Types

### ChangeDb

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

### ChangeRecord

One change request, as stored. store.ts turns it into the ApprovalRequest the UI shows.

```ts
{ 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

```ts
Pick<ChangeRecord, "environment" | "key" | "from" | "to" | "reason" | "requester" | "hash" | "createdAt" | "expiresAt">
```

### ChangePatch

```ts
Partial<Pick<ChangeRecord, "state" | "decidedBy" | "decidedAt" | "staleReason" | "appliedBy" | "appliedAt">>
```

### AuditEvent

```ts
{ at: string; who: string; what: string }
```

### Environment

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

### ChangeChat

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`.
