fix(platform): unblock org-template imports against modular workspace templates
Two adjacent fixes that surfaced trying to bring the molecule-dev org
template back up against the new standalone workspace-template-* repos.
1) handlers/org.go — expand ${VAR} in workspace_dir before validation.
The molecule-dev pm/workspace.yaml (and any operator's per-host
binding) ships `workspace_dir: ${WORKSPACE_DIR}` so each operator
can pick the host path PM bind-mounts. Without expansion the literal
"${WORKSPACE_DIR}" string reaches validateWorkspaceDir and fails with
"must be an absolute path", aborting the whole org import.
Other fields (channel config, prompts) already go through expandWithEnv;
workspace_dir was the last hold-out.
2) provisioner/provisioner.go — inject PYTHONPATH=/app for every
workspace container. Standalone template Dockerfiles 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 here fixes every adapter image at
once instead of needing 8 PRs against template repos. Operator
override still wins (workspace EnvVars are appended after, so Docker
takes the later duplicate).
Note: this unblocks the import path but does NOT make claude-code /
hermes / etc. boot. The runtime itself has a separate top-level
`from adapters import` that breaks against modular templates —
tracked at workspace-runtime#1.
Tests: TestBuildContainerEnv_InjectsPYTHONPATH +
TestBuildContainerEnv_WorkspaceEnvVarsCanOverridePYTHONPATH lock the
default + operator-override invariants. expandWithEnv is already covered
by TestExpandWithEnv_* — the workspace_dir use site is a one-line call
to that primitive.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bf9fb7cb51
commit
ff2394c085
@ -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 != "" {
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user