> ## Documentation Index
> Fetch the complete documentation index at: https://docs.claw.boo/llms.txt
> Use this file to discover all available pages before exploring further.

# Governance API

> REST reference for governance: list/set USD budgets, resume a paused scope, read the forensic audit log, and route a delegation approval.

REST surface for Clawboo's spend [governance](/concepts/governance): the hard USD budget kill-switch (list / set / resume budgets), the append-only forensic audit log, the delegation-approval handshake, and the persisted approval-decision history. The budget kill-switch and audit are always on; budgets are uncapped by default (a budget row exists only when you create one), so no USD is enforced until you set a cap.

These routes live in `budgets.ts`, `governanceAudit.ts`, `delegationApproval.ts`, and `approvals.ts`. All POST routes read a JSON body parsed by `express.json({ limit: '2mb' })`.

<Note>
  The atomic spend increment that actually pauses a run happens inside the executor loop (`recordSpend` under `BEGIN IMMEDIATE`), not over REST. These routes are the human-facing surface: set a cap, read the ledger, resume a paused scope, and view the audit trail.
</Note>

## Routes

| Method | Path                                             | Summary                                                                                       | Stream? |
| ------ | ------------------------------------------------ | --------------------------------------------------------------------------------------------- | ------- |
| GET    | `/api/governance/budgets`                        | List every budget row                                                                         | No      |
| POST   | `/api/governance/budgets`                        | Set or raise a budget cap                                                                     | No      |
| POST   | `/api/governance/budgets/:scope/:scopeId/resume` | Resume a paused scope (human override)                                                        | No      |
| GET    | `/api/governance/audit`                          | Read the forensic audit log (filterable)                                                      | No      |
| POST   | `/api/governance/delegation-approval`            | Route a delegated child's risky action to the leader's approval queue (blocks until resolved) | No      |
| GET    | `/api/approvals`                                 | List persisted approval decisions                                                             | No      |
| POST   | `/api/approvals`                                 | Persist an approval decision                                                                  | No      |

<Info>
  Two distinct approval surfaces share this page. `POST /api/governance/delegation-approval` is the **live, blocking handshake** over the `tool_call_approvals` table; it waits for the leader to resolve (or for a TTL to expire). `/api/approvals` is a separate **decision-history log** over the `approval_history` table, a fire-and-forget CRUD of past allow/deny decisions. They do not write the same table.
</Info>

***

## `GET /api/governance/budgets`

Lists every budget row, newest-updated first. There is no filter on this route; the handler always returns the full list.

* **Path/query params**: none.
* **Request body**: none.

### Responses

**`200 OK`**: every budget row:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  budgets: Array<{
    id: string
    scope: 'agent' | 'mission' | 'team' | 'tenant'
    scopeId: string
    limitUsdCents: number
    spentUsdCents: number // whole-cent display mirror = floor(spentMicroCents / 10000)
    spentMicroCents: number // lossless sub-cent spend accumulator
    status: 'active' | 'soft_capped' | 'paused'
    mode: 'cap' | 'warn'
    tenantId: string | null
    createdAt: number
    updatedAt: number
  }>
}
```

A `cap`-mode budget auto-pauses the run at 100% of `limitUsdCents` (the kill-switch). A `warn`-mode budget (the default posture) records spend and emits warning events at the 80% / 100% crossings but never reaches `paused`; its status clamps to `soft_capped`.

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
curl http://localhost:18790/api/governance/budgets
```

***

## `POST /api/governance/budgets`

Sets a budget cap for a scope, or raises an existing cap. A new scope starts at spent 0 / `active`. Re-setting the limit recomputes status from the existing spend, so raising the cap above the current spend un-pauses the scope (the "raise the cap to resume" path). The body is validated by a zod schema.

* **Path/query params**: none.
* **Request body**:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  scope: 'agent' | 'mission' | 'team' | 'tenant'  // required
  scopeId: string                                  // required, min length 1
  limitUsdCents: number                            // required, positive integer (a 0 cap is rejected)
  mode?: 'cap' | 'warn'                             // default 'warn' (track-and-warn)
  tenantId?: string | null
}
```

<Note>
  A cap of `0` is rejected by the schema (`limitUsdCents` must be a positive integer). "Uncapped" is the *absence* of a budget row, not a 0 limit. There is no delete route; to make a scope uncapped again, leave it without a row, or set a large cap and resume it.
</Note>

### Responses

**`400 Bad Request`**: the body failed zod validation (wrong scope, missing `scopeId`, non-positive `limitUsdCents`, …):

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "invalid body", "details": {} }
```

`details` is the zod `flatten()` of the validation failure.

**`200 OK`**: the upserted budget row:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  budget: {
    id: string
    scope: 'agent' | 'mission' | 'team' | 'tenant'
    scopeId: string
    limitUsdCents: number
    spentUsdCents: number
    spentMicroCents: number
    status: 'active' | 'soft_capped' | 'paused'
    mode: 'cap' | 'warn'
    tenantId: string | null
    createdAt: number
    updatedAt: number
  }
}
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
# A $5.00 hard cap on a team (500 cents, cap-mode auto-pause)
curl -X POST http://localhost:18790/api/governance/budgets \
  -H 'Content-Type: application/json' \
  -d '{"scope":"team","scopeId":"<team-id>","limitUsdCents":500,"mode":"cap"}'
