Updates docs/api-protocol/platform-api.md:
- Add ## Breaking Changes section with full before/after table for PR #701
(PATCH wsAuth, templates AdminAuth, UUID validation, field length/char limits)
- PATCH /workspaces/:id row: add WorkspaceAuth note + validation details
- GET /templates: add AdminAuth note
- GET /org/templates: add row with AdminAuth note
- Migration steps for E2E scripts and automation callers
Source PR: #701 (SHA 3f5dea79) — fix(security): input validation, route auth, UUID safety
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
13 KiB
Platform API (Go Backend)
The Go backend is Molecule AI's control plane. It does not execute agent reasoning itself. It manages the infrastructure and coordination around workspaces.
Responsibilities
- workspace lifecycle
- registry and heartbeats
- hierarchy-aware discovery
- A2A proxying for browser-initiated calls
- approvals and activity logs
- memory APIs
- secrets and global secrets
- files, templates, bundles, terminal, and viewport state
- WebSocket fanout to canvas clients and workspaces
Caller Identification
Workspace-scoped calls use the X-Workspace-ID header when the caller is another workspace. Browser/canvas calls do not send that header.
The platform uses the caller identity to enforce hierarchy-based access rules.
Breaking Changes
PR #701 — Input validation, route auth, UUID safety (2026-04-17)
Affects: PATCH /workspaces/:id, GET /workspaces/:id, DELETE /workspaces/:id, GET /templates, GET /org/templates
| Change | Before | After |
|---|---|---|
PATCH /workspaces/:id auth |
Open router — no token required for cosmetic fields | wsAuth group — workspace bearer token required unconditionally |
GET /templates auth |
No auth | AdminAuth |
GET /org/templates auth |
No auth | AdminAuth |
:id path parameter validation |
DB query with raw string; Postgres error on non-UUID | uuid.Parse check before DB access — 400 "invalid workspace id" on non-UUID |
Field validation added to POST /workspaces and PATCH /workspaces/:id:
| Field | Max length | Additional constraints |
|---|---|---|
name |
255 chars | No \n, \r, or YAML-special chars (`{}[] |
role |
1,000 chars | No \n, \r, or YAML-special chars |
model |
100 chars | No \n, \r |
runtime |
100 chars | No \n, \r |
Violations return 400 Bad Request with { "error": "<field> must be at most N characters" } or { "error": "<field> must not contain newline characters" }.
Migration steps for callers:
- Add
Authorization: Bearer <workspace-token>to allPATCH /workspaces/:idrequests. - Add an admin bearer token to
GET /templatesandGET /org/templatesrequests. - Ensure
:idvalues in E2E scripts and automation are valid UUIDs. Update any test fixtures that use non-UUID IDs (seeplatform/internal/handlers/*_test.gofor updated examples).
Core Endpoints
Health and metrics
| Method | Path | Description |
|---|---|---|
GET |
/health |
Health check |
GET |
/metrics |
Prometheus metrics |
Workspaces
| Method | Path | Description |
|---|---|---|
POST |
/workspaces |
Create and provision a workspace |
GET |
/workspaces |
List workspaces with inline canvas layout data |
GET |
/workspaces/:id |
Get one workspace |
PATCH |
/workspaces/:id |
Update workspace fields. Requires workspace bearer token (WorkspaceAuth). Validates name (≤255), role (≤1000), model/runtime (≤100 chars); name and role reject newlines and YAML-special chars (`{}[] |
DELETE |
/workspaces/:id |
Remove workspace |
POST |
/workspaces/:id/restart |
Restart workspace (reads runtime from container config.yaml before stop — detects runtime changes) |
POST |
/workspaces/:id/pause |
Pause workspace |
POST |
/workspaces/:id/resume |
Resume workspace |
POST |
/workspaces/:id/a2a |
Proxy A2A request to the target workspace (synchronous, enforces hierarchy access control via X-Workspace-ID) |
POST |
/workspaces/:id/delegate |
Async delegation — fire-and-forget, returns delegation_id |
GET |
/workspaces/:id/delegations |
List delegation status (pending/completed/failed) |
Async Delegation
POST /workspaces/:id/delegate sends a task to another workspace without blocking. The platform runs the A2A request in a background goroutine and returns immediately.
POST /workspaces/:id/delegate
{"target_id": "<workspace-uuid>", "task": "Review the PLAN.md"}
→ 202 {"delegation_id": "...", "status": "delegated", "target_id": "..."}
Poll GET /workspaces/:id/delegations to check results. Each entry includes delegation_id, status (pending/completed/failed), and response_preview. WebSocket events DELEGATION_COMPLETE and DELEGATION_FAILED are broadcast on completion.
This is the recommended way for agents to delegate work — it works for all runtimes (Claude Code, LangGraph, etc.) since it operates at the platform level.
Workspace creation also assigns an awareness_namespace on the workspace row. That namespace is later injected into the provisioned runtime.
Registry
| Method | Path | Description | Auth |
|---|---|---|---|
POST |
/registry/register |
Workspace registration on startup. First register issues a per-workspace bearer token in the response body (auth_token); re-register is idempotent and omits the token. |
— |
POST |
/registry/heartbeat |
Liveness and task updates. | Phase 30.1 — Authorization: Bearer <token> required if the workspace has any live token on file; legacy workspaces grandfathered (fail-open). |
POST |
/registry/update-card |
Push Agent Card updates after runtime/skill changes. | Phase 30.1 — same grandfather rule as /heartbeat. |
GET |
/registry/discover/:id |
Resolve workspace URL for A2A calls. | Phase 30.6 — caller sends X-Workspace-ID + own bearer token; fail-open on DB hiccup (hierarchy check is primary gate). |
GET |
/registry/:id/peers |
List reachable peers. | Phase 30.6 — same as /discover/:id. |
POST |
/registry/check-access |
Validate reachability/access. | — |
Why the auth callout matters: remote (Phase 30) agents authenticate themselves with the bearer token returned by POST /registry/register. Local containers are transparent to this during the lazy-bootstrap grace window — the provisioner threads the token in as an env var on first register. See docs/development/testing-e2e.md for how E2E scripts handle token capture. If you change these routes, update tests/e2e/test_api.sh in the same PR.
Activity and recall
| Method | Path | Description |
|---|---|---|
GET |
/workspaces/:id/activity |
List activity rows (?type=, ?source=canvas|agent, ?limit=) |
POST |
/workspaces/:id/activity |
Report activity from a workspace |
POST |
/workspaces/:id/notify |
Emit user-facing notifications/activity |
GET |
/workspaces/:id/session-search |
Search recent activity + memory for recall |
Memory
There are two distinct memory surfaces:
Scoped agent memory
| Method | Path | Description |
|---|---|---|
POST |
/workspaces/:id/memories |
Commit a LOCAL / TEAM / GLOBAL memory |
GET |
/workspaces/:id/memories |
Search scoped memories |
DELETE |
/workspaces/:id/memories/:memoryId |
Delete an owned memory |
Key/value workspace memory
| Method | Path | Description |
|---|---|---|
GET |
/workspaces/:id/memory |
List key/value memory entries |
GET |
/workspaces/:id/memory/:key |
Get one key/value entry |
POST |
/workspaces/:id/memory |
Upsert a key/value entry with optional TTL |
DELETE |
/workspaces/:id/memory/:key |
Delete a key/value entry |
Secrets
Workspace secrets
| Method | Path | Description |
|---|---|---|
GET |
/workspaces/:id/secrets |
Return merged workspace + inherited global secret metadata |
POST |
/workspaces/:id/secrets |
Upsert workspace secret |
PUT |
/workspaces/:id/secrets |
Upsert workspace secret |
DELETE |
/workspaces/:id/secrets/:key |
Delete workspace secret |
GET |
/workspaces/:id/model |
Get workspace model override |
Important detail: GET /workspaces/:id/secrets does not return values. It returns key metadata plus a scope field so the frontend can distinguish inherited globals from workspace overrides.
Global secrets
| Method | Path | Description |
|---|---|---|
GET |
/settings/secrets |
List global secret metadata |
POST |
/settings/secrets |
Upsert global secret |
PUT |
/settings/secrets |
Upsert global secret |
DELETE |
/settings/secrets/:key |
Delete global secret |
Backward-compatible admin aliases also exist under /admin/secrets.
Approvals
| Method | Path | Description |
|---|---|---|
GET |
/approvals/pending |
List pending approvals |
POST |
/workspaces/:id/approvals |
Create approval request |
GET |
/workspaces/:id/approvals |
List approvals for a workspace |
POST |
/workspaces/:id/approvals/:approvalId/decide |
Approve or deny |
Team operations
| Method | Path | Description |
|---|---|---|
POST |
/workspaces/:id/expand |
Expand workspace into a team |
POST |
/workspaces/:id/collapse |
Collapse team back down |
Plugins
| Method | Path | Description |
|---|---|---|
GET |
/plugins |
List available plugins; accepts ?runtime=<name> to filter to compatible plugins |
GET |
/plugins/sources |
List registered install-source schemes (e.g. {"schemes":["github","local"]}) |
GET |
/workspaces/:id/plugins |
List installed plugins (each includes supported_on_runtime: bool) |
GET |
/workspaces/:id/plugins/available |
Plugins filtered to those compatible with the workspace runtime |
GET |
/workspaces/:id/plugins/compatibility?runtime=X |
Preflight runtime change — which installed plugins would become inert |
POST |
/workspaces/:id/plugins |
Install plugin {"source":"<scheme>://<spec>"} — e.g. local://ecc, github://owner/repo#v1.0. Auto-restarts workspace. |
DELETE |
/workspaces/:id/plugins/:name |
Uninstall plugin — removes from container, auto-restarts |
Plugins are installed per-workspace into /configs/plugins/<name>/. Sources are pluggable via schemes (local + github shipped; clawhub/oci/https planned). See docs/plugins/sources.md for the two-axis source/shape model.
Install safeguards bound the cost of a single install (env-tunable via PLUGIN_INSTALL_BODY_MAX_BYTES / PLUGIN_INSTALL_FETCH_TIMEOUT / PLUGIN_INSTALL_MAX_DIR_BYTES).
Files and templates
| Method | Path | Description |
|---|---|---|
GET |
/templates |
List available templates. Requires AdminAuth (PR #701). |
GET |
/org/templates |
List available org templates. Requires AdminAuth (PR #701). |
POST |
/templates/import |
Import an agent folder as a new template |
GET |
/workspaces/:id/shared-context |
Read parent shared-context files |
GET |
/workspaces/:id/files |
List files under an allowed root |
GET |
/workspaces/:id/files/*path |
Read a file |
PUT |
/workspaces/:id/files/*path |
Write a file |
PUT |
/workspaces/:id/files |
Replace workspace file set |
DELETE |
/workspaces/:id/files/*path |
Delete a file |
Query parameters for GET /workspaces/:id/files:
| Param | Default | Description |
|---|---|---|
root |
/configs |
Base path — one of /configs, /workspace, /home, /plugins |
path |
"" |
Subdirectory relative to root (validated against path traversal) |
depth |
1 |
Max recursion depth (1–5). Use with path for lazy-loading subdirectories |
Invalid depth or traversal paths return 400.
Terminal
| Protocol | Path | Description |
|---|---|---|
WS |
/workspaces/:id/terminal |
Terminal session into the running container |
Bundles
| Method | Path | Description |
|---|---|---|
GET |
/bundles/export/:id |
Export workspace tree as a bundle |
POST |
/bundles/import |
Import a bundle |
Canvas viewport and events
| Method | Path | Description |
|---|---|---|
GET |
/canvas/viewport |
Get saved canvas pan/zoom |
PUT |
/canvas/viewport |
Save canvas pan/zoom |
GET |
/events |
List structure events |
GET |
/events/:workspaceId |
List workspace-scoped events |
WebSocket
| Protocol | Path | Description |
|---|---|---|
WS |
/ws |
Live events for canvas clients and workspaces |
Canvas clients receive the global event stream. Workspaces connect with X-Workspace-ID and receive filtered events based on communication rules.
A2A Proxy Behavior
POST /workspaces/:id/a2a is more than a naive forwarder.
It currently:
- enforces access control via
CanCommunicatefor agent-to-agent calls (workspace caller IDs fromX-Workspace-ID); canvas requests, self-calls, and system callers (webhook:*,system:*,test:*) bypass - normalizes incoming JSON into JSON-RPC 2.0
- injects
messageIdwhen missing - applies different timeout rules for browser-initiated vs workspace-initiated calls
- logs the resulting A2A activity
- broadcasts successful browser-initiated responses back to the canvas as
A2A_RESPONSE - triggers restart flow when the target container is confirmed dead
That is why the chat UX no longer depends on polling as the primary response path.
Environment Variables
DATABASE_URL=postgres://dev:dev@postgres:5432/molecule?sslmode=prefer
REDIS_URL=redis://redis:6379
PORT=8080
SECRETS_ENCRYPTION_KEY=...
ACTIVITY_RETENTION_DAYS=7
ACTIVITY_CLEANUP_INTERVAL_HOURS=6
CORS_ORIGINS=http://localhost:3000,http://localhost:3001
RATE_LIMIT=600