budgets.ts, governanceAudit.ts, delegationApproval.ts, and approvals.ts. All POST routes read a JSON body parsed by express.json({ limit: '2mb' }).
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.Routes
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.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:
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
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:
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.Responses
400 Bad Request: the body failed zod validation (wrong scope, missing scopeId, non-positive limitUsdCents, …):
details is the zod flatten() of the validation failure.
200 OK: the upserted budget row:
Example
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:
- Request body (optional):
Responses
400 Bad Request: :scope is not one of the four scope names:
400 Bad Request: the body failed zod validation (e.g. a non-positive graceUsdCents):
404 Not Found: no budget row exists for that (scope, scopeId):
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):
Example
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:
- Request body: none.
Responses
200 OK: the matching audit rows (always returned, even when empty):
Example
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) 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:
Responses
400 Bad Request: missing leaderAgentId:
200 OK: a prior sticky allow_always for this leader + scope short-circuits the prompt:
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:
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.
Example
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:
- Request body: none.
Responses
200 OK: the decision history rows:
500 Internal Server Error: a DB error; note the empty records:
Example
POST /api/approvals
Persists a single approval decision into the approval_history table and returns the inserted row.
- Path/query params: none.
- Request body:
Responses
400 Bad Request: the body is missing or not an object:
400 Bad Request: a required field (agentId, action, or toolName) is missing/falsy:
400 Bad Request: action is not one of the three allowed values:
200 OK: the decision was persisted (record is the inserted row, or null if the insert returned nothing):
500 Internal Server Error: an insert failure:
Example
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.
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.See also
- Governance concepts, budgets, the kill-switch, circuit breakers, caps, and approvals
- Verification (builder ≠ judge), what produces
verificationaudit events - Governance dashboard, the UI over these routes
- Approvals panel, where a leader resolves a delegation approval
- Tools & MCP API,
/api/tools/approvals*, thetool_call_approvalsresolve path the handshake reuses - Production defaults, the warn-mode budget posture
- Environment variables,
CLAWBOO_APPROVAL_TTL_MSand the reaper interval - REST API overview