---
title: API Reference
description: Complete reference for all Molecule AI Platform HTTP and WebSocket endpoints.
---

# API Reference

The Molecule AI Platform exposes a REST API (default port 8080) for workspace management, agent registry, communication, and administration. All endpoints return JSON unless otherwise noted.

<Callout type="warn">
  **Breaking changes — PR #701 (2026-04-17)**

  - **`PATCH /workspaces/:id` now requires authentication.** Previously, requests without a bearer token could update cosmetic fields (name, x/y position). All `PATCH` calls now require `Authorization: Bearer <workspace-token>` or receive **401 Unauthorized**.
  - **`GET /templates` and `GET /org/templates` now require AdminAuth.** Unauthenticated callers receive **401 Unauthorized**.
  - **All `/workspaces/:id` endpoints validate the `:id` path parameter** as a UUID. Non-UUID values return **400 Bad Request** before any database interaction.

  **Migration:** add `Authorization: Bearer <workspace-token>` to all `PATCH /workspaces/:id` calls. Use an admin bearer token for `GET /templates` and `GET /org/templates`. Ensure `:id` values in automation scripts are valid UUIDs.
</Callout>

**Base URL:** `http://localhost:8080` (self-hosted) or `https://api.moleculesai.app` (SaaS)

---

## Authentication Model

The platform uses three authentication middleware variants depending on the sensitivity of the route.

### AdminAuth

Strict bearer-token authentication. Required for any route where a forged request could leak prompts/memory, create/mutate workspaces, or leak operational data.

```
Authorization: Bearer <token>
```

**Fail-open behavior:** When no live tokens exist globally (fresh install), AdminAuth passes all requests through. Once the first token is created, all AdminAuth routes require a valid bearer.

### WorkspaceAuth

Per-workspace bearer token binding. Workspace A's token cannot access workspace B's sub-routes. Used for the entire `/workspaces/:id/*` group (except the A2A proxy, which uses `CanCommunicate`).

```
Authorization: Bearer <workspace-token>
```

### CanvasOrBearer

Accepts either a valid bearer token OR a request whose `Origin` header matches `CORS_ORIGINS`. Used only for cosmetic-only routes where a forged request has zero data/security impact.

Currently applies only to `PUT /canvas/viewport`. Do not extend to data-sensitive routes.

---

## Health and Monitoring

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/health` | None | Returns `200 OK` if the platform is running. Use for load balancer health checks. |
| GET | `/metrics` | None | Prometheus text format (v0.0.4) metrics. Scrape-safe, no auth required. |
| GET | `/admin/liveness` | AdminAuth | Per-subsystem `supervised.Snapshot()` ages. Check before debugging stuck scheduler/heartbeat goroutines. |

---

## Workspaces

Core workspace CRUD and lifecycle operations.

### CRUD

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/workspaces` | AdminAuth | Create a new workspace. Accepts `name`, `runtime`, `template`, `parent_id`, `tier`, `workspace_dir`, and other fields. Runtime is auto-detected from template config if omitted (defaults to `langgraph`). |
| GET | `/workspaces` | AdminAuth | List all workspaces with status, runtime, agent card, position, and hierarchy info. |
| GET | `/workspaces/:id` | WorkspaceAuth | Get a single workspace by ID. |
| PATCH | `/workspaces/:id` | WorkspaceAuth | Update workspace fields. A workspace bearer token is always required — unauthenticated calls return 401. Validates field constraints: `name` ≤ 255 chars, `role` ≤ 1,000 chars, `model` and `runtime` ≤ 100 chars each; `name` and `role` must not contain newlines (`\\n`, `\\r`) or YAML-special characters (`{}[]|>*&!`). Oversized or invalid field values return 400. `:id` must be a valid UUID. Financial fields (`budget_limit`) are not accepted here — use `PATCH /workspaces/:id/budget` (AdminAuth). |
| DELETE | `/workspaces/:id` | AdminAuth | Delete a workspace. Stops the container, revokes all auth tokens, and removes all associated data. |

