feat: implement shell sandbox and security policies inspired by goclaw

This commit is contained in:
DBT
2026-02-12 07:57:48 +00:00
parent 6408b72086
commit 5e9813e3f2
4 changed files with 89 additions and 16 deletions

Binary file not shown.

View File

@@ -44,7 +44,7 @@ func NewAgentLoop(cfg *config.Config, msgBus *bus.MessageBus, provider providers
toolsRegistry.Register(&tools.ReadFileTool{}) toolsRegistry.Register(&tools.ReadFileTool{})
toolsRegistry.Register(&tools.WriteFileTool{}) toolsRegistry.Register(&tools.WriteFileTool{})
toolsRegistry.Register(&tools.ListDirTool{}) toolsRegistry.Register(&tools.ListDirTool{})
toolsRegistry.Register(tools.NewExecTool(workspace)) toolsRegistry.Register(tools.NewExecTool(cfg.Tools.Shell, workspace))
if cs != nil { if cs != nil {
toolsRegistry.Register(tools.NewRemindTool(cs)) toolsRegistry.Register(tools.NewRemindTool(cs))

View File

@@ -5,6 +5,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
"time"
"github.com/caarlos0/env/v11" "github.com/caarlos0/env/v11"
) )
@@ -112,8 +113,30 @@ type WebToolsConfig struct {
Search WebSearchConfig `json:"search"` 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 { type ToolsConfig struct {
Web WebToolsConfig `json:"web"` Web WebToolsConfig `json:"web"`
Shell ShellConfig `json:"shell"`
Filesystem FilesystemConfig `json:"filesystem"`
} }
var ( var (
@@ -212,6 +235,21 @@ func DefaultConfig() *Config {
MaxResults: 5, 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"},
},
}, },
} }
} }

View File

@@ -10,6 +10,8 @@ import (
"regexp" "regexp"
"strings" "strings"
"time" "time"
"clawgo/pkg/config"
) )
type ExecTool struct { type ExecTool struct {
@@ -18,26 +20,23 @@ type ExecTool struct {
denyPatterns []*regexp.Regexp denyPatterns []*regexp.Regexp
allowPatterns []*regexp.Regexp allowPatterns []*regexp.Regexp
restrictToWorkspace bool restrictToWorkspace bool
sandboxEnabled bool
sandboxImage string
} }
func NewExecTool(workingDir string) *ExecTool { func NewExecTool(cfg config.ShellConfig, workspace string) *ExecTool {
denyPatterns := []*regexp.Regexp{ denyPatterns := make([]*regexp.Regexp, 0)
regexp.MustCompile(`\brm\s+-[rf]{1,2}\b`), for _, p := range cfg.DeniedCmds {
regexp.MustCompile(`\bdel\s+/[fq]\b`), denyPatterns = append(denyPatterns, regexp.MustCompile(`\b`+regexp.QuoteMeta(p)+`\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{ return &ExecTool{
workingDir: workingDir, workingDir: workspace,
timeout: 60 * time.Second, timeout: cfg.Timeout,
denyPatterns: denyPatterns, denyPatterns: denyPatterns,
allowPatterns: nil, restrictToWorkspace: cfg.RestrictPath,
restrictToWorkspace: false, 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 return fmt.Sprintf("Error: %s", guardError), nil
} }
if t.sandboxEnabled {
return t.executeInSandbox(ctx, command, cwd)
}
cmdCtx, cancel := context.WithTimeout(ctx, t.timeout) cmdCtx, cancel := context.WithTimeout(ctx, t.timeout)
defer cancel() defer cancel()
@@ -125,6 +128,38 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) (st
return output, nil 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 { func (t *ExecTool) guardCommand(command, cwd string) string {
cmd := strings.TrimSpace(command) cmd := strings.TrimSpace(command)
lower := strings.ToLower(cmd) lower := strings.ToLower(cmd)