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:
parent
babc6e8599
commit
0912aa7b89
@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@ -46,7 +47,16 @@ func (r *Runner) Run() error {
|
|||||||
r.handleApplyStep(refactorJSONs)
|
r.handleApplyStep(refactorJSONs)
|
||||||
continue
|
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:
|
default:
|
||||||
|
// fallback for any other step
|
||||||
prompt := fmt.Sprintf(`Recipe Overview:
|
prompt := fmt.Sprintf(`Recipe Overview:
|
||||||
%s
|
%s
|
||||||
|
|
||||||
@ -244,3 +254,101 @@ func createUnifiedPatch(changes []FileChange, patchPath string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user