### Lifecycle

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/workspaces/:id/restart` | WorkspaceAuth | Restart the workspace container. Sends a `restart_context` A2A message after successful re-registration. |
| POST | `/workspaces/:id/pause` | WorkspaceAuth | Stop the container and set status to `paused`. Paused workspaces skip health sweep, liveness monitor, and auto-restart. Resume manually via `/resume`. |
| POST | `/workspaces/:id/resume` | WorkspaceAuth | Re-provision a paused workspace. Status transitions to `provisioning`. |
| POST | `/workspaces/:id/hibernate` | WorkspaceAuth | Immediately hibernate a workspace (stop container, set status to `hibernated`). Useful for manual cost control. See hibernation note below. |

<Callout type="info">
  **Workspace hibernation**

  A workspace with `hibernation_idle_minutes` set in its config will be **automatically hibernated** by the platform after that many idle minutes (no active tasks, no recent heartbeat). The monitor checks every 2 minutes.

  `hibernated` differs from `paused`:
  - **`paused`** — manual, resumes only via `POST /resume`.
  - **`hibernated`** — automatic (or via `POST /hibernate`), resumes **automatically** when an A2A message arrives.

  When a message is sent to a hibernated workspace, the platform returns:
  ```
  HTTP 503  Retry-After: 15
  {"waking": true}
  ```
  Callers should retry after ~15 seconds. The workspace typically returns to `online` within that window.

  To opt a workspace into auto-hibernation, add to its `config.yaml`:
  ```yaml
  hibernation_idle_minutes: 30   # hibernate after 30 min idle; null (default) = disabled
  ```

  **Atomic hibernation guarantee:** The platform uses a single atomic SQL claim (`UPDATE … WHERE active_tasks = 0`) before stopping the container. If a task arrives between the idle check and the container stop, the claim fails and hibernation is aborted — no in-flight tasks are silently lost.
</Callout>

### Budget

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/workspaces/:id/budget` | AdminAuth | Read a workspace's current spend and ceiling. Returns `budget_limit`, `monthly_spend`, and `budget_remaining` (all in USD cents). |
| PATCH | `/workspaces/:id/budget` | AdminAuth | Set or clear a workspace's monthly spend ceiling. Body: `{ "budget_limit": N }` (positive integer, USD cents) or `{ "budget_limit": null }` to remove the cap. Negative values → 400. Returns same shape as GET. |

**Request / response shape:**

```json
// PATCH request body
{ "budget_limit": 500 }   // $5.00/month ceiling
{ "budget_limit": null }  // no ceiling

// GET and PATCH success response (200)
{
  "budget_limit":     500,   // null when no ceiling
  "monthly_spend":    312,   // accumulated spend this period, USD cents
  "budget_remaining": 188    // null when no ceiling; max(0, limit-spend) — can be negative
}
```

<Callout type="warn">
  **`budget_limit` and `monthly_spend` are absent from `GET /workspaces/:id`**

  Financial fields are stripped unconditionally from the workspace detail
  response — they do not appear for any caller, authenticated or not. Always
  use `GET /workspaces/:id/budget` (AdminAuth) to read spend data.

  `budget_limit` is also **not** accepted on the general `PATCH /workspaces/:id`
  endpoint. Use the dedicated `/budget` route.
</Callout>

<Callout type="info">
  **Enforcement and fail-open behaviour**

  When `monthly_spend >= budget_limit`, `POST /workspaces/:id/a2a` returns:
  ```
  HTTP 402 Payment Required
  {"error": "workspace budget limit exceeded"}
  ```
  Channel sends (Slack, Telegram, Discord, Lark) are also budget-gated with
  the same 402 response. The workspace itself is **not paused** — it keeps
  running; only inbound A2A and channel traffic is blocked.

  **Fail-open:** if the budget check encounters a DB error, traffic is allowed
  through rather than blocked. The spend ceiling is a soft guardrail, not a
  hard guarantee.
