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

internal/policy/policy.go

1 package policy
2
3 import (
4 "crypto/rand"
5 "encoding/hex"
6 "errors"
7 "fmt"
8 "io"
9 "os"
10 "path"
11 "regexp"
12 "strings"
13
14 yaml "go.yaml.in/yaml/v3"
15
16 "github.com/koment-dev/koment/internal/api"
17 )
18
19 const (
20 // APIVersion is matched exactly, exactly as an annotation record's is.
21 APIVersion = api.Version
22
23 // KindPolicy is the resource kind of a repository policy.
24 KindPolicy = "Policy"
25
26 ModeStrict = "strict"
27 FileName = ".koment/policy.yaml"
28 SchemaURL = api.SchemaBase + "policy.schema.json"
29 )
30
31 // Intrinsic names one class of source comment that may remain inline.
32 type Intrinsic string
33
34 const (
35 IntrinsicToolchain Intrinsic = "toolchain-directive"
36 IntrinsicGeneratedMarker Intrinsic = "generated-marker"
37 IntrinsicUpstreamLink Intrinsic = "upstream-link"
38 IntrinsicDeprecated Intrinsic = "deprecated"
39 IntrinsicPublicAPI Intrinsic = "public-api"
40 )
41
42 // Principle names one extra rule the generated agent contract states. A
43 // principle is a claim a reviewer can check, not a preference.
44 type Principle string
45
46 const (
47 PrincipleBackCompatEvidence Principle = "back-compat-evidence"
48 )
49
50 var Principles = []Principle{PrincipleBackCompatEvidence}
51
52 var principleText = map[Principle]string{
53 PrincipleBackCompatEvidence: "A back-compatibility claim needs evidence: a migration path the binary performs, " +
54 "or an ADR naming the version the old shape was cut off at. Without either, the change is breaking " +
55 "and its commit subject says so with `feat!:`.",
56 }
57
58 // Adapter names one generated agent instruction surface.
59 type Adapter string
60
61 const (
62 AdapterAgents Adapter = "agents"
63 AdapterClaude Adapter = "claude"
64 AdapterCopilot Adapter = "copilot"
65 AdapterCursor Adapter = "cursor"
66 AdapterCodex Adapter = "codex"
67 AdapterOpencode Adapter = "opencode"
68 )
69
70 // Policy is the repository enforcement contract, shaped like every other
71 // committed koment resource (ADR 0121).
72 type Policy struct {
73 APIVersion string `yaml:"apiVersion"`
74 Kind string `yaml:"kind"`
75 Spec Spec `yaml:"spec"`
76 }
77
78 // Spec is everything the repository decided. A policy has no metadata because
79 // it is a singleton with no identity of its own: the file path is the name.
80 type Spec struct {
81 Comments CommentsPolicy `yaml:"comments"`
82 Agents AgentsPolicy `yaml:"agents"`
83 }
84
85 // CommentsPolicy configures strict classification and repository exclusions.
86 type CommentsPolicy struct {
87 Mode string `yaml:"mode"`
88 Intrinsic []Intrinsic `yaml:"intrinsic"`
89 GeneratedPaths []string `yaml:"generatedPaths,omitempty"`
90 VendoredPaths []string `yaml:"vendoredPaths,omitempty"`
91 }
92
93 // AgentsPolicy selects generated instruction adapters and the principles they
94 // state.
95 type AgentsPolicy struct {
96 Adapters []Adapter `yaml:"adapters"`
97 Principles []Principle `yaml:"principles,omitempty"`
98 }
99
100 // Default returns the strict policy installed for a new repository.
101 func Default() Policy {
102 return Policy{
103 APIVersion: APIVersion,
104 Kind: KindPolicy,
105 Spec: Spec{
106 Comments: CommentsPolicy{
107 Mode: ModeStrict,
108 Intrinsic: []Intrinsic{
109 IntrinsicToolchain, IntrinsicGeneratedMarker, IntrinsicUpstreamLink,
110 IntrinsicDeprecated, IntrinsicPublicAPI,
111 },
112 GeneratedPaths: []string{"**/*.gen.go", "**/*.generated.go"},
113 VendoredPaths: []string{"vendor/**"},
114 },
115 Agents: AgentsPolicy{
116 Adapters: []Adapter{
117 AdapterAgents, AdapterClaude, AdapterCopilot, AdapterCursor, AdapterCodex,
118 AdapterOpencode,
119 },
120 Principles: []Principle{PrincipleBackCompatEvidence},
121 },
122 },
123 }
124 }
125
126 // Load reads and strictly validates the repository policy.
127 func Load(rootPath string) (configured Policy, returnedError error) {
128 root, err := os.OpenRoot(rootPath)
129 if err != nil {
130 return Policy{}, fmt.Errorf("opening repository root %s: %w", rootPath, err)
131 }
132 defer func() {
133 if closeErr := root.Close(); closeErr != nil {
134 returnedError = errors.Join(returnedError, closeErr)
135 }
136 }()
137 content, err := root.ReadFile(FileName)
138 if err != nil {
139 return Policy{}, fmt.Errorf("reading %s: %w", FileName, err)
140 }
141 configured, upgraded, err := decode(content)
142 if err != nil {
143 return Policy{}, err
144 }
145 if err := configured.Validate(); err != nil {
146 return Policy{}, fmt.Errorf("in %s: %w", FileName, err)
147 }
148 if upgraded {
149 if err := writeTo(root, configured); err != nil {
150 return Policy{}, err
151 }
152 }
153 return configured, nil
154 }
155
156 type policyShape struct {
157 APIVersion string `yaml:"apiVersion"`
158 Version *int `yaml:"version"`
159 }
160
161 func decode(content []byte) (Policy, bool, error) {
162 var shape policyShape
163 if err := yaml.Unmarshal(content, &shape); err != nil {
164 return Policy{}, false, fmt.Errorf("parsing %s: %w", FileName, err)
165 }
166 switch {
167 case shape.APIVersion == APIVersion:
168 var configured Policy
169 if err := decodeOneDocument(content, &configured); err != nil {
170 return Policy{}, false, err
171 }
172 return configured, false, nil
173 case shape.APIVersion != "":
174 return Policy{}, false, fmt.Errorf(
175 "incompatible %s: apiVersion %q is not supported; this binary reads %s (ADR 0121)",
176 FileName, shape.APIVersion, APIVersion)
177 case shape.Version != nil && *shape.Version == LegacyVersion:
178 var legacy legacyPolicy
179 if err := decodeOneDocument(content, &legacy); err != nil {
180 return Policy{}, false, err
181 }
182 return upgradeLegacy(legacy), true, nil
183 default:
184 return Policy{}, false, fmt.Errorf(
185 "incompatible %s: no apiVersion; a koment policy starts with `apiVersion: %s` (ADR 0121)",
186 FileName, APIVersion)
187 }
188 }
189
190 func decodeOneDocument(content []byte, into any) error {
191 decoder := yaml.NewDecoder(strings.NewReader(string(content)))
192 decoder.KnownFields(true)
193 if err := decoder.Decode(into); err != nil {
194 return fmt.Errorf("parsing %s: %w", FileName, err)
195 }
196 var trailing any
197 if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
198 if err == nil {
199 return fmt.Errorf("parsing %s: multiple YAML documents are not allowed", FileName)
200 }
201 return fmt.Errorf("parsing %s after the policy: %w", FileName, err)
202 }
203 return nil
204 }
205
206 // Install writes the default policy only when none exists.
207 func Install(rootPath string) (Policy, bool, error) {
208 configured, err := Load(rootPath)
209 if err == nil {
210 return configured, false, nil
211 }
212 if !errors.Is(err, os.ErrNotExist) {
213 return Policy{}, false, err
214 }
215 configured = Default()
216 if err := Save(rootPath, configured); err != nil {
217 return Policy{}, false, err
218 }
219 return configured, true, nil
220 }
221
222 // Save writes a validated policy atomically beneath the repository root.
223 func Save(rootPath string, configured Policy) (returnedError error) {
224 if err := configured.Validate(); err != nil {
225 return err
226 }
227 root, err := os.OpenRoot(rootPath)
228 if err != nil {
229 return fmt.Errorf("opening repository root %s: %w", rootPath, err)
230 }
231 defer func() {
232 if closeErr := root.Close(); closeErr != nil {
233 returnedError = errors.Join(returnedError, closeErr)
234 }
235 }()
236 return writeTo(root, configured)
237 }
238
239 func writeTo(root *os.Root, configured Policy) error {
240 encoded, err := encode(configured)
241 if err != nil {
242 return err
243 }
244 if err := root.MkdirAll(path.Dir(FileName), 0o755); err != nil {
245 return fmt.Errorf("creating %s: %w", path.Dir(FileName), err)
246 }
247 return writeAtomically(root, FileName, encoded)
248 }
249
250 func encode(configured Policy) ([]byte, error) {
251 var encoded strings.Builder
252 encoded.WriteString("# yaml-language-server: $schema=" + SchemaURL + "\n")
253 encoder := yaml.NewEncoder(&encoded)
254 encoder.SetIndent(2)
255 if err := encoder.Encode(configured); err != nil {
256 return nil, fmt.Errorf("encoding %s: %w", FileName, err)
257 }
258 if err := encoder.Close(); err != nil {
259 return nil, fmt.Errorf("encoding %s: %w", FileName, err)
260 }
261 return []byte(encoded.String()), nil
262 }
263
264 // Validate rejects policy drift and unsupported bypasses.
265 func (p Policy) Validate() error {
266 if p.APIVersion != APIVersion {
267 return fmt.Errorf("apiVersion %q, want %q", p.APIVersion, APIVersion)
268 }
269 if p.Kind != KindPolicy {
270 return fmt.Errorf("kind %q, want %q", p.Kind, KindPolicy)
271 }
272 if p.Spec.Comments.Mode != ModeStrict {
273 return fmt.Errorf("spec.comments.mode %q, want %q", p.Spec.Comments.Mode, ModeStrict)
274 }
275 if err := validateIntrinsics(p.Spec.Comments.Intrinsic); err != nil {
276 return err
277 }
278 if err := validateGlobs("spec.comments.generatedPaths", p.Spec.Comments.GeneratedPaths); err != nil {
279 return err
280 }
281 if err := validateGlobs("spec.comments.vendoredPaths", p.Spec.Comments.VendoredPaths); err != nil {
282 return err
283 }
284 if err := validatePrinciples(p.Spec.Agents.Principles); err != nil {
285 return err
286 }
287 return validateAdapters(p.Spec.Agents.Adapters)
288 }
289
290 // States returns the wording of every principle this policy enables, in the
291 // order the vocabulary declares them so that a regenerated contract does not
292 // diff against itself.
293 func (p Policy) States() []string {
294 stated := make([]string, 0, len(p.Spec.Agents.Principles))
295 for _, principle := range Principles {
296 for _, enabled := range p.Spec.Agents.Principles {
297 if enabled == principle {
298 stated = append(stated, principleText[principle])
299 }
300 }
301 }
302 return stated
303 }
304
305 // Allows reports whether an intrinsic class is enabled.
306 func (p Policy) Allows(intrinsic Intrinsic) bool {
307 for _, allowed := range p.Spec.Comments.Intrinsic {
308 if allowed == intrinsic {
309 return true
310 }
311 }
312 return false
313 }
314
315 // Excludes reports whether a generated or vendored path is outside enforcement.
316 func (p Policy) Excludes(file string) bool {
317 for _, pattern := range append(append([]string{}, p.Spec.Comments.GeneratedPaths...), p.Spec.Comments.VendoredPaths...) {
318 if matches(pattern, file) {
319 return true
320 }
321 }
322 return false
323 }
324
325 func validateIntrinsics(values []Intrinsic) error {
326 allowed := map[Intrinsic]bool{
327 IntrinsicToolchain: true, IntrinsicGeneratedMarker: true, IntrinsicUpstreamLink: true,
328 IntrinsicDeprecated: true, IntrinsicPublicAPI: true,
329 }
330 seen := map[Intrinsic]bool{}
331 for _, value := range values {
332 if !allowed[value] {
333 return fmt.Errorf("spec.comments.intrinsic contains unsupported class %q", value)
334 }
335 if seen[value] {
336 return fmt.Errorf("spec.comments.intrinsic contains %q more than once", value)
337 }
338 seen[value] = true
339 }
340 return nil
341 }
342
343 func validatePrinciples(values []Principle) error {
344 seen := map[Principle]bool{}
345 for _, value := range values {
346 if _, known := principleText[value]; !known {
347 return fmt.Errorf("spec.agents.principles contains unsupported principle %q", value)
348 }
349 if seen[value] {
350 return fmt.Errorf("spec.agents.principles contains %q more than once", value)
351 }
352 seen[value] = true
353 }
354 return nil
355 }
356
357 func validateAdapters(values []Adapter) error {
358 allowed := map[Adapter]bool{
359 AdapterAgents: true, AdapterClaude: true, AdapterCopilot: true,
360 AdapterCursor: true, AdapterCodex: true, AdapterOpencode: true,
361 }
362 seen := map[Adapter]bool{}
363 for _, value := range values {
364 if !allowed[value] {
365 return fmt.Errorf("spec.agents.adapters contains unsupported adapter %q", value)
366 }
367 if seen[value] {
368 return fmt.Errorf("spec.agents.adapters contains %q more than once", value)
369 }
370 seen[value] = true
371 }
372 return nil
373 }
374
375 func validateGlobs(field string, patterns []string) error {
376 for _, pattern := range patterns {
377 switch {
378 case pattern == "":
379 return fmt.Errorf("%s contains an empty pattern", field)
380 case strings.Contains(pattern, `\`):
381 return fmt.Errorf("%s pattern %q must use forward slashes", field, pattern)
382 case strings.HasPrefix(pattern, "/"):
383 return fmt.Errorf("%s pattern %q must be repository-relative", field, pattern)
384 case strings.Contains("/"+pattern+"/", "/../"):
385 return fmt.Errorf("%s pattern %q escapes the repository", field, pattern)
386 }
387 if _, err := globExpression(pattern); err != nil {
388 return fmt.Errorf("%s pattern %q: %w", field, pattern, err)
389 }
390 }
391 return nil
392 }
393
394 func matches(pattern, file string) bool {
395 expression, err := globExpression(pattern)
396 return err == nil && expression.MatchString(file)
397 }
398
399 func globExpression(pattern string) (*regexp.Regexp, error) {
400 var expression strings.Builder
401 expression.WriteString("^")
402 for index := 0; index < len(pattern); index++ {
403 character := pattern[index]
404 switch character {
405 case '*':
406 if index+1 < len(pattern) && pattern[index+1] == '*' {
407 index++
408 if index+1 < len(pattern) && pattern[index+1] == '/' {
409 index++
410 expression.WriteString("(?:.*/)?")
411 } else {
412 expression.WriteString(".*")
413 }
414 } else {
415 expression.WriteString("[^/]*")
416 }
417 case '?':
418 expression.WriteString("[^/]")
419 default:
420 expression.WriteString(regexp.QuoteMeta(string(character)))
421 }
422 }
423 expression.WriteString("$")
424 return regexp.Compile(expression.String())
425 }
426
427 func writeAtomically(root *os.Root, name string, content []byte) error {
428 var entropy [8]byte
429 if _, err := rand.Read(entropy[:]); err != nil {
430 return fmt.Errorf("creating temporary name for %s: %w", name, err)
431 }
432 temporaryName := name + "." + hex.EncodeToString(entropy[:])
433 temporary, err := root.OpenFile(temporaryName, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o644)
434 if err != nil {
435 return fmt.Errorf("creating temporary file beside %s: %w", name, err)
436 }
437 defer func() { _ = root.Remove(temporaryName) }()
438 if _, err := temporary.Write(content); err != nil {
439 _ = temporary.Close()
440 return fmt.Errorf("writing %s: %w", temporaryName, err)
441 }
442 if err := temporary.Close(); err != nil {
443 return fmt.Errorf("closing %s: %w", temporaryName, err)
444 }
445 if err := root.Rename(temporaryName, name); err != nil {
446 return fmt.Errorf("replacing %s: %w", name, err)
447 }
448 return nil
449 }

Find an annotation

Search file paths, rationale, kinds, and authors.

moveEnter openEsc close