Merge pull request #483 from Molecule-AI/fix/platform-modular-template-support

fix(platform): unblock org-template imports against modular workspace templates
This commit is contained in:
Hongming Wang 2026-04-16 07:55:26 -07:00 committed by GitHub
commit 40c825b8ed
3 changed files with 67 additions and 0 deletions

View File

@ -357,6 +357,17 @@ func (h *OrgHandler) createWorkspaceTree(ws OrgWorkspace, parentID *string, defa
role = ws.Role
}
// Expand ${VAR} references in workspace_dir against the org's .env files
// before validation. Without this, a template that ships
// `workspace_dir: ${WORKSPACE_DIR}` (so each operator can pick the host
// path to bind-mount) reaches validateWorkspaceDir as the literal
// "${WORKSPACE_DIR}" string and fails with "must be an absolute path".
// Other fields (channel config, prompts) already go through expandWithEnv;
// workspace_dir was the last hold-out.
if ws.WorkspaceDir != "" {
ws.WorkspaceDir = expandWithEnv(ws.WorkspaceDir, loadWorkspaceEnv(orgBaseDir, ws.FilesDir))
}
// Validate and convert workspace_dir to NULL if empty
var workspaceDir interface{}
if ws.WorkspaceDir != "" {

View File

@ -380,6 +380,15 @@ func buildContainerEnv(cfg WorkspaceConfig) []string {
fmt.Sprintf("MOLECULE_URL=%s", cfg.PlatformURL),
fmt.Sprintf("TIER=%d", cfg.Tier),
"PLUGINS_DIR=/plugins",
// PYTHONPATH=/app makes ADAPTER_MODULE imports resolve regardless of
// runtime cwd. Standalone workspace-template repos COPY adapter.py to
// /app and set ENV ADAPTER_MODULE=adapter, but molecule-runtime is a
// pip console_script entry point so cwd isn't on sys.path automatically.
// Setting PYTHONPATH from the provisioner fixes every adapter image
// (claude-code, hermes, langgraph, …) without needing to PR each
// standalone template repo. Per-template ENV in the Dockerfile can
// still override (Dockerfile ENV is overridden by docker -e at runtime).
"PYTHONPATH=/app",
}
if cfg.AwarenessNamespace != "" && cfg.AwarenessURL != "" {
env = append(env, fmt.Sprintf("AWARENESS_NAMESPACE=%s", cfg.AwarenessNamespace))

View File

@ -469,6 +469,53 @@ func TestBuildContainerEnv_InjectsBothPlatformURLAndMoleculeAIURL(t *testing.T)
}
}
func TestBuildContainerEnv_InjectsPYTHONPATH(t *testing.T) {
// Standalone workspace-template repos COPY adapter.py to /app and rely on
// `import adapter` resolving via PYTHONPATH. molecule-runtime is a pip
// console_script entry, so cwd isn't on sys.path automatically. The
// provisioner injects PYTHONPATH=/app so every adapter image works
// without per-template Dockerfile patching. See workspace-runtime#1
// for the runtime-side bug this works around.
cfg := WorkspaceConfig{WorkspaceID: "ws-x", PlatformURL: "http://x", Tier: 1}
env := buildContainerEnv(cfg)
want := "PYTHONPATH=/app"
for _, e := range env {
if e == want {
return
}
}
t.Errorf("expected env to contain %q, got %v", want, env)
}
func TestBuildContainerEnv_WorkspaceEnvVarsCanOverridePYTHONPATH(t *testing.T) {
// Operator escape hatch: a per-workspace EnvVars["PYTHONPATH"] = "/custom"
// MUST appear AFTER the default in the env slice so Docker uses the
// later one. Without this, an operator who needs a custom path can't
// override the provisioner default.
cfg := WorkspaceConfig{
WorkspaceID: "ws-x",
PlatformURL: "http://x",
Tier: 1,
EnvVars: map[string]string{"PYTHONPATH": "/custom:/app"},
}
env := buildContainerEnv(cfg)
defaultIdx, customIdx := -1, -1
for i, e := range env {
if e == "PYTHONPATH=/app" {
defaultIdx = i
}
if e == "PYTHONPATH=/custom:/app" {
customIdx = i
}
}
if defaultIdx < 0 || customIdx < 0 {
t.Fatalf("expected both default and custom PYTHONPATH entries, got %v", env)
}
if customIdx < defaultIdx {
t.Errorf("custom PYTHONPATH (idx=%d) must come AFTER default (idx=%d) so Docker takes the operator override", customIdx, defaultIdx)
}
}
func TestBuildContainerEnv_MoleculeAIURLAlwaysMatchesPlatformURL(t *testing.T) {
// Regression guard: MOLECULE_URL must never drift from PLATFORM_URL —
// if someone changes one they must change the other. This test pins