12
12
# See the License for the specific language governing permissions and
13
13
# limitations under the License.
14
14
15
- """Approval handler for managing tool approvals."""
15
+ """Handles the logic for managing and evaluating approval requests against policies and grants.
16
+
17
+ This module provides the `ApprovalHandler` class, which is responsible for:
18
+ - Parsing and storing approval responses.
19
+ - Determining if a given function call requires approval based on registered policies and existing grants.
20
+ - Generating approval requests (challenges) when necessary.
21
+ - Checking if actions on resources are permitted by existing grants.
22
+ """
16
23
17
24
from __future__ import annotations
18
25
33
40
34
41
35
42
class ApprovalHandler (object ):
36
- """Handles approval requests and responses."""
43
+ """Manages the lifecycle of approval requests, from checking policies to storing grants.
44
+
45
+ This class provides static methods to interact with the approval system. It does not
46
+ maintain its own state but operates on the state provided to its methods (typically
47
+ session state).
48
+ """
37
49
38
50
@classmethod
39
51
def parse_and_store_approval_responses (
40
52
cls ,
41
53
initial_grants : list [ApprovalGrant ],
42
54
approval_responses : list [ApprovalResponse ],
43
55
) -> list [ApprovalGrant ]:
56
+ """Parses approval responses and extracts new grants.
57
+
58
+ Compares grants from approval responses with existing initial grants to identify
59
+ and return only the newly added grants.
60
+
61
+ Args:
62
+ initial_grants: A list of already existing approval grants.
63
+ approval_responses: A list of approval responses, each potentially containing grants.
64
+
65
+ Returns:
66
+ A list of new `ApprovalGrant` objects that were not present in `initial_grants`.
67
+ """
44
68
extra_grants = []
45
69
for approval_response in approval_responses :
46
70
added_grants = [
@@ -61,6 +85,33 @@ def get_approval_request(
61
85
user_id : str ,
62
86
session_id : str ,
63
87
) -> Optional [Dict [str , Any ]]:
88
+ """Determines if a function call requires approval and generates a request if so.
89
+
90
+ This method checks the given `function_call` against registered approval policies
91
+ and existing grants in the `state`. If approval is required (i.e., there are
92
+ pending challenges), it updates the `tool_context` to suspend the function call
93
+ and request approval.
94
+
95
+ If the function call is already approved or doesn't require approval, it updates
96
+ the `tool_context` to mark the function call as resumed.
97
+
98
+ If a deny grant explicitly forbids the call, an `ApprovalDenied` exception is caught,
99
+ and the function call is marked as cancelled.
100
+
101
+ Args:
102
+ function_call: The `FunctionCall` object to be checked.
103
+ state: The current session state, containing existing grants and suspended calls.
104
+ tool_context: The context for the current tool call, used to update its status
105
+ and request approval.
106
+ user_id: The ID of the user initiating the call.
107
+ session_id: The ID of the current session.
108
+
109
+ Returns:
110
+ A dictionary with a "status" key if approval is requested or denied:
111
+ - {"status": "approval_requested"} if challenges are pending.
112
+ - {"status": "denied", "denied_challenges": ...} if the call is denied.
113
+ Returns `None` if the function call can proceed without further approval.
114
+ """
64
115
try :
65
116
if approval_request := cls ._get_pending_challenges (
66
117
state = state ,
@@ -115,16 +166,30 @@ def _get_pending_challenges(
115
166
user_id : str ,
116
167
session_id : str ,
117
168
) -> Optional [ApprovalRequest ]:
118
- """Check if an approval exists for the given tool call.
169
+ """Checks a tool call against policies and grants to identify pending challenges.
170
+
171
+ This method evaluates the `tool_call` against all registered `ApprovalPolicy`
172
+ objects relevant to the tool. It then checks existing `ApprovalGrant` objects
173
+ in the `state` to see if the required actions on resources are permitted.
174
+
175
+ If any required action/resource pair is explicitly denied by a grant, this method
176
+ raises an `ApprovalDenied` exception.
177
+
178
+ If there are action/resource pairs required by policies that are not covered by
179
+ any existing allow grants, these are collected into an `ApprovalRequest`.
119
180
120
181
Args:
121
- state: The session state.
122
- tool_name: The name of the tool.
123
- tool_args: The arguments being passed to the tool.
124
- function_call_id: The ID of the function call.
182
+ state: The session state or a dictionary containing at least `approvals__grants`.
183
+ tool_call: The `FunctionCall` to be evaluated.
184
+ user_id: The ID of the user initiating the call.
185
+ session_id: The ID of the current session.
186
+
187
+ Returns:
188
+ An `ApprovalRequest` object if there are unmet challenges, otherwise `None`.
125
189
126
190
Raises:
127
- ApprovalDenied: if the an approval with effect Deny matches.
191
+ ApprovalDenied: If an existing grant explicitly denies one of the required
192
+ action/resource pairs.
128
193
"""
129
194
130
195
policies = ApprovalPolicyRegistry .get_tool_policies (tool_call .name )
@@ -195,24 +260,40 @@ def _get_pending_challenges(
195
260
196
261
@classmethod
197
262
def _get_existing_grants (cls , state : State ) -> list [ApprovalGrant ]:
263
+ """Retrieves and validates existing approval grants from the state.
264
+
265
+ Args:
266
+ state: The session state, expected to contain an `approvals__grants` key
267
+ with a list of grant dictionaries.
268
+
269
+ Returns:
270
+ A list of `ApprovalGrant` objects.
271
+ """
198
272
return [
199
273
ApprovalGrant .model_validate (grant )
200
274
for grant in state .get ("approvals__grants" , [])
201
275
]
202
276
203
277
@staticmethod
204
- def _resource_met (policy_resource , grant_resource ):
205
- """Check if a policy resource matches a grant resource.
278
+ def _resource_met (policy_resource : str , grant_resource : str ) -> bool :
279
+ """Checks if a policy resource string matches a grant resource string, supporting wildcards.
280
+
281
+ The `grant_resource` can contain wildcards (`*`) which match any sequence of characters
282
+ within a segment, or an entire segment if the segment itself is `*`.
283
+ Resource strings are colon-separated (e.g., "namespace:type:identifier").
206
284
207
- Grant resources can contain wildcards (*) which match any value
208
- in that position.
285
+ Examples:
286
+ - `_resource_met("tool:files:read", "tool:files:*")` -> `True`
287
+ - `_resource_met("tool:files:read", "tool:*:read")` -> `True`
288
+ - `_resource_met("tool:files:read", "*")` -> `True`
289
+ - `_resource_met("foo:bar", "foo:baz")` -> `False`
209
290
210
291
Args:
211
- policy_resource: The resource being accessed
212
- grant_resource: The resource in the grant, may contain wildcards
292
+ policy_resource: The specific resource being accessed (e.g., "tool:files:/data/my_file.txt").
293
+ grant_resource: The resource pattern from a grant (e.g., "tool:files:*", "*").
213
294
214
295
Returns:
215
- bool: True if the policy resource matches the grant resource pattern
296
+ ` True` if the `policy_resource` matches the `grant_resource` pattern, `False` otherwise.
216
297
"""
217
298
# Full wildcard matches anything
218
299
if grant_resource == "*" :
@@ -245,7 +326,20 @@ def _resource_met(policy_resource, grant_resource):
245
326
return True
246
327
247
328
@classmethod
248
- def _check_actor (cls , actor , grantee ) -> bool :
329
+ def _check_actor (cls , actor : ApprovalActor , grantee : ApprovalActor ) -> bool :
330
+ """Recursively checks if an `actor` matches a `grantee` definition, including `on_behalf_of`.
331
+
332
+ This method verifies that the `id` and `type` of the `actor` match the `grantee`.
333
+ If `grantee.on_behalf_of` is set, it recursively checks that `actor.on_behalf_of`
334
+ also matches.
335
+
336
+ Args:
337
+ actor: The `ApprovalActor` requesting access.
338
+ grantee: The `ApprovalActor` specified in a grant.
339
+
340
+ Returns:
341
+ `True` if the actor matches the grantee definition, `False` otherwise.
342
+ """
249
343
# Check if the grantee IDs match
250
344
if not cls ._check_actor_id (actor_id = actor .id , grantee_id = grantee .id ):
251
345
return False
@@ -264,7 +358,19 @@ def _check_actor(cls, actor, grantee) -> bool:
264
358
return True
265
359
266
360
@classmethod
267
- def _check_actor_id (cls , actor_id , grantee_id ) -> bool :
361
+ def _check_actor_id (cls , actor_id : str , grantee_id : str ) -> bool :
362
+ """Checks if an actor ID matches a grantee ID, supporting wildcards.
363
+
364
+ Similar to `_resource_met`, but for actor IDs. The `grantee_id` can contain
365
+ wildcards (`*`) to match parts of or the entire `actor_id`.
366
+
367
+ Args:
368
+ actor_id: The ID of the actor requesting access.
369
+ grantee_id: The ID pattern from a grant.
370
+
371
+ Returns:
372
+ `True` if the `actor_id` matches the `grantee_id` pattern, `False` otherwise.
373
+ """
268
374
if actor_id == grantee_id :
269
375
return True
270
376
@@ -306,16 +412,24 @@ def _action_granted_for_resource(
306
412
policy_resource : str ,
307
413
actor : ApprovalActor ,
308
414
) -> Optional [Literal [ApprovalEffect .allow , ApprovalEffect .deny ]]:
309
- """Check if the given action on the resource is granted by this grant.
415
+ """Checks if a specific action on a resource by an actor is permitted or denied by a single grant.
416
+
417
+ This method verifies:
418
+ 1. The `policy_action` is listed in `grant.actions`.
419
+ 2. The `actor` matches `grant.grantee` (using `_check_actor`).
420
+ 3. The `policy_resource` matches one of the `grant.resources` (using `_resource_met`).
421
+
422
+ If all conditions are met, it returns the `grant.effect` (allow or deny).
310
423
311
424
Args:
312
- grant: The approval grant to check against
313
- policy_action: The action being requested
314
- policy_resource: The resource being accessed
315
- actor: The actor requesting the action
425
+ grant: The `ApprovalGrant` to check against.
426
+ policy_action: The action being attempted (e.g., "tool:files:read").
427
+ policy_resource: The resource being accessed (e.g., "tool:files:/data/my_doc.txt").
428
+ actor: The `ApprovalActor` attempting the action.
316
429
317
430
Returns:
318
- The effect (allow/deny) if granted, None if not applicable
431
+ `ApprovalEffect.allow` or `ApprovalEffect.deny` if the grant applies and matches,
432
+ otherwise `None`.
319
433
"""
320
434
# Check if the action is in the grant's actions
321
435
if policy_action not in grant .actions :
@@ -340,16 +454,25 @@ def _check_action_on_resource_against_grants(
340
454
grants : list [ApprovalGrant ],
341
455
actor : ApprovalActor ,
342
456
) -> Optional [Literal [ApprovalEffect .allow , ApprovalEffect .deny ]]:
343
- """Check if the given action on the resource is granted by any of the grants.
457
+ """Evaluates an action on a resource against a list of grants to determine its effective status.
458
+
459
+ Deny grants are prioritized. If any deny grant matches the action, resource, and actor,
460
+ `ApprovalEffect.deny` is returned immediately.
461
+ If no deny grants match, allow grants are checked. If an allow grant matches,
462
+ `ApprovalEffect.allow` is returned.
463
+ If no grants match, `None` is returned, indicating the action is not explicitly
464
+ allowed or denied by the provided grants.
344
465
345
466
Args:
346
- action: The action being requested
347
- resource: The resource being accessed
348
- grants: The list of grants to check against
349
- actor: The actor requesting the action
467
+ action: The `ApprovalAction` being attempted.
468
+ resource: The resource string being accessed.
469
+ grants: A list of `ApprovalGrant` objects to check against.
470
+ actor: The `ApprovalActor` attempting the action.
350
471
351
472
Returns:
352
- The effect (allow/deny) if granted, None if not applicable
473
+ `ApprovalEffect.allow` if an allow grant matches and no deny grant matches.
474
+ `ApprovalEffect.deny` if a deny grant matches.
475
+ `None` if no grants explicitly cover the action/resource/actor combination.
353
476
"""
354
477
# Prioritize deny grants, then check allow grants
355
478
allow_grants = [
0 commit comments