返回 DeepSeek-Reasonix
remember_policy.go
根目录 / internal / memory / remember_policy.go
1 package memory
2
3 import (
4 "encoding/json"
5 "regexp"
6 "strings"
7 "unicode"
8
9 "reasonix/internal/secrets"
10 )
11
12 const maxAutoRememberBodyRunes = 6000
13
14 var rememberEmailPattern = regexp.MustCompile(`(?i)\b[a-z0-9.!#$%&'*+/=?^_` + "`" + `{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)+\b`)
15
16 // RememberAssessment explains whether an interactive host may safely allow a
17 // remember call without a confirmation dialog.
18 type RememberAssessment struct {
19 AutoAllow bool
20 Reason string
21 Name string
22 Type Type
23 Scope FactScope
24 }
25
26 // AssessRememberWrite permits only bounded, non-sensitive project/reference
27 // creates. Global facts, preferences, feedback, updates, and potential
28 // duplicates remain explicit user decisions.
29 func AssessRememberWrite(store Store, args json.RawMessage) RememberAssessment {
30 in, err := parseRememberRequest(args)
31 if err != nil {
32 return RememberAssessment{Reason: "invalid remember request"}
33 }
34 ref := parseMemoryReference(rememberRequestName(in))
35 assessment := RememberAssessment{
36 Name: ref.name,
37 Type: NormalizeType(in.Type),
38 Scope: NormalizeFactScope(in.Scope),
39 }
40 if ref.qualified {
41 if strings.TrimSpace(in.Scope) != "" && assessment.Scope != ref.scope {
42 assessment.Reason = "memory reference scope conflicts with explicit scope"
43 return assessment
44 }
45 assessment.Scope = ref.scope
46 }
47 if strings.TrimSpace(in.Description) == "" || strings.TrimSpace(in.Body) == "" {
48 assessment.Reason = "description and body are required"
49 return assessment
50 }
51 if store.Dir == "" {
52 assessment.Reason = "project memory store is unavailable"
53 return assessment
54 }
55 typ := strings.ToLower(strings.TrimSpace(in.Type))
56 if typ != string(TypeProject) && typ != string(TypeReference) {
57 assessment.Reason = "only explicitly classified project/reference facts are low-risk"
58 return assessment
59 }
60 if assessment.Scope != FactScopeProject {
61 assessment.Reason = "global memory requires confirmation"
62 return assessment
63 }
64 if strings.TrimSpace(in.ID) != "" || in.ExpectedRevision > 0 {
65 assessment.Reason = "memory updates require confirmation"
66 return assessment
67 }
68 if assessment.Name == "" {
69 assessment.Reason = "memory name cannot be derived"
70 return assessment
71 }
72 if len([]rune(in.Body)) > maxAutoRememberBodyRunes {
73 assessment.Reason = "memory body exceeds the automatic-write budget"
74 return assessment
75 }
76 if rememberRequestSensitive(in) {
77 assessment.Reason = "memory may contain sensitive information"
78 return assessment
79 }
80 if rememberRequestOverlaps(store, in, assessment.Name) {
81 assessment.Reason = "an existing memory may already cover this fact"
82 return assessment
83 }
84 assessment.AutoAllow = true
85 assessment.Reason = "new low-risk project fact"
86 return assessment
87 }
88
89 func rememberRequestSensitive(in rememberRequest) bool {
90 text := strings.Join([]string{in.Name, in.Title, in.Description, in.Body}, "\n")
91 if secrets.Redact(text) != text || rememberEmailPattern.MatchString(text) {
92 return true
93 }
94 upper := strings.ToUpper(text)
95 return strings.Contains(upper, "BEGIN PRIVATE KEY") || strings.Contains(upper, "BEGIN OPENSSH PRIVATE KEY")
96 }
97
98 func rememberRequestOverlaps(store Store, in rememberRequest, name string) bool {
99 wantTitle := normalizedMemoryPhrase(in.Title)
100 wantDescription := normalizedMemoryPhrase(in.Description)
101 for _, existing := range store.ListAll() {
102 if slug(existing.Name) == name {
103 return true
104 }
105 if wantTitle != "" && normalizedMemoryPhrase(existing.Title) == wantTitle {
106 return true
107 }
108 if wantDescription != "" && normalizedMemoryPhrase(existing.Description) == wantDescription {
109 return true
110 }
111 }
112 return false
113 }
114
115 func normalizedMemoryPhrase(value string) string {
116 return strings.Map(func(r rune) rune {
117 if unicode.IsLetter(r) || unicode.IsDigit(r) {
118 return unicode.ToLower(r)
119 }
120 return -1
121 }, value)
122 }
123
123 lines GO