diff --git a/coordinator/internal/agent/setupui/server.go b/coordinator/internal/agent/setupui/server.go index 7d9cca7..16becaf 100644 --- a/coordinator/internal/agent/setupui/server.go +++ b/coordinator/internal/agent/setupui/server.go @@ -300,6 +300,25 @@ type statusView struct { TokenSet bool `json:"token_set"` } +// ensureVenvTaskRunner rewrites the saved config so its task runner uses the +// managed venv python when one exists and the config does not already pin one. +func (s *Server) ensureVenvTaskRunner() { + raw, err := os.ReadFile(s.cfgPath) + if err != nil { + return + } + var file agent.ConfigFile + if json.Unmarshal(raw, &file) != nil || len(file.TaskRunner) > 0 { + return + } + if venv := s.venvPython(); venv != "" { + file.TaskRunner = []string{venv, "-m", "scimesh.worker.task"} + if payload, err := json.MarshalIndent(file, "", " "); err == nil { + _ = os.WriteFile(s.cfgPath, append(payload, '\n'), 0o600) + } + } +} + func (s *Server) handleStatus(w http.ResponseWriter, r *http.Request) { view := statusView{ConfigPath: s.cfgPath, LogPath: s.logPath, Running: s.sup.Alive(), Pid: s.sup.Pid()} if raw, err := os.ReadFile(s.cfgPath); err == nil { @@ -365,6 +384,14 @@ func (s *Server) handleSaveConfig(w http.ResponseWriter, r *http.Request) { if file.CPUCount < 1 { file.CPUCount = 1 } + // The wizard UI bakes the venv python into the runner after an install; + // an API-driven or scripted flow may not, so the server guarantees it: + // workloads execute through scimesh's task runner, which lives in the venv. + if len(file.TaskRunner) == 0 { + if venv := s.venvPython(); venv != "" { + file.TaskRunner = []string{venv, "-m", "scimesh.worker.task"} + } + } if err := agent.SaveConfigFile(s.cfgPath, file); err != nil { s.log.Error("save wizard config", "err", err) writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "could not write the config file"}) @@ -393,6 +420,7 @@ func (s *Server) handleStart(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "no configuration saved yet"}) return } + s.ensureVenvTaskRunner() pid, err := s.sup.Start(s.cfgPath, s.logPath) if err != nil { writeJSON(w, http.StatusConflict, map[string]string{"error": err.Error()}) @@ -514,6 +542,20 @@ func (s *Server) handleInstallRuntime(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, installRuntimeResponse{OK: true, Python: venvPython, Installed: true}) } +// venvPython returns the managed venv python when the runtime installer has +// created one, so the task runner can be pointed at it automatically. +func (s *Server) venvPython() string { + for _, candidate := range []string{ + filepath.Join(s.dir, "venv", "bin", "python"), + filepath.Join(s.dir, "venv", "Scripts", "python.exe"), + } { + if _, err := os.Stat(candidate); err == nil { + return candidate + } + } + return "" +} + // installScimeshWithPip installs the package with the venv's own pip, // streaming into the agent log so a long build is not silent. func installScimeshWithPip(ctx context.Context, venvPython, pkg string) error { diff --git a/coordinator/internal/agent/setupui/server_test.go b/coordinator/internal/agent/setupui/server_test.go index 44e1ab7..ab617f6 100644 --- a/coordinator/internal/agent/setupui/server_test.go +++ b/coordinator/internal/agent/setupui/server_test.go @@ -409,3 +409,48 @@ func TestInstallRuntimeWheelDownloadFailureIsExplained(t *testing.T) { t.Errorf("error = %v, want a SCIMESH_PIP_PACKAGE hint", data["error"]) } } + +func TestStartPinsTheVenvTaskRunner(t *testing.T) { + sup := &fakeSup{} + server, base := newTestServer(t, sup) + postJSON(t, base, "/api/config", map[string]any{ + "coordinator_url": "http://coord:8080", "token": "t", "work_dir": ".", + }) + // Simulate the runtime installer: create the venv python marker. + venvPython := filepath.Join(server.dir, "venv", "bin", "python") + _ = os.MkdirAll(filepath.Dir(venvPython), 0o755) + _ = os.WriteFile(venvPython, []byte("#!/bin/sh\nexit 0\n"), 0o755) + + rec, _ := postJSON(t, base, "/api/start", map[string]any{}) + if rec.Code != http.StatusOK { + t.Fatalf("start: got %d, want 200", rec.Code) + } + config, err := agent.LoadConfigFile(server.cfgPath) + if err != nil { + t.Fatal(err) + } + if len(config.TaskRunner) != 3 || config.TaskRunner[0] != venvPython || config.TaskRunner[1] != "-m" || config.TaskRunner[2] != "scimesh.worker.task" { + t.Errorf("task runner = %v, want the venv python runner", config.TaskRunner) + } +} + +func TestSaveConfigPinsVenvRunnerWhenPresent(t *testing.T) { + server, base := newTestServer(t, &fakeSup{}) + venvPython := filepath.Join(server.dir, "venv", "bin", "python") + _ = os.MkdirAll(filepath.Dir(venvPython), 0o755) + _ = os.WriteFile(venvPython, []byte("#!/bin/sh\nexit 0\n"), 0o755) + + rec, _ := postJSON(t, base, "/api/config", map[string]any{ + "coordinator_url": "http://coord:8080", "token": "t", "work_dir": ".", + }) + if rec.Code != http.StatusOK { + t.Fatalf("config: got %d", rec.Code) + } + config, err := agent.LoadConfigFile(server.cfgPath) + if err != nil { + t.Fatal(err) + } + if len(config.TaskRunner) != 3 || config.TaskRunner[0] != venvPython { + t.Errorf("task runner = %v, want the venv python", config.TaskRunner) + } +}