diff --git a/src/google/adk/auth/auth_preprocessor.py b/src/google/adk/auth/auth_preprocessor.py index d452084c71..c548d6349a 100644 --- a/src/google/adk/auth/auth_preprocessor.py +++ b/src/google/adk/auth/auth_preprocessor.py @@ -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, diff --git a/tests/unittests/auth/test_auth_preprocessor.py b/tests/unittests/auth/test_auth_preprocessor.py index 21c9b30fb9..6b1b17ccd6 100644 --- a/tests/unittests/auth/test_auth_preprocessor.py +++ b/tests/unittests/auth/test_auth_preprocessor.py @@ -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 @@ -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, @@ -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')