Skip to content

Commit 4afd578

Browse files
committed
tidy
1 parent 86d4a96 commit 4afd578

16 files changed

+684
-87
lines changed

src/google/adk/approval/__init__.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,5 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
# from .approval_handler import ApprovalHandler
16-
# from .approval_tool import ApprovalConfig
17-
# from .approval_processor import request_processor
15+
"""Provides foundational classes and handlers for managing approvals in the ADK."""
16+
Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,12 @@
1+
2+
"""Defines the core data structures for representing approval grants.
3+
4+
This module includes classes for:
5+
- `ApprovalActor`: Represents an entity (user, agent, tool) involved in an approval.
6+
- `ApprovalEffect`: Enumerates the possible outcomes of an approval (allow, deny, challenge).
7+
- `ApprovalGrant`: Encapsulates the details of a permission grant, including the
8+
effect, actions, resources, grantee, grantor, and optional expiration.
9+
"""
110
from __future__ import annotations
211
from datetime import datetime
312
from enum import Enum
@@ -8,35 +17,43 @@
817

918
class ApprovalActor(BaseModel):
1019
id: str
20+
"""A unique identifier for the actor (e.g., user ID, agent session ID, tool call ID)."""
1121
type: str = Literal["user", "agent", "tool"]
22+
"""The type of the actor."""
1223
on_behalf_of: ApprovalActor | None = None
24+
"""The actor on whose behalf this actor is operating, if any (e.g., an agent acting on behalf of a user)."""
1325

1426

1527
class ApprovalEffect(str, Enum):
1628
allow = "allow"
29+
"""Indicates that the requested action is permitted."""
1730
deny = "deny"
31+
"""Indicates that the requested action is explicitly forbidden."""
1832
challenge = "challenge"
33+
"""Indicates that further information or confirmation is required before allowing or denying."""
1934

2035

2136
ApprovalAction = str
37+
"""Type alias for an action string (e.g., 'tool:read_file', 'agent:use')."""
2238
ApprovalResource = str
39+
"""Type alias for a resource string (e.g., 'tool:files:/path/to/file', 'agent:agent_name')."""
2340

2441

2542
class ApprovalGrant(BaseModel):
2643
"""Effect the actions on the resources to the grantee by the grantor until the expiration."""
2744

2845
effect: Literal[ApprovalEffect.allow, ApprovalEffect.deny]
29-
"""Whether to grant an allow or deny."""
46+
"""The effect of this grant, either allowing or denying the specified actions on the resources."""
3047
actions: list[ApprovalAction]
31-
"""The actions to which the grant will effect."""
48+
"""A list of actions (e.g., 'tool:read_file') that this grant permits or denies."""
3249
resources: list[ApprovalResource]
33-
"""The resources that this grant affects."""
50+
"""A list of resources (e.g., 'tool:files:/path/to/data.txt') to which this grant applies."""
3451
grantee: ApprovalActor
35-
"""Who the grant applies to."""
52+
"""The actor (user, agent, or tool) to whom the permissions are granted."""
3653
grantor: ApprovalActor
37-
"""The permission holder that granted toe access (e.g. user, or delegated by a parent agent)."""
38-
expiration_time: Optional[datetime] = None # Optional expiration time
39-
"""The time after which the grant ceases to be valid."""
54+
"""The actor who authorized this grant (e.g., an end-user or a delegating agent)."""
55+
expiration_time: Optional[datetime] = None
56+
"""The optional time after which this grant is no longer valid. If None, the grant does not expire."""
4057

4158
comment: Optional[str] = None
42-
"""Comment from the grantor (typically the end user) from the point of granting. This is used when communicating a grant update to a model, for example a deny, to explain the reason."""
59+
"""An optional comment from the grantor, often used to explain the reason for a denial or to provide context for an approval."""

src/google/adk/approval/approval_handler.py

Lines changed: 152 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,14 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

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+
"""
1623

1724
from __future__ import annotations
1825

@@ -33,14 +40,31 @@
3340

3441

3542
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+
"""
3749

3850
@classmethod
3951
def parse_and_store_approval_responses(
4052
cls,
4153
initial_grants: list[ApprovalGrant],
4254
approval_responses: list[ApprovalResponse],
4355
) -> 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+
"""
4468
extra_grants = []
4569
for approval_response in approval_responses:
4670
added_grants = [
@@ -61,6 +85,33 @@ def get_approval_request(
6185
user_id: str,
6286
session_id: str,
6387
) -> 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+
"""
64115
try:
65116
if approval_request := cls._get_pending_challenges(
66117
state=state,
@@ -115,16 +166,30 @@ def _get_pending_challenges(
115166
user_id: str,
116167
session_id: str,
117168
) -> 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`.
119180
120181
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`.
125189
126190
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.
128193
"""
129194

130195
policies = ApprovalPolicyRegistry.get_tool_policies(tool_call.name)
@@ -195,24 +260,40 @@ def _get_pending_challenges(
195260

196261
@classmethod
197262
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+
"""
198272
return [
199273
ApprovalGrant.model_validate(grant)
200274
for grant in state.get("approvals__grants", [])
201275
]
202276

203277
@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").
206284
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`
209290
210291
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:*", "*").
213294
214295
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.
216297
"""
217298
# Full wildcard matches anything
218299
if grant_resource == "*":
@@ -245,7 +326,20 @@ def _resource_met(policy_resource, grant_resource):
245326
return True
246327

247328
@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+
"""
249343
# Check if the grantee IDs match
250344
if not cls._check_actor_id(actor_id=actor.id, grantee_id=grantee.id):
251345
return False
@@ -264,7 +358,19 @@ def _check_actor(cls, actor, grantee) -> bool:
264358
return True
265359

266360
@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+
"""
268374
if actor_id == grantee_id:
269375
return True
270376

@@ -306,16 +412,24 @@ def _action_granted_for_resource(
306412
policy_resource: str,
307413
actor: ApprovalActor,
308414
) -> 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).
310423
311424
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.
316429
317430
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`.
319433
"""
320434
# Check if the action is in the grant's actions
321435
if policy_action not in grant.actions:
@@ -340,16 +454,25 @@ def _check_action_on_resource_against_grants(
340454
grants: list[ApprovalGrant],
341455
actor: ApprovalActor,
342456
) -> 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.
344465
345466
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.
350471
351472
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.
353476
"""
354477
# Prioritize deny grants, then check allow grants
355478
allow_grants = [

0 commit comments

Comments
 (0)