</Callout>

---

## Registry

Workspace registration and heartbeat endpoints. Called by workspace runtimes, not by end users.

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/registry/register` | None | Register a workspace with the platform. Sets status to `online`. Body includes agent URL, agent card, capabilities. |
| POST | `/registry/heartbeat` | Bearer (if token exists) | Send a heartbeat. Updates Redis TTL key (60s expiry). Body can include `active_tasks`, `current_task`, `error_rate`. Triggers `degraded` status if `error_rate > 0.5`. |
| POST | `/registry/update-card` | Bearer (if token exists) | Update the workspace's agent card (name, description, skills, etc.). |

---

## Discovery

Peer discovery and access control verification.

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/registry/discover/:id` | Bearer + `X-Workspace-ID` | Discover a workspace's agent card and URL. Requires caller identification. Fails open on DB hiccup since hierarchy check is primary. |
| GET | `/registry/:id/peers` | Bearer + `X-Workspace-ID` | List all peers (siblings, parent, children) that the caller can communicate with. |
| POST | `/registry/check-access` | None | Check whether two workspaces can communicate. Body: `{ "caller_id": "...", "target_id": "..." }`. Returns `{ "allowed": true/false }`. |

---

## Communication

### A2A Proxy

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/workspaces/:id/a2a` | CanCommunicate | Proxy an A2A JSON-RPC message to the target workspace. Caller identified via `X-Workspace-ID` header. Canvas requests (no header) bypass access check. On connection error, checks if container is dead and triggers auto-restart. |

### Delegation

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/workspaces/:id/delegate` | WorkspaceAuth | Async fire-and-forget delegation. Supports idempotency keys. Body includes target workspace, prompt, and metadata. |
| GET | `/workspaces/:id/delegations` | WorkspaceAuth | List delegation status for a workspace. Returns delegation rows with status, result, timestamps. |

---

## Configuration

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/workspaces/:id/config` | WorkspaceAuth | Get the workspace's `config.yaml` contents. |
| PATCH | `/workspaces/:id/config` | WorkspaceAuth | Update the workspace config. "Save & Restart" writes config and auto-restarts; "Save" writes only and shows a restart banner in the Canvas. |

---

## Secrets

### Per-Workspace Secrets

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/workspaces/:id/secrets` | WorkspaceAuth | List secret keys for a workspace (keys only, values masked). |
| POST | `/workspaces/:id/secrets` | WorkspaceAuth | Set a secret `{ "key": "...", "value": "..." }`. Auto-restarts the workspace. |
| PUT | `/workspaces/:id/secrets` | WorkspaceAuth | Alias for POST (upsert semantics). Auto-restarts the workspace. |
| DELETE | `/workspaces/:id/secrets/:key` | WorkspaceAuth | Delete a secret by key. Auto-restarts the workspace. |
| GET | `/workspaces/:id/model` | WorkspaceAuth | Return the model configuration derived from available API keys (which provider keys are set). |

### Global Secrets

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/settings/secrets` | AdminAuth | List global secrets (keys only, values masked). |
| PUT | `/settings/secrets` | AdminAuth | Set a global secret `{ "key": "...", "value": "..." }`. Auto-restarts every non-paused/non-removed workspace that does not shadow the key with a workspace-level override. |
| POST | `/settings/secrets` | AdminAuth | Alias for PUT. |
| DELETE | `/settings/secrets/:key` | AdminAuth | Delete a global secret. Same auto-restart fan-out as PUT. |

Legacy aliases `GET/POST/DELETE /admin/secrets[/:key]` also exist and behave identically.

---

## Memory

### Key-Value Memory

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/workspaces/:id/memory` | WorkspaceAuth | List all key-value memory entries for a workspace. |
| POST | `/workspaces/:id/memory` | WorkspaceAuth | Set a memory entry `{ "key": "...", "value": "..." }`. |
| DELETE | `/workspaces/:id/memory/:key` | WorkspaceAuth | Delete a memory entry by key. |

