snapshot of 962fc3c82c774ab371e3fc4e84c55ef26e61268d Annotations about the code that implements koment.

internal/agentpolicy/hooks.go

1 package agentpolicy
2
3 import (
4 "encoding/json"
5 "fmt"
6 "path/filepath"
7 "regexp"
8 "strings"
9 )
10
11 type toolHookInput struct {
12 ToolName string `json:"tool_name"`
13 ToolInput struct {
14 Command string `json:"command"`
15 FilePath string `json:"filePath"`
16 Content string `json:"content"`
17 } `json:"tool_input"`
18 }
19
20 const toolHookApplyPatch = "apply_patch"
21 const toolHookOpencodeEdit = "opencode_edit"
22
23 // PreToolOutput blocks a Go patch that adds ordinary comment intent.
24 func PreToolOutput(input []byte) ([]byte, error) {
25 var request toolHookInput
26 if err := json.Unmarshal(input, &request); err != nil {
27 return nil, fmt.Errorf("parsing PreToolUse input: %w", err)
28 }
29 var body string
30 switch request.ToolName {
31 case toolHookApplyPatch:
32 body = request.ToolInput.Command
33 case toolHookOpencodeEdit:
34 body = syntheticPatchFromEdit(request.ToolInput.FilePath, request.ToolInput.Content)
35 default:
36 return []byte("{}\n"), nil
37 }
38 comments := addedCommentIntent(body)
39 if len(comments) == 0 {
40 return []byte("{}\n"), nil
41 }
42 reason := fmt.Sprintf("koment policy blocked ordinary comment intent (%s). Record the rationale against nearby code with koment_add instead. If a comment already exists, use koment_convert_comment; retaining one requires koment_acknowledge_comment with explicit acknowledgement.", strings.Join(comments, ", "))
43 response := map[string]any{
44 "hookSpecificOutput": map[string]any{
45 "hookEventName": "PreToolUse",
46 "permissionDecision": "deny",
47 "permissionDecisionReason": reason,
48 },
49 }
50 encoded, err := json.Marshal(response)
51 if err != nil {
52 return nil, fmt.Errorf("encoding PreToolUse output: %w", err)
53 }
54 return append(encoded, '\n'), nil
55 }
56
57 func syntheticPatchFromEdit(filePath, content string) string {
58 var builder strings.Builder
59 builder.WriteString("*** Update File: ")
60 builder.WriteString(filepath.ToSlash(filePath))
61 builder.WriteByte('\n')
62 builder.WriteString("@@\n")
63 for _, line := range strings.Split(content, "\n") {
64 builder.WriteByte('+')
65 builder.WriteString(line)
66 builder.WriteByte('\n')
67 }
68 return builder.String()
69 }
70
71 // StopWasContinued reports whether the Stop hook already continued this turn.
72 func StopWasContinued(input []byte) (bool, error) {
73 var request struct {
74 StopHookActive bool `json:"stop_hook_active"`
75 }
76 if err := json.Unmarshal(input, &request); err != nil {
77 return false, fmt.Errorf("parsing Stop input: %w", err)
78 }
79 return request.StopHookActive, nil
80 }
81
82 func addedCommentIntent(patch string) []string {
83 lines := strings.Split(patch, "\n")
84 file := ""
85 var found []string
86 for index := 0; index < len(lines); index++ {
87 line := lines[index]
88 if name, ok := patchFile(line); ok {
89 file = name
90 continue
91 }
92 if !strings.HasSuffix(file, ".go") || !isAddedComment(line) {
93 continue
94 }
95 start := index
96 for index+1 < len(lines) && isAddedComment(lines[index+1]) {
97 index++
98 }
99 group := lines[start : index+1]
100 if intrinsicPatchComment(group) || publicDocumentationPatch(group, followingCode(lines, index+1)) {
101 continue
102 }
103 found = append(found, fmt.Sprintf("%s: %s", file, strings.TrimSpace(strings.TrimPrefix(group[0], "+"))))
104 }
105 return found
106 }
107
108 func patchFile(line string) (string, bool) {
109 prefixes := []string{"*** Add File: ", "*** Update File: "}
110 for _, prefix := range prefixes {
111 if strings.HasPrefix(line, prefix) {
112 return filepath.ToSlash(strings.TrimSpace(strings.TrimPrefix(line, prefix))), true
113 }
114 }
115 return "", false
116 }
117
118 func isAddedComment(line string) bool {
119 if !strings.HasPrefix(line, "+") || strings.HasPrefix(line, "+++") {
120 return false
121 }
122 trimmed := strings.TrimSpace(strings.TrimPrefix(line, "+"))
123 return strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "/*") || strings.HasPrefix(trimmed, "*")
124 }
125
126 func intrinsicPatchComment(group []string) bool {
127 raw := strings.Join(group, "\n")
128 if strings.Contains(raw, "https://") || strings.Contains(raw, "http://") ||
129 strings.Contains(raw, "Deprecated:") ||
130 (strings.Contains(raw, "Code generated") && strings.Contains(raw, "DO NOT EDIT.")) {
131 return true
132 }
133 for _, line := range group {
134 text := strings.TrimSpace(strings.TrimPrefix(line, "+"))
135 text = strings.TrimSpace(strings.TrimPrefix(text, "//"))
136 allowed := false
137 for _, prefix := range []string{"go:", "+build", "line ", "nolint", "lint:", "revive:", "gosec", "export ", "#cgo"} {
138 if strings.HasPrefix(text, prefix) {
139 allowed = true
140 break
141 }
142 }
143 if !allowed {
144 return false
145 }
146 }
147 return true
148 }
149
150 func publicDocumentationPatch(group []string, code string) bool {
151 first := strings.TrimSpace(strings.TrimPrefix(group[0], "+"))
152 first = strings.TrimSpace(strings.TrimPrefix(strings.TrimPrefix(first, "//"), "/*"))
153 name := strings.Fields(first)
154 if len(name) == 0 {
155 return false
156 }
157 if name[0] == "Package" && strings.HasPrefix(strings.TrimSpace(code), "package ") {
158 return true
159 }
160 if name[0][0] < 'A' || name[0][0] > 'Z' {
161 return false
162 }
163 quoted := regexp.QuoteMeta(strings.Trim(name[0], "`'\".,:;()"))
164 declaration := regexp.MustCompile(`^(?:func\s+(?:\([^)]*\)\s+)?|type\s+|var\s+|const\s+)` + quoted + `\b`)
165 return declaration.MatchString(strings.TrimSpace(code))
166 }
167
168 func followingCode(lines []string, start int) string {
169 for _, line := range lines[start:] {
170 if strings.HasPrefix(line, "*** ") || strings.HasPrefix(line, "@@") || strings.HasPrefix(line, "-") {
171 continue
172 }
173 candidate := line
174 if strings.HasPrefix(candidate, "+") || strings.HasPrefix(candidate, " ") {
175 candidate = candidate[1:]
176 }
177 trimmed := strings.TrimSpace(candidate)
178 if trimmed == "" || strings.HasPrefix(trimmed, "//") || strings.HasPrefix(trimmed, "/*") || strings.HasPrefix(trimmed, "*") {
179 continue
180 }
181 return candidate
182 }
183 return ""
184 }

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close