chore(manifest): prune to 4 actively-supported runtimes
Deletes the 5 unsupported workspace_templates from manifest.json
(langgraph, crewai, autogen, deepagents, gemini-cli). The runtime
matrix is now claude-code / hermes / openclaw / codex — the four
templates with shipping images, working A2A integration, and active
CI publish-image cascades.
Mirrors the prune in:
- workspace-server/internal/handlers/runtime_registry.go
(fallbackRuntimes for dev/test contexts that boot without the
manifest mounted)
- workspace-server/internal/handlers/workspace_provision.go
(sanitizeRuntime: empty/unknown → "claude-code", was "langgraph";
removes the langgraph/deepagents-specific runtime_config skip
branch — they're no longer supported, so the block is dead)
- tests for both: rename TestEnsureDefaultConfig_LangGraph →
_Hermes, TestEnsureDefaultConfig_EmptyRuntimeDefaultsToLangGraph
→ _ClaudeCode, drop TestEnsureDefaultConfig_DeepAgents,
update TestSanitizeRuntime_Allowlist + the two
TestResolveRestartTemplate_* cases that pinned langgraph-default
as the safe-default name
Why this is safe: production reads manifest.json at boot and uses it
as the authoritative allowlist; the 5 removed runtimes have not
shipped working images for ≥1 release cycle. Any provision request
naming one will now coerce to claude-code (with a log line) instead
of returning a runtime that has no functioning template repo.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
35cb6ba089
commit
f33e59ba8c
@ -26,12 +26,7 @@
|
||||
],
|
||||
"workspace_templates": [
|
||||
{"name": "claude-code-default", "repo": "Molecule-AI/molecule-ai-workspace-template-claude-code", "ref": "main"},
|
||||
{"name": "langgraph", "repo": "Molecule-AI/molecule-ai-workspace-template-langgraph", "ref": "main"},
|
||||
{"name": "crewai", "repo": "Molecule-AI/molecule-ai-workspace-template-crewai", "ref": "main"},
|
||||
{"name": "autogen", "repo": "Molecule-AI/molecule-ai-workspace-template-autogen", "ref": "main"},
|
||||
{"name": "deepagents", "repo": "Molecule-AI/molecule-ai-workspace-template-deepagents", "ref": "main"},
|
||||
{"name": "hermes", "repo": "Molecule-AI/molecule-ai-workspace-template-hermes", "ref": "main"},
|
||||
{"name": "gemini-cli", "repo": "Molecule-AI/molecule-ai-workspace-template-gemini-cli", "ref": "main"},
|
||||
{"name": "openclaw", "repo": "Molecule-AI/molecule-ai-workspace-template-openclaw", "ref": "main"},
|
||||
{"name": "codex", "repo": "Molecule-AI/molecule-ai-workspace-template-codex", "ref": "main"}
|
||||
],
|
||||
|
||||
@ -94,12 +94,12 @@ func TestResolveRestartTemplate_ApplyTemplate_NameMatch(t *testing.T) {
|
||||
// the restart handler needs to lay down the new runtime's base files
|
||||
// via `<runtime>-default/`. Matches the existing behaviour comment.
|
||||
func TestResolveRestartTemplate_ApplyTemplate_RuntimeDefault(t *testing.T) {
|
||||
root := newTemplateDir(t, "langgraph-default")
|
||||
root := newTemplateDir(t, "hermes-default")
|
||||
|
||||
path, label := resolveRestartTemplate(root, "Some Workspace", "langgraph", restartTemplateInput{
|
||||
path, label := resolveRestartTemplate(root, "Some Workspace", "hermes", restartTemplateInput{
|
||||
ApplyTemplate: true,
|
||||
})
|
||||
if path == "" || label != "langgraph-default" {
|
||||
if path == "" || label != "hermes-default" {
|
||||
t.Errorf("apply_template + dbRuntime should resolve runtime-default; got path=%q label=%q", path, label)
|
||||
}
|
||||
}
|
||||
@ -227,17 +227,18 @@ func TestResolveRestartTemplate_CWE22_TraversalRuntime_FallsThrough(t *testing.T
|
||||
// string in dbRuntime resolves langgraph-default (the safe default) rather
|
||||
// than any attacker-chosen path. The attacker gains no additional access.
|
||||
func TestResolveRestartTemplate_CWE22_TraversalRuntime_CannotOverrideKnownRuntime(t *testing.T) {
|
||||
root := newTemplateDir(t, "langgraph-default")
|
||||
root := newTemplateDir(t, "claude-code-default")
|
||||
|
||||
path, label := resolveRestartTemplate(root, "Some Workspace", "../../../etc", restartTemplateInput{
|
||||
ApplyTemplate: true,
|
||||
})
|
||||
// Must resolve to langgraph-default, not to an escaped path
|
||||
expected := filepath.Join(root, "langgraph-default")
|
||||
// Must resolve to claude-code-default (the safe default after sanitizeRuntime),
|
||||
// not to an escaped path
|
||||
expected := filepath.Join(root, "claude-code-default")
|
||||
if path != expected {
|
||||
t.Errorf("traversal runtime must resolve to langgraph-default; got path=%q", path)
|
||||
t.Errorf("traversal runtime must resolve to claude-code-default; got path=%q", path)
|
||||
}
|
||||
if label != "langgraph-default" {
|
||||
t.Errorf("label must be langgraph-default; got %q", label)
|
||||
if label != "claude-code-default" {
|
||||
t.Errorf("label must be claude-code-default; got %q", label)
|
||||
}
|
||||
}
|
||||
|
||||
@ -73,15 +73,10 @@ type manifestFile struct {
|
||||
// supported in the wild. "external" is always a valid runtime —
|
||||
// manifest or not — because it has no template repo.
|
||||
var fallbackRuntimes = map[string]struct{}{
|
||||
"langgraph": {},
|
||||
"claude-code": {},
|
||||
"openclaw": {},
|
||||
"crewai": {},
|
||||
"autogen": {},
|
||||
"deepagents": {},
|
||||
"hermes": {},
|
||||
"openclaw": {},
|
||||
"codex": {},
|
||||
"gemini-cli": {},
|
||||
"external": {},
|
||||
}
|
||||
|
||||
|
||||
@ -510,13 +510,13 @@ func yamlQuote(s string) string {
|
||||
func sanitizeRuntime(raw string) string {
|
||||
raw = strings.TrimSpace(raw)
|
||||
if raw == "" {
|
||||
return "langgraph"
|
||||
return "claude-code"
|
||||
}
|
||||
if _, ok := knownRuntimes[raw]; ok {
|
||||
return raw
|
||||
}
|
||||
log.Printf("provisioner: rejected unknown runtime %q, falling back to langgraph", raw)
|
||||
return "langgraph"
|
||||
log.Printf("provisioner: rejected unknown runtime %q, falling back to claude-code", raw)
|
||||
return "claude-code"
|
||||
}
|
||||
|
||||
// ensureDefaultConfig generates minimal config files in memory for workspaces without a template.
|
||||
@ -562,12 +562,7 @@ func (h *WorkspaceHandler) ensureDefaultConfig(workspaceID string, payload model
|
||||
// and preflight already validates that the env vars are present before
|
||||
// the agent loop starts. Hardcoding token names here caused #1028
|
||||
// (expired CLAUDE_CODE_OAUTH_TOKEN baked into config.yaml).
|
||||
switch runtime {
|
||||
case "langgraph", "deepagents":
|
||||
// These runtimes read API keys from env directly, no runtime_config needed.
|
||||
default:
|
||||
configYAML += "runtime_config:\n timeout: 0\n"
|
||||
}
|
||||
configYAML += "runtime_config:\n timeout: 0\n"
|
||||
|
||||
files["config.yaml"] = []byte(configYAML)
|
||||
|
||||
|
||||
@ -189,14 +189,14 @@ func TestResolveOrgTemplate_NoMatchInOrgTemplates(t *testing.T) {
|
||||
|
||||
// ==================== ensureDefaultConfig ====================
|
||||
|
||||
func TestEnsureDefaultConfig_LangGraph(t *testing.T) {
|
||||
func TestEnsureDefaultConfig_Hermes(t *testing.T) {
|
||||
broadcaster := newTestBroadcaster()
|
||||
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
|
||||
|
||||
payload := models.CreateWorkspacePayload{
|
||||
Name: "Test Agent",
|
||||
Tier: 1,
|
||||
Runtime: "langgraph",
|
||||
Runtime: "hermes",
|
||||
}
|
||||
|
||||
files := handler.ensureDefaultConfig("ws-test-123", payload)
|
||||
@ -212,14 +212,14 @@ func TestEnsureDefaultConfig_LangGraph(t *testing.T) {
|
||||
if !contains(content, `name: "Test Agent"`) {
|
||||
t.Errorf("config.yaml missing quoted name, got:\n%s", content)
|
||||
}
|
||||
if !contains(content, "runtime: langgraph") {
|
||||
if !contains(content, "runtime: hermes") {
|
||||
t.Errorf("config.yaml missing runtime, got:\n%s", content)
|
||||
}
|
||||
if !contains(content, "tier: 1") {
|
||||
t.Errorf("config.yaml missing tier, got:\n%s", content)
|
||||
}
|
||||
if !contains(content, `model: "anthropic:claude-opus-4-7"`) {
|
||||
t.Errorf("config.yaml should use default langgraph model, got:\n%s", content)
|
||||
t.Errorf("config.yaml should use default non-claude model, got:\n%s", content)
|
||||
}
|
||||
}
|
||||
|
||||
@ -342,7 +342,7 @@ func TestEnsureDefaultConfig_CrewAIGetsRuntimeConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureDefaultConfig_EmptyRuntimeDefaultsToLangGraph(t *testing.T) {
|
||||
func TestEnsureDefaultConfig_EmptyRuntimeDefaultsToClaudeCode(t *testing.T) {
|
||||
broadcaster := newTestBroadcaster()
|
||||
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
|
||||
|
||||
@ -353,11 +353,11 @@ func TestEnsureDefaultConfig_EmptyRuntimeDefaultsToLangGraph(t *testing.T) {
|
||||
|
||||
files := handler.ensureDefaultConfig("ws-empty-rt", payload)
|
||||
configYAML := string(files["config.yaml"])
|
||||
if !contains(configYAML, "runtime: langgraph") {
|
||||
t.Errorf("empty runtime should default to langgraph, got:\n%s", configYAML)
|
||||
if !contains(configYAML, "runtime: claude-code") {
|
||||
t.Errorf("empty runtime should default to claude-code, got:\n%s", configYAML)
|
||||
}
|
||||
if !contains(configYAML, `model: "anthropic:claude-opus-4-7"`) {
|
||||
t.Errorf("langgraph default model should be anthropic (quoted), got:\n%s", configYAML)
|
||||
if !contains(configYAML, `model: "sonnet"`) {
|
||||
t.Errorf("claude-code default model should be sonnet (quoted), got:\n%s", configYAML)
|
||||
}
|
||||
}
|
||||
|
||||
@ -367,7 +367,7 @@ func TestEnsureDefaultConfig_EmptyNameAndRole(t *testing.T) {
|
||||
|
||||
payload := models.CreateWorkspacePayload{
|
||||
Tier: 1,
|
||||
Runtime: "langgraph",
|
||||
Runtime: "hermes",
|
||||
}
|
||||
|
||||
files := handler.ensureDefaultConfig("ws-empty-name", payload)
|
||||
@ -376,41 +376,11 @@ func TestEnsureDefaultConfig_EmptyNameAndRole(t *testing.T) {
|
||||
if !contains(configYAML, "name: ") {
|
||||
t.Errorf("config.yaml should have name field, got:\n%s", configYAML)
|
||||
}
|
||||
if !contains(configYAML, "runtime: langgraph") {
|
||||
if !contains(configYAML, "runtime: hermes") {
|
||||
t.Errorf("config.yaml should have runtime, got:\n%s", configYAML)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureDefaultConfig_DeepAgents(t *testing.T) {
|
||||
broadcaster := newTestBroadcaster()
|
||||
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
|
||||
|
||||
payload := models.CreateWorkspacePayload{
|
||||
Name: "Deep Agent",
|
||||
Tier: 2,
|
||||
Runtime: "deepagents",
|
||||
Model: "google_genai:gemini-2.5-flash",
|
||||
}
|
||||
|
||||
files := handler.ensureDefaultConfig("ws-deep", payload)
|
||||
|
||||
configYAML := string(files["config.yaml"])
|
||||
if !contains(configYAML, "runtime: deepagents") {
|
||||
t.Errorf("config.yaml missing runtime, got:\n%s", configYAML)
|
||||
}
|
||||
if !contains(configYAML, `model: "google_genai:gemini-2.5-flash"`) {
|
||||
t.Errorf("config.yaml should have model at top level (quoted), got:\n%s", configYAML)
|
||||
}
|
||||
// deepagents should NOT have runtime_config block
|
||||
if contains(configYAML, "runtime_config:") {
|
||||
t.Errorf("config.yaml should NOT have runtime_config for deepagents, got:\n%s", configYAML)
|
||||
}
|
||||
// Should NOT have auth token
|
||||
if _, ok := files[".auth-token"]; ok {
|
||||
t.Error("deepagents should not get .auth-token")
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureDefaultConfig_ModelAlwaysTopLevel(t *testing.T) {
|
||||
broadcaster := newTestBroadcaster()
|
||||
handler := NewWorkspaceHandler(broadcaster, nil, "http://localhost:8080", t.TempDir())
|
||||
@ -458,8 +428,8 @@ func TestEnsureDefaultConfig_RejectsInjectedRuntime(t *testing.T) {
|
||||
t.Errorf("injected initial_prompt key survived as top-level YAML: %+v", parsed)
|
||||
}
|
||||
// Runtime collapsed to default.
|
||||
if got := parsed["runtime"]; got != "langgraph" {
|
||||
t.Errorf("runtime = %v, want langgraph (unknown runtime should fall back)", got)
|
||||
if got := parsed["runtime"]; got != "claude-code" {
|
||||
t.Errorf("runtime = %v, want claude-code (unknown runtime should fall back)", got)
|
||||
}
|
||||
}
|
||||
|
||||
@ -507,19 +477,19 @@ func TestSanitizeRuntime_Allowlist(t *testing.T) {
|
||||
cases := []struct {
|
||||
in, want string
|
||||
}{
|
||||
{"", "langgraph"},
|
||||
{" ", "langgraph"},
|
||||
{"langgraph", "langgraph"},
|
||||
{"", "claude-code"},
|
||||
{" ", "claude-code"},
|
||||
{"claude-code", "claude-code"},
|
||||
{"openclaw", "openclaw"},
|
||||
{"deepagents", "deepagents"},
|
||||
{"hermes", "hermes"},
|
||||
{"codex", "codex"},
|
||||
{"crewai", "crewai"},
|
||||
{"autogen", "autogen"},
|
||||
{"not-a-runtime", "langgraph"}, // unknown → default
|
||||
{"../../sensitive", "langgraph"}, // path traversal probe → default
|
||||
{"langgraph\nevil", "langgraph"}, // newline injection → default (not in allowlist)
|
||||
{"langgraph", "claude-code"}, // deprecated → default
|
||||
{"deepagents", "claude-code"}, // deprecated → default
|
||||
{"crewai", "claude-code"}, // deprecated → default
|
||||
{"autogen", "claude-code"}, // deprecated → default
|
||||
{"not-a-runtime", "claude-code"}, // unknown → default
|
||||
{"../../sensitive", "claude-code"}, // path traversal probe → default
|
||||
{"langgraph\nevil", "claude-code"}, // newline injection → default (not in allowlist)
|
||||
}
|
||||
for _, tc := range cases {
|
||||
if got := sanitizeRuntime(tc.in); got != tc.want {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user