### Agent Memories (HMA-scoped)

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/workspaces/:id/memories` | WorkspaceAuth | List or search agent memories. Supports `?q=` for semantic search (see below). |
| POST | `/workspaces/:id/memories` | WorkspaceAuth | Create an agent memory entry. |
| DELETE | `/workspaces/:id/memories/:id` | WorkspaceAuth | Delete an agent memory by ID. |

#### Semantic search (`?q=`)

When a platform-level embedding function is configured, passing `?q=<text>`
on `GET /workspaces/:id/memories` triggers vector similarity search instead of
the default full-text / ILIKE path:

```
GET /workspaces/{id}/memories?q=authentication+flow&limit=10
Authorization: Bearer {token}
```

Matching entries are returned **ordered by cosine similarity** (most similar
first). Each row includes an additional `similarity_score` field (0–1, higher
is closer):

```json
[
  {
    "id": "mem_abc123",
    "key": "auth-design",
    "value": "We use short-lived JWTs issued by the platform and refreshed via /auth/token.",
    "similarity_score": 0.91,
    "created_at": "2026-04-10T14:22:00Z"
  }
]
```

**Graceful fallback**: if no embedding function is configured, or if the
embedding call fails for a given query, the platform falls back transparently
to the text-search path. The `similarity_score` field is absent in fallback
responses. You do not need to change client code to handle both modes.

---

## Files

Workspace file management. Files are stored in the workspace's config directory.

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/workspaces/:id/files` | WorkspaceAuth | List files in the workspace config directory. |
| GET | `/workspaces/:id/files/*path` | WorkspaceAuth | Read a specific file. |
| PUT | `/workspaces/:id/files/*path` | WorkspaceAuth | Write a file. Creates parent directories as needed. On SaaS workspaces (EC2, no Docker), routes via EC2 Instance Connect endpoint using an ephemeral SSH key pair — the key is scoped to the file-write operation and deleted within 30 seconds. Max payload ~10 MiB. Self-hosted Docker workspaces write via `docker cp` as before. |
| DELETE | `/workspaces/:id/files/*path` | WorkspaceAuth | Delete a file. |
| GET | `/workspaces/:id/shared-context` | WorkspaceAuth | Get the shared context files for a workspace (aggregated from parent hierarchy). |

---

## Activity

Activity logging and search for A2A communications, task updates, and agent logs.

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/workspaces/:id/activity` | WorkspaceAuth | List activity logs for a workspace. Supports `?source=canvas` or `?source=agent` filter, and `?type=delegation` for A2A topology overlay polling. |
| POST | `/workspaces/:id/activity` | WorkspaceAuth | Log an activity entry (used by workspace runtimes to self-report). |
| POST | `/workspaces/:id/notify` | WorkspaceAuth | Agent-to-user push message via WebSocket. Delivers a notification to connected Canvas clients. |

---

## Audit Ledger

Tamper-evident audit trail for workspace events. Used by the Canvas Audit Trail panel.

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/workspaces/:id/audit` | WorkspaceAuth | List audit entries for a workspace. Supports `?event_type=delegation\|decision\|gate\|hitl`, `?cursor=<cursor>`, and `?limit=<n>` (default 50). |

### Audit entry schema

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Unique entry ID |
| `event_type` | string | `delegation`, `decision`, `gate`, or `hitl` |
| `actor` | string | Workspace ID that generated the event |
| `summary` | string | Human-readable event description |
| `chain_valid` | bool | `false` if the entry's hash does not match the prior chain — indicates possible tampering |
| `created_at` | string (ISO 8601) | Event timestamp |
| `cursor` | string \| null | Opaque pagination cursor; `null` when there are no more entries |

Example response:

