package tools import ( "context" "fmt" "os" "path/filepath" "regexp" "strings" "time" "github.com/sipeed/picoclaw/pkg/config" ) type ExecTool struct { workingDir string timeout time.Duration denyPatterns []*regexp.Regexp allowPatterns []*regexp.Regexp restrictToWorkspace bool sandbox *Sandbox } func NewExecTool(workingDir string, restrict bool, limits config.ResourceLimits) *ExecTool { denyPatterns := []*regexp.Regexp{ regexp.MustCompile(`\brm\s+-[rf]{1,2}\b`), regexp.MustCompile(`\bdel\s+/[fq]\b`), regexp.MustCompile(`\brmdir\s+/s\b`), regexp.MustCompile(`\b(format|mkfs|diskpart)\b\s`), // Match disk wiping commands (must be followed by space/args) regexp.MustCompile(`\bdd\s+if=`), regexp.MustCompile(`>\s*/dev/sd[a-z]\b`), // Block writes to disk devices (but allow /dev/null) regexp.MustCompile(`\b(shutdown|reboot|poweroff)\b`), regexp.MustCompile(`:\(\)\s*\{.*\};\s*:`), } return &ExecTool{ workingDir: workingDir, timeout: 60 * time.Second, denyPatterns: denyPatterns, allowPatterns: nil, restrictToWorkspace: restrict, sandbox: NewSandbox(limits), } } func (t *ExecTool) Name() string { return "exec" } func (t *ExecTool) Description() string { return "Execute a shell command and return its output. Use with caution." } func (t *ExecTool) Parameters() map[string]interface{} { return map[string]interface{}{ "type": "object", "properties": map[string]interface{}{ "command": map[string]interface{}{ "type": "string", "description": "The shell command to execute", }, "working_dir": map[string]interface{}{ "type": "string", "description": "Optional working directory for the command", }, }, "required": []string{"command"}, } } func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult { command, ok := args["command"].(string) if !ok { return ErrorResult("command is required") } cwd := t.workingDir if wd, ok := args["working_dir"].(string); ok && wd != "" { cwd = wd } if cwd == "" { wd, err := os.Getwd() if err == nil { cwd = wd } } if guardError := t.guardCommand(command, cwd); guardError != "" { return ErrorResult(guardError) } output, err := t.sandbox.ExecuteCommand(ctx, command, cwd) if err != nil { if len(output) > 0 { return ErrorResult(fmt.Sprintf("Command failed: %v\nOutput:\n%s", err, string(output))) } return ErrorResult(fmt.Sprintf("Command failed: %v", err)) } return NewToolResult(string(output)) } func (t *ExecTool) guardCommand(command, cwd string) string { // If restrictToWorkspace is false, we assume the user wants full control, // including the ability to run "dangerous" commands or access any path. if !t.restrictToWorkspace { return "" } cmd := strings.TrimSpace(command) lower := strings.ToLower(cmd) for _, pattern := range t.denyPatterns { if pattern.MatchString(lower) { return "Command blocked by safety guard (dangerous pattern detected)" } } if len(t.allowPatterns) > 0 { allowed := false for _, pattern := range t.allowPatterns { if pattern.MatchString(lower) { allowed = true break } } if !allowed { return "Command blocked by safety guard (not in allowlist)" } } if t.restrictToWorkspace { if strings.Contains(cmd, "..\\") || strings.Contains(cmd, "../") { return "Command blocked by safety guard (path traversal detected)" } cwdPath, err := filepath.Abs(cwd) if err != nil { return "" } pathPattern := regexp.MustCompile(`[A-Za-z]:\\[^\\\"']+|/[^\s\"']+`) matches := pathPattern.FindAllString(cmd, -1) for _, raw := range matches { p, err := filepath.Abs(raw) if err != nil { continue } rel, err := filepath.Rel(cwdPath, p) if err != nil { continue } if strings.HasPrefix(rel, "..") { return "Command blocked by safety guard (path outside working dir)" } } } return "" } func (t *ExecTool) SetTimeout(timeout time.Duration) { t.timeout = timeout } func (t *ExecTool) SetRestrictToWorkspace(restrict bool) { t.restrictToWorkspace = restrict } func (t *ExecTool) SetAllowPatterns(patterns []string) error { t.allowPatterns = make([]*regexp.Regexp, 0, len(patterns)) for _, p := range patterns { re, err := regexp.Compile(p) if err != nil { return fmt.Errorf("invalid allow pattern %q: %w", p, err) } t.allowPatterns = append(t.allowPatterns, re) } return nil }