diff --git a/clawgo_test b/clawgo_test index de0e127..53255cf 100755 Binary files a/clawgo_test and b/clawgo_test differ diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 0ab87ca..65106b4 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -44,7 +44,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers toolsRegistry.Register(&tools.ReadFileTool{}) toolsRegistry.Register(&tools.WriteFileTool{}) toolsRegistry.Register(&tools.ListDirTool{}) - toolsRegistry.Register(tools.NewExecTool(workspace)) + toolsRegistry.Register(tools.NewExecTool(cfg.Tools.Shell, workspace)) if cs != nil { toolsRegistry.Register(tools.NewRemindTool(cs)) diff --git a/pkg/config/config.go b/pkg/config/config.go index 9ad12db..352c72e 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "sync" + "time" "github.com/caarlos0/env/v11" ) @@ -112,8 +113,30 @@ type WebToolsConfig struct { Search WebSearchConfig `json:"search"` } +type ShellConfig struct { + Enabled bool `json:"enabled" env:"CLAWGO_TOOLS_SHELL_ENABLED"` + WorkingDir string `json:"working_dir" env:"CLAWGO_TOOLS_SHELL_WORKING_DIR"` + Timeout time.Duration `json:"timeout" env:"CLAWGO_TOOLS_SHELL_TIMEOUT"` + DeniedCmds []string `json:"denied_cmds" env:"CLAWGO_TOOLS_SHELL_DENIED_CMDS"` + AllowedCmds []string `json:"allowed_cmds" env:"CLAWGO_TOOLS_SHELL_ALLOWED_CMDS"` + Sandbox SandboxConfig `json:"sandbox"` + RestrictPath bool `json:"restrict_path" env:"CLAWGO_TOOLS_SHELL_RESTRICT_PATH"` +} + +type SandboxConfig struct { + Enabled bool `json:"enabled" env:"CLAWGO_TOOLS_SHELL_SANDBOX_ENABLED"` + Image string `json:"image" env:"CLAWGO_TOOLS_SHELL_SANDBOX_IMAGE"` +} + +type FilesystemConfig struct { + AllowedPaths []string `json:"allowed_paths" env:"CLAWGO_TOOLS_FILESYSTEM_ALLOWED_PATHS"` + DeniedPaths []string `json:"denied_paths" env:"CLAWGO_TOOLS_FILESYSTEM_DENIED_PATHS"` +} + type ToolsConfig struct { - Web WebToolsConfig `json:"web"` + Web WebToolsConfig `json:"web"` + Shell ShellConfig `json:"shell"` + Filesystem FilesystemConfig `json:"filesystem"` } var ( @@ -212,6 +235,21 @@ func DefaultConfig() *Config { MaxResults: 5, }, }, + Shell: ShellConfig{ + Enabled: true, + Timeout: 60 * time.Second, + DeniedCmds: []string{ + "rm -rf /", "dd if=", "mkfs", "shutdown", "reboot", + }, + Sandbox: SandboxConfig{ + Enabled: false, + Image: "golang:alpine", + }, + }, + Filesystem: FilesystemConfig{ + AllowedPaths: []string{}, + DeniedPaths: []string{"/etc/shadow", "/etc/passwd"}, + }, }, } } diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index d8aea40..f8daa78 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -10,6 +10,8 @@ import ( "regexp" "strings" "time" + + "clawgo/pkg/config" ) type ExecTool struct { @@ -18,26 +20,23 @@ type ExecTool struct { denyPatterns []*regexp.Regexp allowPatterns []*regexp.Regexp restrictToWorkspace bool + sandboxEnabled bool + sandboxImage string } -func NewExecTool(workingDir string) *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*:`), +func NewExecTool(cfg config.ShellConfig, workspace string) *ExecTool { + denyPatterns := make([]*regexp.Regexp, 0) + for _, p := range cfg.DeniedCmds { + denyPatterns = append(denyPatterns, regexp.MustCompile(`\b`+regexp.QuoteMeta(p)+`\b`)) } return &ExecTool{ - workingDir: workingDir, - timeout: 60 * time.Second, + workingDir: workspace, + timeout: cfg.Timeout, denyPatterns: denyPatterns, - allowPatterns: nil, - restrictToWorkspace: false, + restrictToWorkspace: cfg.RestrictPath, + sandboxEnabled: cfg.Sandbox.Enabled, + sandboxImage: cfg.Sandbox.Image, } } @@ -88,6 +87,10 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) (st return fmt.Sprintf("Error: %s", guardError), nil } + if t.sandboxEnabled { + return t.executeInSandbox(ctx, command, cwd) + } + cmdCtx, cancel := context.WithTimeout(ctx, t.timeout) defer cancel() @@ -125,6 +128,38 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) (st return output, nil } +func (t *ExecTool) executeInSandbox(ctx context.Context, command, cwd string) (string, error) { + // 实现 Docker 沙箱执行逻辑 + absCwd, _ := filepath.Abs(cwd) + dockerArgs := []string{ + "run", "--rm", + "-v", fmt.Sprintf("%s:/app", absCwd), + "-w", "/app", + t.sandboxImage, + "sh", "-c", command, + } + + cmdCtx, cancel := context.WithTimeout(ctx, t.timeout) + defer cancel() + + cmd := exec.CommandContext(cmdCtx, "docker", dockerArgs...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + + err := cmd.Run() + output := stdout.String() + if stderr.Len() > 0 { + output += "\nSTDERR:\n" + stderr.String() + } + + if err != nil { + output += fmt.Sprintf("\nSandbox Exit code: %v", err) + } + + return output, nil +} + func (t *ExecTool) guardCommand(command, cwd string) string { cmd := strings.TrimSpace(command) lower := strings.ToLower(cmd)