From 948ac69d0e2467ba9843648e307d64d5753b7e09 Mon Sep 17 00:00:00 2001 From: pandego <7780875+pandego@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:29:56 +0100 Subject: [PATCH 1/4] fix(application-integration): use exchanged auth credential for connector tools --- .../application_integration_toolset.py | 22 ++++--- .../test_application_integration_toolset.py | 62 +++++++++++++++++++ 2 files changed, 75 insertions(+), 9 deletions(-) diff --git a/src/google/adk/tools/application_integration_tool/application_integration_toolset.py b/src/google/adk/tools/application_integration_tool/application_integration_toolset.py index 5e068fba1d..960b5eb417 100644 --- a/src/google/adk/tools/application_integration_tool/application_integration_toolset.py +++ b/src/google/adk/tools/application_integration_tool/application_integration_toolset.py @@ -275,15 +275,19 @@ async def get_tools( self, readonly_context: Optional[ReadonlyContext] = None, ) -> List[RestApiTool]: - return ( - [ - tool - for tool in self._tools - if self._is_tool_selected(tool, readonly_context) - ] - if self._openapi_toolset is None - else await self._openapi_toolset.get_tools(readonly_context) - ) + if self._openapi_toolset is not None: + return await self._openapi_toolset.get_tools(readonly_context) + + if self._auth_config and self._auth_config.exchanged_auth_credential: + for tool in self._tools: + if isinstance(tool, IntegrationConnectorTool) and tool._auth_scheme: + tool._auth_credential = self._auth_config.exchanged_auth_credential + + return [ + tool + for tool in self._tools + if self._is_tool_selected(tool, readonly_context) + ] @override async def close(self) -> None: diff --git a/tests/unittests/tools/application_integration_tool/test_application_integration_toolset.py b/tests/unittests/tools/application_integration_tool/test_application_integration_toolset.py index 787bb8ce66..b3b74b9f72 100644 --- a/tests/unittests/tools/application_integration_tool/test_application_integration_toolset.py +++ b/tests/unittests/tools/application_integration_tool/test_application_integration_toolset.py @@ -671,3 +671,65 @@ async def test_init_with_connection_with_auth_override_disabled_and_custom_auth( assert (await toolset.get_tools())[0]._operation == "EXECUTE_ACTION" assert not (await toolset.get_tools())[0]._auth_scheme assert not (await toolset.get_tools())[0]._auth_credential + + +@pytest.mark.asyncio +async def test_get_tools_uses_exchanged_auth_credential_when_available( + project, + location, + mock_integration_client, + mock_connections_client, + mock_openapi_action_spec_parser, + connection_details_auth_override_enabled, +): + connection_name = "test-connection" + actions_list = ["create"] + mock_connections_client.return_value.get_connection_details.return_value = ( + connection_details_auth_override_enabled + ) + + oauth2_data_google_cloud = { + "type": "oauth2", + "flows": { + "authorizationCode": { + "authorizationUrl": "https://test-url/o/oauth2/auth", + "tokenUrl": "https://test-url/token", + "scopes": { + "https://test-url/auth/test-scope": "test scope", + }, + } + }, + } + + oauth2_scheme = dict_to_auth_scheme(oauth2_data_google_cloud) + raw_auth_credential = AuthCredential( + auth_type=AuthCredentialTypes.OAUTH2, + oauth2=OAuth2Auth( + client_id="test-client-id", + client_secret="test-client-secret", + ), + ) + + toolset = ApplicationIntegrationToolset( + project, + location, + connection=connection_name, + actions=actions_list, + auth_scheme=oauth2_scheme, + auth_credential=raw_auth_credential, + ) + + exchanged_auth_credential = AuthCredential( + auth_type=AuthCredentialTypes.OAUTH2, + oauth2=OAuth2Auth( + client_id="test-client-id", + client_secret="test-client-secret", + access_token="exchanged-access-token", + ), + ) + toolset._auth_config.exchanged_auth_credential = exchanged_auth_credential + + tools = await toolset.get_tools() + + assert len(tools) == 1 + assert tools[0]._auth_credential == exchanged_auth_credential From bf30978fca91b3a9e9d33ee9e0d2cae7a51aab98 Mon Sep 17 00:00:00 2001 From: pandego <7780875+pandego@users.noreply.github.com> Date: Sat, 28 Feb 2026 20:41:10 +0100 Subject: [PATCH 2/4] fix: avoid mutating cached tools when applying exchanged auth --- .../application_integration_toolset.py | 46 +++++++++++++++++-- .../test_application_integration_toolset.py | 3 ++ 2 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/google/adk/tools/application_integration_tool/application_integration_toolset.py b/src/google/adk/tools/application_integration_tool/application_integration_toolset.py index 960b5eb417..1b9a1c1515 100644 --- a/src/google/adk/tools/application_integration_tool/application_integration_toolset.py +++ b/src/google/adk/tools/application_integration_tool/application_integration_toolset.py @@ -270,6 +270,25 @@ def _parse_spec_to_toolset(self, spec_dict, connection_details): ) ) + def _clone_connector_tool_with_auth_credential( + self, + tool: IntegrationConnectorTool, + auth_credential: AuthCredential, + ) -> IntegrationConnectorTool: + return IntegrationConnectorTool( + name=tool.name, + description=tool.description, + connection_name=tool._connection_name, + connection_host=tool._connection_host, + connection_service_name=tool._connection_service_name, + entity=tool._entity, + action=tool._action, + operation=tool._operation, + rest_api_tool=tool._rest_api_tool, + auth_scheme=tool._auth_scheme, + auth_credential=auth_credential, + ) + @override async def get_tools( self, @@ -278,17 +297,34 @@ async def get_tools( if self._openapi_toolset is not None: return await self._openapi_toolset.get_tools(readonly_context) - if self._auth_config and self._auth_config.exchanged_auth_credential: - for tool in self._tools: - if isinstance(tool, IntegrationConnectorTool) and tool._auth_scheme: - tool._auth_credential = self._auth_config.exchanged_auth_credential + exchanged_auth_credential = ( + self._auth_config.exchanged_auth_credential + if self._auth_config + else None + ) - return [ + selected_tools = [ tool for tool in self._tools if self._is_tool_selected(tool, readonly_context) ] + if not exchanged_auth_credential: + return selected_tools + + resolved_tools: List[RestApiTool] = [] + for tool in selected_tools: + if isinstance(tool, IntegrationConnectorTool) and tool._auth_scheme: + resolved_tools.append( + self._clone_connector_tool_with_auth_credential( + tool, exchanged_auth_credential + ) + ) + else: + resolved_tools.append(tool) + + return resolved_tools + @override async def close(self) -> None: if self._openapi_toolset: diff --git a/tests/unittests/tools/application_integration_tool/test_application_integration_toolset.py b/tests/unittests/tools/application_integration_tool/test_application_integration_toolset.py index b3b74b9f72..5d51b29e25 100644 --- a/tests/unittests/tools/application_integration_tool/test_application_integration_toolset.py +++ b/tests/unittests/tools/application_integration_tool/test_application_integration_toolset.py @@ -729,7 +729,10 @@ async def test_get_tools_uses_exchanged_auth_credential_when_available( ) toolset._auth_config.exchanged_auth_credential = exchanged_auth_credential + original_tool = toolset._tools[0] tools = await toolset.get_tools() assert len(tools) == 1 + assert tools[0] is not original_tool assert tools[0]._auth_credential == exchanged_auth_credential + assert original_tool._auth_credential == raw_auth_credential From 7329225660196c4d606da99520ec48b1a0cce3ce Mon Sep 17 00:00:00 2001 From: pandego <7780875+pandego@users.noreply.github.com> Date: Wed, 4 Mar 2026 12:09:51 +0100 Subject: [PATCH 3/4] fix(types): return BaseTool list in application integration toolset --- .../application_integration_toolset.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/google/adk/tools/application_integration_tool/application_integration_toolset.py b/src/google/adk/tools/application_integration_tool/application_integration_toolset.py index 1b9a1c1515..a5edf18ea0 100644 --- a/src/google/adk/tools/application_integration_tool/application_integration_toolset.py +++ b/src/google/adk/tools/application_integration_tool/application_integration_toolset.py @@ -29,6 +29,7 @@ from ...auth.auth_credential import ServiceAccountCredential from ...auth.auth_schemes import AuthScheme from ...auth.auth_tool import AuthConfig +from ..base_tool import BaseTool from ..base_toolset import BaseToolset from ..base_toolset import ToolPredicate from ..openapi_tool.auth.auth_helpers import service_account_scheme_credential @@ -293,7 +294,7 @@ def _clone_connector_tool_with_auth_credential( async def get_tools( self, readonly_context: Optional[ReadonlyContext] = None, - ) -> List[RestApiTool]: + ) -> List[BaseTool]: if self._openapi_toolset is not None: return await self._openapi_toolset.get_tools(readonly_context) @@ -312,7 +313,7 @@ async def get_tools( if not exchanged_auth_credential: return selected_tools - resolved_tools: List[RestApiTool] = [] + resolved_tools: List[BaseTool] = [] for tool in selected_tools: if isinstance(tool, IntegrationConnectorTool) and tool._auth_scheme: resolved_tools.append( From 63514a6cc947b613ed5908ce2e65359ad8e6da12 Mon Sep 17 00:00:00 2001 From: pandego Date: Thu, 5 Mar 2026 10:03:48 +0100 Subject: [PATCH 4/4] fix(types): satisfy mypy in ApplicationIntegrationToolset --- .../application_integration_toolset.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/google/adk/tools/application_integration_tool/application_integration_toolset.py b/src/google/adk/tools/application_integration_tool/application_integration_toolset.py index a5edf18ea0..83e1c815f6 100644 --- a/src/google/adk/tools/application_integration_tool/application_integration_toolset.py +++ b/src/google/adk/tools/application_integration_tool/application_integration_toolset.py @@ -15,9 +15,11 @@ from __future__ import annotations import logging +from typing import Any from typing import List from typing import Optional from typing import Union +from typing import cast from fastapi.openapi.models import HTTPBearer from typing_extensions import override @@ -44,7 +46,7 @@ # TODO(cheliu): Apply a common toolset interface -class ApplicationIntegrationToolset(BaseToolset): +class ApplicationIntegrationToolset(BaseToolset): # type: ignore[misc] """ApplicationIntegrationToolset generates tools from a given Application Integration or Integration Connector resource. @@ -183,11 +185,15 @@ def __init__( "Invalid request, Either integration or (connection and" " (entity_operations or actions)) should be provided." ) - self._openapi_toolset = None - self._tools = [] + self._openapi_toolset: Optional[OpenAPIToolset] = None + self._tools: list[IntegrationConnectorTool] = [] self._parse_spec_to_toolset(spec, connection_details) - def _parse_spec_to_toolset(self, spec_dict, connection_details): + def _parse_spec_to_toolset( + self, + spec_dict: dict[str, Any], + connection_details: dict[str, Any], + ) -> None: """Parses the spec dict to OpenAPI toolset.""" if self._service_account_json: sa_credential = ServiceAccountCredential.model_validate_json( @@ -296,7 +302,10 @@ async def get_tools( readonly_context: Optional[ReadonlyContext] = None, ) -> List[BaseTool]: if self._openapi_toolset is not None: - return await self._openapi_toolset.get_tools(readonly_context) + return cast( + List[BaseTool], + await self._openapi_toolset.get_tools(readonly_context), + ) exchanged_auth_credential = ( self._auth_config.exchanged_auth_credential