Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/google/adk/auth/auth_preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,13 @@ async def run_async(
function_call.id in tools_to_resume
for function_call in function_calls
]):
# If this tool call was authored by another agent, skip it to let
# that agent's own auth processor handle it. Without this check, a
# shared session's events could cause one agent to resume and
# execute a different agent's auth-gated tool call using its own
# (potentially differently-scoped) canonical_tools.
if event.author != agent.name:
continue
if function_response_event := await handle_function_calls_async(
invocation_context,
event,
Expand Down
86 changes: 86 additions & 0 deletions tests/unittests/auth/test_auth_preprocessor.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ def mock_llm_agent(self):
from google.adk.agents.llm_agent import LlmAgent

agent = Mock(spec=LlmAgent)
agent.name = 'test_agent'
agent.canonical_tools = AsyncMock(return_value=[])
return agent

Expand Down Expand Up @@ -418,6 +419,7 @@ async def test_processes_multiple_auth_responses_and_resumes_tools(

original_event = Mock(spec=Event)
original_event.content = Mock() # Non-None content
original_event.author = 'test_agent'
original_event.get_function_calls.return_value = [
original_function_call_1,
original_function_call_2,
Expand Down Expand Up @@ -457,6 +459,90 @@ async def test_processes_multiple_auth_responses_and_resumes_tools(
# Verify the function response event was yielded
assert result == [mock_function_response_event]

@pytest.mark.asyncio
@patch('google.adk.auth.auth_preprocessor.AuthHandler')
@patch('google.adk.auth.auth_tool.AuthConfig.model_validate')
@patch('google.adk.auth.auth_preprocessor.handle_function_calls_async')
async def test_does_not_resume_tool_call_authored_by_another_agent(
self,
mock_handle_function_calls,
mock_auth_config_validate,
mock_auth_handler_class,
processor,
mock_invocation_context,
mock_llm_request,
mock_auth_config,
):
"""A forged/foreign auth-response referencing a function call owned by
a different agent must not be resumed by the current agent's processor.

Regression test for the missing author-boundary check that
request_confirmation.py already enforces for the equivalent
tool-confirmation resume path.
"""
auth_response_1 = Mock()
auth_response_1.name = REQUEST_EUC_FUNCTION_CALL_NAME
auth_response_1.id = 'auth_id_1'
auth_response_1.response = mock_auth_config

user_event_with_response = Mock(spec=Event)
user_event_with_response.author = 'user'
user_event_with_response.content = Mock()
user_event_with_response.get_function_responses.return_value = [
auth_response_1
]
user_event_with_response.get_function_calls.return_value = []

system_function_call_1 = Mock()
system_function_call_1.id = 'auth_id_1'
system_function_call_1.name = REQUEST_EUC_FUNCTION_CALL_NAME
system_function_call_1.args = {
'function_call_id': 'tool_id_1',
'auth_config': mock_auth_config,
}

system_event = Mock(spec=Event)
system_event.content = Mock()
system_event.get_function_calls.return_value = [system_function_call_1]

original_function_call_1 = Mock()
original_function_call_1.id = 'tool_id_1'

# This event belongs to a DIFFERENT agent than the one running the
# current processor - the fix must refuse to resume it.
original_event = Mock(spec=Event)
original_event.content = Mock()
original_event.author = 'a_different_agent'
original_event.get_function_calls.return_value = [
original_function_call_1
]

mock_invocation_context.session.events = [
original_event,
system_event,
user_event_with_response,
]

mock_auth_config_validate.return_value = mock_auth_config
mock_auth_handler = Mock(spec=AuthHandler)
mock_auth_handler.parse_and_store_auth_response = AsyncMock()
mock_auth_handler_class.return_value = mock_auth_handler

result = []
async for event in processor.run_async(
mock_invocation_context, mock_llm_request
):
result.append(event)

# The credential itself is still stored (that part is legitimate - the
# user really did respond to an auth prompt)...
assert mock_auth_handler.parse_and_store_auth_response.call_count == 1

# ...but the tool call authored by a different agent must NOT be
# resumed/executed by this agent's processor.
mock_handle_function_calls.assert_not_called()
assert result == []

@pytest.mark.asyncio
@patch('google.adk.auth.auth_preprocessor.AuthHandler')
@patch('google.adk.auth.auth_tool.AuthConfig.model_validate')
Expand Down