```json
{
  "entries": [
    {
      "id": "aud_xyz789",
      "event_type": "delegation",
      "actor": "ws_abc123",
      "summary": "Delegated task 'fix CI' to Backend Engineer",
      "chain_valid": true,
      "created_at": "2026-04-17T14:05:00Z"
    }
  ],
  "cursor": "eyJpZCI6ImF1ZF94eXo3ODkifQ"
}
```

### Session Search

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/workspaces/:id/session-search` | WorkspaceAuth | Search activity logs with filters for type, date range, and text content. Returns paginated results. |

---

## Workflow Checkpoints

Step-level progress persistence for long-running Temporal workflows. Workspaces with `runtime: langgraph` (Temporal) automatically save a checkpoint after each of the three workflow stages (`task_receive`, `llm_call`, `task_complete`) and resume from the last completed stage on restart.

<Callout type="info">
  **Automatic resume behavior (runtime: langgraph only)**

  When a Temporal workspace restarts mid-workflow, the runtime reads the highest-index checkpoint and sets `resume_from_step` accordingly. Already-completed stages are skipped — the agent picks up exactly where it left off without re-running earlier steps.

  Checkpoint I/O is non-fatal: network errors are silently swallowed. A crashed or unreachable platform never prevents the agent from running.
</Callout>

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/workspaces/:id/checkpoints` | WorkspaceAuth | Upsert a step checkpoint. Body: `{ "workflow_id": "...", "step_name": "task_receive\|llm_call\|task_complete", "step_index": 0, "payload": {...} }`. Uses `ON CONFLICT DO UPDATE` — safe to call multiple times. |
| GET | `/workspaces/:id/checkpoints/:wfid` | WorkspaceAuth | Return all checkpoints for a workflow, ordered by `step_index DESC`. Returns 404 if no checkpoints exist for the workflow. |
| DELETE | `/workspaces/:id/checkpoints/:wfid` | WorkspaceAuth | Clear all checkpoints for a workflow. Called by the runtime on clean task completion. Returns 404 if none exist. |

**Step names and indices:**

| Step | `step_index` | Meaning |
|------|-------------|---------|
| `task_receive` | 0 | Task received from A2A message |
| `llm_call` | 1 | LLM inference completed |
| `task_complete` | 2 | Task result sent back to caller |

---

## Schedules

Cron-based scheduled tasks per workspace.

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/workspaces/:id/schedules` | WorkspaceAuth | List all schedules for a workspace. |
| POST | `/workspaces/:id/schedules` | WorkspaceAuth | Create a schedule. Body: `{ "expression": "0 */6 * * *", "timezone": "UTC", "prompt": "...", "enabled": true }`. |
| PATCH | `/workspaces/:id/schedules/:scheduleId` | WorkspaceAuth | Update a schedule (expression, timezone, prompt, enabled). |
| DELETE | `/workspaces/:id/schedules/:scheduleId` | WorkspaceAuth | Delete a schedule. |
| POST | `/workspaces/:id/schedules/:scheduleId/run` | WorkspaceAuth | Manually trigger a schedule immediately. |
| GET | `/workspaces/:id/schedules/:scheduleId/history` | WorkspaceAuth | List past runs for a schedule. Includes status (`success`, `error`, `skipped`) and `error_detail`. |

Schedule `source` field: `template` for org/import-seeded schedules, `runtime` for Canvas/API-created. The `last_status` includes `skipped` when the scheduler concurrency-aware-skips a busy workspace.

---

## Channels

Social channel integrations (Telegram, Slack, etc.) for workspace agents.

### Per-Workspace Channels

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/workspaces/:id/channels` | WorkspaceAuth | List channels for a workspace. |
| POST | `/workspaces/:id/channels` | WorkspaceAuth | Create a channel. Body includes platform type, JSONB config, and allowlist. |
| PATCH | `/workspaces/:id/channels/:channelId` | WorkspaceAuth | Update a channel's config or allowlist. |
| DELETE | `/workspaces/:id/channels/:channelId` | WorkspaceAuth | Delete a channel. |
| POST | `/workspaces/:id/channels/:channelId/send` | WorkspaceAuth | Send an outbound message through the channel. |
| POST | `/workspaces/:id/channels/:channelId/test` | WorkspaceAuth | Test the channel connection (send a test message). |

