From c58c4cf11aea37e086015356350843e86ded4794 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=87=8E=E7=94=9F=E6=B4=BECoder=EF=BD=9E?= Date: Sat, 14 Feb 2026 01:02:41 +0800 Subject: [PATCH] fix shell risk gate dry-run flow for destructive git clean --- pkg/tools/risk.go | 2 +- pkg/tools/shell.go | 2 +- pkg/tools/shell_test.go | 45 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) create mode 100644 pkg/tools/shell_test.go diff --git a/pkg/tools/risk.go b/pkg/tools/risk.go index 3f86c67..4ceee87 100644 --- a/pkg/tools/risk.go +++ b/pkg/tools/risk.go @@ -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`), diff --git a/pkg/tools/shell.go b/pkg/tools/shell.go index 3e8ebdb..25ff5ee 100644 --- a/pkg/tools/shell.go +++ b/pkg/tools/shell.go @@ -254,7 +254,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 } diff --git a/pkg/tools/shell_test.go b/pkg/tools/shell_test.go new file mode 100644 index 0000000..2ececbb --- /dev/null +++ b/pkg/tools/shell_test.go @@ -0,0 +1,45 @@ +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) + } +}