diff --git a/src/google/adk/tools/application_integration_tool/integration_connector_tool.py b/src/google/adk/tools/application_integration_tool/integration_connector_tool.py index b4c60714215..758f5fefdc5 100644 --- a/src/google/adk/tools/application_integration_tool/integration_connector_tool.py +++ b/src/google/adk/tools/application_integration_tool/integration_connector_tool.py @@ -173,6 +173,12 @@ async def run_async( 'message': 'Needs your authorization to access your data.', } + # The access token added below must stay out of this log line and out of + # the caller's args, which are also handed to after-tool callbacks and + # recorded on the tool span. + logger.info('Running tool: %s with args: %s', self.name, args) + args = args.copy() + # Attach parameters from auth into main parameters list if auth_result.auth_credential: # Attach parameters from auth into main parameters list @@ -192,7 +198,6 @@ async def run_async( args['entity'] = self._entity args['operation'] = self._operation args['action'] = self._action - logger.info('Running tool: %s with args: %s', self.name, args) return await self._rest_api_tool.call(args=args, tool_context=tool_context) def __str__(self): diff --git a/src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py b/src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py index b9f91f8e212..843339434ed 100644 --- a/src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +++ b/src/google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py @@ -548,8 +548,11 @@ async def call( "message": "Needs your authorization to access your data.", } - # Attach parameters from auth into main parameters list - api_params, api_args = self._operation_parser.get_parameters().copy(), args + # Work on a copy of args: the caller's dict is also handed to after-tool + # callbacks and recorded on the tool span, so the defaults and auth + # parameters added below must not land in it. + api_params = self._operation_parser.get_parameters().copy() + api_args = args.copy() # Add any required arguments that are missing and have defaults: for api_param in api_params: diff --git a/tests/unittests/tools/application_integration_tool/test_integration_connector_tool.py b/tests/unittests/tools/application_integration_tool/test_integration_connector_tool.py index c2e0ea15592..2b0a27fe8df 100644 --- a/tests/unittests/tools/application_integration_tool/test_integration_connector_tool.py +++ b/tests/unittests/tools/application_integration_tool/test_integration_connector_tool.py @@ -282,6 +282,43 @@ async def test_run_with_auth_async( assert result == {"status": "success", "data": "mock_data"} +@pytest.mark.asyncio +async def test_run_with_auth_async_keeps_token_out_of_caller_args_and_logs( + integration_tool_with_auth, mock_rest_api_tool, caplog +): + """The caller's args also feed after-tool callbacks and the tool span.""" + input_args = {"user_id": "user123"} + + with mock.patch.object( + ToolAuthHandler, "from_tool_context", autospec=True + ) as mock_from_tool_context: + mock_from_tool_context.return_value.prepare_auth_credentials = ( + mock.AsyncMock( + return_value=AuthPreparationResult( + state="done", + auth_credential=AuthCredential( + auth_type=AuthCredentialTypes.HTTP, + http=HttpAuth( + scheme="bearer", + credentials=HttpCredentials(token="secret-token"), + ), + ), + ) + ) + ) + with caplog.at_level("INFO"): + await integration_tool_with_auth.run_async( + args=input_args, tool_context={} + ) + + assert input_args == {"user_id": "user123"} + sent_args = mock_rest_api_tool.call.call_args.kwargs["args"] + assert sent_args["dynamic_auth_config"] == { + "oauth2_auth_code_flow.access_token": "secret-token" + } + assert "secret-token" not in caplog.text + + class TestIntegrationConnectorToolWithJsonSchema: def test_get_declaration_with_json_schema_feature_enabled( diff --git a/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py b/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py index 5cd2d340e87..00b89adcfc9 100644 --- a/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py +++ b/tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py @@ -340,6 +340,42 @@ async def test_call_success( # Check the result assert result == {"result": "success"} + @patch( + "google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request" + ) + @pytest.mark.asyncio + async def test_call_does_not_add_auth_params_to_caller_args( + self, + mock_request, + mock_tool_context, + sample_endpoint, + sample_operation, + ): + """The caller's args also feed after-tool callbacks and the tool span.""" + mock_response = MagicMock() + mock_response.json.return_value = {"result": "success"} + mock_request.return_value = mock_response + auth_scheme, auth_credential = token_to_scheme_credential( + "apikey", "header", "X-API-Key", "secret-api-key" + ) + tool = RestApiTool( + name="test_tool", + description="Test Tool", + endpoint=sample_endpoint, + operation=sample_operation, + auth_scheme=auth_scheme, + auth_credential=auth_credential, + ) + args = {"testBodyParam": "value"} + + await tool.call(args=args, tool_context=mock_tool_context) + + assert args == {"testBodyParam": "value"} + assert ( + mock_request.call_args.kwargs["headers"]["X-API-Key"] + == "secret-api-key" + ) + @patch( "google.adk.tools.openapi_tool.openapi_spec_parser.rest_api_tool._request" )