### Global Channel Endpoints

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/channels/adapters` | None | List available social platform adapters (Telegram, Slack, etc.). |
| POST | `/channels/discover` | AdminAuth | Auto-detect available chats/groups for a bot token. |
| POST | `/webhooks/:type` | None | Incoming webhook endpoint for social platforms. The `:type` parameter identifies the platform (e.g., `telegram`, `slack`). |

---

## Plugins

Plugin registry and per-workspace plugin management.

### Global Plugin Registry

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/plugins` | None | List all plugins in the registry. Supports `?runtime=` filter to show only compatible plugins. |
| GET | `/plugins/sources` | None | List registered install-source schemes (e.g., `github://`, `local://`). |

### Per-Workspace Plugins

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/workspaces/:id/plugins` | WorkspaceAuth | List installed plugins for a workspace. |
| POST | `/workspaces/:id/plugins` | WorkspaceAuth | Install a plugin. Body: `{ "source": "github://org/repo" }`. Safeguards: 64 KiB body limit, 5 min fetch timeout, 100 MiB max staged-tree. |
| DELETE | `/workspaces/:id/plugins/:name` | WorkspaceAuth | Uninstall a plugin by name. |
| GET | `/workspaces/:id/plugins/available` | WorkspaceAuth | List plugins available for this workspace (filtered by workspace runtime). |
| GET | `/workspaces/:id/plugins/compatibility` | WorkspaceAuth | Preflight runtime-change check. Query: `?runtime=X`. Returns which currently-installed plugins would be incompatible with the target runtime. |

---

## Auth Tokens

Bearer token management for workspaces.

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/workspaces/:id/tokens` | WorkspaceAuth | List active tokens for a workspace (token values are masked). |
| POST | `/workspaces/:id/tokens` | WorkspaceAuth | Create a new bearer token for the workspace. |
| DELETE | `/workspaces/:id/tokens/:tokenId` | WorkspaceAuth | Revoke a specific token. |

### Test Token (Development Only)

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/admin/workspaces/:id/test-token` | None | Mint a fresh bearer token for E2E scripts. Returns 404 unless `MOLECULE_ENV != production` or `MOLECULE_ENABLE_TEST_TOKENS=1`. |

---

## Teams

Expand and collapse team views in the Canvas hierarchy.

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/workspaces/:id/expand` | WorkspaceAuth | Expand a team workspace to show its children on the canvas. |
| POST | `/workspaces/:id/collapse` | WorkspaceAuth | Collapse a team workspace to hide its children. |

---

## Templates and Bundles

### Templates

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/templates` | AdminAuth | List available workspace templates with their runtime, description, and config schema. |
| POST | `/templates/import` | AdminAuth | Import a workspace template from a `github://` source URL. |

### Org Templates

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/org/templates` | AdminAuth | List available organization templates. |
| POST | `/org/import` | AdminAuth | Import an org template. Applies `resolveInsideRoot` path sanitization. Creates the full workspace hierarchy defined in `org.yaml`. |

### Bundles

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/bundles/export/:id` | AdminAuth | Export a workspace (or workspace tree) as a portable bundle. Includes config, secrets (keys only), memory, schedules, and hierarchy. |
| POST | `/bundles/import` | AdminAuth | Import a previously-exported bundle. Recreates the workspace tree with all associated data. |

---

## Approvals

Human-in-the-loop approval system for agent actions.

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| POST | `/workspaces/:id/approvals` | WorkspaceAuth | Create an approval request. Body includes the action description, metadata, and options. |
| GET | `/workspaces/:id/approvals` | WorkspaceAuth | List approval requests for a workspace. |
| POST | `/workspaces/:id/approvals/:id/decide` | WorkspaceAuth | Approve or reject an approval request. Body: `{ "decision": "approve" }` or `{ "decision": "reject" }`. |
| GET | `/approvals/pending` | AdminAuth | List all pending approval requests across all workspaces. |

