Skip to content

feat(external-runtime): first-class BYO-compute workspaces + manifest-driven runtime registry - #2064

Merged
HongmingWang-Rabbit merged 5 commits into
stagingfrom
feat/external-runtime-first-class
Apr 26, 2026
Merged

feat(external-runtime): first-class BYO-compute workspaces + manifest-driven runtime registry#2064
HongmingWang-Rabbit merged 5 commits into
stagingfrom
feat/external-runtime-first-class

Conversation

@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor

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

  • New `workspace-server/internal/handlers/runtime_registry.go` reads the repo's `manifest.json` at service init and builds `knownRuntimes` from `workspace_templates[].name` (with `-default` suffix strip).
  • Falls back to a hardcoded map if manifest.json isn't readable (tests, dev containers).
  • `external` is always injected (no template repo exists for it).
  • Fixes existing drift: `gemini-cli` was in manifest but missing from the old Go allowlist, so provisioning silently coerced it to langgraph.

2. First-class external workspace flow

Backend — `POST /workspaces` with `runtime: "external"` (and no URL):

  • Row inserted with `status='awaiting_agent'` (distinct from `provisioning` — canvas won't trip the provisioning-timeout UX).
  • Workspace auth token minted via `wsauth.IssueToken`.
  • Response body includes a `connection` object:
    ```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:

  • New "External agent (bring your own compute)" checkbox in the Create dialog. Hides template/model/hermes fields.
  • New `ExternalConnectModal` shown once post-create, with Python / curl / Fields tabs and copy-to-clipboard buttons.
  • Token is interpolated into the snippet client-side so the copied block is truly ready to run.

Tests

  • Go: 5 new `runtime_registry_test.go` cases (happy path, suffix strip, external always injected, missing-file error, malformed JSON, real manifest sanity). All existing handler tests still pass.
  • TypeScript: no type errors on new files (`ExternalConnectModal.tsx`, updated `CreateWorkspaceDialog.tsx`).

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.

…-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>
Comment thread canvas/src/components/ExternalConnectModal.tsx Fixed
@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor Author

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.

@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor Author

Skipping review this pass: required check Canvas tabs E2E still failing — tenant TLS: timed out after 600s. Same infrastructure symptom as #2108/#2096 in this window (Cloudflare/cloudflared propagation flake, not PR code). Will re-evaluate when staging infra is healthy.

@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor Author

Re-triggered failed CI (CP #284 just merged at 21:49Z, fixing the silent-DNS-failure root cause that was stranding tests at tenant TLS / DNS propagation timed out). Re-evaluate once the new run completes.

@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor Author

Still skipping: failed E2E correlates with the active Cloudflare Minor Service Outage (status page indicator updated 22:08:40Z). CP #284's fast-fail surfaces CF API failures in ~16s instead of 900s — that's working as intended; the underlying CF outage is the blocker. Re-trigger when cloudflarestatus.com returns to operational.

@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor Author

Review note: PR size

Critical: 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)

  • Stack — Submit the runtime registry + types first (smallest, foundational), then the workspace-server changes that consume it, then the canvas UI on top
  • Vertical — Three feature slices: (1) registry CRUD, (2) provisioner integration, (3) UI for external connection

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:

  1. The PR is split (preferred), or
  2. You confirm you want me to take it as-is and accept that the review will be shallower than usual

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.

@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor Author

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:

  1. Split into ~200-line slices (preferred per the skill — Stack or Vertical)
  2. Tell me to take it as-is and accept a shallower-than-usual review

Otherwise no movement from me.

hongmingwang-moleculeai and others added 2 commits April 26, 2026 16:34
… 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>
@HongmingWang-Rabbit

Copy link
Copy Markdown
Contributor Author

Pushed 808cc54 — addresses the github-code-quality finding on ExternalConnectModal.tsx:75 (redundant info !== null check; info is already narrowed to non-null by the line 56 if (!info) return null; guard). Switched to JSX shorthand open per the bot's recommendation. Behaviorally identical, one-line change.

Resolved the review thread. Merge gate should now be unblocked.

@HongmingWang-Rabbit
HongmingWang-Rabbit added this pull request to the merge queue Apr 26, 2026
Merged via the queue into staging with commit b08c632 Apr 26, 2026
15 checks passed
@molecule-ai
molecule-ai Bot deleted the feat/external-runtime-first-class branch May 20, 2026 06:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant