> ## 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.

# Agents

> Manage agents — AI coding agents with configurable roles, CLI adapters, skills, and credentials. Agents in a crew share one Linux container.

An **agent** is an AI coding agent with a configurable role, CLI adapter, skills, and credentials. Agents belonging to the same crew share one Linux container. These endpoints cover the full agent lifecycle: creating and configuring permanent agents, hiring short-lived ephemeral "contractors", managing their skills and credentials, driving chats and runs, inspecting workspace files, and tuning the persona and self-learning layers that shape each agent's behavior.

<Note>
  All `/api/v1/agents/*` endpoints require authentication and workspace context (the `workspace_id` query parameter). The chat-message proxy (`/api/v1/chats/{chatId}/messages`) is the exception — it resolves the workspace from the chat row and takes no `workspace_id`.
</Note>

## Endpoints

| Method | Endpoint                                                                  | Purpose                                                          |
| ------ | ------------------------------------------------------------------------- | ---------------------------------------------------------------- |
| GET    | [/api/v1/agents/crews-status](#crews-status)                              | Lightweight agent counts by status for toolbar/dashboard widgets |
| GET    | [/api/v1/agent-load](#agent-load)                                         | Agent load metrics across the workspace                          |
| GET    | [/api/v1/agents](#list-agents)                                            | List agents (optionally filtered by crew)                        |
| POST   | [/api/v1/agents](#create-agent)                                           | Create a new agent                                               |
| GET    | [/api/v1/agents/{agentId}](#get-agent)                                    | Get a single agent                                               |
| PATCH  | [/api/v1/agents/{agentId}](#update-agent)                                 | Partially update an agent                                        |
| DELETE | [/api/v1/agents/{agentId}](#delete-agent)                                 | Soft-delete an agent                                             |
| POST   | [/api/v1/agents/hire](#hire-agent)                                        | Spawn a new ephemeral agent under a crew                         |
| POST   | [/api/v1/agents/{agentId}/rehire](#rehire-agent)                          | Resurrect or extend an ephemeral agent                           |
| POST   | [/api/v1/agents/{agentId}/approve-hire](#approve-hire)                    | Approve a guided-autonomy pending hire                           |
| GET    | [/api/v1/agents/{agentId}/persona](#get-persona)                          | Get the resolved persona                                         |
| PUT    | [/api/v1/agents/{agentId}/persona](#set-persona)                          | Operator-only direct persona write                               |
| DELETE | [/api/v1/agents/{agentId}/persona](#reset-persona)                        | Remove the agent persona layer                                   |
| GET    | [/api/v1/agents/{agentId}/persona/history](#get-persona-history)          | List persona version history                                     |
| POST   | [/api/v1/agents/{agentId}/persona/suggest](#suggest-persona)              | Agent-initiated persona proposal                                 |
| GET    | [/api/v1/agents/{agentId}/learning](#get-self-learning)                   | Get the self-learning flag and audit triple                      |
| PATCH  | [/api/v1/agents/{agentId}/learning](#set-self-learning)                   | Flip the self-learning flag                                      |
| GET    | [/api/v1/agents/{agentId}/inbox](#agent-inbox-summary)                    | Consolidated "what's waiting on this agent" payload              |
| GET    | [/api/v1/agents/{agentId}/peers](#list-agent-peers)                       | List the agent's peer-card index                                 |
| GET    | [/api/v1/agents/{agentId}/peers/{userId}](#get-agent-peer-card)           | Get one peer card's full content                                 |
| DELETE | [/api/v1/agents/{agentId}/peers/{userId}](#delete-agent-peer-card)        | Delete one peer card                                             |
| GET    | [/api/v1/agents/{agentId}/skills](#list-skills)                           | List skill assignments                                           |
| POST   | [/api/v1/agents/{agentId}/skills](#add-skill)                             | Assign a skill                                                   |
| DELETE | [/api/v1/agents/{agentId}/skills/{skillId}](#remove-skill)                | Remove a skill                                                   |
| GET    | [/api/v1/agents/{agentId}/credentials](#list-credentials)                 | List credential assignments                                      |
| POST   | [/api/v1/agents/{agentId}/credentials](#assign-credential)                | Assign a credential                                              |
| DELETE | [/api/v1/agents/{agentId}/credentials/{assignmentId}](#remove-credential) | Remove a credential                                              |
| GET    | [/api/v1/agents/{agentId}/chats](#list-chats)                             | List chat sessions                                               |
| POST   | [/api/v1/agents/{agentId}/chats](#create-chat)                            | Create a new chat session                                        |
| GET    | [/api/v1/agents/{agentId}/runs](#agent-runs)                              | List execution history                                           |
| GET    | [/api/v1/agents/{agentId}/debug](#agent-proxy-endpoints)                  | Debug information for a running agent                            |
| GET    | [/api/v1/agents/{agentId}/files](#agent-proxy-endpoints)                  | List files in agent workspace                                    |
| GET    | [/api/v1/agents/{agentId}/files/download](#agent-proxy-endpoints)         | Download a file                                                  |
| PUT    | [/api/v1/agents/{agentId}/files/save](#agent-proxy-endpoints)             | Save/upload a file                                               |
| GET    | [/api/v1/agents/{agentId}/container-files](#container-files)              | List files inside the running container                          |
| GET    | [/api/v1/agents/{agentId}/git-log](#git-log)                              | Recent git commits from the container workspace                  |
| GET    | [/api/v1/agents/{agentId}/logs](#agent-proxy-endpoints)                   | Get agent logs                                                   |
| POST   | [/api/v1/agents/{agentId}/stop](#agent-proxy-endpoints)                   | Stop a running agent                                             |
| GET    | [/api/v1/chats/{chatId}/messages](#chat-messages)                         | Get messages for a chat session                                  |

## Status & Metrics

Lightweight, read-only aggregates that power toolbar and dashboard widgets.

### Crews Status

```
GET /api/v1/agents/crews-status?workspace_id={workspaceId}

# Response is a per-workspace aggregate counting agents by status
# (IDLE/RUNNING/ERROR — no STOPPED bucket). Drives the 'Crews idle/online'
# badge in the top toolbar.
```

Returns lightweight agent counts by status for toolbar/dashboard widgets.

**Response:** `200 OK`

```json theme={null}
{
  "total": 12,
  "running": 3,
  "error": 1,
  "idle": 8,
  "queued": 0
}
```

The agent status buckets (`total`, `running`, `error`, `idle`) are always present — zero counts are not omitted. Any status that is not `RUNNING` or `ERROR` (including `IDLE` and `PENDING_REVIEW`) folds into the `idle` bucket; there is no `stopped` bucket. The `queued` field counts admission-queue *assignments* (not agents) currently in the `QUEUED` state — independent of the agent buckets, and `0` on servers that pre-date the queue migration (`internal/api/agents.go:111-151`).

***

### Agent Load

```
GET /api/v1/agent-load?workspace_id={workspaceId}
```

Returns agent load metrics across the workspace.

***

## Lifecycle & CRUD

Create, read, update, and soft-delete permanent agents.

### List Agents

```
GET /api/v1/agents?workspace_id={workspaceId}
```

**Query Parameters:**

| Parameter      | Type   | Description              |
| -------------- | ------ | ------------------------ |
| `workspace_id` | string | Required. Workspace ID   |
| `crew_id`      | string | Optional. Filter by crew |

**Response:** `200 OK`

```json theme={null}
[
  {
    "id": "agent_abc",
    "crew_id": "crew_123",
    "workspace_id": "ws_456",
    "name": "Backend Dev",
    "slug": "backend-dev",
    "description": "Go backend developer",
    "role_title": "Senior Developer",
    "agent_role": "AGENT",
    "lead_mode": null,
    "status": "IDLE",
    "cli_adapter": "CLAUDE_CODE",
    "llm_provider": "ANTHROPIC",
    "llm_model": "claude-sonnet-4-20250514",
    "system_prompt": "You are a Go backend developer...",
    "avatar_seed": "backend-dev",
    "avatar_style": "bottts",
    "timeout_seconds": 1800,
    "tool_profile": "CODING",
    "memory_enabled": true,
    "cli_tools": null,
    "schedule_cron": null,
    "schedule_prompt": null,
    "schedule_enabled": false,
    "schedule_last_run": null,
    "schedule_next_run": null,
    "mcp_config_json": null,
    "created_at": "2024-01-15T10:00:00Z",
    "updated_at": "2024-01-15T10:00:00Z",
    "crew": {
      "name": "Engineering",
      "slug": "engineering",
      "color": "blue",
      "avatar_style": "bottts"
    },
    "_count": {
      "skills": 2,
      "credentials": 1,
      "chats": 5
    },
    "created_by_user_id": "user_789",
    "ephemeral": false,
    "expires_at": null,
    "expired_at": null,
    "parent_lead_id": null,
    "hire_reason": null
  }
]
```

`mcp_config_json` and `created_by_user_id` carry `omitempty` — they are dropped from the JSON entirely when null/empty rather than serialized as `null`.

#### Response Fields

| Field                | Type    | Description                                                      |
| -------------------- | ------- | ---------------------------------------------------------------- |
| `id`                 | string  | Agent ID                                                         |
| `crew_id`            | string  | Parent crew ID                                                   |
| `workspace_id`       | string  | Workspace ID                                                     |
| `name`               | string  | Display name                                                     |
| `slug`               | string  | URL-safe identifier                                              |
| `description`        | string? | Agent description                                                |
| `role_title`         | string? | Human-readable role title                                        |
| `agent_role`         | string  | `AGENT` or `LEAD`                                                |
| `lead_mode`          | string? | `"active"` or `"passive"` (LEAD agents only)                     |
| `status`             | string  | `IDLE`, `RUNNING`, or `ERROR`                                    |
| `cli_adapter`        | string  | CLI tool used to run the agent                                   |
| `llm_provider`       | string? | LLM provider name                                                |
| `llm_model`          | string? | LLM model identifier                                             |
| `system_prompt`      | string? | System prompt for the agent                                      |
| `avatar_seed`        | string? | Seed for avatar generation                                       |
| `avatar_style`       | string? | Avatar style (e.g., `"bottts"`, `"pixel-art"`)                   |
| `timeout_seconds`    | integer | Maximum execution time                                           |
| `tool_profile`       | string  | Tool access profile                                              |
| `memory_enabled`     | boolean | Whether agent memory is enabled                                  |
| `cli_tools`          | string? | Comma-separated list of allowed CLI tools                        |
| `schedule_cron`      | string? | Cron expression for scheduled runs                               |
| `schedule_prompt`    | string? | Prompt used for scheduled runs                                   |
| `schedule_enabled`   | boolean | Whether schedule is active                                       |
| `schedule_last_run`  | string? | ISO 8601 timestamp of last scheduled run                         |
| `schedule_next_run`  | string? | ISO 8601 timestamp of next scheduled run                         |
| `mcp_config_json`    | string? | Agent-level MCP server config (omitted when empty)               |
| `crew`               | object? | Embedded crew info (name, slug, color, avatar\_style)            |
| `_count`             | object  | Counts: skills, credentials, chats                               |
| `created_by_user_id` | string  | Creator user ID; omitted for legacy rows with no creator         |
| `ephemeral`          | boolean | `true` for hired contractor agents, `false` for permanent agents |
| `expires_at`         | string? | RFC3339 TTL deadline (ephemeral agents only)                     |
| `expired_at`         | string? | RFC3339 ghost timestamp once the TTL elapses                     |
| `parent_lead_id`     | string? | LEAD agent that parented this ephemeral                          |
| `hire_reason`        | string? | Timestamped hire/rehire reason log                               |

***

### Create Agent

```
POST /api/v1/agents?workspace_id={workspaceId}
```

**Auth:** `OWNER`, `ADMIN`, or `MANAGER` role

**Request Body:**

| Field             | Type    | Required    | Default         | Description                                                                                                     |
| ----------------- | ------- | ----------- | --------------- | --------------------------------------------------------------------------------------------------------------- |
| `name`            | string  | Yes         | --              | Display name (2-100 characters)                                                                                 |
| `slug`            | string  | Yes         | --              | URL-safe identifier (2-50 chars, lowercase letters/digits/hyphens)                                              |
| `crew_id`         | string  | Conditional | `null`          | Parent crew ID. Required when `agent_role` is `LEAD`; optional for `AGENT` (`internal/api/agents_create.go:73`) |
| `description`     | string  | No          | `null`          | Agent description                                                                                               |
| `role_title`      | string  | No          | `null`          | Human-readable role title                                                                                       |
| `agent_role`      | string  | No          | `"AGENT"`       | `AGENT` or `LEAD`                                                                                               |
| `lead_mode`       | string  | No          | `"active"`      | `"active"` or `"passive"` (LEAD agents only)                                                                    |
| `cli_adapter`     | string  | No          | `"CLAUDE_CODE"` | CLI adapter                                                                                                     |
| `llm_provider`    | string  | No          | `null`          | LLM provider                                                                                                    |
| `llm_model`       | string  | No          | `null`          | LLM model                                                                                                       |
| `system_prompt`   | string  | No          | `null`          | System prompt                                                                                                   |
| `avatar_seed`     | string  | No          | `null`          | Avatar seed                                                                                                     |
| `avatar_style`    | string  | No          | `null`          | Avatar style                                                                                                    |
| `timeout_seconds` | integer | No          | `1800`          | Max execution time in seconds                                                                                   |
| `tool_profile`    | string  | No          | `"CODING"`      | Tool profile                                                                                                    |
| `memory_enabled`  | boolean | No          | `false`         | Enable agent memory                                                                                             |

```json theme={null}
{
  "name": "Backend Dev",
  "slug": "backend-dev",
  "crew_id": "crew_123",
  "agent_role": "AGENT",
  "cli_adapter": "CLAUDE_CODE",
  "system_prompt": "You are a Go backend developer...",
  "memory_enabled": true,
  "tool_profile": "CODING"
}
```

**Agent Roles:**

| Role    | Description                          | Crew Required        |
| ------- | ------------------------------------ | -------------------- |
| `AGENT` | Worker that executes tasks           | Yes                  |
| `LEAD`  | Crew leader that plans and delegates | Yes (max 1 per crew) |

**CLI Adapters:** `CLAUDE_CODE`, `CODEX_CLI`, `GEMINI_CLI`, `OPENCODE`, `CURSOR_CLI`, `FACTORY_DROID`

**Tool Profiles:** `MINIMAL`, `CODING`, `FULL`

**Response:** `201 Created` -- returns the created agent object.

| Status | Condition                                              |
| ------ | ------------------------------------------------------ |
| `400`  | Invalid name, slug, role, or missing required crew\_id |
| `402`  | License agent limit exceeded                           |
| `403`  | Insufficient role                                      |
| `409`  | Slug already taken, or crew already has a LEAD agent   |

***

### Get Agent

```
GET /api/v1/agents/{agentId}?workspace_id={workspaceId}
```

**Response:** `200 OK` -- full agent object.

| Status | Condition       |
| ------ | --------------- |
| `404`  | Agent not found |

***

### Update Agent

```
PATCH /api/v1/agents/{agentId}?workspace_id={workspaceId}
```

Partial update -- only provided fields are changed.

**Auth:** Per-agent edit gate (`canEditAgent` in `internal/api/agents_update.go:27`, defined at `internal/api/rbac.go:151`). `OWNER` / `ADMIN` may edit any agent; `MANAGER` may edit only agents they created or agents in crews where they hold per-crew `ADMIN`/`OWNER`; `MEMBER` / `VIEWER` are refused.

**Request Body:** Same fields as Create, all optional. `status` is not mutable through this endpoint. Additionally supports:

| Field              | Type    | Description                                                 |
| ------------------ | ------- | ----------------------------------------------------------- |
| `cli_tools`        | string  | Comma-separated CLI tools                                   |
| `schedule_cron`    | string  | Cron expression                                             |
| `schedule_prompt`  | string  | Schedule prompt                                             |
| `schedule_enabled` | boolean | Enable/disable schedule                                     |
| `mcp_config_json`  | string  | Agent MCP config JSON (must contain an `mcpServers` object) |

**Response:** `200 OK` -- updated agent object.

***

### Delete Agent

```
DELETE /api/v1/agents/{agentId}?workspace_id={workspaceId}
```

Soft-deletes the agent (sets `deleted_at`).

<Warning>
  This is a destructive operation. Same per-agent edit gate as Update (`canEditAgent` in `internal/api/agents_query.go:266`, defined at `internal/api/rbac.go:151`). `OWNER` / `ADMIN` delete any agent; `MANAGER` deletes only agents they created or agents in crews where they hold per-crew `ADMIN`/`OWNER`.
</Warning>

**Response:** `200 OK`

```json theme={null}
{ "success": true }
```

***

## Hire / Rehire / Approve

Ephemeral agents ("contractors") are short-lived agents spawned against a
crew with a TTL. When the TTL elapses the agent becomes a **ghost** (the
row is preserved for audit but no longer counts against the crew's quota).
The hire flow is gated by the crew's **autonomy policy** (`strict` /
`guided` / `trusted` / `full`) and bounded by the crew's
`max_ephemeral_agents` quota (ghosts excluded).

<Note>
  All three endpoints require `OWNER`, `ADMIN`, or `MANAGER` role (`canRole(role, "create")`).
</Note>

### Hire Agent

```
POST /api/v1/agents/hire?workspace_id={workspaceId}
```

Spawns a new ephemeral agent under a crew. The crew's autonomy policy
decides the outcome:

| Autonomy  | Outcome                                                                                                                                                        |
| --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `strict`  | `403` — the hire is rejected with a structured reason.                                                                                                         |
| `guided`  | `202` — the agent row is created in `PENDING_REVIEW`, a **blocking** inbox waitpoint is dropped, and the agent will not start until [approved](#approve-hire). |
| `trusted` | `201` — the agent goes live immediately; a non-blocking inbox visibility item is recorded.                                                                     |
| `full`    | `201` — the agent goes live immediately; journal-only, no inbox row.                                                                                           |

**Auth:** `OWNER`, `ADMIN`, or `MANAGER` role

**Request Body:**

| Field            | Type    | Required    | Description                                                                                                 |
| ---------------- | ------- | ----------- | ----------------------------------------------------------------------------------------------------------- |
| `crew_id`        | string  | Conditional | Target crew by id. Provide exactly one of `crew_id` / `crew_slug`.                                          |
| `crew_slug`      | string  | Conditional | Target crew by slug. Mutually exclusive with `crew_id`.                                                     |
| `template_slug`  | string  | Yes         | Crew template the ephemeral is built from (built-in or workspace-owned).                                    |
| `ttl_minutes`    | integer | No          | Lifetime in minutes. Clamped to `1–1440`; a value of `0`/negative falls back to the server floor of **30**. |
| `reason`         | string  | Yes         | Audit/history trail for the hire.                                                                           |
| `model`          | string  | No          | LLM model override. Empty falls back to the template default at provisioning time.                          |
| `parent_lead_id` | string  | No          | A `LEAD` agent in the **same crew + workspace** that parents this ephemeral.                                |

```json theme={null}
{
  "crew_slug": "engineering",
  "template_slug": "backend-dev",
  "ttl_minutes": 60,
  "reason": "Spike: investigate the flaky deploy script",
  "model": "claude-sonnet-4-20250514"
}
```

**Response:** `201 Created` (live) or `202 Accepted` (pending review).

```json theme={null}
{
  "id": "agent_abc",
  "crew_id": "crew_123",
  "workspace_id": "ws_456",
  "slug": "backend-dev-eph-1a2b3c",
  "name": "Backend Dev",
  "status": "IDLE",
  "ephemeral": true,
  "expires_at": "2026-06-04T13:00:00Z",
  "expired_at": null,
  "parent_lead_id": null,
  "hire_reason": "[2026-06-04T12:00:00Z] hire: Spike: investigate the flaky deploy script",
  "pending_review": false,
  "inbox_item_id": "ib-cuid",
  "decision": "auto_log_journal"
}
```

| Field            | Type    | Description                                                                                               |
| ---------------- | ------- | --------------------------------------------------------------------------------------------------------- |
| `id`             | string  | New agent id.                                                                                             |
| `crew_id`        | string? | Resolved crew id.                                                                                         |
| `workspace_id`   | string  | Workspace id.                                                                                             |
| `slug`           | string  | Generated ephemeral slug (`<template>-eph-<hex>`).                                                        |
| `name`           | string  | Template name.                                                                                            |
| `status`         | string  | `IDLE` on a live hire, `PENDING_REVIEW` when the policy is `guided`.                                      |
| `ephemeral`      | boolean | Always `true`.                                                                                            |
| `expires_at`     | string? | RFC3339 TTL deadline.                                                                                     |
| `expired_at`     | string? | RFC3339 ghost timestamp (always `null` on a fresh hire).                                                  |
| `parent_lead_id` | string? | Echo of the parenting LEAD, when supplied.                                                                |
| `hire_reason`    | string? | Timestamped reason log (rehires append to this).                                                          |
| `pending_review` | boolean | `true` only on the `guided` → `202` path.                                                                 |
| `inbox_item_id`  | string  | Inbox row id (blocking waitpoint on `guided`, visibility item on `trusted`). Omitted when empty (`full`). |
| `decision`       | string  | The policy decision that produced this outcome.                                                           |

| Status | Condition                                                                               |
| ------ | --------------------------------------------------------------------------------------- |
| `201`  | Live ephemeral (`trusted` / `full`).                                                    |
| `202`  | `PENDING_REVIEW` + blocking inbox item (`guided`).                                      |
| `400`  | Missing/invalid `template_slug`, `reason`, or `crew_id`/`crew_slug` (neither, or both). |
| `403`  | `strict` autonomy rejected the hire, or caller lacks MANAGER+.                          |
| `404`  | Crew or template not found in this workspace.                                           |
| `429`  | Crew quota (`max_ephemeral_agents`) reached — rehire a ghost or raise the quota.        |

#### Credential assignment

On a successful hire (`201`/`202`), Crewship auto-assigns the available
workspace Anthropic credentials (`API_KEY` / `AI_CLI_TOKEN`, first-created
wins) to the new agent so it can authenticate on first run. This is
best-effort and runs after the agent row is committed: assignment failures are
journaled as `credential.auto_assign_failed` and never fail the hire, and a
workspace with no Anthropic credential journals `credential.auto_assign_empty`.
Assign manually later via `POST /api/v1/agents/{agentId}/credentials` if needed.

### Rehire Agent

```
POST /api/v1/agents/{agentId}/rehire?workspace_id={workspaceId}
```

Resurrects an expired ephemeral ("ghost"): clears `expired_at`, pushes
`expires_at` forward by the new TTL, and **appends** a new reason line to
the `hire_reason` history. The container is not rebuilt here — the
chatbridge auto-provisions a fresh one on the next message. Rehiring a
still-live ephemeral (extending its TTL before it ghosts) is free and does
not consume an extra quota slot.

**Auth:** `OWNER`, `ADMIN`, or `MANAGER` role

**Request Body:**

| Field         | Type    | Required | Description                                                             |
| ------------- | ------- | -------- | ----------------------------------------------------------------------- |
| `ttl_minutes` | integer | No       | New lifetime in minutes. Same `1–1440` clamp / 30-minute floor as Hire. |
| `reason`      | string  | Yes      | Appended to the `hire_reason` history trail.                            |

```json theme={null}
{
  "ttl_minutes": 90,
  "reason": "Deploy script needs another pass"
}
```

**Response:** `200 OK` — same shape as the Hire response, echoing the
agent's persisted `status` (rehire does not change it) and the updated
`expires_at` / `hire_reason`.

| Status | Condition                                                             |
| ------ | --------------------------------------------------------------------- |
| `200`  | Rehire succeeded; agent is live again.                                |
| `400`  | Missing `reason`, or invalid JSON.                                    |
| `403`  | Caller lacks MANAGER+, or `strict` autonomy rejected the rehire.      |
| `404`  | Agent not found in this workspace, or the agent is **not ephemeral**. |
| `429`  | Crew quota reached (only when rehiring a ghost).                      |

### Approve Hire

```
POST /api/v1/agents/{agentId}/approve-hire?workspace_id={workspaceId}
```

The guided-autonomy approval step. Flips a `PENDING_REVIEW` ephemeral to
`IDLE` (an atomic conditional UPDATE, so two concurrent approvals can't
both win), resolves the blocking inbox waitpoint addressed to the agent,
and writes an `agent.hire_approved` audit entry. The chatbridge refuses to
start a `PENDING_REVIEW` agent, so this is what actually releases the gate.

**Auth:** `OWNER`, `ADMIN`, or `MANAGER` role

**Request Body:** empty.

**Response:** `200 OK`

```json theme={null}
{
  "id": "agent_abc",
  "status": "IDLE",
  "crew_id": "crew_123"
}
```

| Status | Condition                                                                                                   |
| ------ | ----------------------------------------------------------------------------------------------------------- |
| `200`  | Agent flipped to `IDLE`; chatbridge will now serve messages.                                                |
| `403`  | Caller lacks MANAGER+.                                                                                      |
| `404`  | Agent not found in this workspace.                                                                          |
| `409`  | Agent is not in `PENDING_REVIEW` (already approved, hired under non-guided autonomy, or a permanent agent). |

***

## Agent Persona

The persona layer is per-agent `PERSONA.md` markdown that the
orchestrator injects into every system prompt. Resolution is layered:
the agent layer (if a file exists on disk) wins; otherwise the crew
default layer; otherwise the synthesized default built from the
agent's role + role title. Direct writes are operator-only; agents
edit only via the **suggest** endpoint, which the policy resolver
gates per crew-autonomy level (see [Autonomy & self-learning](/guides/autonomy-and-self-learning)).

### Get Persona

```
GET /api/v1/agents/{agentId}/persona
```

Returns the resolved persona — agent layer if a file is present,
crew default otherwise, synthesised default if neither layer is
configured.

**Response:** `200 OK`

```json theme={null}
{
  "agent_id": "agent-uuid",
  "layer": "agent",
  "from_default": false,
  "content": "# PERSONA.md...\n",
  "bytes": 1842,
  "cap_bytes": 1500
}
```

| Field          | Type      | Description                                                                                                                       |
| -------------- | --------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `layer`        | `string`  | `agent` or `crew` — which layer the response came from. The synthesised default is reported as `crew` with `from_default = true`. |
| `from_default` | `bool`    | `true` when no file exists on either layer and the synthesised "You are the {role}…" stub was returned                            |
| `cap_bytes`    | `integer` | Max accepted size on `PUT` / `POST suggest` (`memory.PersonaCapBytes` = 1500 bytes)                                               |

### Set Persona

```
PUT /api/v1/agents/{agentId}/persona
Content-Type: application/json
```

Operator-only direct write of the agent layer. Records a row in
`memory_versions` so [Get Persona History](#get-persona-history) can
replay the chain.

**Request:**

```json theme={null}
{ "content": "# Engineer\n\nFocus on backend services...\n" }
```

**Response:** `200 OK`

```json theme={null}
{ "layer": "agent", "bytes": 1842, "updated": "2026-05-27T18:42:11Z" }
```

| Status | Condition                                     |
| ------ | --------------------------------------------- |
| `413`  | `content` exceeds `cap_bytes`                 |
| `400`  | Invalid JSON or missing storage configuration |
| `500`  | Storage write/fsync failure                   |

<Note>
  Agents cannot use this endpoint to mutate their own persona — `ActionPersonaDirectWrite` is `DecisionRejected` across every autonomy level in Phase 1. Agent edits flow exclusively through [Suggest Persona](#suggest-persona).
</Note>

### Reset Persona

```
DELETE /api/v1/agents/{agentId}/persona
```

<Warning>
  Removes the agent layer file. The next `GET` falls through to the crew layer (or the synthesised default).
</Warning>

**Response:** `204 No Content`

### Get Persona History

```
GET /api/v1/agents/{agentId}/persona/history?limit=20
```

Lists rows from `memory_versions` filtered to this agent's
`PERSONA.md` path. Pairs with [`GET /api/v1/admin/memory/versions/{id}/content`](/api-reference/admin#get-api-v1-admin-memory-versions-id-content) for content drill-down — the history endpoint deliberately omits content to keep responses small.

**Query Parameters:**

| Param   | Default | Max   | Description                     |
| ------- | ------- | ----- | ------------------------------- |
| `limit` | `20`    | `100` | Page size, clamped server-side. |

**Response:** `200 OK`

```json theme={null}
{
  "entries": [
    {
      "id": "mv-cuid",
      "sha256": "deadbeef…",
      "bytes": 1842,
      "written_at": "2026-05-27T18:42:11Z",
      "written_by": "user-uuid",
      "parent_sha": "cafef00d…"
    }
  ]
}
```

`parent_sha` is omitted on the first version (when the chain starts).

### Suggest Persona

```
POST /api/v1/agents/{agentId}/persona/suggest
Content-Type: application/json
```

Agent-initiated persona proposal. The crew's autonomy policy decides
the outcome:

| Crew autonomy                   | Outcome                                                                                                                       |
| ------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `strict` / `guided` / `trusted` | Inbox approval item created. Agent learns `pending` so it can reference the proposal in subsequent runs.                      |
| `full`                          | Auto-apply + journal entry — unless the agent's `self_learning_enabled = 0`, which demotes auto-apply back to inbox approval. |

`ActionPersonaDirectWrite` is rejected across every autonomy level —
this endpoint is the **only** path an agent can use to write a
persona.

**Request:**

```json theme={null}
{
  "content": "# Engineer\n\nFocus on backend services...\n",
  "rationale": "Operator briefly emphasised the on-call rotation; reflecting that in the persona."
}
```

**Response:** `200 OK` (proposal queued or applied) / `403` (policy rejected)

```json theme={null}
{
  "agent_id": "agent-uuid",
  "decision": "inbox_approve",
  "bytes": 1842,
  "rationale": "...",
  "timestamp": "2026-05-27T18:42:11Z",
  "applied": false,
  "pending": true
}
```

| Field                | Type      | Description                                                                                                      |
| -------------------- | --------- | ---------------------------------------------------------------------------------------------------------------- |
| `decision`           | `string`  | Policy decision that drove the outcome (e.g. `inbox_approve`, `auto_journal`, `rejected`).                       |
| `applied`            | `bool`    | `true` only when the suggestion was auto-applied to disk (`full` autonomy + `self_learning_enabled = 1`).        |
| `pending`            | `bool`    | `true` when the proposal was queued for operator approval (present on the inbox path).                           |
| `self_learning_gate` | `string?` | `"off"` only when a per-agent `self_learning_enabled = 0` demoted an auto-apply decision back to inbox approval. |

There is no `inbox_id` in this response — the proposal is recorded as an `audit_logs` row (`persona.suggest_pending`), not returned to the caller.

| Status | Condition                                                           |
| ------ | ------------------------------------------------------------------- |
| `400`  | Missing / empty `content` field                                     |
| `403`  | Policy rejected the suggestion (autonomy floor)                     |
| `413`  | `content` exceeds `cap_bytes`                                       |
| `503`  | Server built without storage configuration (`outputBasePath` empty) |

***

## Agent Self-Learning Posture

Per-agent flag controlling whether `ALLOW` decisions from the Keeper
evaluators **auto-apply** (`enabled = true`) or **queue for operator
approval** in the inbox (`enabled = false`). Orthogonal to the crew's
`autonomy_level`: the per-action policy gate (`policy.DecideAction`)
is the upstream authority — this flag only decides what happens to
already-ALLOWed decisions. See [Autonomy & self-learning](/guides/autonomy-and-self-learning).

### Get Self-Learning

```
GET /api/v1/agents/{agentId}/learning
```

Returns the current flag plus the audit triple (who flipped it, when,
why). Any authenticated workspace member can read — the value is
non-secret diagnostic state.

**Response:** `200 OK`

```json theme={null}
{
  "agent_id": "agent-uuid",
  "enabled": true,
  "set_by_user_id": "user-uuid",
  "set_at": "2026-05-26T09:14:22Z",
  "reason": "Trusted crew, agent has cleared 50+ approvals without an override"
}
```

| Field            | Type      | Notes                                                                             |
| ---------------- | --------- | --------------------------------------------------------------------------------- |
| `enabled`        | `bool`    | Persisted as `0` / `1` on `agents.self_learning_enabled`.                         |
| `set_by_user_id` | `string?` | Omitted on agents that have never had the flag flipped.                           |
| `set_at`         | `string?` | RFC3339; omitted when never set.                                                  |
| `reason`         | `string?` | Operator-supplied rationale from the most recent `PATCH`; omitted when never set. |

### Set Self-Learning

```
PATCH /api/v1/agents/{agentId}/learning
Content-Type: application/json
```

Flip the flag. Requires `OWNER` / `ADMIN` (`canRole(role, "manage")`)
— self-learning weakens the inbox-approval invariant, so the operator
who turns it on must be senior enough to own the consequences.

**Request:**

```json theme={null}
{
  "enabled": true,
  "reason": "Trusted crew, agent has cleared 50+ approvals without an override"
}
```

| Field     | Type     | Required | Notes                                                                                                                                    |
| --------- | -------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------- |
| `enabled` | `bool`   | yes      | Decoded as `*bool` server-side — a missing or `null` field returns `400`, **not** a silent flip to `false`. Be explicit about direction. |
| `reason`  | `string` | yes      | Non-empty after trim. Persisted on the row so a later audit can answer "who turned this on, when, and why".                              |

**Response:** `200 OK` — same shape as `GET`, reflecting the new state and the freshly-recorded audit triple.

| Status | Condition                                         |
| ------ | ------------------------------------------------- |
| `400`  | `enabled` field missing / null, or `reason` empty |
| `403`  | Caller is not OWNER / ADMIN                       |
| `404`  | Agent not found in the current workspace          |

***

## Agent Inbox Summary

```
GET /api/v1/agents/{agentId}/inbox
```

Consolidated "what's waiting on this agent" payload used by the
Crews preview panel — one round-trip instead of four parallel fetches
across approvals, assignments, escalations, and peer messages. UI is
the primary consumer; operators can poll the endpoint for an at-a-glance
load check.

**Response:** `200 OK`

```json theme={null}
{
  "approvals_pending": 2,
  "assignments_open": 5,
  "escalations_open": 0,
  "peer_messages": [
    {
      "id": "pm-uuid",
      "from_agent_name": "Martin",
      "from_agent_slug": "martin",
      "question": "What's the latest on the deploy script?",
      "response": null,
      "escalated": false,
      "status": "open",
      "created_at": "2026-05-27T12:30:00Z",
      "direction": "incoming"
    }
  ],
  "cost_usd_this_month": 4.21,
  "llm_calls_this_month": 312,
  "tokens_used_this_month": 184221
}
```

| Field                    | Type    | Description                                                                                                                                                  |
| ------------------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `approvals_pending`      | `int`   | Inbox waitpoint items awaiting this agent's decision.                                                                                                        |
| `assignments_open`       | `int`   | Tasks routed to this agent that have not reached a terminal state.                                                                                           |
| `escalations_open`       | `int`   | Peer conversations escalated up the lead chain that still target this agent.                                                                                 |
| `peer_messages`          | `array` | Last N peer messages (both directions). `direction` is `"incoming"` or `"outgoing"`; `response` and `duration_ms` are populated only for resolved exchanges. |
| `cost_usd_this_month`    | `float` | Sum of `cost_ledger.cost_usd` for this agent in the current calendar month. `0` when the `cost_ledger` table is absent (older workspaces).                   |
| `llm_calls_this_month`   | `int`   | Count of `cost_ledger` rows for this agent in the current month.                                                                                             |
| `tokens_used_this_month` | `int64` | Sum of `input_tokens + output_tokens` from `cost_ledger` in the current month.                                                                               |

| Status | Condition                                |
| ------ | ---------------------------------------- |
| `401`  | Workspace context missing                |
| `404`  | Agent not found in the current workspace |

***

## Agent Peer Cards

Per-`(agent, user)` markdown notes produced by the `PeerCardSync`
routine — the agent's own "what I know about this person" file.
These endpoints are **operator-facing**: the cards live on disk
under `{outputBase}/crews/{crewID}/agents/{slug}/.memory/peers/`,
and every read/write/delete writes a `peer_card_audit` row for GDPR
SAR coverage.

The user-facing surface (view-mine / opt-out / delete-mine) lives
under `/api/v1/users/me/peer-cards` and shares the same disk + DB
primitives — these agent-flavor endpoints exist so an operator can
clean up cards on behalf of a user (compliance ticket, leaver
workflow) or inspect what an agent has been writing about people.

The `{userId}` path parameter is the raw `user_id`; the server
derives the `user_slug` via `memory.UserSlug` so the URL stays
debuggable.

### List Agent Peers

```
GET /api/v1/agents/{agentId}/peers
```

Returns the agent's peer-card index (no card content — fetch via the
single-card endpoint below). Workspace-scoped; agents in other
workspaces return `404`.

**Response:** `200 OK`

```json theme={null}
{
  "agent_id": "agent-uuid",
  "peers": [
    {
      "id": "pc-uuid",
      "user_id": "user-uuid",
      "user_slug": "alice",
      "bytes": 412,
      "created_at": "2026-05-10T...",
      "updated_at": "2026-05-26T..."
    }
  ]
}
```

| Status | Condition                                                      |
| ------ | -------------------------------------------------------------- |
| `404`  | Agent not found in the current workspace                       |
| `409`  | Agent has no `crew_id` — peer cards require a crew-scoped path |

### Get Agent Peer Card

```
GET /api/v1/agents/{agentId}/peers/{userId}
```

Returns one card's full content (markdown). Writes a `read` row to
`peer_card_audit`.

**Response:** `200 OK`

```json theme={null}
{
  "agent_id": "agent-uuid",
  "user_id": "user-uuid",
  "user_slug": "alice",
  "content": "# Alice Chen\n\n- Prefers async over sync...\n",
  "bytes": 412
}
```

| Status | Condition                                                   |
| ------ | ----------------------------------------------------------- |
| `404`  | Agent not found, or no card exists for this `(agent, user)` |
| `409`  | Agent has no `crew_id`                                      |
| `503`  | `outputBasePath` not configured server-side                 |

### Delete Agent Peer Card

```
DELETE /api/v1/agents/{agentId}/peers/{userId}
```

<Warning>
  Removes the card from disk and the `peer_cards` row, then writes a `delete` audit row. Idempotent — deleting a non-existent card returns `204` rather than `404` so an operator script can retry safely.
</Warning>

**Response:** `204 No Content`

<Note>
  The GDPR cascade endpoint [`DELETE /api/v1/admin/users/{userId}/data`](/api-reference/admin#delete-api-v1-admin-users-userid-data) deletes peer cards across **all** agents in the workspace for a single user — that's the right path for a full SAR. This per-agent endpoint is for the narrower "clean up just this agent's notes about this user" case.
</Note>

***

## Agent Skills

Assign, list, and remove the skills available to an agent.

### List Skills

```
GET /api/v1/agents/{agentId}/skills?workspace_id={workspaceId}
```

**Response:** `200 OK` -- array of skill assignment objects.

### Add Skill

```
POST /api/v1/agents/{agentId}/skills?workspace_id={workspaceId}
```

**Auth:** `OWNER`, `ADMIN`, or `MANAGER` role (`canRole(role, "create")`)

**Request Body:**

| Field      | Type   | Required | Description                               |
| ---------- | ------ | -------- | ----------------------------------------- |
| `skill_id` | string | Yes      | Skill to assign                           |
| `config`   | string | No       | Per-assignment config blob (stored as-is) |

```json theme={null}
{
  "skill_id": "skill_abc"
}
```

**Response:** `201 Created` -- `{ "id": "<assignment_id>" }` on a fresh assign.

The assign is idempotent: re-assigning an already-installed skill returns `200 OK` with `{ "id": "<existing_id>", "already_assigned": true }` rather than a `409`.

### Remove Skill

```
DELETE /api/v1/agents/{agentId}/skills/{skillId}?workspace_id={workspaceId}
```

**Response:** `204 No Content`

***

## Agent Credentials

Assign, list, and remove the credentials injected into an agent's container.

### List Credentials

```
GET /api/v1/agents/{agentId}/credentials?workspace_id={workspaceId}
```

**Response:** `200 OK` -- array of credential assignment objects.

### Assign Credential

```
POST /api/v1/agents/{agentId}/credentials?workspace_id={workspaceId}
```

**Auth:** `OWNER` or `ADMIN` role (`canRole(role, "manage")` in `internal/api/agent_credentials.go:89`)

**Request Body:**

| Field           | Type    | Required | Description                                                           |
| --------------- | ------- | -------- | --------------------------------------------------------------------- |
| `credential_id` | string  | Yes      | Credential to assign (must exist in this workspace)                   |
| `env_var_name`  | string  | Yes      | Environment variable name to inject into the agent container          |
| `priority`      | integer | No       | Resolution priority when multiple credentials share an `env_var_name` |

```json theme={null}
{
  "credential_id": "cred_abc",
  "env_var_name": "ANTHROPIC_API_KEY",
  "priority": 0
}
```

**Response:** `201 Created`

```json theme={null}
{ "id": "<assignment_id>" }
```

| Status | Condition                                 |
| ------ | ----------------------------------------- |
| `400`  | Missing `credential_id` or `env_var_name` |
| `403`  | Insufficient role                         |
| `404`  | Agent or credential not found             |
| `409`  | Credential already assigned to this agent |

### Remove Credential

```
DELETE /api/v1/agents/{agentId}/credentials/{assignmentId}?workspace_id={workspaceId}
```

The path segment is the **assignment ID** (`agent_credentials.id`), not the credential id.

**Auth:** `OWNER` or `ADMIN` role (`canRole(role, "manage")` in `internal/api/agent_credentials.go:146`)

**Response:** `200 OK`

```json theme={null}
{ "success": true }
```

***

## Agent Chats & Runs

List and create interactive chat sessions, and review execution history.

### List Chats

```
GET /api/v1/agents/{agentId}/chats?workspace_id={workspaceId}
```

**Response:** `200 OK` -- array of chat session objects.

### Create Chat

```
POST /api/v1/agents/{agentId}/chats?workspace_id={workspaceId}
```

Creates a new interactive chat session with the agent.

**Response:** `201 Created`

***

### Agent Runs

```
GET /api/v1/agents/{agentId}/runs?workspace_id={workspaceId}
```

List execution history for an agent.

**Response:** `200 OK` -- array of run objects.

***

## Agent Proxy Endpoints

These endpoints proxy requests through the crewshipd sidecar to the agent's container.

<Warning>
  `POST /api/v1/agents/{agentId}/stop` halts a running agent.
</Warning>

| Endpoint                                               | Method | Description                                                 |
| ------------------------------------------------------ | ------ | ----------------------------------------------------------- |
| `GET /api/v1/agents/{agentId}/debug`                   | GET    | Debug information for a running agent                       |
| `GET /api/v1/agents/{agentId}/files`                   | GET    | List files in agent workspace                               |
| `GET /api/v1/agents/{agentId}/files/download?path=...` | GET    | Download a file                                             |
| `PUT /api/v1/agents/{agentId}/files/save`              | PUT    | Save/upload a file                                          |
| `GET /api/v1/agents/{agentId}/container-files`         | GET    | List files inside the running container (see below)         |
| `GET /api/v1/agents/{agentId}/git-log`                 | GET    | Recent git commits from the container workspace (see below) |
| `GET /api/v1/agents/{agentId}/logs`                    | GET    | Get agent logs                                              |
| `POST /api/v1/agents/{agentId}/stop`                   | POST   | Stop a running agent                                        |

### Container Files

```
GET /api/v1/agents/{agentId}/container-files?subdir={path}
```

Lists files inside the agent's running container (as opposed to `/files`, which lists the persistent workspace mount). Proxied through crewshipd; the request is rejected before the IPC hop if the agent is not assigned to a crew. (`internal/api/proxy_files.go:350`)

**Auth:** Session or CLI token + workspace membership with at least `read`-tier role (`canRole(role, "read")`)

**Query Parameters:**

| Parameter | Type   | Description                                                                                      |
| --------- | ------ | ------------------------------------------------------------------------------------------------ |
| `subdir`  | string | Optional sub-path within the container's working directory (normalized to reject `..` traversal) |

**Response:** `200 OK` -- JSON array of file entries (unwrapped from the IPC layer's `{files: [...]}` envelope). Returns `[]` when the sidecar response is unparseable or empty.

| Status | Condition                                        |
| ------ | ------------------------------------------------ |
| `400`  | `subdir` path is invalid (traversal or absolute) |
| `403`  | Caller lacks `read`-tier role                    |
| `404`  | Agent not found, or agent not assigned to a crew |
| `502`  | Sidecar IPC call failed                          |

### Git Log

```
GET /api/v1/agents/{agentId}/git-log
```

Fetches recent git commits from the agent's container workspace. Proxied through crewshipd; the agent's `slug` is forwarded as `agent_slug` so the sidecar can scope the log to that agent's directory inside the shared crew container. (`internal/api/proxy.go:312`)

**Auth:** Session or CLI token + workspace membership with at least `read`-tier role (`canRole(role, "read")`)

**Response:** `200 OK` -- JSON array of commit entries (unwrapped from the IPC layer's `{commits: [...]}` envelope). Returns `[]` when the sidecar response is unparseable or empty.

| Status | Condition                                        |
| ------ | ------------------------------------------------ |
| `403`  | Caller lacks `read`-tier role                    |
| `404`  | Agent not found, or agent not assigned to a crew |
| `502`  | Sidecar IPC call failed                          |

***

## Chat Messages

```
GET /api/v1/chats/{chatId}/messages
```

Get messages for a specific chat session. Proxied through crewshipd. Supports `?offset=` and `?limit=` (default `50`, max `500`).

**Auth:** Session or CLI token with `read`-tier role, plus membership of the chat's workspace. The workspace is resolved from the chat row (no `workspace_id` query param needed); the caller must appear in `workspace_members` for that workspace or the call returns `403`. (`internal/api/proxy.go:258`)

**Response:** `200 OK` -- proxied message payload. A chat that doesn't exist yet (new session before its first message) returns `{ "messages": [] }` rather than `404`.

| Status | Condition                                                                 |
| ------ | ------------------------------------------------------------------------- |
| `403`  | Caller lacks `read`-tier role, or is not a member of the chat's workspace |
| `502`  | Sidecar IPC call failed                                                   |