---

## Canvas

Canvas viewport persistence (cosmetic only).

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/canvas/viewport` | None | Get the saved canvas viewport (zoom, pan position). Open endpoint for bootstrap-friendliness. |
| PUT | `/canvas/viewport` | CanvasOrBearer | Save the canvas viewport. Accepts bearer OR matching `Origin` header. Worst case on forgery: viewport corruption, recovered by page refresh. |

---

## Traces

LLM trace retrieval from Langfuse.

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/workspaces/:id/traces` | WorkspaceAuth | List LLM traces for a workspace from Langfuse. |

---

## Audit Ledger

HMAC-SHA256-chained immutable agent event log for compliance record-keeping (EU AI Act Art. 12 / Art. 13). Each event is cryptographically chained to the previous one — tampering with any record breaks all subsequent HMACs.

<Callout type="warn">
  **`AUDIT_LEDGER_SALT` required.** The platform and workspace containers must share the same `AUDIT_LEDGER_SALT` environment variable to compute and verify event HMACs. Set it in both your platform env and workspace container env. If the variable is absent, `chain_valid` returns `null` (not `false`) — no records are lost, verification is simply unavailable.
</Callout>

### Query

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/workspaces/:id/audit` | WorkspaceAuth | Query the audit ledger for a workspace. Returns events in descending chronological order with inline chain verification. |

**Query parameters:**

| Parameter | Type | Description |
|-----------|------|-------------|
| `agent_id` | string | Filter to a specific agent. |
| `session_id` | string | Filter to a specific session. |
| `from` | RFC 3339 | Start of time range (e.g. `2026-04-01T00:00:00Z`). |
| `to` | RFC 3339 | End of time range. |
| `limit` | int | Max records to return. Capped at **500**. |
| `offset` | int | Pagination offset. |

**Response shape:**

```json
{
  "events": [
    {
      "id": "uuid",
      "workspace_id": "uuid",
      "agent_id": "my-researcher",
      "session_id": "sess_abc123",
      "event_type": "tool_call",
      "payload": { "tool": "bash", "input": "ls /workspace" },
      "hmac": "sha256hex...",
      "prev_hmac": "sha256hex...",
      "created_at": "2026-04-17T12:00:00Z"
    }
  ],
  "chain_valid": true
}
```

`chain_valid` values:
- `true` — all HMACs verified; ledger is intact.
- `false` — at least one HMAC mismatch; possible tampering.
- `null` — `AUDIT_LEDGER_SALT` is absent from the platform env; verification skipped.

### Workspace-side: recording events

In your workspace template, wire `LedgerHooks` into the agent pipeline:

```python
from molecule_audit.hooks import LedgerHooks

hooks = LedgerHooks(agent_id="my-researcher", session_id=session_id)

async with hooks:
    # hooks.on_task_start / on_llm_call / on_tool_call / on_task_end
    # fire automatically at each pipeline stage
    result = await agent.run(task)
```

`LedgerHooks` is exception-safe — a failed ledger write never aborts the agent task.

### CLI chain verification

```bash
# Verify the full chain for an agent; exit 0 = intact
python -m molecule_audit.verify --agent-id my-researcher

# Custom DB URL
python -m molecule_audit.verify --agent-id my-researcher --db postgresql://user:pass@host/db
```

Exit codes: `0` = chain valid · `1` = broken chain · `2` = `AUDIT_LEDGER_SALT` missing · `3` = DB error.

---

## Events

Append-only event log for structure changes.

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/events` | AdminAuth | List all structure events across all workspaces. |
| GET | `/events/:workspaceId` | AdminAuth | List structure events for a specific workspace. |

---

## Terminal

WebSocket-based terminal access to workspace containers.

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| WS | `/workspaces/:id/terminal` | WorkspaceAuth | Open a WebSocket terminal session to the workspace container. Provides interactive shell access. |

