From aacd3cab47ab5315155d0050810f01a70d83f083 Mon Sep 17 00:00:00 2001 From: Naveen Chatlapalli Date: Thu, 17 Sep 2026 22:38:16 -0500 Subject: [PATCH] fix(tools): keep auth credentials out of the caller's tool args RestApiTool.call() merged the tool's auth parameters (an API key or a bearer token) into the args dict it was given. The tool pipeline hands that same dict to after-tool callbacks and records it on the execute_tool span as gcp.vertex.agent.tool_call_args (content capture is on by default), so the credential ended up in traces and in whatever after-tool plugins record. This affected every OpenAPIToolset, APIHubToolset, GoogleApiToolset and ApplicationIntegrationToolset tool. IntegrationConnectorTool did the same with the end user's OAuth access token, and also logged it at INFO. Work on a copy of args in both places, as AuthenticatedFunctionTool already does, and log the connector's args before the token is added. Fixes #7162 Claude-Session: https://claude.ai/code/session_013vXxD1ga1hnCq2uFRwNks7 --- .../integration_connector_tool.py | 7 +++- .../openapi_spec_parser/rest_api_tool.py | 7 +++- .../test_integration_connector_tool.py | 37 +++++++++++++++++++ .../openapi_spec_parser/test_rest_api_tool.py | 36 ++++++++++++++++++ 4 files changed, 84 insertions(+), 3 deletions(-) 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" )