Repository navigation
Expand file tree
/
Copy pathmodel_policy.go
More file actions
90 lines (84 loc) · 3 KB
/
Copy pathmodel_policy.go
File metadata and controls
90 lines (84 loc) · 3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"strings"
"time"
"unicode/utf8"
"github.com/renezander030/draftcat/internal/config"
)
func modelPolicyPreview(text string, max int) string {
if max <= 0 || utf8.RuneCountInString(text) <= max {
return text
}
runes := []rune(text)
return string(runes[:max]) + "..."
}
func enforceModelPolicy(ctx context.Context, cfg *config.Config, role, phase, text string) error {
rule, err := cfg.ModelPolicy.Match(role, phase, text)
if err != nil {
return err
}
if rule == nil {
return nil
}
action := strings.ToLower(strings.TrimSpace(rule.Action))
decision := "policy_deny"
var operatorID int64
expires := time.Now()
if action == "review" {
if opChan == nil {
recordErr := recordModelPolicyDecision(ctx, cfg, role, phase, text, rule, decision, operatorID, expires)
if recordErr != nil {
return fmt.Errorf("model policy %q requires review but no operator channel is running (receipt: %w)", rule.ID, recordErr)
}
return fmt.Errorf("model policy %q requires review but no operator channel is running", rule.ID)
}
timeout := cfg.Timeouts.OperatorApprovalWindow()
expires = time.Now().Add(timeout)
msg := fmt.Sprintf("[draftcat] Model %s requires review\n\nrule: %s\nrole: %s\nreason: %s\n\n%s",
phase, rule.ID, role, rule.Reason, modelPolicyPreview(text, cfg.ModelPolicy.PreviewLimit()))
reviewCtx, cancel := context.WithTimeout(ctx, timeout)
dec, reviewErr := opChan.SendForApproval(reviewCtx, msg, nil)
cancel()
if reviewErr == nil && dec.Action == "approve" {
decision, operatorID = "approve", dec.ApproverID
if err := recordModelPolicyDecision(ctx, cfg, role, phase, text, rule, decision, operatorID, expires); err != nil {
return fmt.Errorf("model policy approval receipt unavailable: %w", err)
}
return nil
}
if reviewErr != nil {
decision = "timeout"
} else if dec.Action != "" {
decision = dec.Action
}
}
if err := recordModelPolicyDecision(ctx, cfg, role, phase, text, rule, decision, operatorID, expires); err != nil {
return fmt.Errorf("model policy decision receipt unavailable: %w", err)
}
return fmt.Errorf("model %s blocked by policy %q: %s", phase, rule.ID, rule.Reason)
}
func recordModelPolicyDecision(ctx context.Context, cfg *config.Config, role, phase, text string,
rule *config.ModelPolicyRule, decision string, operatorID int64, expires time.Time) error {
if state == nil {
return nil
}
sum := sha256.Sum256([]byte(text))
payloadHash := hex.EncodeToString(sum[:])
step := config.StepConfig{Name: "model-" + phase + ":" + rule.ID, Type: "approval", Role: role, Risk: config.RiskHigh}
policy := rule.ID + ": " + rule.Reason
envelope := newApprovalEnvelope([]byte(os.Getenv("DRAFTCAT_APPROVAL_SECRET")),
runIDFromContext(ctx), pipelineFromContext(ctx), step, time.Now(), expires,
decision, operatorID, payloadHash, 1, boolCount(decision == "approve"), policy)
return persistApprovalReceipt(envelope)
}
func boolCount(v bool) int {
if v {
return 1
}
return 0
}