refactor(recipe): enhance shell command security with global safe list

- Introduce global safeCommands() map for command whitelisting.
- Implement case-insensitive prefix checking for allowed commands.
- Simplify argument handling by removing redundant int conversions.
- Update error messages and comments for clarity on security policies.
- Remove outdated comments and adjust prompt text for consistency.
This commit is contained in:
Greg Gauthier 2026-03-07 19:50:06 +00:00
parent 4603a1ec7a
commit 383d28a91a

View File

@ -258,7 +258,7 @@ func createUnifiedPatch(changes []FileChange, patchPath string) error {
return nil
}
// executeReadOnlyShell — now handles numbers in args (like tree -L 3)
// executeReadOnlyShell — safe, whitelisted, read-only shell execution with user confirmation
func (r *Runner) executeReadOnlyShell(step Step, previousResults []string) {
prompt := fmt.Sprintf(`You need additional context from the filesystem for this step.
@ -285,7 +285,7 @@ Return ONLY a JSON array of read-only commands. Example:
}
]
Only use safe read-only commands from the allowed list.`,
Only use safe read-only commands.`,
r.Recipe.Overview,
strings.Join(previousResults, "\n\n---\n\n"),
step.Objective,
@ -312,7 +312,7 @@ Only use safe read-only commands from the allowed list.`,
type ShellCommand struct {
Command string `json:"command"`
Args []interface{} `json:"args"` // allows strings and numbers
Args []interface{} `json:"args"`
}
var cmds []ShellCommand
@ -321,6 +321,9 @@ Only use safe read-only commands from the allowed list.`,
return
}
// Use the GLOBAL safe list for the security check
safeMap := safeCommands()
for _, cmd := range cmds {
// Build argument list, converting numbers to strings
args := make([]string, len(cmd.Args))
@ -330,8 +333,6 @@ Only use safe read-only commands from the allowed list.`,
args[i] = v
case float64:
args[i] = strconv.FormatFloat(v, 'f', -1, 64)
case int, int64:
args[i] = fmt.Sprintf("%v", v)
default:
args[i] = fmt.Sprintf("%v", v)
}
@ -354,16 +355,18 @@ Only use safe read-only commands from the allowed list.`,
continue
}
// Whitelist check
// FINAL SECURITY CHECK — use the global safe list
allowed := false
for _, allowedCmd := range r.Recipe.AllowedShellCommands {
if strings.HasPrefix(cmd.Command, allowedCmd) {
trimmedCmd := strings.ToLower(strings.TrimSpace(cmd.Command))
for safe := range safeMap {
if strings.HasPrefix(trimmedCmd, strings.ToLower(safe)) {
allowed = true
break
}
}
if !allowed {
fmt.Printf(" ❌ Command not allowed: %s\n", cmd.Command)
fmt.Printf(" ❌ Command not allowed by global safety policy: %s\n", cmd.Command)
continue
}