forked from molecule-ai/molecule-core
Covers today's quality + infra pass: brand/structural cleanup, MCP per-domain refactor (1697 -> 89 lines, 87 tools), canvas ConfirmDialog unification, 4 platform handler decompositions (+47 Go tests), E2E hardening for Phase 30.1/30.6 auth, and two new CI jobs (e2e-api + shellcheck). - CLAUDE.md: updated test counts (Go 536, canvas 357, SDK 121, MCP 97, workspace 1084); documented MCP per-domain split + new api.ts; added handler-decomposition section; Phase 30.1/30.6 auth callout; new CI jobs; env vars cross-ref. - PLAN.md: Phase 31 "Quality + Infra Pass" marked shipped; test totals refreshed to 2,295. - README.zh-CN.md: license badge MIT -> BSL 1.1; added BSL license block. - docs/api-protocol/platform-api.md: registry table gains Auth column documenting Phase 30.1 bearer-token and Phase 30.6 X-Workspace-ID requirements on heartbeat/update-card/discover/peers. - docs/development/local-development.md: updated stale test counts; added e2e-api + shellcheck CI jobs; pointer to new testing-e2e.md. - docs/development/testing-e2e.md: new — per-script reference, auth prerequisites, local run, CI coverage, adding-a-new-check checklist. - docs/edit-history/2026-04-13.md: top-of-file summary section added spanning PRs #1-#8; preserves existing per-feature entries below. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
253 lines
11 KiB
Markdown
253 lines
11 KiB
Markdown
# 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.
|
||
|
||
## 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 name, role, tier, runtime, workspace_dir, parent, etc. |
|
||
| `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.
|
||
|
||
```json
|
||
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`](../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 |
|
||
| `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 `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
|
||
|
||
```bash
|
||
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
|
||
```
|
||
|
||
## Related Docs
|
||
|
||
- [Registry & Heartbeat](./registry-and-heartbeat.md)
|
||
- [Communication Rules](./communication-rules.md)
|
||
- [Workspace Runtime](../agent-runtime/workspace-runtime.md)
|
||
- [Canvas UI](../frontend/canvas.md)
|