google-adk 1.2.1__py3-none-any.whl → 1.4.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- google/adk/a2a/__init__.py +13 -0
- google/adk/a2a/converters/__init__.py +13 -0
- google/adk/a2a/converters/part_converter.py +177 -0
- google/adk/agents/invocation_context.py +2 -0
- google/adk/agents/llm_agent.py +1 -6
- google/adk/agents/run_config.py +11 -0
- google/adk/auth/auth_credential.py +4 -0
- google/adk/auth/auth_handler.py +22 -96
- google/adk/auth/auth_preprocessor.py +3 -3
- google/adk/auth/auth_tool.py +46 -0
- google/adk/auth/credential_manager.py +261 -0
- google/adk/auth/credential_service/__init__.py +13 -0
- google/adk/auth/credential_service/base_credential_service.py +75 -0
- google/adk/auth/credential_service/in_memory_credential_service.py +64 -0
- google/adk/auth/exchanger/__init__.py +21 -0
- google/adk/auth/exchanger/base_credential_exchanger.py +57 -0
- google/adk/auth/exchanger/credential_exchanger_registry.py +58 -0
- google/adk/auth/exchanger/oauth2_credential_exchanger.py +104 -0
- google/adk/auth/oauth2_credential_util.py +107 -0
- google/adk/auth/refresher/__init__.py +21 -0
- google/adk/auth/refresher/base_credential_refresher.py +74 -0
- google/adk/auth/refresher/credential_refresher_registry.py +59 -0
- google/adk/auth/refresher/oauth2_credential_refresher.py +126 -0
- google/adk/cli/agent_graph.py +34 -32
- google/adk/cli/browser/index.html +2 -2
- google/adk/cli/browser/main-JAAWEV7F.js +92 -0
- google/adk/cli/browser/polyfills-B6TNHZQ6.js +17 -0
- google/adk/cli/cli.py +10 -0
- google/adk/cli/cli_deploy.py +80 -21
- google/adk/cli/cli_tools_click.py +132 -61
- google/adk/cli/fast_api.py +46 -41
- google/adk/cli/utils/agent_loader.py +15 -2
- google/adk/code_executors/container_code_executor.py +10 -6
- google/adk/code_executors/vertex_ai_code_executor.py +8 -2
- google/adk/evaluation/_eval_set_results_manager_utils.py +44 -0
- google/adk/evaluation/_eval_sets_manager_utils.py +108 -0
- google/adk/evaluation/eval_metrics.py +0 -5
- google/adk/evaluation/eval_result.py +12 -7
- google/adk/evaluation/eval_set_results_manager.py +6 -1
- google/adk/evaluation/gcs_eval_set_results_manager.py +121 -0
- google/adk/evaluation/gcs_eval_sets_manager.py +196 -0
- google/adk/evaluation/local_eval_set_results_manager.py +6 -18
- google/adk/evaluation/local_eval_sets_manager.py +27 -78
- google/adk/flows/llm_flows/basic.py +9 -0
- google/adk/flows/llm_flows/functions.py +1 -2
- google/adk/models/anthropic_llm.py +1 -1
- google/adk/models/gemini_llm_connection.py +2 -0
- google/adk/models/google_llm.py +57 -16
- google/adk/models/lite_llm.py +2 -1
- google/adk/platform/__init__.py +13 -0
- google/adk/platform/internal/__init__.py +15 -0
- google/adk/platform/internal/thread.py +30 -0
- google/adk/platform/thread.py +31 -0
- google/adk/runners.py +8 -2
- google/adk/sessions/in_memory_session_service.py +12 -1
- google/adk/sessions/vertex_ai_session_service.py +71 -50
- google/adk/tools/__init__.py +2 -0
- google/adk/tools/_automatic_function_calling_util.py +1 -0
- google/adk/tools/_forwarding_artifact_service.py +96 -0
- google/adk/tools/_function_parameter_parse_util.py +1 -0
- google/adk/tools/agent_tool.py +5 -39
- google/adk/tools/application_integration_tool/integration_connector_tool.py +2 -2
- google/adk/tools/authenticated_function_tool.py +107 -0
- google/adk/tools/base_authenticated_tool.py +107 -0
- google/adk/tools/bigquery/bigquery_credentials.py +6 -4
- google/adk/tools/bigquery/bigquery_tool.py +22 -9
- google/adk/tools/bigquery/bigquery_toolset.py +9 -3
- google/adk/tools/bigquery/client.py +7 -3
- google/adk/tools/bigquery/config.py +46 -0
- google/adk/tools/bigquery/metadata_tool.py +114 -91
- google/adk/tools/bigquery/query_tool.py +141 -23
- google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py +7 -4
- google/adk/tools/google_search_tool.py +0 -1
- google/adk/tools/mcp_tool/__init__.py +6 -0
- google/adk/tools/mcp_tool/mcp_session_manager.py +271 -149
- google/adk/tools/mcp_tool/mcp_tool.py +73 -22
- google/adk/tools/mcp_tool/mcp_toolset.py +32 -29
- google/adk/tools/openapi_tool/openapi_spec_parser/rest_api_tool.py +3 -3
- google/adk/tools/openapi_tool/openapi_spec_parser/tool_auth_handler.py +55 -33
- google/adk/tools/retrieval/files_retrieval.py +7 -1
- google/adk/tools/url_context_tool.py +61 -0
- google/adk/tools/vertex_ai_search_tool.py +13 -2
- google/adk/utils/feature_decorator.py +175 -0
- google/adk/version.py +1 -1
- {google_adk-1.2.1.dist-info → google_adk-1.4.1.dist-info}/METADATA +10 -2
- {google_adk-1.2.1.dist-info → google_adk-1.4.1.dist-info}/RECORD +89 -59
- google/adk/cli/browser/main-CS5OLUMF.js +0 -91
- google/adk/cli/browser/polyfills-FFHMD2TL.js +0 -17
- {google_adk-1.2.1.dist-info → google_adk-1.4.1.dist-info}/WHEEL +0 -0
- {google_adk-1.2.1.dist-info → google_adk-1.4.1.dist-info}/entry_points.txt +0 -0
- {google_adk-1.2.1.dist-info → google_adk-1.4.1.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,261 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
from __future__ import annotations
|
16
|
+
|
17
|
+
from typing import Optional
|
18
|
+
|
19
|
+
from ..tools.tool_context import ToolContext
|
20
|
+
from ..utils.feature_decorator import experimental
|
21
|
+
from .auth_credential import AuthCredential
|
22
|
+
from .auth_credential import AuthCredentialTypes
|
23
|
+
from .auth_schemes import AuthSchemeType
|
24
|
+
from .auth_tool import AuthConfig
|
25
|
+
from .exchanger.base_credential_exchanger import BaseCredentialExchanger
|
26
|
+
from .exchanger.credential_exchanger_registry import CredentialExchangerRegistry
|
27
|
+
from .refresher.base_credential_refresher import BaseCredentialRefresher
|
28
|
+
from .refresher.credential_refresher_registry import CredentialRefresherRegistry
|
29
|
+
|
30
|
+
|
31
|
+
@experimental
|
32
|
+
class CredentialManager:
|
33
|
+
"""Manages authentication credentials through a structured workflow.
|
34
|
+
|
35
|
+
The CredentialManager orchestrates the complete lifecycle of authentication
|
36
|
+
credentials, from initial loading to final preparation for use. It provides
|
37
|
+
a centralized interface for handling various credential types and authentication
|
38
|
+
schemes while maintaining proper credential hygiene (refresh, exchange, caching).
|
39
|
+
|
40
|
+
This class is only for use by Agent Development Kit.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
auth_config: Configuration containing authentication scheme and credentials
|
44
|
+
|
45
|
+
Example:
|
46
|
+
```python
|
47
|
+
auth_config = AuthConfig(
|
48
|
+
auth_scheme=oauth2_scheme,
|
49
|
+
raw_auth_credential=service_account_credential
|
50
|
+
)
|
51
|
+
manager = CredentialManager(auth_config)
|
52
|
+
|
53
|
+
# Register custom exchanger if needed
|
54
|
+
manager.register_credential_exchanger(
|
55
|
+
AuthCredentialTypes.CUSTOM_TYPE,
|
56
|
+
CustomCredentialExchanger()
|
57
|
+
)
|
58
|
+
|
59
|
+
# Register custom refresher if needed
|
60
|
+
manager.register_credential_refresher(
|
61
|
+
AuthCredentialTypes.CUSTOM_TYPE,
|
62
|
+
CustomCredentialRefresher()
|
63
|
+
)
|
64
|
+
|
65
|
+
# Load and prepare credential
|
66
|
+
credential = await manager.load_auth_credential(tool_context)
|
67
|
+
```
|
68
|
+
"""
|
69
|
+
|
70
|
+
def __init__(
|
71
|
+
self,
|
72
|
+
auth_config: AuthConfig,
|
73
|
+
):
|
74
|
+
self._auth_config = auth_config
|
75
|
+
self._exchanger_registry = CredentialExchangerRegistry()
|
76
|
+
self._refresher_registry = CredentialRefresherRegistry()
|
77
|
+
|
78
|
+
# Register default exchangers and refreshers
|
79
|
+
# TODO: support service account credential exchanger
|
80
|
+
from .refresher.oauth2_credential_refresher import OAuth2CredentialRefresher
|
81
|
+
|
82
|
+
oauth2_refresher = OAuth2CredentialRefresher()
|
83
|
+
self._refresher_registry.register(
|
84
|
+
AuthCredentialTypes.OAUTH2, oauth2_refresher
|
85
|
+
)
|
86
|
+
self._refresher_registry.register(
|
87
|
+
AuthCredentialTypes.OPEN_ID_CONNECT, oauth2_refresher
|
88
|
+
)
|
89
|
+
|
90
|
+
def register_credential_exchanger(
|
91
|
+
self,
|
92
|
+
credential_type: AuthCredentialTypes,
|
93
|
+
exchanger_instance: BaseCredentialExchanger,
|
94
|
+
) -> None:
|
95
|
+
"""Register a credential exchanger for a credential type.
|
96
|
+
|
97
|
+
Args:
|
98
|
+
credential_type: The credential type to register for.
|
99
|
+
exchanger_instance: The exchanger instance to register.
|
100
|
+
"""
|
101
|
+
self._exchanger_registry.register(credential_type, exchanger_instance)
|
102
|
+
|
103
|
+
async def request_credential(self, tool_context: ToolContext) -> None:
|
104
|
+
tool_context.request_credential(self._auth_config)
|
105
|
+
|
106
|
+
async def get_auth_credential(
|
107
|
+
self, tool_context: ToolContext
|
108
|
+
) -> Optional[AuthCredential]:
|
109
|
+
"""Load and prepare authentication credential through a structured workflow."""
|
110
|
+
|
111
|
+
# Step 1: Validate credential configuration
|
112
|
+
await self._validate_credential()
|
113
|
+
|
114
|
+
# Step 2: Check if credential is already ready (no processing needed)
|
115
|
+
if self._is_credential_ready():
|
116
|
+
return self._auth_config.raw_auth_credential
|
117
|
+
|
118
|
+
# Step 3: Try to load existing processed credential
|
119
|
+
credential = await self._load_existing_credential(tool_context)
|
120
|
+
|
121
|
+
# Step 4: If no existing credential, load from auth response
|
122
|
+
# TODO instead of load from auth response, we can store auth response in
|
123
|
+
# credential service.
|
124
|
+
was_from_auth_response = False
|
125
|
+
if not credential:
|
126
|
+
credential = await self._load_from_auth_response(tool_context)
|
127
|
+
was_from_auth_response = True
|
128
|
+
|
129
|
+
# Step 5: If still no credential available, return None
|
130
|
+
if not credential:
|
131
|
+
return None
|
132
|
+
|
133
|
+
# Step 6: Exchange credential if needed (e.g., service account to access token)
|
134
|
+
credential, was_exchanged = await self._exchange_credential(credential)
|
135
|
+
|
136
|
+
# Step 7: Refresh credential if expired
|
137
|
+
if not was_exchanged:
|
138
|
+
credential, was_refreshed = await self._refresh_credential(credential)
|
139
|
+
|
140
|
+
# Step 8: Save credential if it was modified
|
141
|
+
if was_from_auth_response or was_exchanged or was_refreshed:
|
142
|
+
await self._save_credential(tool_context, credential)
|
143
|
+
|
144
|
+
return credential
|
145
|
+
|
146
|
+
async def _load_existing_credential(
|
147
|
+
self, tool_context: ToolContext
|
148
|
+
) -> Optional[AuthCredential]:
|
149
|
+
"""Load existing credential from credential service or cached exchanged credential."""
|
150
|
+
|
151
|
+
# Try loading from credential service first
|
152
|
+
credential = await self._load_from_credential_service(tool_context)
|
153
|
+
if credential:
|
154
|
+
return credential
|
155
|
+
|
156
|
+
# Check if we have a cached exchanged credential
|
157
|
+
if self._auth_config.exchanged_auth_credential:
|
158
|
+
return self._auth_config.exchanged_auth_credential
|
159
|
+
|
160
|
+
return None
|
161
|
+
|
162
|
+
async def _load_from_credential_service(
|
163
|
+
self, tool_context: ToolContext
|
164
|
+
) -> Optional[AuthCredential]:
|
165
|
+
"""Load credential from credential service if available."""
|
166
|
+
credential_service = tool_context._invocation_context.credential_service
|
167
|
+
if credential_service:
|
168
|
+
# Note: This should be made async in a future refactor
|
169
|
+
# For now, assuming synchronous operation
|
170
|
+
return await credential_service.load_credential(
|
171
|
+
self._auth_config, tool_context
|
172
|
+
)
|
173
|
+
return None
|
174
|
+
|
175
|
+
async def _load_from_auth_response(
|
176
|
+
self, tool_context: ToolContext
|
177
|
+
) -> Optional[AuthCredential]:
|
178
|
+
"""Load credential from auth response in tool context."""
|
179
|
+
return tool_context.get_auth_response(self._auth_config)
|
180
|
+
|
181
|
+
async def _exchange_credential(
|
182
|
+
self, credential: AuthCredential
|
183
|
+
) -> tuple[AuthCredential, bool]:
|
184
|
+
"""Exchange credential if needed and return the credential and whether it was exchanged."""
|
185
|
+
exchanger = self._exchanger_registry.get_exchanger(credential.auth_type)
|
186
|
+
if not exchanger:
|
187
|
+
return credential, False
|
188
|
+
|
189
|
+
exchanged_credential = await exchanger.exchange(
|
190
|
+
credential, self._auth_config.auth_scheme
|
191
|
+
)
|
192
|
+
return exchanged_credential, True
|
193
|
+
|
194
|
+
async def _refresh_credential(
|
195
|
+
self, credential: AuthCredential
|
196
|
+
) -> tuple[AuthCredential, bool]:
|
197
|
+
"""Refresh credential if expired and return the credential and whether it was refreshed."""
|
198
|
+
refresher = self._refresher_registry.get_refresher(credential.auth_type)
|
199
|
+
if not refresher:
|
200
|
+
return credential, False
|
201
|
+
|
202
|
+
if await refresher.is_refresh_needed(
|
203
|
+
credential, self._auth_config.auth_scheme
|
204
|
+
):
|
205
|
+
refreshed_credential = await refresher.refresh(
|
206
|
+
credential, self._auth_config.auth_scheme
|
207
|
+
)
|
208
|
+
return refreshed_credential, True
|
209
|
+
|
210
|
+
return credential, False
|
211
|
+
|
212
|
+
def _is_credential_ready(self) -> bool:
|
213
|
+
"""Check if credential is ready to use without further processing."""
|
214
|
+
raw_credential = self._auth_config.raw_auth_credential
|
215
|
+
if not raw_credential:
|
216
|
+
return False
|
217
|
+
|
218
|
+
# Simple credentials that don't need exchange or refresh
|
219
|
+
return raw_credential.auth_type in (
|
220
|
+
AuthCredentialTypes.API_KEY,
|
221
|
+
AuthCredentialTypes.HTTP,
|
222
|
+
# Add other simple auth types as needed
|
223
|
+
)
|
224
|
+
|
225
|
+
async def _validate_credential(self) -> None:
|
226
|
+
"""Validate credential configuration and raise errors if invalid."""
|
227
|
+
if not self._auth_config.raw_auth_credential:
|
228
|
+
if self._auth_config.auth_scheme.type_ in (
|
229
|
+
AuthSchemeType.oauth2,
|
230
|
+
AuthSchemeType.openIdConnect,
|
231
|
+
):
|
232
|
+
raise ValueError(
|
233
|
+
"raw_auth_credential is required for auth_scheme type "
|
234
|
+
f"{self._auth_config.auth_scheme.type_}"
|
235
|
+
)
|
236
|
+
|
237
|
+
raw_credential = self._auth_config.raw_auth_credential
|
238
|
+
if raw_credential:
|
239
|
+
if (
|
240
|
+
raw_credential.auth_type
|
241
|
+
in (
|
242
|
+
AuthCredentialTypes.OAUTH2,
|
243
|
+
AuthCredentialTypes.OPEN_ID_CONNECT,
|
244
|
+
)
|
245
|
+
and not raw_credential.oauth2
|
246
|
+
):
|
247
|
+
raise ValueError(
|
248
|
+
"auth_config.raw_credential.oauth2 required for credential type "
|
249
|
+
f"{raw_credential.auth_type}"
|
250
|
+
)
|
251
|
+
# Additional validation can be added here
|
252
|
+
|
253
|
+
async def _save_credential(
|
254
|
+
self, tool_context: ToolContext, credential: AuthCredential
|
255
|
+
) -> None:
|
256
|
+
"""Save credential to credential service if available."""
|
257
|
+
credential_service = tool_context._invocation_context.credential_service
|
258
|
+
if credential_service:
|
259
|
+
# Update the exchanged credential in config
|
260
|
+
self._auth_config.exchanged_auth_credential = credential
|
261
|
+
await credential_service.save_credential(self._auth_config, tool_context)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
from __future__ import annotations
|
16
|
+
|
17
|
+
from abc import ABC
|
18
|
+
from abc import abstractmethod
|
19
|
+
from typing import Optional
|
20
|
+
|
21
|
+
from ...tools.tool_context import ToolContext
|
22
|
+
from ...utils.feature_decorator import experimental
|
23
|
+
from ..auth_credential import AuthCredential
|
24
|
+
from ..auth_tool import AuthConfig
|
25
|
+
|
26
|
+
|
27
|
+
@experimental
|
28
|
+
class BaseCredentialService(ABC):
|
29
|
+
"""Abstract class for Service that loads / saves tool credentials from / to
|
30
|
+
the backend credential store."""
|
31
|
+
|
32
|
+
@abstractmethod
|
33
|
+
async def load_credential(
|
34
|
+
self,
|
35
|
+
auth_config: AuthConfig,
|
36
|
+
tool_context: ToolContext,
|
37
|
+
) -> Optional[AuthCredential]:
|
38
|
+
"""
|
39
|
+
Loads the credential by auth config and current tool context from the
|
40
|
+
backend credential store.
|
41
|
+
|
42
|
+
Args:
|
43
|
+
auth_config: The auth config which contains the auth scheme and auth
|
44
|
+
credential information. auth_config.get_credential_key will be used to
|
45
|
+
build the key to load the credential.
|
46
|
+
|
47
|
+
tool_context: The context of the current invocation when the tool is
|
48
|
+
trying to load the credential.
|
49
|
+
|
50
|
+
Returns:
|
51
|
+
Optional[AuthCredential]: the credential saved in the store.
|
52
|
+
|
53
|
+
"""
|
54
|
+
|
55
|
+
@abstractmethod
|
56
|
+
async def save_credential(
|
57
|
+
self,
|
58
|
+
auth_config: AuthConfig,
|
59
|
+
tool_context: ToolContext,
|
60
|
+
) -> None:
|
61
|
+
"""
|
62
|
+
Saves the exchanged_auth_credential in auth config to the backend credential
|
63
|
+
store.
|
64
|
+
|
65
|
+
Args:
|
66
|
+
auth_config: The auth config which contains the auth scheme and auth
|
67
|
+
credential information. auth_config.get_credential_key will be used to
|
68
|
+
build the key to save the credential.
|
69
|
+
|
70
|
+
tool_context: The context of the current invocation when the tool is
|
71
|
+
trying to save the credential.
|
72
|
+
|
73
|
+
Returns:
|
74
|
+
None
|
75
|
+
"""
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
from __future__ import annotations
|
16
|
+
|
17
|
+
from typing import Optional
|
18
|
+
|
19
|
+
from typing_extensions import override
|
20
|
+
|
21
|
+
from ...tools.tool_context import ToolContext
|
22
|
+
from ...utils.feature_decorator import experimental
|
23
|
+
from ..auth_credential import AuthCredential
|
24
|
+
from ..auth_tool import AuthConfig
|
25
|
+
from .base_credential_service import BaseCredentialService
|
26
|
+
|
27
|
+
|
28
|
+
@experimental
|
29
|
+
class InMemoryCredentialService(BaseCredentialService):
|
30
|
+
"""Class for in memory implementation of credential service(Experimental)"""
|
31
|
+
|
32
|
+
def __init__(self):
|
33
|
+
super().__init__()
|
34
|
+
self._credentials = {}
|
35
|
+
|
36
|
+
@override
|
37
|
+
async def load_credential(
|
38
|
+
self,
|
39
|
+
auth_config: AuthConfig,
|
40
|
+
tool_context: ToolContext,
|
41
|
+
) -> Optional[AuthCredential]:
|
42
|
+
credential_bucket = self._get_bucket_for_current_context(tool_context)
|
43
|
+
return credential_bucket.get(auth_config.credential_key)
|
44
|
+
|
45
|
+
@override
|
46
|
+
async def save_credential(
|
47
|
+
self,
|
48
|
+
auth_config: AuthConfig,
|
49
|
+
tool_context: ToolContext,
|
50
|
+
) -> None:
|
51
|
+
credential_bucket = self._get_bucket_for_current_context(tool_context)
|
52
|
+
credential_bucket[auth_config.credential_key] = (
|
53
|
+
auth_config.exchanged_auth_credential
|
54
|
+
)
|
55
|
+
|
56
|
+
def _get_bucket_for_current_context(self, tool_context: ToolContext) -> str:
|
57
|
+
app_name = tool_context._invocation_context.app_name
|
58
|
+
user_id = tool_context._invocation_context.user_id
|
59
|
+
|
60
|
+
if app_name not in self._credentials:
|
61
|
+
self._credentials[app_name] = {}
|
62
|
+
if user_id not in self._credentials[app_name]:
|
63
|
+
self._credentials[app_name][user_id] = {}
|
64
|
+
return self._credentials[app_name][user_id]
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
"""Credential exchanger module."""
|
16
|
+
|
17
|
+
from .base_credential_exchanger import BaseCredentialExchanger
|
18
|
+
|
19
|
+
__all__ = [
|
20
|
+
"BaseCredentialExchanger",
|
21
|
+
]
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
"""Base credential exchanger interface."""
|
16
|
+
|
17
|
+
from __future__ import annotations
|
18
|
+
|
19
|
+
import abc
|
20
|
+
from typing import Optional
|
21
|
+
|
22
|
+
from ...utils.feature_decorator import experimental
|
23
|
+
from ..auth_credential import AuthCredential
|
24
|
+
from ..auth_schemes import AuthScheme
|
25
|
+
|
26
|
+
|
27
|
+
class CredentialExchangError(Exception):
|
28
|
+
"""Base exception for credential exchange errors."""
|
29
|
+
|
30
|
+
|
31
|
+
@experimental
|
32
|
+
class BaseCredentialExchanger(abc.ABC):
|
33
|
+
"""Base interface for credential exchangers.
|
34
|
+
|
35
|
+
Credential exchangers are responsible for exchanging credentials from
|
36
|
+
one format or scheme to another.
|
37
|
+
"""
|
38
|
+
|
39
|
+
@abc.abstractmethod
|
40
|
+
async def exchange(
|
41
|
+
self,
|
42
|
+
auth_credential: AuthCredential,
|
43
|
+
auth_scheme: Optional[AuthScheme] = None,
|
44
|
+
) -> AuthCredential:
|
45
|
+
"""Exchange credential if needed.
|
46
|
+
|
47
|
+
Args:
|
48
|
+
auth_credential: The credential to exchange.
|
49
|
+
auth_scheme: The authentication scheme (optional, some exchangers don't need it).
|
50
|
+
|
51
|
+
Returns:
|
52
|
+
The exchanged credential.
|
53
|
+
|
54
|
+
Raises:
|
55
|
+
CredentialExchangError: If credential exchange fails.
|
56
|
+
"""
|
57
|
+
pass
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
"""Credential exchanger registry."""
|
16
|
+
|
17
|
+
from __future__ import annotations
|
18
|
+
|
19
|
+
from typing import Dict
|
20
|
+
from typing import Optional
|
21
|
+
|
22
|
+
from ...utils.feature_decorator import experimental
|
23
|
+
from ..auth_credential import AuthCredentialTypes
|
24
|
+
from .base_credential_exchanger import BaseCredentialExchanger
|
25
|
+
|
26
|
+
|
27
|
+
@experimental
|
28
|
+
class CredentialExchangerRegistry:
|
29
|
+
"""Registry for credential exchanger instances."""
|
30
|
+
|
31
|
+
def __init__(self):
|
32
|
+
self._exchangers: Dict[AuthCredentialTypes, BaseCredentialExchanger] = {}
|
33
|
+
|
34
|
+
def register(
|
35
|
+
self,
|
36
|
+
credential_type: AuthCredentialTypes,
|
37
|
+
exchanger_instance: BaseCredentialExchanger,
|
38
|
+
) -> None:
|
39
|
+
"""Register an exchanger instance for a credential type.
|
40
|
+
|
41
|
+
Args:
|
42
|
+
credential_type: The credential type to register for.
|
43
|
+
exchanger_instance: The exchanger instance to register.
|
44
|
+
"""
|
45
|
+
self._exchangers[credential_type] = exchanger_instance
|
46
|
+
|
47
|
+
def get_exchanger(
|
48
|
+
self, credential_type: AuthCredentialTypes
|
49
|
+
) -> Optional[BaseCredentialExchanger]:
|
50
|
+
"""Get the exchanger instance for a credential type.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
credential_type: The credential type to get exchanger for.
|
54
|
+
|
55
|
+
Returns:
|
56
|
+
The exchanger instance if registered, None otherwise.
|
57
|
+
"""
|
58
|
+
return self._exchangers.get(credential_type)
|
@@ -0,0 +1,104 @@
|
|
1
|
+
# Copyright 2025 Google LLC
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
"""OAuth2 credential exchanger implementation."""
|
16
|
+
|
17
|
+
from __future__ import annotations
|
18
|
+
|
19
|
+
import logging
|
20
|
+
from typing import Optional
|
21
|
+
|
22
|
+
from google.adk.auth.auth_credential import AuthCredential
|
23
|
+
from google.adk.auth.auth_schemes import AuthScheme
|
24
|
+
from google.adk.auth.auth_schemes import OAuthGrantType
|
25
|
+
from google.adk.auth.oauth2_credential_util import create_oauth2_session
|
26
|
+
from google.adk.auth.oauth2_credential_util import update_credential_with_tokens
|
27
|
+
from google.adk.utils.feature_decorator import experimental
|
28
|
+
from typing_extensions import override
|
29
|
+
|
30
|
+
from .base_credential_exchanger import BaseCredentialExchanger
|
31
|
+
from .base_credential_exchanger import CredentialExchangError
|
32
|
+
|
33
|
+
try:
|
34
|
+
from authlib.integrations.requests_client import OAuth2Session
|
35
|
+
|
36
|
+
AUTHLIB_AVIALABLE = True
|
37
|
+
except ImportError:
|
38
|
+
AUTHLIB_AVIALABLE = False
|
39
|
+
|
40
|
+
logger = logging.getLogger("google_adk." + __name__)
|
41
|
+
|
42
|
+
|
43
|
+
@experimental
|
44
|
+
class OAuth2CredentialExchanger(BaseCredentialExchanger):
|
45
|
+
"""Exchanges OAuth2 credentials from authorization responses."""
|
46
|
+
|
47
|
+
@override
|
48
|
+
async def exchange(
|
49
|
+
self,
|
50
|
+
auth_credential: AuthCredential,
|
51
|
+
auth_scheme: Optional[AuthScheme] = None,
|
52
|
+
) -> AuthCredential:
|
53
|
+
"""Exchange OAuth2 credential from authorization response.
|
54
|
+
if credential exchange failed, the original credential will be returned.
|
55
|
+
|
56
|
+
Args:
|
57
|
+
auth_credential: The OAuth2 credential to exchange.
|
58
|
+
auth_scheme: The OAuth2 authentication scheme.
|
59
|
+
|
60
|
+
Returns:
|
61
|
+
The exchanged credential with access token.
|
62
|
+
|
63
|
+
Raises:
|
64
|
+
CredentialExchangError: If auth_scheme is missing.
|
65
|
+
"""
|
66
|
+
if not auth_scheme:
|
67
|
+
raise CredentialExchangError(
|
68
|
+
"auth_scheme is required for OAuth2 credential exchange"
|
69
|
+
)
|
70
|
+
|
71
|
+
if not AUTHLIB_AVIALABLE:
|
72
|
+
# If authlib is not available, we cannot exchange the credential.
|
73
|
+
# We return the original credential without exchange.
|
74
|
+
# The client using this tool can decide to exchange the credential
|
75
|
+
# themselves using other lib.
|
76
|
+
logger.warning(
|
77
|
+
"authlib is not available, skipping OAuth2 credential exchange."
|
78
|
+
)
|
79
|
+
return auth_credential
|
80
|
+
|
81
|
+
if auth_credential.oauth2 and auth_credential.oauth2.access_token:
|
82
|
+
return auth_credential
|
83
|
+
|
84
|
+
client, token_endpoint = create_oauth2_session(auth_scheme, auth_credential)
|
85
|
+
if not client:
|
86
|
+
logger.warning("Could not create OAuth2 session for token exchange")
|
87
|
+
return auth_credential
|
88
|
+
|
89
|
+
try:
|
90
|
+
tokens = client.fetch_token(
|
91
|
+
token_endpoint,
|
92
|
+
authorization_response=auth_credential.oauth2.auth_response_uri,
|
93
|
+
code=auth_credential.oauth2.auth_code,
|
94
|
+
grant_type=OAuthGrantType.AUTHORIZATION_CODE,
|
95
|
+
)
|
96
|
+
update_credential_with_tokens(auth_credential, tokens)
|
97
|
+
logger.debug("Successfully exchanged OAuth2 tokens")
|
98
|
+
except Exception as e:
|
99
|
+
# TODO reconsider whether we should raise errors in this case
|
100
|
+
logger.error("Failed to exchange OAuth2 tokens: %s", e)
|
101
|
+
# Return original credential on failure
|
102
|
+
return auth_credential
|
103
|
+
|
104
|
+
return auth_credential
|