nvidia-nat-a2a 1.5.0a20251229__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.

Potentially problematic release.


This version of nvidia-nat-a2a might be problematic. Click here for more details.

nat/meta/pypi.md ADDED
@@ -0,0 +1,36 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
+ SPDX-License-Identifier: Apache-2.0
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ -->
17
+
18
+ ![NVIDIA NeMo Agent Toolkit](https://media.githubusercontent.com/media/NVIDIA/NeMo-Agent-Toolkit/refs/heads/main/docs/source/_static/banner.png "NeMo Agent toolkit banner image")
19
+
20
+
21
+ # NVIDIA NeMo Agent Toolkit A2A Subpackage
22
+ Subpackage for A2A Protocol integration in NeMo Agent toolkit.
23
+
24
+ This package provides A2A (Agent-to-Agent) Protocol functionality, allowing NeMo Agent toolkit workflows to connect to remote A2A agents and invoke their skills as functions. This package includes both the client and server components of the A2A protocol.
25
+
26
+ ## Features
27
+ ### Client
28
+ - Connect to remote A2A agents via HTTP with JSON-RPC transport
29
+ - Discover agent capabilities through Agent Cards
30
+ - Submit tasks to remote agents with async execution
31
+
32
+ ### Server
33
+ - Serve A2A agents via HTTP with JSON-RPC transport
34
+ - Support for A2A agent executor pattern
35
+
36
+ For more information about the NVIDIA NeMo Agent Toolkit, please visit the [NeMo Agent Toolkit GitHub Repo](https://github.com/NVIDIA/NeMo-Agent-Toolkit).
@@ -0,0 +1,14 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
@@ -0,0 +1,15 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """Authentication support for A2A clients."""
@@ -0,0 +1,418 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """Bridge NAT AuthProviderBase to A2A SDK CredentialService."""
16
+
17
+ import asyncio
18
+ import logging
19
+
20
+ from a2a.client import ClientCallContext
21
+ from a2a.client import CredentialService
22
+ from a2a.types import AgentCard
23
+ from a2a.types import APIKeySecurityScheme
24
+ from a2a.types import HTTPAuthSecurityScheme
25
+ from a2a.types import OAuth2SecurityScheme
26
+ from a2a.types import OpenIdConnectSecurityScheme
27
+ from a2a.types import SecurityScheme
28
+ from nat.authentication.interfaces import AuthProviderBase
29
+ from nat.builder.context import Context
30
+ from nat.data_models.authentication import AuthResult
31
+ from nat.data_models.authentication import BasicAuthCred
32
+ from nat.data_models.authentication import BearerTokenCred
33
+ from nat.data_models.authentication import CookieCred
34
+ from nat.data_models.authentication import HeaderCred
35
+ from nat.data_models.authentication import QueryCred
36
+
37
+ logger = logging.getLogger(__name__)
38
+
39
+
40
+ class A2ACredentialService(CredentialService):
41
+ """
42
+ Adapts NAT AuthProviderBase to A2A SDK CredentialService interface.
43
+
44
+ This class bridges NAT's authentication system with the A2A SDK's authentication
45
+ mechanism, allowing A2A clients to use NAT's auth providers (API Key, OAuth2, etc.)
46
+ to authenticate with A2A agents.
47
+
48
+ The adapter:
49
+ - Calls NAT auth provider to obtain credentials
50
+ - Maps NAT credential types to A2A security scheme requirements
51
+ - Handles token expiration and automatic refresh
52
+ - Supports session-based multi-user authentication
53
+
54
+ Args:
55
+ auth_provider: NAT authentication provider instance
56
+ agent_card: Agent card containing security scheme definitions
57
+ """
58
+
59
+ def __init__(
60
+ self,
61
+ auth_provider: AuthProviderBase,
62
+ agent_card: AgentCard | None = None,
63
+ ):
64
+ self._auth_provider = auth_provider
65
+ self._agent_card = agent_card
66
+ self._cached_auth_result: AuthResult | None = None
67
+ self._auth_lock = asyncio.Lock()
68
+
69
+ # Validate provider compatibility with agent's security requirements
70
+ self._validate_provider_compatibility()
71
+
72
+ async def get_credentials(
73
+ self,
74
+ security_scheme_name: str,
75
+ context: ClientCallContext | None,
76
+ ) -> str | None:
77
+ """
78
+ Retrieve credentials for a security scheme.
79
+
80
+ This method:
81
+ 1. Gets user_id from NAT context
82
+ 2. Authenticates via NAT auth provider
83
+ 3. Handles token expiration and refresh
84
+ 4. Maps credentials to the requested security scheme
85
+
86
+ Args:
87
+ security_scheme_name: Name of the security scheme from AgentCard
88
+ context: Client call context with optional session information
89
+
90
+ Returns:
91
+ Credential string or None if not available
92
+ """
93
+ # Get user_id from NAT context
94
+ user_id = Context.get().user_id
95
+
96
+ # Authenticate and get credentials from NAT provider
97
+ auth_result = await self._authenticate(user_id)
98
+
99
+ if not auth_result:
100
+ logger.warning("Authentication failed, no credentials available")
101
+ return None
102
+
103
+ # Map NAT credentials to A2A format based on security scheme
104
+ credential = self._extract_credential_for_scheme(auth_result, security_scheme_name)
105
+
106
+ if credential:
107
+ logger.debug(
108
+ "Successfully retrieved credentials for scheme '%s'",
109
+ security_scheme_name,
110
+ )
111
+ else:
112
+ logger.warning(
113
+ "No compatible credentials found for scheme '%s'",
114
+ security_scheme_name,
115
+ )
116
+
117
+ return credential
118
+
119
+ async def _authenticate(self, user_id: str | None) -> AuthResult | None:
120
+ """
121
+ Authenticate and get credentials from NAT auth provider.
122
+
123
+ Handles token expiration by triggering re-authentication if needed.
124
+ Uses a lock to prevent concurrent authentication requests and race conditions.
125
+
126
+ Args:
127
+ user_id: User identifier for authentication
128
+
129
+ Returns:
130
+ AuthResult with credentials or None on failure
131
+ """
132
+ try:
133
+ # Fast path: check cache without lock
134
+ auth_result = self._cached_auth_result
135
+ if auth_result and not auth_result.is_expired():
136
+ return auth_result
137
+
138
+ # Acquire lock to serialize authentication attempts
139
+ async with self._auth_lock:
140
+ # Double-check: another coroutine may have refreshed while we waited for lock
141
+ auth_result = self._cached_auth_result
142
+ if auth_result and not auth_result.is_expired():
143
+ logger.debug("Credentials were refreshed by another coroutine while waiting for lock")
144
+ return auth_result
145
+
146
+ # Log if we're refreshing expired credentials
147
+ if auth_result and auth_result.is_expired():
148
+ logger.info("Cached credentials expired, re-authenticating")
149
+
150
+ # Call NAT auth provider (provider is responsible for token refresh/validity)
151
+ auth_result = await self._auth_provider.authenticate(user_id=user_id)
152
+
153
+ # Cache the result while holding the lock
154
+ self._cached_auth_result = auth_result
155
+
156
+ # Warn if provider returned expired credentials (provider bug)
157
+ if auth_result and auth_result.is_expired():
158
+ logger.warning("Auth provider returned already-expired credentials. "
159
+ "This may indicate a bug in the auth provider's token refresh logic.")
160
+
161
+ return auth_result
162
+
163
+ except Exception as e:
164
+ logger.error("Authentication failed: %s", e, exc_info=True)
165
+ return None
166
+
167
+ def _extract_credential_for_scheme(self, auth_result: AuthResult, security_scheme_name: str) -> str | None:
168
+ """
169
+ Extract appropriate credential based on security scheme type.
170
+
171
+ Maps NAT credential types to A2A security scheme requirements:
172
+ - BearerTokenCred -> OAuth2, OIDC, HTTP Bearer
173
+ - HeaderCred -> API Key in header
174
+ - QueryCred -> API Key in query
175
+ - CookieCred -> API Key in cookie
176
+ - BasicAuthCred -> HTTP Basic
177
+
178
+ Args:
179
+ auth_result: Authentication result containing credentials
180
+ security_scheme_name: Name of the security scheme
181
+
182
+ Returns:
183
+ Credential string or None
184
+ """
185
+ # Get scheme definition from agent card
186
+ scheme_def = self._get_scheme_definition(security_scheme_name)
187
+
188
+ # Try to match NAT credentials to security scheme
189
+ for cred in auth_result.credentials:
190
+ # Check compatibility and extract credential value
191
+ credential_value = None
192
+
193
+ if isinstance(cred, BearerTokenCred) and self._is_bearer_compatible(scheme_def):
194
+ credential_value = cred.token.get_secret_value()
195
+ elif isinstance(cred, HeaderCred) and self._is_header_compatible(scheme_def, cred.name):
196
+ credential_value = cred.value.get_secret_value()
197
+ elif isinstance(cred, QueryCred) and self._is_query_compatible(scheme_def, cred.name):
198
+ credential_value = cred.value.get_secret_value()
199
+ elif isinstance(cred, CookieCred) and self._is_cookie_compatible(scheme_def, cred.name):
200
+ credential_value = cred.value.get_secret_value()
201
+ elif isinstance(cred, BasicAuthCred) and self._is_basic_compatible(scheme_def):
202
+ # For HTTP Basic, encode username:password as base64
203
+ import base64
204
+
205
+ username = cred.username.get_secret_value()
206
+ password = cred.password.get_secret_value()
207
+ credentials = f"{username}:{password}"
208
+ credential_value = base64.b64encode(credentials.encode()).decode()
209
+
210
+ if credential_value:
211
+ return credential_value
212
+
213
+ return None
214
+
215
+ def _get_scheme_definition(self, scheme_name: str) -> SecurityScheme | None:
216
+ """
217
+ Get security scheme definition from agent card.
218
+
219
+ Args:
220
+ scheme_name: Name of the security scheme
221
+
222
+ Returns:
223
+ SecurityScheme definition or None
224
+ """
225
+ if not self._agent_card or not self._agent_card.security_schemes:
226
+ return None
227
+ return self._agent_card.security_schemes.get(scheme_name)
228
+
229
+ def _validate_provider_compatibility(self) -> None:
230
+ """
231
+ Validate that the auth provider type is compatible with agent's security schemes.
232
+
233
+ This performs early validation at connection time to fail fast if there's a
234
+ configuration mismatch between the NAT auth provider and the A2A agent's
235
+ security requirements.
236
+
237
+ Raises:
238
+ ValueError: If the provider is incompatible with all required security schemes
239
+ """
240
+ if not self._agent_card or not self._agent_card.security_schemes:
241
+ # No security schemes defined, nothing to validate
242
+ logger.debug("No security schemes defined in agent card, skipping validation")
243
+ return
244
+
245
+ provider_type = type(self._auth_provider).__name__
246
+ schemes = self._agent_card.security_schemes
247
+
248
+ logger.info("Validating auth provider '%s' against agent security schemes: %s",
249
+ provider_type,
250
+ list(schemes.keys()))
251
+
252
+ # Check if provider type is compatible with at least one security scheme
253
+ compatible_schemes = []
254
+ incompatible_schemes = []
255
+
256
+ for scheme_name, scheme in schemes.items():
257
+ is_compatible = self._is_provider_compatible_with_scheme(scheme)
258
+ if is_compatible:
259
+ compatible_schemes.append(scheme_name)
260
+ else:
261
+ incompatible_schemes.append((scheme_name, type(scheme.root).__name__))
262
+
263
+ if not compatible_schemes:
264
+ # Provider is not compatible with any security scheme
265
+ scheme_details = ", ".join(f"{name} ({scheme_type})" for name, scheme_type in incompatible_schemes)
266
+ raise ValueError(f"Auth provider '{provider_type}' is not compatible with agent's "
267
+ f"security requirements. Agent requires: {scheme_details}")
268
+
269
+ logger.info("Auth provider '%s' is compatible with schemes: %s", provider_type, compatible_schemes)
270
+
271
+ def _is_provider_compatible_with_scheme(self, scheme: SecurityScheme) -> bool:
272
+ """
273
+ Check if the current auth provider can satisfy a security scheme.
274
+
275
+ Args:
276
+ scheme: Security scheme from agent card
277
+
278
+ Returns:
279
+ True if provider is compatible with the scheme
280
+ """
281
+ provider_type = type(self._auth_provider).__name__
282
+
283
+ # OAuth2/OIDC schemes require OAuth2 providers
284
+ if isinstance(scheme.root, OAuth2SecurityScheme | OpenIdConnectSecurityScheme):
285
+ return "OAuth2" in provider_type
286
+
287
+ # API Key schemes (can be in header, query, or cookie)
288
+ if isinstance(scheme.root, APIKeySecurityScheme):
289
+ return "APIKey" in provider_type
290
+
291
+ # HTTP Auth schemes (Basic or Bearer)
292
+ if isinstance(scheme.root, HTTPAuthSecurityScheme):
293
+ scheme_lower = scheme.root.scheme.lower()
294
+ if scheme_lower == "basic":
295
+ return "HTTPBasic" in provider_type or "BasicAuth" in provider_type
296
+ elif scheme_lower == "bearer":
297
+ # Bearer can be satisfied by OAuth2 or API Key providers
298
+ return "OAuth2" in provider_type or "APIKey" in provider_type
299
+
300
+ # Unknown or unsupported scheme type
301
+ logger.warning("Unknown security scheme type: %s", type(scheme.root).__name__)
302
+ return False
303
+
304
+ @staticmethod
305
+ def _is_bearer_compatible(scheme_def: SecurityScheme | None) -> bool:
306
+ """
307
+ Check if security scheme accepts Bearer tokens.
308
+
309
+ Bearer tokens are compatible with:
310
+ - OAuth2SecurityScheme
311
+ - OpenIdConnectSecurityScheme
312
+ - HTTPAuthSecurityScheme with scheme='bearer'
313
+
314
+ Args:
315
+ scheme_def: Security scheme definition
316
+
317
+ Returns:
318
+ True if Bearer token is compatible
319
+ """
320
+ if not scheme_def:
321
+ return False
322
+
323
+ # Check for OAuth2 or OIDC schemes
324
+ if isinstance(scheme_def.root, OAuth2SecurityScheme | OpenIdConnectSecurityScheme):
325
+ return True
326
+
327
+ # Check for HTTP Bearer scheme
328
+ if isinstance(scheme_def.root, HTTPAuthSecurityScheme):
329
+ return scheme_def.root.scheme.lower() == "bearer"
330
+
331
+ return False
332
+
333
+ @staticmethod
334
+ def _is_header_compatible(scheme_def: SecurityScheme | None, header_name: str) -> bool:
335
+ """
336
+ Check if security scheme accepts header-based API keys.
337
+
338
+ Args:
339
+ scheme_def: Security scheme definition
340
+ header_name: Name of the header containing the credential
341
+
342
+ Returns:
343
+ True if header credential is compatible
344
+ """
345
+ if not scheme_def:
346
+ return False
347
+
348
+ # Check for API Key in header
349
+ if isinstance(scheme_def.root, APIKeySecurityScheme):
350
+ if scheme_def.root.in_ == "header":
351
+ # Match header name (case-insensitive)
352
+ return scheme_def.root.name.lower() == header_name.lower()
353
+
354
+ return False
355
+
356
+ @staticmethod
357
+ def _is_query_compatible(scheme_def: SecurityScheme | None, param_name: str) -> bool:
358
+ """
359
+ Check if security scheme accepts query parameter API keys.
360
+
361
+ Args:
362
+ scheme_def: Security scheme definition
363
+ param_name: Name of the query parameter
364
+
365
+ Returns:
366
+ True if query credential is compatible
367
+ """
368
+ if not scheme_def:
369
+ return False
370
+
371
+ # Check for API Key in query
372
+ if isinstance(scheme_def.root, APIKeySecurityScheme):
373
+ if scheme_def.root.in_ == "query":
374
+ return scheme_def.root.name == param_name
375
+
376
+ return False
377
+
378
+ @staticmethod
379
+ def _is_cookie_compatible(scheme_def: SecurityScheme | None, cookie_name: str) -> bool:
380
+ """
381
+ Check if security scheme accepts cookie-based API keys.
382
+
383
+ Args:
384
+ scheme_def: Security scheme definition
385
+ cookie_name: Name of the cookie
386
+
387
+ Returns:
388
+ True if cookie credential is compatible
389
+ """
390
+ if not scheme_def:
391
+ return False
392
+
393
+ # Check for API Key in cookie
394
+ if isinstance(scheme_def.root, APIKeySecurityScheme):
395
+ if scheme_def.root.in_ == "cookie":
396
+ return scheme_def.root.name == cookie_name
397
+
398
+ return False
399
+
400
+ @staticmethod
401
+ def _is_basic_compatible(scheme_def: SecurityScheme | None) -> bool:
402
+ """
403
+ Check if security scheme accepts HTTP Basic authentication.
404
+
405
+ Args:
406
+ scheme_def: Security scheme definition
407
+
408
+ Returns:
409
+ True if Basic auth is compatible
410
+ """
411
+ if not scheme_def:
412
+ return False
413
+
414
+ # Check for HTTP Basic scheme
415
+ if isinstance(scheme_def.root, HTTPAuthSecurityScheme):
416
+ return scheme_def.root.scheme.lower() == "basic"
417
+
418
+ return False
@@ -0,0 +1,14 @@
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.