```

***

## `POST /api/governance/budgets/:scope/:scopeId/resume`

Human override: force a paused scope back to `active` (the kill-switch re-arms on the next crossing). A bare resume of a scope whose spend already meets or exceeds its limit will re-pause on the next cost event; pass `graceUsdCents` to raise the cap above the current spend so the run can make forward progress. The response surfaces `willRepause: true` when you resume an at/over-limit scope without grace, so a UI can warn the operator.

* **Path params**:

| Param     | Type                                         | Notes                                          |
| --------- | -------------------------------------------- | ---------------------------------------------- |
| `scope`   | `'agent' \| 'mission' \| 'team' \| 'tenant'` | Validated; an unknown value returns **400**.   |
| `scopeId` | `string`                                     | The scope's id; matched together with `scope`. |

* **Request body** (optional):

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  graceUsdCents?: number  // positive integer; headroom to grant when still at/over the limit
}
```

### Responses

**`400 Bad Request`**: `:scope` is not one of the four scope names:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "invalid scope" }
```

**`400 Bad Request`**: the body failed zod validation (e.g. a non-positive `graceUsdCents`):

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "invalid body", "details": {} }
```

**`404 Not Found`**: no budget row exists for that `(scope, scopeId)`:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "budget not found" }
```

**`200 OK`**: the resumed budget row plus the re-pause warning flag. `willRepause` is `true` when `spentUsdCents >= limitUsdCents` (you resumed without enough grace and the next cost event re-pauses it):

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  budget: {
    id: string
    scope: 'agent' | 'mission' | 'team' | 'tenant'
    scopeId: string
    limitUsdCents: number
    spentUsdCents: number
    spentMicroCents: number
    status: 'active' | 'soft_capped' | 'paused'
    mode: 'cap' | 'warn'
    tenantId: string | null
    createdAt: number
    updatedAt: number
  }
  willRepause: boolean
}
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
# Resume a paused team budget and grant $1.00 (100 cents) of headroom
curl -X POST http://localhost:18790/api/governance/budgets/team/<team-id>/resume \
  -H 'Content-Type: application/json' \
  -d '{"graceUsdCents":100}'
```

***

## `GET /api/governance/audit`

Reads the append-only forensic audit log: installs, approvals, tool calls, budget events, cap hits, verifications, and circuit breaks, newest first. There is no write endpoint; the audit is written in-process by the subsystems that emit events. Each row's `summary` (scrubbed JSON at write time) is masked again at the rendering boundary for credential-shaped keys (defense in depth).

* **Query params**:

| Param       | Type                                                                                                   | Default | Notes                                                                     |
| ----------- | ------------------------------------------------------------------------------------------------------ | ------- | ------------------------------------------------------------------------- |
| `agentId`   | `string`                                                                                               | unset   | Filter to one agent.                                                      |
| `eventType` | `'install' \| 'approval' \| 'tool_call' \| 'budget' \| 'cap_hit' \| 'verification' \| 'circuit_break'` | unset   | Ignored if not one of these literals.                                     |
| `since`     | `number`                                                                                               | unset   | Lower bound on `createdAt` (epoch ms); only used if a finite value `> 0`. |
| `limit`     | `number`                                                                                               | `200`   | Clamped to `≤ 1000`; only used if a finite value `> 0`.                   |

* **Request body**: none.

### Responses

**`200 OK`**: the matching audit rows (always returned, even when empty):

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  audit: Array<{
    id: string
    eventType:
      'install' | 'approval' | 'tool_call' | 'budget' | 'cap_hit' | 'verification' | 'circuit_break'
    agentId: string | null
    taskId: string | null
    teamId: string | null
    tenantId: string | null
    summary: string // scrubbed + redacted JSON text
    createdAt: number
  }>
}
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
# The last 50 budget events for one agent
curl "http://localhost:18790/api/governance/audit?agentId=<agent-id>&eventType=budget&limit=50"
```

***

## `POST /api/governance/delegation-approval`

Plumbs a delegated child's risky action back to the **leader's** approval queue. A prior sticky `allow_always` for the `(leader, scope)` pair skips the prompt; otherwise the handler opens a pending row in the `tool_call_approvals` table (visible in the [Approvals UI](/using/approvals)) and **blocks until the leader resolves it or the TTL / poll-deadline expires**, so a forgotten approval times out rather than deadlocking. The scope key is `delegate:<kind>`.

* **Path/query params**: none.
* **Request body**:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  leaderAgentId: string       // required
  kind?: string               // delegation kind → scope key 'delegate:<kind>'; default 'code'
  targetAgentName?: string    // the agent the work is delegated to (for the prompt text)
  task?: string               // the delegated task description
  taskId?: string             // the board task this delegation gates (lets the TTL reaper unblock it)
}
```

### Responses

**`400 Bad Request`**: missing `leaderAgentId`:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "error": "leaderAgentId required" }
```

**`200 OK`**: a prior sticky `allow_always` for this leader + scope short-circuits the prompt:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "resolution": "allow_always" }
```

