From 7d81ee0ea4c41e147dae69222288426c50230eb8 Mon Sep 17 00:00:00 2001 From: "molecule-ai[bot]" <276602405+molecule-ai[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 07:53:24 +0000 Subject: [PATCH] docs: add docs/api-protocol/platform-api.md --- content/docs/api-protocol/platform-api.md | 283 ++++++++++++++++++++++ 1 file changed, 283 insertions(+) create mode 100644 content/docs/api-protocol/platform-api.md diff --git a/content/docs/api-protocol/platform-api.md b/content/docs/api-protocol/platform-api.md new file mode 100644 index 0000000..3781d62 --- /dev/null +++ b/content/docs/api-protocol/platform-api.md @@ -0,0 +1,283 @@ +# 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": " must be at most N characters" }` or `{ "error": " must not contain newline characters" }`. + +**Migration steps for callers:** +1. Add `Authorization: Bearer ` 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 + +| 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 (`{}[]|>*&!`). `:id` must be a valid UUID. See [Breaking Changes](#breaking-changes). | +| `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": "", "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 ` 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=` 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":"://"}` — 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//`. 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. **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 `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)