Audit data controls (Claude Agent SDK)

Premium

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

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/audit-data-controls-claude

Brings in: audit-log, button, card-registry, chat, claude-agent-server, composer, data, data-controls, data-retention, input, locked, message, prompt-suggestions, standard-cards, team, 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 data page's view for whoever is asking, or ?download=EXP-001 for a prepared export's file.

No props.

Also takes the props of Request.

<POST>

One thing a person did on the page. The store checks who they are and what they typed, and logs it either way.

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.

<DataPage>

The person's side: everything that happened, how long data is kept, and export, retention and delete.

No props.

<Page>

No props.

<AuditDataChat>

A chat with an agent that reads the audit log and can prepare an export. It can ask to delete data or change retention, but you allow the request here, and an owner confirms it on the data page.

PropTypeDefaultDescription
chatrequiredDataChat—The chat state from useCandorChat (AI SDK) or useClaudeAgent (Claude Agent SDK); the block works with either.
dataHrefrequiredstring—Where the data controls page lives, linked from the chat.

Functions

dataToolTitle(tool: string, input: unknown)

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

connectAuditDataDb(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.

drizzleAuditDataRepo(db: AuditDataDb)
seedAuditData(db: AuditDataDb)

Load the demo's threads, retention setting and first audit rows, leaving existing data alone: rows that exist are kept, and the audit rows are loaded only into an empty log. Run it once after migrating, e.g. from a script: await seedAuditData(connectAuditDataDb(process.env.DATABASE_URL!)).

demoData()

The demo's data, for the in-memory store and for seeding a database (./db/seed.ts).

memoryAuditDataRepo()

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.

setAuditDataRepo(repo: AuditDataRepo)

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

auditDataRepo()

The storage in use: whatever setAuditDataRepo() was given, else Postgres when DATABASE_URL is set, else memory.

contextOf(request: Request)

Who is asking. Replace with your auth; the demo reads a cookie the data page sets.

nameOf(id: string)

Turns an id on an audit row into a name, when it's known. Unknown ids stay ids.

approvalRequested(ctx: Ctx, tool: string, key?: string)

The agent asked for the person's go-ahead on a tool call. key (the tool call's id) makes it count once.

approvalDecided(ctx: Ctx, tool: string, decision: "approved" | "rejected" | "expired", key?: string)

The person's answer to that request. key makes it count once (the AI SDK sends the answer back with every later message). An answer nobody gave, because the run ended first, is recorded as expired.

findEntries(ctx: Ctx, input: { actor?: string | undefined; action?: string | undefined; refusalsOnly?: boolean | undefined; limit?: number | undefined })

Search the log. The search itself is written to the log.

summarise(ctx: Ctx, input: { days?: number | undefined })

Count the log. Counts are of rows that exist; nothing is estimated.

prepareExport(ctx: Ctx, by: string = dataAgent.id)

Runs once the person allowed it in the chat: the export is made, and the person downloads it from the page.

requestDeletion(ctx: Ctx, reason: string)

Runs once the person allowed it in the chat. Nothing is deleted: an owner has to confirm on the data page.

requestRetention(ctx: Ctx, input: { days: number; reason: string })

Runs once the person allowed it in the chat. Nothing changes until an owner confirms on the data page.

wouldRemove(days: Retention)

What a retention period would remove now: counted from the threads, not estimated.

exportNow(ctx: Ctx)

A person exports from the page. Their click is their own go-ahead; the export row names them.

downloadExport(id: string)
deleteHistory(ctx: Ctx, confirm: string)

Delete every thread. Owners only, and only with the account's name typed exactly.

setRetention(ctx: Ctx, days: Retention, confirm?: string)

Change how long threads are kept. Owners only; a period that removes threads needs the account's name typed.

withdrawRequest(ctx: Ctx, id: string)

An owner turns down a request the agent filed.

pageFor(ctx: Ctx)
resetStore()

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

Types

AuditDataDb
PgDatabase<PgQueryResultHKT, any>
Thread
{ id: string; at: string }
RetentionSetting
How long threads are kept. `version` counts the changes, starting at 1.
{ days: Retention; version: number; changedAt: string }
ExportRecord
A prepared export. The file is made from the data when it's downloaded.
{ id: string; at: string; by: string; threads: number }
DataRequest
{ id: string kind: "deletion" | "retention" /** For a retention change: days to keep threads, or null for until they're deleted. */ days?: Retention | undefined reason: string state: "pending" | "done" | "withdrawn" at: string }
Retention
(typeof retentionChoices)[number]
Ctx
Who is acting, and the id of the HTTP request they acted in (it goes on every audit row).
{ viewer: Person; requestId: string }
DataRequestCard
{ id: string; title: string; reason: string; effect: string; undo: string; href: string }
DataChat
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 }