mirror of
https://github.com/YspCoder/clawgo.git
synced 2026-04-30 19:47:30 +08:00
fix safety
This commit is contained in:
@@ -7,47 +7,24 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"clawgo/pkg/config"
|
||||
"clawgo/pkg/logger"
|
||||
)
|
||||
|
||||
var blockedRootWipePattern = regexp.MustCompile(`(?i)(^|[;&|\n])\s*rm\b[^\n;&|]*\s(?:'/'|"/"|/)(?:\s|$)`)
|
||||
|
||||
type ExecTool struct {
|
||||
workingDir string
|
||||
timeout time.Duration
|
||||
denyPatterns []*regexp.Regexp
|
||||
allowPatterns []*regexp.Regexp
|
||||
restrictToWorkspace bool
|
||||
sandboxEnabled bool
|
||||
sandboxImage string
|
||||
riskCfg config.RiskConfig
|
||||
workingDir string
|
||||
timeout time.Duration
|
||||
sandboxEnabled bool
|
||||
sandboxImage string
|
||||
}
|
||||
|
||||
func NewExecTool(cfg config.ShellConfig, workspace string) *ExecTool {
|
||||
denyPatterns := make([]*regexp.Regexp, 0, len(cfg.DeniedCmds))
|
||||
for _, p := range cfg.DeniedCmds {
|
||||
denyPatterns = append(denyPatterns, regexp.MustCompile(`(?i)\b`+regexp.QuoteMeta(p)+`\b`))
|
||||
}
|
||||
|
||||
allowPatterns := make([]*regexp.Regexp, 0, len(cfg.AllowedCmds))
|
||||
for _, p := range cfg.AllowedCmds {
|
||||
allowPatterns = append(allowPatterns, regexp.MustCompile(`(?i)\b`+regexp.QuoteMeta(p)+`\b`))
|
||||
}
|
||||
|
||||
return &ExecTool{
|
||||
workingDir: workspace,
|
||||
timeout: cfg.Timeout,
|
||||
denyPatterns: denyPatterns,
|
||||
allowPatterns: allowPatterns,
|
||||
restrictToWorkspace: cfg.RestrictPath,
|
||||
sandboxEnabled: cfg.Sandbox.Enabled,
|
||||
sandboxImage: cfg.Sandbox.Image,
|
||||
riskCfg: cfg.Risk,
|
||||
workingDir: workspace,
|
||||
timeout: cfg.Timeout,
|
||||
sandboxEnabled: cfg.Sandbox.Enabled,
|
||||
sandboxImage: cfg.Sandbox.Image,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,10 +48,6 @@ func (t *ExecTool) Parameters() map[string]interface{} {
|
||||
"type": "string",
|
||||
"description": "Optional working directory for the command",
|
||||
},
|
||||
"force": map[string]interface{}{
|
||||
"type": "boolean",
|
||||
"description": "Bypass risk gate for destructive operations (still strongly discouraged).",
|
||||
},
|
||||
},
|
||||
"required": []string{"command"},
|
||||
}
|
||||
@@ -98,19 +71,6 @@ func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) (st
|
||||
}
|
||||
}
|
||||
|
||||
if guardError := t.guardCommand(command, cwd); guardError != "" {
|
||||
return fmt.Sprintf("Error: %s", guardError), nil
|
||||
}
|
||||
|
||||
force, _ := args["force"].(bool)
|
||||
if blockMsg, dryRunCmd := t.applyRiskGate(command, force); blockMsg != "" {
|
||||
if dryRunCmd != "" {
|
||||
dryRunResult, _ := t.executeCommand(ctx, dryRunCmd, cwd)
|
||||
return fmt.Sprintf("%s\n\nDry-run command: %s\nDry-run output:\n%s", blockMsg, dryRunCmd, dryRunResult), nil
|
||||
}
|
||||
return blockMsg, nil
|
||||
}
|
||||
|
||||
if t.sandboxEnabled {
|
||||
return t.executeInSandbox(ctx, command, cwd)
|
||||
}
|
||||
@@ -150,125 +110,10 @@ func (t *ExecTool) executeInSandbox(ctx context.Context, command, cwd string) (s
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (t *ExecTool) guardCommand(command, cwd string) string {
|
||||
cmd := strings.TrimSpace(command)
|
||||
lower := strings.ToLower(cmd)
|
||||
|
||||
if blockedRootWipePattern.MatchString(lower) {
|
||||
return "Command blocked by safety guard (removing root path / is forbidden)"
|
||||
}
|
||||
|
||||
for _, pattern := range t.denyPatterns {
|
||||
if pattern.MatchString(cmd) {
|
||||
return "Command blocked by safety guard (dangerous pattern detected)"
|
||||
}
|
||||
}
|
||||
|
||||
if len(t.allowPatterns) > 0 {
|
||||
allowed := false
|
||||
for _, pattern := range t.allowPatterns {
|
||||
if pattern.MatchString(cmd) {
|
||||
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("(?i)" + p)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid allow pattern %q: %w", p, err)
|
||||
}
|
||||
t.allowPatterns = append(t.allowPatterns, re)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *ExecTool) applyRiskGate(command string, force bool) (string, string) {
|
||||
if !t.riskCfg.Enabled {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
assessment := assessCommandRisk(command)
|
||||
logger.InfoCF("risk", "Command risk assessed", map[string]interface{}{
|
||||
"level": assessment.Level,
|
||||
"command": truncateCmd(command, 200),
|
||||
"reasons": assessment.Reasons,
|
||||
})
|
||||
|
||||
if assessment.Level != RiskDestructive {
|
||||
return "", ""
|
||||
}
|
||||
|
||||
if t.riskCfg.RequireForceFlag && !force {
|
||||
msg := "Error: destructive command blocked by risk gate. Re-run with force=true if intentional."
|
||||
if t.riskCfg.RequireDryRun {
|
||||
if dryRunCmd, ok := buildDryRunCommand(command); ok {
|
||||
return msg, dryRunCmd
|
||||
}
|
||||
}
|
||||
return msg, ""
|
||||
}
|
||||
|
||||
if !t.riskCfg.AllowDestructive {
|
||||
return "Error: destructive command is disabled by policy (tools.shell.risk.allow_destructive=false).", ""
|
||||
}
|
||||
|
||||
if t.riskCfg.RequireDryRun && !force {
|
||||
if dryRunCmd, ok := buildDryRunCommand(command); ok {
|
||||
return "Risk gate: dry-run required first. Review output, then execute intentionally with force=true.", dryRunCmd
|
||||
}
|
||||
return "Error: destructive command requires explicit force=true because no dry-run strategy is available.", ""
|
||||
}
|
||||
return "", ""
|
||||
}
|
||||
|
||||
func (t *ExecTool) executeCommand(ctx context.Context, command, cwd string) (string, error) {
|
||||
cmdCtx, cancel := context.WithTimeout(ctx, t.timeout)
|
||||
defer cancel()
|
||||
@@ -305,13 +150,3 @@ func (t *ExecTool) executeCommand(ctx context.Context, command, cwd string) (str
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func truncateCmd(s string, max int) string {
|
||||
if len(s) <= max {
|
||||
return s
|
||||
}
|
||||
if max <= 3 {
|
||||
return s[:max]
|
||||
}
|
||||
return s[:max-3] + "..."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user