Files
molecule-core/docs/api-reference.md
Molecule AI Dev Engineer B (MiniMax) 74bba182a5
CI / Python Lint & Test (pull_request) Successful in 5s
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 7s
E2E Peer Visibility (literal MCP list_peers) / detect-changes (pull_request) Successful in 7s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 5s
Lint forbidden tenant-env keys / Scan workspace_secrets writers for forbidden env keys (pull_request) Successful in 6s
Lint forbidden tenant-env keys / Scan for repo-host token write into tenant workspace surface (pull_request) Successful in 5s
Harness Replays / detect-changes (pull_request) Successful in 6s
sop-checklist / review-refire (pull_request_target) Has been skipped
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (local) (pull_request) Has been skipped
Harness Replays / Harness Replays (pull_request) Successful in 2s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 7s
E2E Peer Visibility (literal MCP list_peers) / E2E Peer Visibility (pull_request) Successful in 6s
reserved-path-review / reserved-path-review (pull_request_target) Failing after 8s
sop-checklist / na-declarations (pull_request) N/A: (none)
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 16s
E2E API Smoke Test / detect-changes (pull_request) Successful in 17s
sop-checklist / all-items-acked (pull_request_target) Successful in 9s
gate-check-v3 / gate-check (pull_request_target) Failing after 13s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 2s
lint-required-no-paths / lint-required-no-paths (pull_request) Successful in 18s
E2E Chat / detect-changes (pull_request) Successful in 28s
CI / Detect changes (pull_request) Successful in 32s
CI / Canvas (Next.js) (pull_request) Successful in 3s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 1s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (stub) (pull_request) Successful in 30s
E2E Chat / E2E Chat (pull_request) Successful in 4s
CI / Canvas Deploy Status (pull_request) Successful in 1s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 34s
Check migration collisions / Migration version collision check (pull_request) Successful in 48s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Successful in 2m17s
Local Provision Lifecycle E2E / Local Provision Lifecycle E2E (real image + MiniMax LLM, advisory) (pull_request) Failing after 1m58s
CI / Platform (Go) (pull_request) Successful in 2m30s
CI / all-required (pull_request) Successful in 5s
E2E Staging External Runtime / E2E Staging External Runtime (pull_request) Successful in 5m36s
security-review / approved (pull_request_target) Approved via pull_request_review trigger
security-review / approved (pull_request_review) Successful in 8s
qa-review / approved (pull_request_target) Approved via pull_request_review trigger
reserved-path-review / reserved-path-review (pull_request_review) Successful in 10s
qa-review / approved (pull_request_review) Successful in 10s
audit-force-merge / audit (pull_request_target) Successful in 7s
sop-checklist / all-items-acked (pull_request) Compensated by status-reaper (non-required pull_request/pull_request_review governance shadow overridden by successful pull_request_target status; see .gitea/scripts/status-reaper.py)
fix(approvals#66): requester-initiated withdraw endpoint
Closes the long-standing gap where an agent had no way to retract an
approval it had raised but no longer needed. Issue #66 — the PM
re-dispatched this as INDEPENDENT of the RFC #2843 gate, and approved
the plan with the following guardrails (7600d2ed):

1. ADDITIVE + REVERSIBLE MIGRATION. The up migration widens
   approval_requests.status CHECK from {pending, approved, denied,
   escalated} to also include 'withdrawn'. The down migration deletes
   any 'withdrawn' rows AND narrows the CHECK back. Rollback-safe even
   if the endpoint has been exercised in the deploy window.

2. AUTHZ AGAINST CREATOR-WORKSPACE-ID, NOT PATH :id. The handler reads
   approval_requests.workspace_id (the row's creator) and compares it
   to the URL path's :id. The path :id is the GATE's workspace for
   cross-workspace approval gates (#2574, #2593) — using it as the
   authz anchor would reject legitimate creators when the gate and
   creator are different workspaces.

3. PENDING-ONLY STATE GUARD. The UPDATE has WHERE status='pending',
   and a 0-rows-affected result returns 409 Conflict (not 404) so the
   caller can distinguish 'row vanished' from 'row exists but already
   moved'. This is the same shape requests.Cancel uses for the
   analogous race.

4. DOCSTRING POINTER. The ListAll comment (which was reverted in
   bcabd207 because it inaccurately claimed a withdraw path existed)
   now points at the real endpoint instead.

NEW ENDPOINT: POST /workspaces/:id/approvals/:approvalId/withdraw
  - workspace-token auth (matches the existing approvals surface)
  - body: empty
  - 200 on success (status='withdrawn', decided_by='requester')
  - 403 if the caller's workspace != the row's creator workspace
  - 404 if the approval doesn't exist (or UUID is malformed)
  - 409 if the approval is no longer 'pending'
  - 500 on DB error
  - broadcasts APPROVAL_WITHDRAWN on the row's creator workspace_id
    (matches Decide's broadcast convention)

NEW FILES:
- migrations/20260614010000_approval_withdrawn_status.up.sql — widen CHECK
- migrations/20260614010000_approval_withdrawn_status.down.sql — narrow + purge

MODIFIED:
- internal/handlers/approvals.go — new Withdraw method + ListAll comment
- internal/handlers/approvals_test.go — 5 new tests:
  - TestApprovals_Withdraw_Success (happy path)
  - TestApprovals_Withdraw_NotPendingReturns409 (state guard)
  - TestApprovals_Withdraw_NotFound (404)
  - TestApprovals_Withdraw_CrossWorkspaceAuthzReject (the load-bearing
    cross-workspace authz test — verifies the authz check short-
    circuits before UPDATE; uses sqlmock.ExpectationsWereMet to
    confirm no UPDATE was issued)
  - TestApprovals_Withdraw_CrossWorkspaceGateOK (the #2574 / #2593
    scenario where the row's creator workspace matches the path's :id
    and withdraw proceeds normally)
- internal/router/router.go — wire the route (wsAuth)
- docs/api-reference.md, docs/api-protocol/platform-api.md — table entries

LOCAL VALIDATION:
- go test ./internal/handlers/  -> clean (26.4s, all 5 new + all existing)
- go test ./internal/provisioner/ -> clean (0.08s, no regressions from earlier)
- go vet ./...                 -> clean
- go build ./...               -> clean

Refs #66. PM-approved plan: 7600d2ed.
2026-06-14 12:43:50 +00:00

8.9 KiB

API Reference

This document describes the REST API exposed by the Molecule AI workspace server (Go/Gin, default port :8080). Clients include the Canvas frontend, workspace agents communicating over A2A, and external tooling such as the MCP server and CLI.

Base URL: http://localhost:8080 (development default) Rate limit: 600 req/min (configurable via RATE_LIMIT) CORS origins: http://localhost:3000,http://localhost:3001 by default (configurable via CORS_ORIGINS)


Authentication

Three middleware classes gate server-side routes:

  • AdminAuth — strict bearer-only. Required for any route that can leak prompts/memory, create/mutate workspaces, or expose ops intel. Lazy-bootstrap fail-open when no live tokens exist globally.
  • WorkspaceAuth — binds a bearer token to a specific workspace :id. A token for workspace A cannot be used against workspace B's sub-routes.
  • CanvasOrBearer — accepts a bearer token OR a request Origin matching CORS_ORIGINS. Used only for cosmetic routes with zero data/security impact (currently PUT /canvas/viewport only). Do not extend to routes that leak data or create resources.

Full contract: docs/runbooks/admin-auth.md.


Routes

Method Path Handler
GET /health inline
GET /metrics metrics.Handler() — Prometheus text format; no auth, scrape-safe
POST/GET/PATCH/DELETE /workspaces[/:id] workspace.go — GET /workspaces, POST /workspaces, and DELETE /workspaces/:id require AdminAuth. DELETE /workspaces/:id also requires X-Confirm-Name: <workspace name>; cascading deletes still require ?confirm=true. PATCH /workspaces/:id enforces field-level authz: cosmetic fields (name, role, x, y, canvas) pass through; sensitive fields (tier, parent_id, runtime, workspace_dir) require a valid bearer token when any live token exists.
GET/PATCH /workspaces/:id/config workspace.go
GET/POST /workspaces/:id/memory workspace.go
DELETE /workspaces/:id/memory/:key workspace.go
POST/PATCH/DELETE /workspaces/:id/agent agent.go
POST /workspaces/:id/agent/move agent.go
GET/POST/PUT /workspaces/:id/secrets secrets.go (POST/PUT auto-restarts workspace)
DELETE /workspaces/:id/secrets/:key secrets.go (DELETE auto-restarts workspace)
GET /workspaces/:id/model secrets.go
GET /settings/secrets secrets.go — list global secrets (keys only, values masked)
PUT/POST /settings/secrets secrets.go — set a global secret {key, value}; auto-restarts every non-paused/non-removed/non-external workspace that does not shadow the key with a workspace-level override
DELETE /settings/secrets/:key secrets.go — delete a global secret; same auto-restart fan-out as PUT/POST
POST /admin/workspaces/:id/tokens admin_workspace_tokens.go — mint a real workspace bearer token; requires AdminAuth; plaintext is returned once
GET/POST/DELETE /admin/secrets[/:key] secrets.go — legacy aliases for /settings/secrets
WS /workspaces/:id/terminal terminal.go
POST/GET /workspaces/:id/approvals approvals.go
POST /workspaces/:id/approvals/:id/decide approvals.go
POST /workspaces/:id/approvals/:id/withdraw approvals.go — requester pulls back a pending approval (#66)
GET /approvals/pending approvals.go
POST/GET /workspaces/:id/memories memories.go
DELETE /workspaces/:id/memories/:id memories.go
GET /workspaces/:id/traces traces.go
GET/POST /workspaces/:id/activity activity.go
POST /workspaces/:id/notify activity.go (agent→user push message via WebSocket)
POST /workspaces/:id/restart workspace.go
POST /workspaces/:id/pause workspace.go (stops container, status→paused)
POST /workspaces/:id/resume workspace.go (re-provisions paused workspace)
POST /workspaces/:id/a2a workspace.go
POST /workspaces/:id/delegate delegation.go (async fire-and-forget)
GET /workspaces/:id/delegations delegation.go (list delegation status)
GET/POST /workspaces/:id/schedules schedules.go (cron CRUD)
PATCH/DELETE /workspaces/:id/schedules/:scheduleId schedules.go
POST /workspaces/:id/schedules/:scheduleId/run schedules.go (manual trigger)
GET /workspaces/:id/schedules/:scheduleId/history schedules.go (past runs)
GET/POST /workspaces/:id/channels channels.go (social channel CRUD)
PATCH/DELETE /workspaces/:id/channels/:channelId channels.go
POST /workspaces/:id/channels/:channelId/send channels.go (outbound message)
POST /workspaces/:id/channels/:channelId/test channels.go (test connection)
GET /channels/adapters channels.go (list available platforms)
POST /channels/discover channels.go (auto-detect chats for a bot token)
POST /webhooks/:type channels.go (incoming social webhook)
GET/PUT/DELETE /workspaces/:id/files[/*path] templates.go
GET /canvas/viewport viewport.go — open, no auth required (cosmetic, bootstrap-friendly)
PUT /canvas/viewport viewport.go — CanvasOrBearer middleware; accepts bearer OR Origin matching CORS_ORIGINS. Cosmetic-only route — worst case viewport corruption, recovered by page refresh.
GET /templates templates.go
POST /templates/import templates.go — AdminAuth required
POST /registry/register registry.go
POST /registry/heartbeat registry.go — requires Authorization: Bearer <token> once a workspace has any live token on file (legacy workspaces grandfathered)
POST /registry/update-card registry.go — requires Authorization: Bearer <token> once a workspace has any live token on file
GET /registry/discover/:id discovery.go — requires X-Workspace-ID + bearer token on the caller side
GET /registry/:id/peers discovery.go — requires X-Workspace-ID + bearer token on the caller side
POST /registry/check-access discovery.go
GET /plugins plugins.go (list registry; supports ?runtime= filter)
GET /plugins/sources plugins.go (list registered install-source schemes)
GET/POST/DELETE /workspaces/:id/plugins[/:name] plugins.go — list, install ({"source":"scheme://spec"}), uninstall per-workspace
GET /workspaces/:id/plugins/available plugins.go (filtered by workspace runtime)
GET /workspaces/:id/plugins/compatibility?runtime=X plugins.go (preflight runtime-change check)
GET/POST /workspaces/:id/tokens tokens.go — list active tokens (prefix + metadata), create new token (plaintext returned once). Max 50 per workspace.
DELETE /workspaces/:id/tokens/:tokenId tokens.go — revoke specific token by ID
GET /bundles/export/:id bundle.go — AdminAuth required
POST /bundles/import bundle.go — AdminAuth required
GET /org/templates org.go (list available org templates)
POST /org/import org.go — AdminAuth required; applies resolveInsideRoot path sanitiser on template paths
GET /events events.go — AdminAuth required
GET /events/:workspaceId events.go — AdminAuth required
GET /admin/liveness inline — AdminAuth required. Returns per-subsystem supervised.Snapshot() ages; use to check health of scheduler/heartbeat goroutines
GET /ws socket.go

Database

Migration files live in workspace-server/migrations/ (latest: 022_workspace_schedules_source). Each migration ships as a .up.sql/.down.sql pair. The migration runner globs *.sql, filters out .down.sql files, sorts alphabetically, and executes each file on boot. All .up.sql files must be idempotent (CREATE TABLE IF NOT EXISTS, ALTER TABLE ... IF NOT EXISTS) because the runner re-applies every migration on every boot.

Key Tables

Table Description
workspaces Core entity — status, runtime, agent_card JSONB, heartbeat columns, current_task, workspace_dir
canvas_layouts Per-workspace x/y canvas position
structure_events Append-only event log (workspace lifecycle, agent, approval events)
activity_logs A2A communications, task updates, agent logs, errors. error_detail is populated by the scheduler so cron run history can surface failure reasons.
workspace_schedules Cron tasks — expression, timezone, prompt, run history, source ('template' for org/import-seeded, 'runtime' for Canvas/API-created), last_status (includes 'skipped' when the scheduler concurrency-skips a busy workspace)
workspace_channels Social channel integrations (Telegram, Slack, etc.) with JSONB config and allowlist
agents Agent records
workspace_secrets Per-workspace encrypted secrets
global_secrets Platform-wide encrypted secrets
workspace_auth_tokens Bearer tokens; auto-revoked on workspace delete
agent_memories HMA scoped memory (LOCAL / TEAM / GLOBAL)
approvals Human-in-the-loop approval requests