**`200 OK`**: no sticky rule, so a pending approval was opened and awaited; the resolution is whatever the leader chose, or a terminal expiry/timeout:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  resolution: 'allow_once' | 'allow_always' | 'deny' | 'expired' | 'timeout'
}
```

`expired` = the approval's TTL elapsed before resolution; `timeout` = the poll-deadline elapsed (or the row vanished). Both are terminal "not approved" outcomes; the caller treats them as a denial.

<Warning>
  This route awaits a human decision, so it can hang for the duration of the approval TTL / poll window. The TTL is configurable via `CLAWBOO_APPROVAL_TTL_MS`; see [environment variables](/reference/environment-variables). A reaper expires abandoned approvals out-of-band.
</Warning>

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
curl -X POST http://localhost:18790/api/governance/delegation-approval \
  -H 'Content-Type: application/json' \
  -d '{"leaderAgentId":"<leader-id>","kind":"code","targetAgentName":"Reviewer Boo","task":"run the destructive migration"}'
```

***

## `GET /api/approvals`

Lists persisted approval **decisions** from the `approval_history` table, newest first. This is a decision-history log distinct from the live delegation handshake above. Wrapped in a try/catch; a DB failure returns **500** with an empty `records` array.

* **Query params**:

| Param     | Type     | Default | Notes                                     |
| --------- | -------- | ------- | ----------------------------------------- |
| `agentId` | `string` | unset   | Filter to one agent; omit for all agents. |
| `limit`   | `number` | `50`    | Clamped to the range `[1, 200]`.          |

* **Request body**: none.

### Responses

**`200 OK`**: the decision history rows:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  ok: true
  records: Array<{
    id: number // autoincrement primary key
    agentId: string
    action: string // 'allow-once' | 'allow-always' | 'deny' (as persisted by POST)
    toolName: string
    details: string | null // JSON text, or null
    createdAt: number
  }>
}
```

**`500 Internal Server Error`**: a DB error; note the empty `records`:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "ok": false, "error": "<message>", "records": [] }
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
curl "http://localhost:18790/api/approvals?agentId=<agent-id>&limit=50"
```

***

## `POST /api/approvals`

Persists a single approval decision into the `approval_history` table and returns the inserted row.

* **Path/query params**: none.
* **Request body**:

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  agentId: string                                   // required
  action: 'allow-once' | 'allow-always' | 'deny'    // required, validated against this set
  toolName: string                                  // required
  details?: Record<string, unknown> | null          // JSON-stringified before storage
}
```

### Responses

**`400 Bad Request`**: the body is missing or not an object:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "ok": false, "error": "Invalid JSON body" }
```

**`400 Bad Request`**: a required field (`agentId`, `action`, or `toolName`) is missing/falsy:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "ok": false, "error": "agentId, action, and toolName are required" }
```

**`400 Bad Request`**: `action` is not one of the three allowed values:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "ok": false, "error": "Invalid action" }
```

**`200 OK`**: the decision was persisted (`record` is the inserted row, or `null` if the insert returned nothing):

```ts theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{
  ok: true
  record: {
    id: number
    agentId: string
    action: string
    toolName: string
    details: string | null
    createdAt: number
  } | null
}
```

**`500 Internal Server Error`**: an insert failure:

```json theme={"theme":{"light":"github-dark","dark":"github-dark"}}
{ "ok": false, "error": "<message>" }
```

### Example

```bash theme={"theme":{"light":"github-dark","dark":"github-dark"}}
curl -X POST http://localhost:18790/api/approvals \
  -H 'Content-Type: application/json' \
  -d '{"agentId":"<agent-id>","action":"allow-always","toolName":"web_search"}'
```

***

## Error envelope

The budget, audit, and delegation-approval routes use the standard `{ error: string }` envelope; the budget POST routes also attach a zod `details` object on validation failures. The `/api/approvals` routes use a distinct `{ ok: boolean, error: string }` shape; the GET also carries `records: []` on its 500, and the POST returns `{ ok: true, record }` / `{ ok: true, records }` on success.

<Note>
  When the server is bound to a loopback interface (the default) there is no auth on `/api/*`. Binding to a non-loopback interface without `STUDIO_ACCESS_TOKEN` only logs a warning; setting the token activates the access gate in front of every route here. See [security](/operating/security).
</Note>

## See also

* [Governance concepts](/concepts/governance), budgets, the kill-switch, circuit breakers, caps, and approvals
* [Verification (builder ≠ judge)](/concepts/verification), what produces `verification` audit events
* [Governance dashboard](/using/governance-dashboard), the UI over these routes
* [Approvals panel](/using/approvals), where a leader resolves a delegation approval
* [Tools & MCP API](/reference/rest-api/tools-and-mcp), `/api/tools/approvals*`, the `tool_call_approvals` resolve path the handshake reuses
* [Production defaults](/operating/production-defaults), the warn-mode budget posture
* [Environment variables](/reference/environment-variables), `CLAWBOO_APPROVAL_TTL_MS` and the reaper interval
* [REST API overview](/reference/rest-api/index)
