Merge pull request #1 from YspCoder/codex/conduct-risk-and-bug-assessment

Fix shell risk gating for destructive `git clean` flow
This commit is contained in:
野生派Coder~
2026-02-14 09:50:16 +08:00
committed by GitHub
3 changed files with 72 additions and 3 deletions

View File

@@ -28,11 +28,11 @@ var destructivePatterns = []*regexp.Regexp{
regexp.MustCompile(`\bchown\b.+\s+/`),
regexp.MustCompile(`\bclawgo\s+uninstall\b`),
regexp.MustCompile(`\bdbt\s+drop\b`),
regexp.MustCompile(`\bgit\s+clean\b`),
}
var moderatePatterns = []*regexp.Regexp{
regexp.MustCompile(`\bgit\s+reset\s+--hard\b`),
regexp.MustCompile(`\bgit\s+clean\b`),
regexp.MustCompile(`\bdocker\s+system\s+prune\b`),
regexp.MustCompile(`\bapt(-get)?\s+install\b`),
regexp.MustCompile(`\byum\s+install\b`),

View File

@@ -29,15 +29,21 @@ type ExecTool struct {
}
func NewExecTool(cfg config.ShellConfig, workspace string) *ExecTool {
denyPatterns := make([]*regexp.Regexp, 0)
denyPatterns := make([]*regexp.Regexp, 0, len(cfg.DeniedCmds))
for _, p := range cfg.DeniedCmds {
denyPatterns = append(denyPatterns, regexp.MustCompile(`\b`+regexp.QuoteMeta(p)+`\b`))
}
allowPatterns := make([]*regexp.Regexp, 0, len(cfg.AllowedCmds))
for _, p := range cfg.AllowedCmds {
allowPatterns = append(allowPatterns, regexp.MustCompile(`\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,
@@ -254,7 +260,7 @@ func (t *ExecTool) applyRiskGate(command string, force bool) (string, string) {
return "Error: destructive command is disabled by policy (tools.shell.risk.allow_destructive=false).", ""
}
if t.riskCfg.RequireDryRun {
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
}

63
pkg/tools/shell_test.go Normal file
View File

@@ -0,0 +1,63 @@
package tools
import (
"testing"
"clawgo/pkg/config"
)
func TestApplyRiskGate_DryRunCanBeBypassedWithForce(t *testing.T) {
tool := &ExecTool{riskCfg: config.RiskConfig{
Enabled: true,
AllowDestructive: true,
RequireDryRun: true,
RequireForceFlag: false,
}}
msg, dryRun := tool.applyRiskGate("git clean -fd", true)
if msg != "" || dryRun != "" {
t.Fatalf("expected force=true to allow execution after dry-run step, got msg=%q dryRun=%q", msg, dryRun)
}
}
func TestApplyRiskGate_RequiresDryRunWithoutForce(t *testing.T) {
tool := &ExecTool{riskCfg: config.RiskConfig{
Enabled: true,
AllowDestructive: true,
RequireDryRun: true,
RequireForceFlag: false,
}}
msg, dryRun := tool.applyRiskGate("git clean -fd", false)
if msg == "" {
t.Fatal("expected dry-run block message")
}
if dryRun == "" {
t.Fatal("expected dry-run command")
}
}
func TestAssessCommandRisk_GitCleanIsDestructive(t *testing.T) {
assessment := assessCommandRisk("git clean -fd")
if assessment.Level != RiskDestructive {
t.Fatalf("expected git clean to be destructive, got %s", assessment.Level)
}
}
func TestNewExecTool_LoadsAllowedCmdsIntoAllowPatterns(t *testing.T) {
tool := NewExecTool(config.ShellConfig{AllowedCmds: []string{"echo"}}, ".")
if len(tool.allowPatterns) != 1 {
t.Fatalf("expected one allow pattern, got %d", len(tool.allowPatterns))
}
}
func TestGuardCommand_BlocksCommandNotInAllowlist(t *testing.T) {
tool := NewExecTool(config.ShellConfig{AllowedCmds: []string{"echo"}}, ".")
if msg := tool.guardCommand("ls -la", "."); msg == "" {
t.Fatal("expected allowlist to block command not in allowed_cmds")
}
if msg := tool.guardCommand("echo hi", "."); msg != "" {
t.Fatalf("expected allowed command to pass guard, got %q", msg)
}
}