diff --git a/coordinator/internal/agent/releasewheel.go b/coordinator/internal/agent/releasewheel.go index a8f0f45..29cac5f 100644 --- a/coordinator/internal/agent/releasewheel.go +++ b/coordinator/internal/agent/releasewheel.go @@ -38,6 +38,9 @@ func NormalizePEP440(version string) string { // with a generous timeout: wheels can be several MB. func DownloadWheel(ctx context.Context, url, dir string) (string, error) { target := filepath.Join(dir, wheelNameFromURL(url)) + if err := os.MkdirAll(dir, 0o700); err != nil { + return "", fmt.Errorf("download wheel: %w", err) + } ctx, cancel := context.WithTimeout(ctx, 10*time.Minute) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) diff --git a/coordinator/internal/agent/setupui/server.go b/coordinator/internal/agent/setupui/server.go index 57e74c5..6b3a15d 100644 --- a/coordinator/internal/agent/setupui/server.go +++ b/coordinator/internal/agent/setupui/server.go @@ -479,6 +479,12 @@ type installRuntimeResponse struct { // step. The venv python path is returned for the wizard to bake into the // task runner. func (s *Server) handleInstallRuntime(w http.ResponseWriter, r *http.Request) { + // The wizard may run the install before any config was saved, so the + // worker directory (venv, wheel) may not exist yet. + if err := os.MkdirAll(s.dir, 0o700); err != nil { + writeJSON(w, http.StatusConflict, map[string]string{"error": "could not create the worker directory"}) + return + } var req installRuntimeRequest if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&req); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON body"}) diff --git a/coordinator/internal/agent/setupui/template.html b/coordinator/internal/agent/setupui/template.html index 758786b..43c8c73 100644 --- a/coordinator/internal/agent/setupui/template.html +++ b/coordinator/internal/agent/setupui/template.html @@ -249,8 +249,8 @@ $('b4').onclick=async()=>{ showStatus(); }; -const checkRow=(name,ok,detail,ms)=> - '