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.
This queue is not where a Keeper escalation is decided. Keeper’s
ESCALATE verdicts resolve through
POST /api/v1/admin/keeper/requests/{requestId}/resolve
(Keeper), and a human ALLOW there additionally mints
the single-use consumable approval the agent presents on its retry. The rows
on this page are Harbormaster’s own rule-matched actions and share no state
with Keeper requests.All endpoints require authentication and are workspace-scoped, and every
endpoint on this page — reads included — additionally requires
OWNER or
ADMIN role (#2233). Before this, the two read endpoints were reachable by
any workspace member; a request’s full payload is shown verbatim in the
approval detail UI, so the read floor now matches the decide/cancel floor
it always should have.Endpoints
Queue
Read the approval queue and inspect individual requests.List approvals
OWNER or ADMIN only (#2233). 403 for any other role.
Query parameters:
Response:
200 OK
has_more is true when more rows exist past offset + count.
Row keys are snake_case, like every other endpoint. They were PascalCase until
harbormaster.Request was given JSON tags: the struct was serialized straight
onto the wire with its Go field names, so the API answered "ID"/"Kind"/
"CreatedAt" while the web client’s zod schema and the generated OpenAPI spec
both declared snake_case. Every browser surface rejected the body and rendered
an empty approvals queue; only the Go CLI worked, because encoding/json
matches field names case-insensitively on decode — which is also why no Go test
caught it. TestApprovals_WireShape_IsSnakeCase now asserts the emitted keys.
decided_at and timeout_at are pointers — they serialize as null when
unset rather than being omitted. TimeoutSecs was an in-memory-only field that
leaked into responses as a constant 0; it is now json:"-" and absent (the
effective deadline is timeout_at).
For ?status=all, the status envelope key echoes back as an empty string (""), not "all" — the handler maps all to “no status filter” internally.
Get approval
OWNER or ADMIN only (#2233). 403 for any other role. The optional
X-Workspace-ID header selects the workspace when it is not already present in
the request context.
Response: 200 OK — the full approval request object, with the same
snake_case fields as a rows[] item from List (including decision_comment,
timeout_at and, when set, routine_version).
Errors:
Decisions
Approve or deny a pending request, and manage the auto-tuning model that learns from those decisions.Decide
OWNER or ADMIN only. 403 for any other role.
Request body:
Response:
200 OK
The decision and its side effects are one transaction. For
kind=ephemeral_hire rows the decision also flips the staged agent
(approve → IDLE, deny → ghosted) and resolves the blocking inbox
waitpoint. All three writes commit together or not at all.If the staged agent can no longer take that transition — it was deleted,
already activated through POST /api/v1/agents/{agentId}/approve-hire,
or ghosted by the TTL sweeper — the whole decision is rolled back and
you get 409 staged hire is no longer decidable. The queue row stays
pending and is still decidable (or will time out on its own); it is
never left approved/denied against an agent that never moved.A kind=ephemeral_hire row that times out ghosts the staged agent in the
same sweep, so the lapsed hire is no longer approvable through either
surface until crewship rehire reopens the cycle — see
Ephemeral agents.Cancel
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.
Response:
200 OK
Reset auto-tuning
OWNER or ADMIN only.
Request body: requires a non-empty tool field — the reset is scoped to a single tool’s auto-tuning state.
Response:
200 OK
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:
See Crew Journal for payload shapes.