---

## WebSocket

Real-time event streaming for Canvas clients.

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| WS | `/ws` | None | Connect to the WebSocket hub. Receives all structure events (`WORKSPACE_ONLINE`, `WORKSPACE_OFFLINE`, `HEARTBEAT`, `CONFIG_UPDATED`, `A2A_RESPONSE`, `AGENT_MESSAGE`, etc.). Canvas clients connect here for real-time updates. |

---

## Server-Sent Events (AG-UI)

Per-workspace SSE stream compatible with the [AG-UI protocol](https://github.com/ag-ui-protocol/ag-ui). Use this endpoint to consume structured agent events from a web client or external tool without a WebSocket library.

| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/workspaces/:id/events/stream` | WorkspaceAuth | Open an SSE stream for the workspace. Returns `Content-Type: text/event-stream`. Sends an initial `: ping` comment on connect, then delivers every event emitted by the workspace in AG-UI envelope format. Events from other workspaces are filtered out. Returns `404` if the workspace does not exist. |

### Event envelope format

Each event is delivered as an SSE `data:` line containing a JSON object:

```json
{
  "type": "AGENT_MESSAGE",
  "timestamp": 1713398400000,
  "data": { ... }
}
```

- **`type`** — event type string (e.g. `AGENT_MESSAGE`, `A2A_RESPONSE`, `TASK_UPDATED`)
- **`timestamp`** — Unix milliseconds at time of broadcast
- **`data`** — event-specific payload (same payload as the WebSocket hub delivers)

### Event types streamed

All event types emitted by `RecordAndBroadcast` **and** `BroadcastOnly` reach the SSE stream. The `BroadcastOnly` path is important: events like `AGENT_MESSAGE`, `A2A_RESPONSE`, and `TASK_UPDATED` skip Redis and would be invisible to a Redis-only subscriber — the in-process SSE layer catches them.

### Example: connect with `curl`

```bash
curl -N \
  -H "Authorization: Bearer <workspace-token>" \
  http://localhost:8080/workspaces/<id>/events/stream
```

```
: ping

data: {"type":"AGENT_MESSAGE","timestamp":1713398401234,"data":{"text":"Starting task..."}}

data: {"type":"TASK_UPDATED","timestamp":1713398405678,"data":{"status":"running"}}
```

### Example: connect from JavaScript

```js
const es = new EventSource(
  `/workspaces/${workspaceId}/events/stream`,
  { headers: { Authorization: `Bearer ${token}` } }
);

es.onmessage = (e) => {
  const event = JSON.parse(e.data);
  console.log(event.type, event.data);
};
```

<Callout type="info">
  The SSE endpoint uses WorkspaceAuth — the bearer token must be bound to the `:id` in the path. A token for workspace A cannot open a stream for workspace B.
</Callout>

---

## Error Responses

All endpoints return standard HTTP status codes:

| Status | Meaning |
|--------|---------|
| 200 | Success |
| 201 | Created |
| 400 | Bad request (malformed body, missing required fields) |
| 401 | Unauthorized (missing or invalid bearer token) |
| 403 | Forbidden (valid token but insufficient access) |
| 404 | Not found (workspace, schedule, channel, etc. does not exist) |
| 409 | Conflict (idempotency key collision on delegation) |
| 429 | Rate limited (exceeds `RATE_LIMIT` requests/min) |
| 500 | Internal server error |

Error response body format:

```json
{
  "error": "human-readable error message"
}
```

---

## Rate Limiting

All endpoints are subject to a global rate limit of `RATE_LIMIT` requests per minute (default: 600). When exceeded, the platform returns `429 Too Many Requests` with a `Retry-After` header.

---

## CORS

The platform sets CORS headers based on the `CORS_ORIGINS` environment variable (comma-separated list, default: `http://localhost:3000,http://localhost:3001`). Preflight (`OPTIONS`) requests are handled automatically by the Gin CORS middleware.
