diff --git a/pkg/workflow/argument_injection_test.go b/pkg/workflow/argument_injection_test.go index 7576a702db4..fc69d570309 100644 --- a/pkg/workflow/argument_injection_test.go +++ b/pkg/workflow/argument_injection_test.go @@ -588,6 +588,28 @@ printf "%s\n" "$@" > "$GH_AW_TEST_UV_ARGS_FILE" } } +func TestValidateUvPackagesWithExecutable_RejectsUnvalidatedExecutablePath(t *testing.T) { + tempDir := t.TempDir() + argsFile := filepath.Join(tempDir, "uv-args.txt") + uvPath := filepath.Join(tempDir, "uv") + script := `#!/bin/sh +printf "%s\n" "$@" > "$GH_AW_TEST_UV_ARGS_FILE" +` + if err := os.WriteFile(uvPath, []byte(script), 0o755); err != nil { + t.Fatalf("failed to write fake uv executable: %v", err) + } + t.Setenv("GH_AW_TEST_UV_ARGS_FILE", argsFile) + t.Chdir(tempDir) + + err := NewCompiler().validateUvPackagesWithExecutable([]string{"requests"}, "./uv") + if err == nil { + t.Fatal("expected relative executable path to be rejected") + } + if _, err := os.Stat(argsFile); !os.IsNotExist(err) { + t.Fatalf("expected invalid executable path to be rejected before uv execution; stat error = %v", err) + } +} + // TestValidateUvPackages_RejectsInvalidPackageName verifies that uv package names // which do not conform to the PyPI naming rules are rejected before being passed // as arguments to the uv CLI. This validation happens before uv/pip is resolved diff --git a/pkg/workflow/pip_validation.go b/pkg/workflow/pip_validation.go index f81dcaab05c..ef1a6fee2bc 100644 --- a/pkg/workflow/pip_validation.go +++ b/pkg/workflow/pip_validation.go @@ -231,6 +231,17 @@ func (c *Compiler) validateUvPackages(workflowData *WorkflowData) error { } func (c *Compiler) validateUvPackagesWithExecutable(packages []string, uvPath string) error { + validatedUvPath, err := fileutil.ValidateExecutablePath(uvPath) + if err != nil { + return NewOperationError( + "validate", + "uv packages", + "", + err, + "Install uv to enable package validation:\n\n$ curl -LsSf https://astral.sh/uv/install.sh | sh", + ) + } + var errors []string for _, pkg := range packages { pkgName := stripUvPackageVersion(pkg) @@ -244,10 +255,10 @@ func (c *Compiler) validateUvPackagesWithExecutable(packages []string, uvPath st } // Use uv pip show to check if package exists on PyPI - // #nosec G204 -- uvPath is resolved from the hardcoded executable name "uv" via - // fileutil.ResolveExecutablePath and validated before calling this method; + // #nosec G204 -- uvPath is validated above and resolved from the hardcoded + // executable name "uv" via fileutil.ResolveExecutablePath by the caller; // pkgName is validated above by validatePipPackageName against the strict PEP 508 allowlist. - cmd := exec.Command(uvPath, "pip", "show", "--no-cache", "--", pkgName) + cmd := exec.Command(validatedUvPath, "pip", "show", "--no-cache", "--", pkgName) _, err := cmd.CombinedOutput() if err != nil { diff --git a/pkg/workflow/semantic_function_refactor_workflow_test.go b/pkg/workflow/semantic_function_refactor_workflow_test.go index 540631e91ef..120d849b3ef 100644 --- a/pkg/workflow/semantic_function_refactor_workflow_test.go +++ b/pkg/workflow/semantic_function_refactor_workflow_test.go @@ -72,7 +72,7 @@ func TestSemanticFunctionRefactorWorkflowCostGuardrails(t *testing.T) { `GH_AW_MAX_AI_CREDITS: "300"`, `\"maxAiCredits\":${GH_AW_MAX_AI_CREDITS}`, `claude-sonnet-5`, - `name: Precompute semantic refactor slice`, + `name: Pre-compute semantic refactor slice`, `/tmp/gh-aw/agent/semantic-function-refactor/analysis.json`, `go run ./scripts/semantic-refactor-slice`, `--max-turns 50`,