Molecule AI
API Protocol

Platform API (Go Backend)

The Go control-plane backend — how it manages workspace infrastructure and coordination without executing agent reasoning.

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

ChangeBeforeAfter
PATCH /workspaces/:id authOpen router — no token required for cosmetic fieldswsAuth group — workspace bearer token required unconditionally
GET /templates authNo authAdminAuth
GET /org/templates authNo authAdminAuth
:id path parameter validationDB query with raw string; Postgres error on non-UUIDuuid.Parse check before DB access — 400 "invalid workspace id" on non-UUID

Field validation added to POST /workspaces and PATCH /workspaces/:id:

FieldMax lengthAdditional constraints
name255 charsNo \n, \r, or YAML-special chars (`{}[]
role1,000 charsNo \n, \r, or YAML-special chars
model100 charsNo \n, \r
runtime100 charsNo \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:

  1. Add Authorization: Bearer <workspace-token> to all PATCH /workspaces/:id requests.
  2. Add an admin bearer token to GET /templates and GET /org/templates requests.
  3. Ensure :id values in E2E scripts and automation are valid UUIDs. Update any test fixtures that use non-UUID IDs (see workspace-server/internal/handlers/*_test.go for updated examples).

Core Endpoints

Health and metrics

MethodPathDescription
GET/healthHealth check
GET/metricsPrometheus metrics

Workspaces

MethodPathDescription
POST/workspacesCreate and provision a workspace
GET/workspacesList workspaces with inline canvas layout data
GET/workspaces/:idGet one workspace
PATCH/workspaces/:idUpdate 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/:idRemove workspace
POST/workspaces/:id/restartRestart workspace (reads runtime from container config.yaml before stop — detects runtime changes)
POST/workspaces/:id/pausePause workspace
POST/workspaces/:id/resumeResume workspace
POST/workspaces/:id/a2aProxy A2A request to the target workspace (synchronous, enforces hierarchy access control via X-Workspace-ID)
POST/workspaces/:id/delegateAsync delegation — fire-and-forget, returns delegation_id
GET/workspaces/:id/delegationsList 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

MethodPathDescriptionAuth
POST/registry/registerWorkspace 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/heartbeatLiveness 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-cardPush Agent Card updates after runtime/skill changes.Phase 30.1 — same grandfather rule as /heartbeat.
GET/registry/discover/:idResolve 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/peersList reachable peers.Phase 30.6 — same as /discover/:id.
POST/registry/check-accessValidate 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

MethodPathDescription
GET/workspaces/:id/activityList activity rows (?type=, ?source=canvas|agent, ?limit=)
POST/workspaces/:id/activityReport activity from a workspace
POST/workspaces/:id/notifyEmit user-facing notifications/activity
GET/workspaces/:id/session-searchSearch recent activity + memory for recall

Memory

There are two distinct memory surfaces:

Scoped agent memory

MethodPathDescription
POST/workspaces/:id/memoriesCommit a LOCAL / TEAM / GLOBAL memory
GET/workspaces/:id/memoriesSearch scoped memories
DELETE/workspaces/:id/memories/:memoryIdDelete an owned memory

Key/value workspace memory

MethodPathDescription
GET/workspaces/:id/memoryList key/value memory entries
GET/workspaces/:id/memory/:keyGet one key/value entry
POST/workspaces/:id/memoryUpsert a key/value entry with optional TTL
DELETE/workspaces/:id/memory/:keyDelete a key/value entry

Secrets

Workspace secrets

MethodPathDescription
GET/workspaces/:id/secretsReturn merged workspace + inherited global secret metadata
POST/workspaces/:id/secretsUpsert workspace secret
PUT/workspaces/:id/secretsUpsert workspace secret
DELETE/workspaces/:id/secrets/:keyDelete workspace secret
GET/workspaces/:id/modelGet 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

MethodPathDescription
GET/settings/secretsList global secret metadata
POST/settings/secretsUpsert global secret
PUT/settings/secretsUpsert global secret
DELETE/settings/secrets/:keyDelete global secret

Backward-compatible admin aliases also exist under /admin/secrets.

Approvals

MethodPathDescription
GET/approvals/pendingList pending approvals
POST/workspaces/:id/approvalsCreate approval request
GET/workspaces/:id/approvalsList approvals for a workspace
POST/workspaces/:id/approvals/:approvalId/decideApprove or deny

Team operations

MethodPathDescription
POST/workspaces/:id/expandExpand workspace into a team
POST/workspaces/:id/collapseCollapse team back down

Plugins

MethodPathDescription
GET/pluginsList available plugins; accepts ?runtime=<name> to filter to compatible plugins
GET/plugins/sourcesList registered install-source schemes (e.g. {"schemes":["github","local"]})
GET/workspaces/:id/pluginsList installed plugins (each includes supported_on_runtime: bool)
GET/workspaces/:id/plugins/availablePlugins filtered to those compatible with the workspace runtime
GET/workspaces/:id/plugins/compatibility?runtime=XPreflight runtime change — which installed plugins would become inert
POST/workspaces/:id/pluginsInstall plugin {"source":"<scheme>://<spec>"} — e.g. local://ecc, github://owner/repo#v1.0. Auto-restarts workspace.
DELETE/workspaces/:id/plugins/:nameUninstall 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

MethodPathDescription
GET/templatesList available templates. Requires AdminAuth (PR #701).
GET/org/templatesList available org templates. Requires AdminAuth (PR #701).
POST/templates/importImport an agent folder as a new template
GET/workspaces/:id/shared-contextRead parent shared-context files
GET/workspaces/:id/filesList files under an allowed root
GET/workspaces/:id/files/*pathRead a file
PUT/workspaces/:id/files/*pathWrite a file
PUT/workspaces/:id/filesReplace workspace file set
DELETE/workspaces/:id/files/*pathDelete a file

Query parameters for GET /workspaces/:id/files:

ParamDefaultDescription
root/configsBase path — one of /configs, /workspace, /home, /plugins
path""Subdirectory relative to root (validated against path traversal)
depth1Max recursion depth (1–5). Use with path for lazy-loading subdirectories

Invalid depth or traversal paths return 400.

Terminal

ProtocolPathDescription
WS/workspaces/:id/terminalTerminal session into the running container

Bundles

MethodPathDescription
GET/bundles/export/:idExport workspace tree as a bundle
POST/bundles/importImport a bundle

Canvas viewport and events

MethodPathDescription
GET/canvas/viewportGet saved canvas pan/zoom
PUT/canvas/viewportSave canvas pan/zoom
GET/eventsList structure events
GET/events/:workspaceIdList workspace-scoped events

WebSocket

ProtocolPathDescription
WS/wsLive 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 CanCommunicate for agent-to-agent calls (workspace caller IDs from X-Workspace-ID); canvas requests, self-calls, and system callers (webhook:*, system:*, test:*) bypass
  • normalizes incoming JSON into JSON-RPC 2.0
  • injects messageId when 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

On this page