返回 DeepSeek-Reasonix
guards.go
根目录 / internal / runtimepolicy / guards.go
1 package runtimepolicy
2
3 import (
4 "encoding/json"
5 "strings"
6
7 "reasonix/internal/evidence"
8 "reasonix/internal/tool"
9 )
10
11 // PlanGuard hard-blocks writes while Plan mode is active, including YOLO.
12 type PlanGuard struct{}
13
14 func (PlanGuard) BeforeTool(ctx CallContext) GuardDecision {
15 if !ctx.PlanReadOnly || !ctx.Profile.MutatesState() {
16 return GuardDecision{Action: GuardAbstain}
17 }
18 return GuardDecision{
19 Action: GuardDeny,
20 Reasons: []string{"plan_boundary"},
21 Message: "blocked: plan mode forbids workspace mutations until the plan is approved",
22 }
23 }
24 func (PlanGuard) AfterTool(ResultContext) []evidence.Receipt { return nil }
25
26 // ConstraintGuard applies explicit user/host limits only.
27 type ConstraintGuard struct{ Constraints Constraints }
28
29 func (g ConstraintGuard) BeforeTool(ctx CallContext) GuardDecision {
30 c := g.Constraints
31 if ctx.Profile.MutatesState() && !c.AllowsMutation() {
32 return GuardDecision{
33 Action: GuardDeny,
34 Reasons: []string{"user_constraint"},
35 Message: "blocked: the current constraints forbid state mutation",
36 }
37 }
38 if (ctx.Profile.ExternalState || looksExternalCommand(ctx)) && !c.AllowsExternal() {
39 return GuardDecision{
40 Action: GuardDeny,
41 Reasons: []string{"user_constraint"},
42 Message: "blocked: the current constraints forbid push/publish/deploy-style actions",
43 }
44 }
45 if ctx.Verification && !c.AllowsTests() {
46 return GuardDecision{
47 Action: GuardDeny,
48 Reasons: []string{"user_constraint"},
49 Message: "blocked: the current constraints forbid verification commands",
50 }
51 }
52 if ctx.Verification && !c.AllowsCommand(bashCommand(ctx)) {
53 return GuardDecision{
54 Action: GuardDeny,
55 Reasons: []string{"user_constraint"},
56 Message: "blocked: verification command is outside the user allowlist",
57 }
58 }
59 return GuardDecision{Action: GuardAbstain}
60 }
61 func (ConstraintGuard) AfterTool(ResultContext) []evidence.Receipt { return nil }
62
63 func bashCommand(ctx CallContext) string {
64 name := strings.ToLower(strings.TrimSpace(ctx.ToolName))
65 if !tool.IsShellToolName(name) {
66 return ""
67 }
68 var payload struct {
69 Command string `json:"command"`
70 }
71 if json.Unmarshal(ctx.Args, &payload) == nil {
72 return strings.TrimSpace(payload.Command)
73 }
74 return ""
75 }
76
77 func looksExternalCommand(ctx CallContext) bool {
78 name := strings.ToLower(strings.TrimSpace(ctx.ToolName))
79 if strings.Contains(name, "deploy") || strings.Contains(name, "publish") || strings.Contains(name, "push") {
80 return true
81 }
82 cmd := strings.ToLower(bashCommand(ctx))
83 if cmd == "" {
84 return false
85 }
86 for _, needle := range []string{"git push", "publish", "kubectl", "deploy", "helm push"} {
87 if strings.Contains(cmd, needle) {
88 return true
89 }
90 }
91 return false
92 }
93
94 // MutationDependencyGuard blocks later mutations after an earlier batch failure.
95 type MutationDependencyGuard struct{ Blocked bool }
96
97 func (g MutationDependencyGuard) BeforeTool(ctx CallContext) GuardDecision {
98 if !g.Blocked || (!ctx.Profile.MutatesState() && !ctx.Verification) {
99 return GuardDecision{Action: GuardAbstain}
100 }
101 return GuardDecision{
102 Action: GuardDeny,
103 Reasons: []string{"receipt"},
104 Message: "blocked: an earlier mutation in this batch failed; later mutations and verifications cannot run",
105 }
106 }
107 func (MutationDependencyGuard) AfterTool(ResultContext) []evidence.Receipt { return nil }
108
109 // OpaqueWriterGuard asks when an unknown writer can be reviewed, else denies.
110 type OpaqueWriterGuard struct{}
111
112 func (OpaqueWriterGuard) BeforeTool(ctx CallContext) GuardDecision {
113 name := strings.ToLower(strings.TrimSpace(ctx.ToolName))
114 if !ctx.Profile.OpaqueWriter() || tool.IsShellToolName(name) {
115 return GuardDecision{Action: GuardAbstain}
116 }
117 if ctx.Interactive {
118 return GuardDecision{
119 Action: GuardAsk,
120 Reasons: []string{"opaque_writer"},
121 Message: "unknown writer requires explicit approval",
122 }
123 }
124 return GuardDecision{
125 Action: GuardDeny,
126 Reasons: []string{"opaque_writer"},
127 Message: "blocked: unknown writer cannot run without an interactive approval channel",
128 }
129 }
130 func (OpaqueWriterGuard) AfterTool(ResultContext) []evidence.Receipt { return nil }
131
131 lines GO