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

# Approvals

> Harbormaster human-in-the-loop approval queue.

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](/guides/harbormaster).

<Note>
  All endpoints require authentication and are workspace-scoped. Decision endpoints additionally require `OWNER` or `ADMIN` role.
</Note>

## Endpoints

| Method | Endpoint                                                    | Purpose                                    |
| ------ | ----------------------------------------------------------- | ------------------------------------------ |
| GET    | [`/api/v1/approvals`](#list-approvals)                      | List approval requests                     |
| GET    | [`/api/v1/approvals/{id}`](#get-approval)                   | Get a single request                       |
| POST   | [`/api/v1/approvals/{id}/decide`](#decide)                  | Approve or deny a request                  |
| POST   | [`/api/v1/approvals/reset-auto-tuning`](#reset-auto-tuning) | Reset 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:**

| Param    | Type    | Default   | Description                                                                                             |
| -------- | ------- | --------- | ------------------------------------------------------------------------------------------------------- |
| `status` | string  | `pending` | One of `pending`, `approved`, `denied`, `timeout`, `cancelled`, `all`. `all` removes the status filter. |
| `limit`  | integer | `50`      | 1-200.                                                                                                  |

**Response:** `200 OK`

```json theme={null}
{
  "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.

| Field              | Type    | Description                                                                      |
| ------------------ | ------- | -------------------------------------------------------------------------------- |
| `rows[].Kind`      | string  | `tool_call`, `cost_threshold`, `destructive_op`, `target_environment`, `custom`. |
| `rows[].Status`    | string  | See Status values above.                                                         |
| `rows[].Payload`   | object  | Tool call context (`tool`, `args`, plus any rule-specific fields).               |
| `rows[].TimeoutAt` | string? | 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:**

| Status | Condition                                     |
| ------ | --------------------------------------------- |
| 401    | No workspace.                                 |
| 404    | ID 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:**

```json theme={null}
{
  "status": "approved",
  "comment": "reviewed with staging validation"
}
```

| Field     | Type   | Required | Description                                                                 |
| --------- | ------ | -------- | --------------------------------------------------------------------------- |
| `status`  | string | Yes      | `approved` or `denied`. Any other value = 400.                              |
| `comment` | string | No       | Free-form decision rationale. Stored in `approvals_queue.decision_comment`. |

**Response:** `200 OK`

```json theme={null}
{
  "status": "approved",
  "decided_by": "user_123"
}
```

**Errors:**

| Status | Condition                                      |
| ------ | ---------------------------------------------- |
| 400    | Bad JSON, missing/invalid `status`.            |
| 401    | Not authenticated.                             |
| 403    | Not OWNER/ADMIN.                               |
| 404    | Request not found.                             |
| 409    | Request 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.

```json theme={null}
{ "reason": "mission aborted" }
```

| Field    | Type   | Required | Description                                                                     |
| -------- | ------ | -------- | ------------------------------------------------------------------------------- |
| `reason` | string | No       | Free-form cancellation rationale. Stored in `approvals_queue.decision_comment`. |

**Response:** `200 OK`

```json theme={null}
{
  "status": "cancelled",
  "cancelled_by": "user_123"
}
```

**Errors:**

| Status | Condition                                           |
| ------ | --------------------------------------------------- |
| 400    | Body present but malformed JSON.                    |
| 401    | Not authenticated.                                  |
| 403    | Not OWNER/ADMIN.                                    |
| 404    | Request not found.                                  |
| 409    | Request 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.

```json theme={null}
{ "tool": "Bash" }
```

| Field  | Type   | Required | Description                                                                                           |
| ------ | ------ | -------- | ----------------------------------------------------------------------------------------------------- |
| `tool` | string | Yes      | Tool whose rolling reward window to clear. Omitting it (or sending `""`) returns 400 `tool required`. |

**Response:** `200 OK`

```json theme={null}
{
  "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:

| Action                        | Entry                                        |
| ----------------------------- | -------------------------------------------- |
| Enqueue                       | `approval.requested` (info)                  |
| Decide approved               | `approval.granted` (info)                    |
| Decide denied                 | `approval.denied` (warn)                     |
| Timeout sweep                 | `approval.timeout` (warn)                    |
| Cancel (agent self-withdraw)  | `approval.cancelled` (notice, actor = agent) |
| Cancel (operator via API/CLI) | `approval.cancelled` (notice, actor = user)  |

See [Crew Journal](/guides/crew-journal) for payload shapes.

## Related

* [Harbormaster guide](/guides/harbormaster).
* [`crewship approvals`](/cli/approvals).
