diff --git a/coordinator/cmd/worker-agent/main.go b/coordinator/cmd/worker-agent/main.go index e3602ed..28875ad 100644 --- a/coordinator/cmd/worker-agent/main.go +++ b/coordinator/cmd/worker-agent/main.go @@ -7,6 +7,7 @@ package main import ( "context" + "errors" "flag" "fmt" "log/slog" @@ -93,7 +94,7 @@ func loadConfig(configPath string) (*agent.Config, error) { } envPath := os.Getenv("SCIMESH_WORKER_CONFIG") if envPath != "" { - if _, err := os.Stat(envPath); err == nil { + if _, err := os.Stat(envPath); err == nil { //nolint:gosec // G703: path is the operator's own env var return agent.LoadConfigFile(envPath) } } @@ -153,7 +154,7 @@ func runSetup(args []string) int { // Block until the signal arrives (never returns an error that matters: a // cancelled context is the normal exit path). err = server.Serve(ctx, listener) - if err != nil && err != http.ErrServerClosed { + if err != nil && !errors.Is(err, http.ErrServerClosed) { logger.Error("setup wizard stopped", "err", err) return 1 } @@ -172,7 +173,8 @@ func openBrowser(url string) { if err != nil { continue } - _ = exec.Command(binary, candidate[1:]...).Start() + //nolint:gosec // G204: candidates are our own fixed list; the url is a loopback literal + _ = exec.CommandContext(context.Background(), binary, candidate[1:]...).Start() return } } diff --git a/coordinator/internal/agent/check.go b/coordinator/internal/agent/check.go index 6b788fd..c243ffb 100644 --- a/coordinator/internal/agent/check.go +++ b/coordinator/internal/agent/check.go @@ -80,6 +80,7 @@ func CheckEnvironment(ctx context.Context) CheckReport { return report } report.Python = CheckItem{Name: "python", OK: true, Detail: python} + //nolint:gosec // G204: python comes from LookPath, the argument list is constant cmd := exec.CommandContext(ctx, python, "-c", "import scimesh; print(scimesh.__version__ if hasattr(scimesh, '__version__') else 'installed')") out, err := cmd.Output() if err != nil { diff --git a/coordinator/internal/agent/configfile.go b/coordinator/internal/agent/configfile.go index 57e63f7..fc9e54d 100644 --- a/coordinator/internal/agent/configfile.go +++ b/coordinator/internal/agent/configfile.go @@ -41,6 +41,7 @@ func DefaultConfigPath() string { // created by the wizard with 0600 permissions, so no credential is exposed to // other local users. func LoadConfigFile(path string) (*Config, error) { + //nolint:gosec // G304: path is --config or SCIMESH_WORKER_CONFIG, operator-supplied raw, err := os.ReadFile(path) if err != nil { return nil, fmt.Errorf("read config file: %w", err) diff --git a/coordinator/internal/agent/setupui/server.go b/coordinator/internal/agent/setupui/server.go index 6b098dd..6561912 100644 --- a/coordinator/internal/agent/setupui/server.go +++ b/coordinator/internal/agent/setupui/server.go @@ -107,12 +107,15 @@ func (s *PIDSupervisor) Start(configPath, logPath string) (int, error) { if err != nil { return 0, fmt.Errorf("resolve worker binary: %w", err) } + //nolint:gosec // G304: logPath lives in the wizard's own config directory logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600) if err != nil { return 0, fmt.Errorf("open worker log: %w", err) } defer func() { _ = logFile.Close() }() - cmd := exec.Command(exe, "--config", configPath) + //nolint:gosec // G204: exe is os.Executable, configPath is the wizard's own file; + // Background ctx: the child's lifecycle is managed by the supervisor, not the context + cmd := exec.CommandContext(context.Background(), exe, "--config", configPath) cmd.Stdout = logFile cmd.Stderr = logFile cmd.Stdin = nil @@ -218,7 +221,7 @@ func New(log *slog.Logger, opts Options) *Server { // Listen binds the loopback listener and returns it; Serve runs the server on // it. Split so tests can inspect the actual ephemeral port. func (s *Server) Listen() (net.Listener, error) { - return net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", s.port)) + return (&net.ListenConfig{}).Listen(context.Background(), "tcp", fmt.Sprintf("127.0.0.1:%d", s.port)) } // OpenBrowser hands the wizard URL to the configured opener (default: no-op). diff --git a/coordinator/internal/agent/setupui/server_test.go b/coordinator/internal/agent/setupui/server_test.go index d751e7a..1272d6b 100644 --- a/coordinator/internal/agent/setupui/server_test.go +++ b/coordinator/internal/agent/setupui/server_test.go @@ -80,7 +80,7 @@ func (f *fakeSup) Alive() bool { func postJSON(t *testing.T, base, path string, body any) (*httptest.ResponseRecorder, map[string]any) { t.Helper() - req, err := http.NewRequest(http.MethodPost, base+path, strings.NewReader(mustJSON(t, body))) + req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, base+path, strings.NewReader(mustJSON(t, body))) if err != nil { t.Fatal(err) } @@ -105,7 +105,7 @@ func postJSON(t *testing.T, base, path string, body any) (*httptest.ResponseReco // gets a different port, so nothing can collide or share pooled connections. func freePort(t *testing.T) int { t.Helper() - listener, err := net.Listen("tcp", "127.0.0.1:0") + listener, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp", "127.0.0.1:0") if err != nil { t.Fatal(err) } @@ -194,7 +194,7 @@ func TestWizardStartStopLifecycle(t *testing.T) { } // Status reflects the running state. - req, _ := http.NewRequest(http.MethodGet, base+"/api/status", nil) + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, base+"/api/status", nil) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) @@ -218,7 +218,7 @@ func TestWizardStatusPrefillsSavedConfig(t *testing.T) { postJSON(t, base, "/api/config", map[string]any{ "coordinator_url": "http://10.0.0.5:8080", "worker_key": "smk_abc", "work_dir": "/w", "worker_name": "n1", }) - req, _ := http.NewRequest(http.MethodGet, base+"/api/status", nil) + req, _ := http.NewRequestWithContext(context.Background(), http.MethodGet, base+"/api/status", nil) resp, err := http.DefaultClient.Do(req) if err != nil { t.Fatal(err) diff --git a/coordinator/internal/storage/sqlite/admin_m2_test.go b/coordinator/internal/storage/sqlite/admin_m2_test.go index ef3a060..46f5c3d 100644 --- a/coordinator/internal/storage/sqlite/admin_m2_test.go +++ b/coordinator/internal/storage/sqlite/admin_m2_test.go @@ -2,6 +2,7 @@ package sqlite import ( "context" + "errors" "testing" "time" @@ -83,7 +84,7 @@ func TestWorkerSetTrust(t *testing.T) { if err := repo.SetTrust(ctx, worker.ID, domain.WorkerTrusted); err != nil { t.Fatal(err) } - if err := repo.SetTrust(ctx, uuid.New(), domain.WorkerTrusted); err != domain.ErrWorkerNotFound { + if err := repo.SetTrust(ctx, uuid.New(), domain.WorkerTrusted); !errors.Is(err, domain.ErrWorkerNotFound) { t.Errorf("unknown worker trust err = %v, want ErrWorkerNotFound", err) } } diff --git a/coordinator/internal/transport/http/ui_auth.go b/coordinator/internal/transport/http/ui_auth.go index 42f7bc9..cf46ea2 100644 --- a/coordinator/internal/transport/http/ui_auth.go +++ b/coordinator/internal/transport/http/ui_auth.go @@ -93,6 +93,7 @@ func (s *Server) handleUILogin(w http.ResponseWriter, r *http.Request) { if next == "" || !strings.HasPrefix(next, "/ui/") { next = "/ui" } + //nolint:gosec // G710: next is validated to start with /ui/ just above http.Redirect(w, r, next, http.StatusSeeOther) }