Skip to content

Commit 4fdf096

Browse files
committed
Validate function call author before resuming auth-gated tool calls
_AuthLlmRequestProcessor.run_async resumed and executed a tool call found by ID in session history without checking that the call was authored by the current agent. request_confirmation.py's equivalent resume path already validates this (original_fc_event.author != agent.name), but auth_preprocessor.py did not. In a shared session, this could cause one agent to resume and execute a different agent's auth-gated tool call using its own canonical_tools, rather than the originating agent's.
1 parent 472e463 commit 4fdf096

2 files changed

Lines changed: 93 additions & 0 deletions

File tree

src/google/adk/auth/auth_preprocessor.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,13 @@ async def run_async(
189189
function_call.id in tools_to_resume
190190
for function_call in function_calls
191191
]):
192+
# If this tool call was authored by another agent, skip it to let
193+
# that agent's own auth processor handle it. Without this check, a
194+
# shared session's events could cause one agent to resume and
195+
# execute a different agent's auth-gated tool call using its own
196+
# (potentially differently-scoped) canonical_tools.
197+
if event.author != agent.name:
198+
continue
192199
if function_response_event := await handle_function_calls_async(
193200
invocation_context,
194201
event,

tests/unittests/auth/test_auth_preprocessor.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def mock_llm_agent(self):
4545
from google.adk.agents.llm_agent import LlmAgent
4646

4747
agent = Mock(spec=LlmAgent)
48+
agent.name = 'test_agent'
4849
agent.canonical_tools = AsyncMock(return_value=[])
4950
return agent
5051

@@ -418,6 +419,7 @@ async def test_processes_multiple_auth_responses_and_resumes_tools(
418419

419420
original_event = Mock(spec=Event)
420421
original_event.content = Mock() # Non-None content
422+
original_event.author = 'test_agent'
421423
original_event.get_function_calls.return_value = [
422424
original_function_call_1,
423425
original_function_call_2,
@@ -457,6 +459,90 @@ async def test_processes_multiple_auth_responses_and_resumes_tools(
457459
# Verify the function response event was yielded
458460
assert result == [mock_function_response_event]
459461

462+
@pytest.mark.asyncio
463+
@patch('google.adk.auth.auth_preprocessor.AuthHandler')
464+
@patch('google.adk.auth.auth_tool.AuthConfig.model_validate')
465+
@patch('google.adk.auth.auth_preprocessor.handle_function_calls_async')
466+
async def test_does_not_resume_tool_call_authored_by_another_agent(
467+
self,
468+
mock_handle_function_calls,
469+
mock_auth_config_validate,
470+
mock_auth_handler_class,
471+
processor,
472+
mock_invocation_context,
473+
mock_llm_request,
474+
mock_auth_config,
475+
):
476+
"""A forged/foreign auth-response referencing a function call owned by
477+
a different agent must not be resumed by the current agent's processor.
478+
479+
Regression test for the missing author-boundary check that
480+
request_confirmation.py already enforces for the equivalent
481+
tool-confirmation resume path.
482+
"""
483+
auth_response_1 = Mock()
484+
auth_response_1.name = REQUEST_EUC_FUNCTION_CALL_NAME
485+
auth_response_1.id = 'auth_id_1'
486+
auth_response_1.response = mock_auth_config
487+
488+
user_event_with_response = Mock(spec=Event)
489+
user_event_with_response.author = 'user'
490+
user_event_with_response.content = Mock()
491+
user_event_with_response.get_function_responses.return_value = [
492+
auth_response_1
493+
]
494+
user_event_with_response.get_function_calls.return_value = []
495+
496+
system_function_call_1 = Mock()
497+
system_function_call_1.id = 'auth_id_1'
498+
system_function_call_1.name = REQUEST_EUC_FUNCTION_CALL_NAME
499+
system_function_call_1.args = {
500+
'function_call_id': 'tool_id_1',
501+
'auth_config': mock_auth_config,
502+
}
503+
504+
system_event = Mock(spec=Event)
505+
system_event.content = Mock()
506+
system_event.get_function_calls.return_value = [system_function_call_1]
507+
508+
original_function_call_1 = Mock()
509+
original_function_call_1.id = 'tool_id_1'
510+
511+
# This event belongs to a DIFFERENT agent than the one running the
512+
# current processor - the fix must refuse to resume it.
513+
original_event = Mock(spec=Event)
514+
original_event.content = Mock()
515+
original_event.author = 'a_different_agent'
516+
original_event.get_function_calls.return_value = [
517+
original_function_call_1
518+
]
519+
520+
mock_invocation_context.session.events = [
521+
original_event,
522+
system_event,
523+
user_event_with_response,
524+
]
525+
526+
mock_auth_config_validate.return_value = mock_auth_config
527+
mock_auth_handler = Mock(spec=AuthHandler)
528+
mock_auth_handler.parse_and_store_auth_response = AsyncMock()
529+
mock_auth_handler_class.return_value = mock_auth_handler
530+
531+
result = []
532+
async for event in processor.run_async(
533+
mock_invocation_context, mock_llm_request
534+
):
535+
result.append(event)
536+
537+
# The credential itself is still stored (that part is legitimate - the
538+
# user really did respond to an auth prompt)...
539+
assert mock_auth_handler.parse_and_store_auth_response.call_count == 1
540+
541+
# ...but the tool call authored by a different agent must NOT be
542+
# resumed/executed by this agent's processor.
543+
mock_handle_function_calls.assert_not_called()
544+
assert result == []
545+
460546
@pytest.mark.asyncio
461547
@patch('google.adk.auth.auth_preprocessor.AuthHandler')
462548
@patch('google.adk.auth.auth_tool.AuthConfig.model_validate')

0 commit comments

Comments
 (0)