Skip to main content
The Harbormaster approval queue is the human-in-the-loop gate for agent actions that match a policy rule. Enqueue is internal-only — it fires from harbormaster.Gate when an agent hits a rule-matching action — so the public surface is the read + decide flow operators use to triage, approve, or deny pending requests. See the Harbormaster guide.
All endpoints require authentication and are workspace-scoped. Decision endpoints additionally require OWNER or ADMIN role.

Endpoints

MethodEndpointPurpose
GET/api/v1/approvalsList approval requests
GET/api/v1/approvals/{id}Get a single request
POST/api/v1/approvals/{id}/decideApprove or deny a request
POST/api/v1/approvals/reset-auto-tuningReset learned auto-tuning state for a tool

Queue

Read the approval queue and inspect individual requests.

List approvals

GET /api/v1/approvals?status=pending&limit=50
Query parameters:
ParamTypeDefaultDescription
statusstringpendingOne of pending, approved, denied, timeout, cancelled, all. all removes the status filter.
limitinteger501-200.
Response: 200 OK
{
  "rows": [
    {
      "ID": "req_01HXYZABCDEF",
      "WorkspaceID": "ws_123",
      "CrewID": "crw_backend",
      "AgentID": "agt_viktor",
      "MissionID": "MIS-42",
      "RequestedBy": "agt_viktor",
      "Kind": "destructive_op",
      "Reason": "rm -rf /data/*",
      "Payload": { "tool": "Bash", "args": { "command": "rm -rf /data/*" } },
      "Status": "pending",
      "DecidedBy": "",
      "DecidedAt": null,
      "DecisionComment": "",
      "TimeoutAt": "2026-04-17T11:23:41Z",
      "CreatedAt": "2026-04-17T10:23:41Z",
      "TimeoutSecs": 0
    }
  ],
  "status": "pending",
  "count": 1
}
The rows[] objects are serialized with their Go field names (PascalCase) — the harbormaster.Request struct carries no JSON tags. The rows, status, and count envelope keys are lowercase. TimeoutSecs is an in-memory-only field that is never read back from the row, so it always serializes as 0 here (the effective deadline is TimeoutAt). DecidedAt and TimeoutAt are pointers — they serialize as null when unset rather than being omitted. For ?status=all, the status envelope key echoes back as an empty string (""), not "all" — the handler maps all to “no status filter” internally.
FieldTypeDescription
rows[].Kindstringtool_call, cost_threshold, destructive_op, target_environment, custom.
rows[].StatusstringSee Status values above.
rows[].PayloadobjectTool call context (tool, args, plus any rule-specific fields).
rows[].TimeoutAtstring?RFC3339 deadline; the sweeper flips past-due rows to timeout.

Get approval

GET /api/v1/approvals/{id}
Returns the full request object (same schema as list rows). Errors:
StatusCondition
401No workspace.
404ID not found or belongs to another workspace.

Decisions

Approve or deny a pending request, and manage the auto-tuning model that learns from those decisions.

Decide

POST /api/v1/approvals/{id}/decide
Auth: OWNER or ADMIN only. 403 for any other role. Request body:
{
  "status": "approved",
  "comment": "reviewed with staging validation"
}
FieldTypeRequiredDescription
statusstringYesapproved or denied. Any other value = 400.
commentstringNoFree-form decision rationale. Stored in approvals_queue.decision_comment.
Response: 200 OK
{
  "status": "approved",
  "decided_by": "user_123"
}
Errors:
StatusCondition
400Bad JSON, missing/invalid status.
401Not authenticated.
403Not OWNER/ADMIN.
404Request not found.
409Request already decided (status != pending).

Cancel

POST /api/v1/approvals/{id}/cancel
Withdraw a still-pending request, moving it to the cancelled status. Unlike decide, this records no approve/deny outcome — it marks the gated action moot (mission aborted, agent retired, duplicate request). Auth: OWNER or ADMIN only. 403 for any other role. Request body: optional. A bare POST cancels with no reason; a present-but-malformed body is a 400.
{ "reason": "mission aborted" }
FieldTypeRequiredDescription
reasonstringNoFree-form cancellation rationale. Stored in approvals_queue.decision_comment.
Response: 200 OK
{
  "status": "cancelled",
  "cancelled_by": "user_123"
}
Errors:
StatusCondition
400Body present but malformed JSON.
401Not authenticated.
403Not OWNER/ADMIN.
404Request not found.
409Request not pending (already decided or cancelled).

Reset auto-tuning

POST /api/v1/approvals/reset-auto-tuning
Harbormaster observes operator decisions over time and auto-tunes its rule confidences (a denied request that would have auto-approved next time is a calibration signal). This endpoint resets that learned state to defaults — useful after a major change in policy or after a misconfiguration polluted the auto-tuning model. Auth: OWNER or ADMIN only. Request body: requires a non-empty tool field — the reset is scoped to a single tool’s auto-tuning state.
{ "tool": "Bash" }
FieldTypeRequiredDescription
toolstringYesTool whose rolling reward window to clear. Omitting it (or sending "") returns 400 tool required.
Response: 200 OK
{
  "tool": "Bash",
  "rows_deleted": 14,
  "workspace_id": "ws_123"
}
rows_deleted is the number of auto-tuning rows wiped for that tool. Idempotent — calling twice in a row returns rows_deleted: 0 on the second call. Errors: 400 bad JSON or missing tool; 401 not authenticated; 403 wrong role; 500 DB error. This does not affect already-decided approval rows, journal history, or the rule definitions themselves — only the per-rule confidence that drives auto-mode (async/sync/required) selection.

Journal side-effects

Every write emits a journal entry:
ActionEntry
Enqueueapproval.requested (info)
Decide approvedapproval.granted (info)
Decide deniedapproval.denied (warn)
Timeout sweepapproval.timeout (warn)
Cancel (agent self-withdraw)approval.cancelled (notice, actor = agent)
Cancel (operator via API/CLI)approval.cancelled (notice, actor = user)
See Crew Journal for payload shapes.