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.
This commit is contained in:
Greg Gauthier 2026-03-07 15:34:50 +00:00
parent babc6e8599
commit 0912aa7b89

View File

@ -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))
}
}
}