返回 DeepSeek-Reasonix
policy.go
根目录 / internal / guardian / policy.go
1 // Package guardian implements an LLM-driven safety reviewer that evaluates tool
2 // calls before they execute. It replaces the interactive human approval step for
3 // "ask" permission decisions: instead of prompting the user, a dedicated sub-agent
4 // with read-only tools inspects the call against a safety policy and returns
5 // allow/deny with a structured risk assessment.
6 package guardian
7
8 import (
9 _ "embed"
10 "encoding/json"
11 "fmt"
12 "strings"
13 )
14
15 //go:embed guardian_policy.md
16 var EmbeddedPolicy []byte
17
18 // Assessment is the structured output the guardian model must produce.
19 type Assessment struct {
20 RiskLevel string `json:"risk_level"`
21 UserAuthorization string `json:"user_authorization"`
22 Outcome string `json:"outcome"`
23 Rationale string `json:"rationale"`
24 }
25
26 // ParseOutcome maps a decision string.
27 func ParseOutcome(s string) string {
28 switch strings.ToLower(strings.TrimSpace(s)) {
29 case "allow":
30 return "allow"
31 case "deny":
32 return "deny"
33 default:
34 return "deny"
35 }
36 }
37
38 // ParseAssessment extracts a GuardianAssessment from the guardian model's raw
39 // output text. Accepts a JSON object directly or a JSON object wrapped in prose
40 // (first { to last }). Non-JSON output returns an error (triggering fail-closed).
41 func ParseAssessment(text string) (Assessment, error) {
42 text = strings.TrimSpace(text)
43 if text == "" {
44 return Assessment{}, fmt.Errorf("guardian review produced empty output")
45 }
46 var a Assessment
47 if err := json.Unmarshal([]byte(text), &a); err == nil {
48 return normalizeAssessment(a)
49 }
50 // Try to extract the first JSON object from prose wrapping.
51 if start := strings.IndexByte(text, '{'); start >= 0 {
52 if end := strings.LastIndexByte(text, '}'); end > start {
53 slice := text[start : end+1]
54 if err := json.Unmarshal([]byte(slice), &a); err == nil {
55 return normalizeAssessment(a)
56 }
57 }
58 }
59 return Assessment{}, fmt.Errorf("guardian output is not valid JSON: %q", firstRunesStr(text, 120))
60 }
61
62 func normalizeAssessment(a Assessment) (Assessment, error) {
63 outcome := ParseOutcome(a.Outcome)
64
65 if a.RiskLevel == "" {
66 if outcome == "allow" {
67 a.RiskLevel = "low"
68 } else {
69 a.RiskLevel = "high"
70 }
71 }
72 riskLevel, err := normalizePolicyEnum("risk_level", a.RiskLevel, validRiskLevels)
73 if err != nil {
74 return Assessment{}, err
75 }
76 a.RiskLevel = riskLevel
77
78 if a.UserAuthorization == "" {
79 a.UserAuthorization = "unknown"
80 }
81 userAuthorization, err := normalizePolicyEnum("user_authorization", a.UserAuthorization, validUserAuthorizations)
82 if err != nil {
83 return Assessment{}, err
84 }
85 a.UserAuthorization = userAuthorization
86
87 if strings.TrimSpace(a.Rationale) == "" {
88 if outcome == "allow" {
89 a.Rationale = "guardian review returned a low-risk allow decision"
90 } else {
91 a.Rationale = "guardian review returned a deny decision without a specific rationale"
92 }
93 }
94 a.Outcome = outcome
95 return enforcePolicyRules(a)
96 }
97
98 var validRiskLevels = map[string]bool{
99 "low": true,
100 "medium": true,
101 "high": true,
102 "critical": true,
103 }
104
105 var validUserAuthorizations = map[string]bool{
106 "unknown": true,
107 "low": true,
108 "medium": true,
109 "high": true,
110 }
111
112 func normalizePolicyEnum(field, value string, valid map[string]bool) (string, error) {
113 normalized := strings.ToLower(strings.TrimSpace(value))
114 if !valid[normalized] {
115 return "", fmt.Errorf("guardian output has unknown %s %q", field, value)
116 }
117 return normalized, nil
118 }
119
120 // enforcePolicyRules applies hard safety constraints that the guardian model
121 // prompt cannot override. These rules are the final backstop: even if the model
122 // produces allow for a critical-risk operation, the code forces deny.
123 func enforcePolicyRules(a Assessment) (Assessment, error) {
124 // Rule 1: critical risk is always deny.
125 if a.RiskLevel == "critical" && a.Outcome != "deny" {
126 a.Outcome = "deny"
127 if a.Rationale == "guardian review returned a low-risk allow decision" {
128 a.Rationale = "guardian review returned a critical-risk action with allow outcome — forced deny"
129 }
130 }
131 // Rule 2: high risk must have at least medium user authorization.
132 if a.RiskLevel == "high" && a.Outcome == "allow" {
133 if a.UserAuthorization != "medium" && a.UserAuthorization != "high" {
134 a.Outcome = "deny"
135 if a.Rationale == "guardian review returned a low-risk allow decision" {
136 a.Rationale = "guardian review allowed a high-risk action without sufficient user authorization — forced deny"
137 }
138 }
139 }
140 // Re-normalize: downstream code checks a.Outcome directly.
141 return a, nil
142 }
143
144 // DenyReason builds the model-facing reason string when the guardian denies a call.
145 func DenyReason(a Assessment) string {
146 return fmt.Sprintf("guardian denied: risk=%s, authorization=%s. %s",
147 a.RiskLevel, a.UserAuthorization, a.Rationale)
148 }
149
150 // CircuitBreakerReason builds the message injected when the circuit breaker trips.
151 func CircuitBreakerReason(consecutive, recent int) string {
152 return fmt.Sprintf("Guardian auto-review has denied too many requests this turn (%d consecutive, %d in recent window). Stop the current approach, report the situation to the user, and request explicit instructions before continuing.",
153 consecutive, recent)
154 }
155
155 lines GO