From 0912aa7b896e5919802e01e9d483d5da8631958a Mon Sep 17 00:00:00 2001 From: Greg Gauthier Date: Sat, 7 Mar 2026 15:34:50 +0000 Subject: [PATCH] feat(recipe): add safe shell command execution Introduce a new mechanism in the recipe runner to execute whitelisted shell commands for steps like initialization, creation, or running tools (e.g., poetry, git). Commands are generated via AI prompt, parsed from JSON, validated against an allowed list, and executed within a strict working directory to ensure safety. --- internal/recipe/runner.go | 108 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) diff --git a/internal/recipe/runner.go b/internal/recipe/runner.go index cc008a8..e38fab5 100644 --- a/internal/recipe/runner.go +++ b/internal/recipe/runner.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "os" + "os/exec" "path/filepath" "strings" @@ -46,7 +47,16 @@ func (r *Runner) Run() error { r.handleApplyStep(refactorJSONs) continue + // === NEW: Safe shell command execution === + case strings.Contains(titleLower, "initialize") || strings.Contains(titleLower, "create") || + strings.Contains(titleLower, "run") || strings.Contains(titleLower, "shell") || + strings.Contains(titleLower, "poetry") || strings.Contains(titleLower, "git") || + strings.Contains(titleLower, "tea"): + r.executeShellCommands(step, previousResults) + continue + default: + // fallback for any other step prompt := fmt.Sprintf(`Recipe Overview: %s @@ -244,3 +254,101 @@ func createUnifiedPatch(changes []FileChange, patchPath string) error { } return nil } + +// NEW: Safe shell command execution +func (r *Runner) executeShellCommands(step Step, previousResults []string) { + prompt := fmt.Sprintf(`You are executing shell commands for this step. + +Recipe Overview: +%s + +Previous step results: +%s + +=== CURRENT STEP === +Objective: %s +Instructions: %s + +Return ONLY a JSON array of commands to run. Each command must be in this exact format: + +[ + { + "command": "poetry", + "args": ["new", "{{.project_name}}"] + } +] + +Only use commands from the allowed list. Never use cd, rm -rf, or anything that could escape the project directory.`, + r.Recipe.Overview, + strings.Join(previousResults, "\n\n---\n\n"), + step.Objective, + step.Instructions) + + messages := []map[string]string{ + {"role": "system", "content": "You are Grok, built by xAI. Precise expert programmer and refactoring assistant."}, + {"role": "user", "content": prompt}, + } + + response := r.Client.Stream(messages, r.Model) + fmt.Println() + + // Parse JSON command list + type ShellCommand struct { + Command string `json:"command"` + Args []string `json:"args"` + } + + var cmds []ShellCommand + start := strings.Index(response, "[") + end := strings.LastIndex(response, "]") + 1 + if start == -1 { + fmt.Println(" ⚠️ No valid shell commands returned — skipping.") + return + } + + if err := json.Unmarshal([]byte(response[start:end]), &cmds); err != nil { + fmt.Printf(" ⚠️ Could not parse shell commands: %v\n", err) + return + } + + // Resolve boundary + cwd := "." + if v, ok := r.Recipe.ResolvedParams["package_path"]; ok { + if s, ok := v.(string); ok && s != "" { + cwd = s + } + } + + for _, cmd := range cmds { + fullCmd := cmd.Command + if len(cmd.Args) > 0 { + fullCmd += " " + strings.Join(cmd.Args, " ") + } + + fmt.Printf(" Running: %s\n", fullCmd) + + // Whitelist check + allowed := false + for _, allowedCmd := range r.Recipe.AllowedShellCommands { + if strings.HasPrefix(cmd.Command, allowedCmd) { + allowed = true + break + } + } + if !allowed { + fmt.Printf(" ❌ Command not allowed: %s\n", cmd.Command) + continue + } + + // Execute with strict cwd + execCmd := exec.Command(cmd.Command, cmd.Args...) + execCmd.Dir = cwd + output, err := execCmd.CombinedOutput() + + if err != nil { + fmt.Printf(" ❌ Failed: %v\n%s\n", err, string(output)) + } else { + fmt.Printf(" ✅ Success\n%s\n", string(output)) + } + } +}