# Audit data controls

An audit log of everything the agent and people did, with export, retention and delete. The agent can search the log and prepare an export; deleting data or changing retention needs the person's go-ahead and their typed confirmation.

- Registry name: `audit-data-controls` (`@candor/audit-data-controls`)
- 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/audit-data-controls

## Install

```bash
npx candor-ui add audit-data-controls
```

Run `npx candor-ui@latest init` once first (https://candor.themethodic.com/get-started.md). Without the CLI: `npx shadcn@latest add @candor/audit-data-controls`.
It brings in: audit-log, button, card-registry, chat, composer, data, data-controls, data-retention, input, locked, message, prompt-suggestions, standard-cards, team, thread, use-candor-chat, work.
npm packages: @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.

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/audit-data-controls/agent.ts`
- `app/audit-data-controls/api/data/route.ts`
- `app/audit-data-controls/api/route.ts`
- `app/audit-data-controls/data/page.tsx`
- `app/audit-data-controls/db/connect.ts`
- `app/audit-data-controls/db/drizzle-repo.ts`
- `app/audit-data-controls/db/schema.ts`
- `app/audit-data-controls/db/seed.ts`
- `app/audit-data-controls/page.tsx`
- `app/audit-data-controls/repo.ts`
- `app/audit-data-controls/store.ts`
- `app/audit-data-controls/ui.tsx`

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

Props:

- `chat: DataChat`: The chat state from useCandorChat (AI SDK) or useClaudeAgent (Claude Agent SDK); the block works with either.
- `dataHref: string`: Where the data controls page lives, linked from the chat.

## Functions

### dataToolTitle

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

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

### connectAuditDataDb

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

```ts
drizzleAuditDataRepo(db: AuditDataDb)
```

### seedAuditData

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

```ts
demoData()
```

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

### memoryAuditDataRepo

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

```ts
setAuditDataRepo(repo: AuditDataRepo)
```

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

### auditDataRepo

```ts
auditDataRepo()
```

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

### contextOf

```ts
contextOf(request: Request)
```

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

### nameOf

```ts
nameOf(id: string)
```

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

### approvalRequested

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

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

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

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

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

### prepareExport

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

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

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

```ts
wouldRemove(days: Retention)
```

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

### exportNow

```ts
exportNow(ctx: Ctx)
```

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

### downloadExport

```ts
downloadExport(id: string)
```

### deleteHistory

```ts
deleteHistory(ctx: Ctx, confirm: string)
```

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

### setRetention

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

```ts
withdrawRequest(ctx: Ctx, id: string)
```

An owner turns down a request the agent filed.

### pageFor

```ts
pageFor(ctx: Ctx)
```

### resetStore

```ts
resetStore()
```

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

## Types

### AuditDataDb

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

### Thread

```ts
{ id: string; at: string }
```

### RetentionSetting

How long threads are kept. `version` counts the changes, starting at 1.

```ts
{ days: Retention; version: number; changedAt: string }
```

### ExportRecord

A prepared export. The file is made from the data when it's downloaded.

```ts
{ id: string; at: string; by: string; threads: number }
```

### DataRequest

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

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

### Ctx

Who is acting, and the id of the HTTP request they acted in (it goes on every audit row).

```ts
{ viewer: Person; requestId: string }
```

### DataRequestCard

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

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