-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpolicy.go
More file actions
205 lines (183 loc) · 4.97 KB
/
Copy pathpolicy.go
File metadata and controls
205 lines (183 loc) · 4.97 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
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
package toolsy
import (
"context"
"errors"
"fmt"
)
// ErrPolicyDenied is the sentinel for fail-closed policy/capability denial.
var ErrPolicyDenied = errors.New("policy denied")
// ErrCapabilityDenied is the sentinel for view/capability denial.
var ErrCapabilityDenied = errors.New("capability denied")
// Decision is a machine-readable allow/deny result from policy/capability checks.
type Decision struct {
Allow bool
Code ErrorCode
Reason string
SafeMessage string
FixableArgs []string
Retryable bool
Err error
}
// AllowDecision explicitly permits a call.
func AllowDecision() Decision {
return Decision{
Allow: true,
Code: "",
Reason: "",
SafeMessage: "",
FixableArgs: nil,
Retryable: false,
Err: nil,
}
}
// DenyDecision rejects a call with a structured policy error.
func DenyDecision(reason string, fixableArgs ...string) Decision {
return Decision{
Allow: false,
Code: CodePolicyDenied,
Reason: reason,
SafeMessage: "",
FixableArgs: append([]string(nil), fixableArgs...),
Retryable: false,
Err: ErrPolicyDenied,
}
}
// PolicyRequest is the type-erased request evaluated before tool execution.
type PolicyRequest struct {
Manifest ToolManifest
Input ToolInput
CallContext CallContext
View RegistryViewSnapshot
}
// Policy performs universal fail-closed call authorization.
type Policy interface {
Decide(ctx context.Context, req PolicyRequest) Decision
}
// PolicyFunc adapts a function to Policy.
type PolicyFunc func(context.Context, PolicyRequest) Decision
// Decide implements Policy.
func (f PolicyFunc) Decide(ctx context.Context, req PolicyRequest) Decision {
if f == nil {
return DenyDecision("policy function is nil")
}
return f(ctx, req)
}
// AuthorizationRequest is the request passed to legacy-compatible authorizers.
type AuthorizationRequest = PolicyRequest
// NewPolicyDeniedError creates a structured ToolError for policy/capability denial.
func NewPolicyDeniedError(reason string, fixableArgs ...string) *ToolError {
if reason == "" {
reason = ErrPolicyDenied.Error()
}
return &ToolError{
Code: CodePolicyDenied,
Reason: reason,
Retryable: false,
FixableArgs: append([]string(nil), fixableArgs...),
SafeMessage: "",
Err: ErrPolicyDenied,
}
}
// NewPolicyDeniedErrorFrom preserves a lower-level authorization cause while exposing policy denial.
func NewPolicyDeniedErrorFrom(err error) *ToolError {
if err == nil {
return NewPolicyDeniedError("")
}
return &ToolError{
Code: CodePolicyDenied,
Reason: err.Error(),
Retryable: false,
FixableArgs: nil,
SafeMessage: "",
Err: fmt.Errorf("%w: %w", ErrPolicyDenied, err),
}
}
// NewCapabilityDeniedError reports a tool call outside the active capability view.
func NewCapabilityDeniedError(toolName string, view RegistryViewSnapshot) *ToolError {
reason := "tool is not available in the active capability view"
if toolName != "" {
reason = "tool " + toolName + " is not available in the active capability view"
}
if view.ID != "" {
reason += " " + view.ID
}
return &ToolError{
Code: CodeCapabilityDenied,
Reason: reason,
Retryable: false,
FixableArgs: []string{"tool_name"},
SafeMessage: "",
Err: ErrCapabilityDenied,
}
}
func decisionError(d Decision) error {
if d.Allow {
return nil
}
if d.Code == "" {
d.Code = CodePolicyDenied
}
if d.Reason == "" {
d.Reason = ErrPolicyDenied.Error()
}
if d.Err == nil {
d.Err = ErrPolicyDenied
}
return &ToolError{
Code: d.Code,
Reason: d.Reason,
Retryable: d.Retryable,
FixableArgs: append([]string(nil), d.FixableArgs...),
SafeMessage: d.SafeMessage,
Err: d.Err,
}
}
func evaluatePolicy(ctx context.Context, p Policy, req PolicyRequest) error {
if p == nil {
return nil
}
return decisionError(p.Decide(ctx, req))
}
type requirementsPolicyMarker interface {
enforcesRequirements() bool
}
func policyEnforcesRequirements(p Policy) bool {
if p == nil {
return false
}
marker, ok := p.(requirementsPolicyMarker)
return ok && marker.enforcesRequirements()
}
type compositePolicy struct {
first Policy
second Policy
}
func (p compositePolicy) Decide(ctx context.Context, req PolicyRequest) Decision {
if err := evaluatePolicy(ctx, p.first, req); err != nil {
if te, ok := AsToolError(err); ok {
return Decision{
Allow: false,
Code: te.Code,
Reason: te.Reason,
SafeMessage: te.SafeMessage,
FixableArgs: te.FixableArgs,
Retryable: te.Retryable,
Err: te.Err,
}
}
return DenyDecision(err.Error())
}
return p.second.Decide(ctx, req)
}
func (p compositePolicy) enforcesRequirements() bool {
return policyEnforcesRequirements(p.first) || policyEnforcesRequirements(p.second)
}
func composePolicies(first, second Policy) Policy {
if first == nil {
return second
}
if second == nil {
return first
}
return compositePolicy{first: first, second: second}
}