Some checks failed
E2E Staging Canvas (Playwright) / detect-changes (pull_request) Successful in 9s
Runtime PR-Built Compatibility / detect-changes (pull_request) Successful in 8s
Handlers Postgres Integration / detect-changes (pull_request) Successful in 8s
Harness Replays / detect-changes (pull_request) Successful in 9s
CI / Python Lint & Test (pull_request) Successful in 6s
CI / Shellcheck (E2E scripts) (pull_request) Successful in 6s
CI / Canvas (Next.js) (pull_request) Successful in 8s
E2E Staging Canvas (Playwright) / Canvas tabs E2E (pull_request) Successful in 10s
Handlers Postgres Integration / Handlers Postgres Integration (pull_request) Successful in 12s
Harness Replays / Harness Replays (pull_request) Successful in 8s
Runtime PR-Built Compatibility / PR-built wheel + import smoke (pull_request) Successful in 8s
CI / Canvas Deploy Reminder (pull_request) Has been skipped
CodeQL / Analyze (${{ matrix.language }}) (python) (pull_request) Failing after 1m36s
cascade-list-drift-gate / check (pull_request) Successful in 5s
CodeQL / Analyze (${{ matrix.language }}) (javascript-typescript) (pull_request) Failing after 1m30s
CodeQL / Analyze (${{ matrix.language }}) (go) (pull_request) Failing after 1m39s
E2E API Smoke Test / E2E API Smoke Test (pull_request) Failing after 2m50s
Retarget main PRs to staging / Retarget to staging (pull_request) Has been skipped
Block internal-flavored paths / Block forbidden paths (pull_request) Successful in 5s
CI / Platform (Go) (pull_request) Successful in 4m29s
CI / Detect changes (pull_request) Successful in 6s
E2E API Smoke Test / detect-changes (pull_request) Successful in 8s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 7s
Adds a 'mock' runtime: virtual workspaces with no container, no EC2,
no LLM. Every A2A reply is synthesised from a small canned-variant
pool ('On it!', 'Got it, on it now.', etc.) deterministically seeded
by (workspace_id, request_id).
Built for funding-demo "200-workspace mock org" — renders an
enterprise-scale org chart on the canvas (CEO/VPs/Managers/ICs)
without burning real LLM credits or provisioning 200 EC2 instances.
Surfaces:
- workspace-server/internal/handlers/mock_runtime.go: A2A proxy
short-circuit, canned-reply pool, deterministic variant pick.
- workspace-server/internal/handlers/a2a_proxy.go: gate the
short-circuit before resolveAgentURL (mock has no URL).
- workspace-server/internal/handlers/org_import.go: skip Docker
provisioning for mock workspaces, set status='online' directly,
drop the per-sibling 2s pacing for mock children (collapses
a 200-workspace import from ~7min → ~1s).
- workspace-server/internal/handlers/runtime_registry.go: register
'mock' in the runtime allowlist (manifest + fallback set).
- workspace-server/internal/registry/healthsweep.go +
orphan_sweeper.go: skip mock workspaces in container-health and
stale-token sweeps (no container by design).
- workspace-server/internal/handlers/workspace_restart.go: mirror
the 'external' Restart no-op for mock.
- manifest.json: register the new
Molecule-AI/molecule-ai-org-template-mock-bigorg repo.
Tests: 5 new in mock_runtime_test.go covering happy-path, non-mock
regression guard, determinism, IsMockRuntime trim/case, JSON-RPC
id echo. All existing handler + registry tests still pass.
Local-verified: imported the 200-workspace template against a fresh
postgres+redis, confirmed all 200 land in 'online' and stay there
through the 30s health-sweep window, exercised A2A on CEO + VPs +
Managers + ICs and saw the variant pool rotate.
Org template lives at
Molecule-AI/molecule-ai-org-template-mock-bigorg (created today)
and is imported via the existing /org/import flow on the canvas
Template Palette.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
153 lines
5.4 KiB
Go
153 lines
5.4 KiB
Go
package handlers
|
|
|
|
// runtime_registry.go — single source of truth for "which runtime
|
|
// strings is the provisioner willing to honor".
|
|
//
|
|
// Before this file, knownRuntimes was a hardcoded Go map in
|
|
// workspace_provision.go, kept in sync MANUALLY with both
|
|
// workspace/build-all.sh and manifest.json's workspace_templates.
|
|
// That drift produced two visible bugs:
|
|
//
|
|
// - "gemini-cli" existed in manifest.json but not the Go map, so
|
|
// the UI/workspace-create rejected it and fell back to langgraph.
|
|
// - "claude-code-default" in manifest vs "claude-code" in Go —
|
|
// operators typing the manifest name got silently coerced.
|
|
//
|
|
// The fix: read manifest.json at boot. manifest.json lives in the
|
|
// monorepo root and is already the declarative registry — adding a
|
|
// runtime now means one line in that file + cutting the image.
|
|
// The Go allowlist is built from it + the hardcoded "external"
|
|
// meta-runtime (which has no template repo — it's a first-class
|
|
// "bring your own compute" option).
|
|
//
|
|
// Fallback: if manifest.json isn't readable (dev container without
|
|
// the file, tests without the workspace tree mounted) we fall back
|
|
// to the pre-refactor hardcoded list so nothing regresses.
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// manifestPath defaults to the repo root next to the binary. In
|
|
// production the workspace-server Dockerfile COPY's manifest.json
|
|
// into /app/manifest.json. Override with WORKSPACE_MANIFEST_PATH
|
|
// when running from an unusual location.
|
|
func manifestPath() string {
|
|
if v := os.Getenv("WORKSPACE_MANIFEST_PATH"); v != "" {
|
|
return v
|
|
}
|
|
// Standard container layout.
|
|
if _, err := os.Stat("/app/manifest.json"); err == nil {
|
|
return "/app/manifest.json"
|
|
}
|
|
// Dev: cwd + ../../manifest.json (run from workspace-server/cmd/server).
|
|
for _, p := range []string{"manifest.json", "../manifest.json", "../../manifest.json"} {
|
|
if abs, err := filepath.Abs(p); err == nil {
|
|
if _, err := os.Stat(abs); err == nil {
|
|
return abs
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// manifestEntry mirrors the shape of a workspace_templates item.
|
|
// Only the fields we read are declared; extras are ignored.
|
|
type manifestEntry struct {
|
|
Name string `json:"name"`
|
|
Repo string `json:"repo"`
|
|
}
|
|
|
|
type manifestFile struct {
|
|
WorkspaceTemplates []manifestEntry `json:"workspace_templates"`
|
|
}
|
|
|
|
// fallbackRuntimes is used when manifest.json can't be loaded. Keeps
|
|
// tests + dev containers working even if the file isn't mounted.
|
|
// Kept slightly broader than the original hardcoded map so a stale
|
|
// manifest doesn't silently drop a runtime that was previously
|
|
// supported in the wild. "external" is always a valid runtime —
|
|
// manifest or not — because it has no template repo.
|
|
var fallbackRuntimes = map[string]struct{}{
|
|
"claude-code": {},
|
|
"hermes": {},
|
|
"openclaw": {},
|
|
"codex": {},
|
|
"external": {},
|
|
// mock — virtual workspace with hardcoded canned A2A replies.
|
|
// No container, no EC2, no template repo. See mock_runtime.go
|
|
// for the full rationale (200-workspace funding-demo org).
|
|
"mock": {},
|
|
}
|
|
|
|
// loadRuntimesFromManifest builds the runtime allowlist from
|
|
// manifest.json. Each workspace_templates[].name is normalized to its
|
|
// base runtime identifier (strips the `-default` suffix templates
|
|
// use for the "vanilla" variant of their runtime) and added to the
|
|
// set. "external" is always injected — it's not a template-backed
|
|
// runtime, it's the BYO-compute meta-runtime.
|
|
//
|
|
// Caller logs + falls back to fallbackRuntimes on any error. Not
|
|
// returning the fallback here ourselves so the caller can decide
|
|
// how loud to be about the miss (prod = WARN, tests = silent).
|
|
func loadRuntimesFromManifest(path string) (map[string]struct{}, error) {
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var m manifestFile
|
|
if err := json.Unmarshal(data, &m); err != nil {
|
|
return nil, err
|
|
}
|
|
out := map[string]struct{}{
|
|
// external is ALWAYS available — it has no template repo, so
|
|
// the manifest doesn't know about it. Injected here so we
|
|
// don't need a special-case in every caller.
|
|
"external": {},
|
|
// mock is ALWAYS available for the same reason as external:
|
|
// virtual workspace, no template repo, never spawns a
|
|
// container. See mock_runtime.go.
|
|
"mock": {},
|
|
}
|
|
for _, e := range m.WorkspaceTemplates {
|
|
name := strings.TrimSpace(e.Name)
|
|
if name == "" {
|
|
continue
|
|
}
|
|
// Normalize template-name → runtime-identifier.
|
|
// Convention: "<runtime>-default" is the vanilla variant of
|
|
// <runtime>. Strip the suffix so both `claude-code` and
|
|
// `claude-code-default` resolve to the same runtime.
|
|
name = strings.TrimSuffix(name, "-default")
|
|
out[name] = struct{}{}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// initKnownRuntimes is called from the package init chain (see
|
|
// workspace_provision.go var initialization) to replace the
|
|
// fallback map with the manifest-derived one. Idempotent —
|
|
// safe to call multiple times.
|
|
func initKnownRuntimes() {
|
|
path := manifestPath()
|
|
if path == "" {
|
|
log.Printf("runtime registry: manifest.json not found, using fallback allowlist (%d entries)", len(fallbackRuntimes))
|
|
return
|
|
}
|
|
loaded, err := loadRuntimesFromManifest(path)
|
|
if err != nil {
|
|
log.Printf("runtime registry: manifest.json load failed (%v) — using fallback allowlist", err)
|
|
return
|
|
}
|
|
knownRuntimes = loaded
|
|
names := make([]string, 0, len(loaded))
|
|
for k := range loaded {
|
|
names = append(names, k)
|
|
}
|
|
log.Printf("runtime registry: loaded %d runtimes from %s: %v", len(loaded), path, names)
|
|
}
|