feat(external-runtime): first-class BYO-compute workspaces + manifest-driven runtime registry - #2064
Conversation
…-driven registry
## Problem
Two issues the external-workspace path was silently dropping:
1. `knownRuntimes` was a hardcoded Go map that drifted from
manifest.json — e.g. `gemini-cli` was in manifest but missing
from the Go allowlist, so any workspace provisioning with
runtime=gemini-cli got silently coerced to langgraph.
2. No end-to-end "bring your own compute" story. The canvas UI
had no way to pick runtime=external; the partial backend code
required the operator to already have a URL ready (chicken-and-
egg with the agent that doesn't exist yet), and no workspace_auth
_token was minted so the external agent couldn't authenticate its
register call.
## Change
### Runtime registry driven by manifest.json
- New `runtime_registry.go` reads `manifest.json` at service init.
Each `workspace_templates[].name` becomes a runtime identifier
(with the `-default` suffix stripped so `claude-code-default`
and `claude-code` resolve to the same runtime).
- `external` is always injected (no template repo exists for it).
- Falls back to a static map on manifest load failure so tests /
dev containers keep working.
- 5 new tests including a real-manifest sanity check.
### First-class external workspace flow
When `POST /workspaces` is called with `runtime: "external"` AND
no URL supplied:
1. Workspace row inserted with `status='awaiting_agent'`
(distinct from `provisioning` so canvas doesn't trip its
provisioning-timeout UX).
2. A workspace_auth_token is minted via `wsauth.IssueToken`.
3. Response body includes a `connection` object with:
- `workspace_id`, `platform_url`, `auth_token`
- `registry_endpoint`, `heartbeat_endpoint`
- `curl_register_template` — zero-dep one-shot register snippet
- `python_snippet` — full SDK setup w/ heartbeat loop,
paired with molecule-sdk-python PR #13's A2AServer
4. The platform URL is resolved from `EXTERNAL_PLATFORM_URL` env
(ops-configurable per tenant) or falls back to request headers.
The legacy `payload.External` + `payload.URL` path is preserved —
org-import and other callers that already have a URL still work.
### Canvas UI
- New "External agent (bring your own compute)" checkbox in
CreateWorkspaceDialog.
- When checked, template/model/hermes-provider fields are hidden
and the POST body includes `runtime: "external"`.
- New `ExternalConnectModal` component: shown once after create,
renders Python / curl / raw-fields tabs with copy-to-clipboard
buttons. Stays mounted as a sibling of the create dialog so the
token survives the create dialog unmount.
- `auth_token` is interpolated into the snippet client-side so the
copied block is truly ready to run — operator only has to fill
in their agent's public URL.
## Tests
- Go: 5 new runtime_registry tests (happy path, -default strip,
external always injected, missing file, malformed JSON, real
manifest sanity). All existing handler tests still pass.
- TypeScript: no type errors on my files; pre-existing
canvas-batch-partial-failure type drift is on main already and
tracked on the #2061 branch.
## Follow-ups (filed separately)
- Cut molecule-sdk-python v0.y to PyPI so the snippet can use
`pip install molecule-ai-sdk` instead of `git+main`.
- Add a `runtime: string` field per template in manifest.json so
one template can declare its runtime explicitly (instead of
deriving it from name conventions). Unblocks N-templates-per-
runtime (e.g. hermes-minimax, hermes-anthropic both runtime=hermes).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Skipping review: required check Canvas tabs E2E failing. Likely the same pre-CP#282 lib/pq prepared-statement collision that has been making this test flake. Re-triggered the failed job — should pass now that #282 is deployed. Will re-evaluate once green. |
|
Re-triggered failed CI (CP #284 just merged at 21:49Z, fixing the silent-DNS-failure root cause that was stranding tests at |
|
Still skipping: failed E2E correlates with the active Cloudflare |
Review note: PR sizeCritical: This PR is 756 lines / 7 files spanning canvas + workspace-server + new feature surface (external-runtime registry + workspace_provision changes + ExternalConnectModal UI). Per the code-review skill's sizing guidance, anything >300 lines is at the edge of "reviewable in one sitting" and >1000 is over the line. 756 is in the gray zone where I should request a split rather than do a cursory pass that misses real issues. Splitting strategies (per the skill)
Either gets each slice into the ~200-line range where a real review can catch correctness issues without rubber-stamping. I'm holding off on the five-axis pass until either:
Auto-merge is already armed; CI is failing on Canvas tabs E2E (pre-CF-cleanup symptom — re-triggered). When CI clears the merge gate is just review approval. Not marking anything Critical in the diff yet — just calling the size out. |
|
Re-pass: still holding per my prior comment flagging the 756-line / 7-file size as too big for a thorough single-pass review. No new commits since 17:40Z, so the size hasn't changed and I haven't been told to take it as-is. CI is green now (post-CF-zone-cleanup at 22:50Z) so the merge gate is just review approval — but the review depth question is still on the table. Two paths forward:
Otherwise no movement from me. |
… open prop [Molecule-Platform-Evolvement-Manager] Addresses github-code-quality finding on PR #2064: > Comparison between inconvertible types > Variable 'info' cannot be of type null, but it is compared to > an expression of type null. By line 75, `info` has been narrowed to non-null via the `if (!info) return null;` guard at line 56 — so `open={info !== null}` always evaluates to `true`. Switch to JSX shorthand `open` for clarity and to silence the static check. Behaviorally identical; the modal still opens whenever the parent renders this component (which only happens with non-null info). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed Resolved the review thread. Merge gate should now be unblocked. |
What
Two tightly-coupled changes that close the "user picks 'external' in the Create Workspace dialog → copy-paste snippet → their remote agent connects" loop:
1. Runtime registry driven by manifest.json
2. First-class external workspace flow
Backend — `POST /workspaces` with `runtime: "external"` (and no URL):
```json
{
"workspace_id": "...",
"platform_url": "https://hongmingwang.moleculesai.app\",
"auth_token": "ws_...",
"registry_endpoint": ".../registry/register",
"heartbeat_endpoint": ".../registry/heartbeat",
"curl_register_template": "...",
"python_snippet": "..."
}
```
Canvas UI:
Tests
Paired SDK
The Python snippet pairs with `molecule-sdk-python` PR #13's `A2AServer` + `RemoteAgentClient` (merged). Pinned to `git+main` until we cut a PyPI release — follow-up to publish.
Screenshot of the UX
External toggle in Create dialog → user clicks Create → connection snippet modal opens with the token shown exactly once → user copies Python block → paste into their agent's deploy → their agent phones home + appears online in canvas.
Related follow-up
Filing "audit manifest.json as single source of truth" as a separate issue — this PR fixed one drift; there are likely more hardcoded lists across the repo.