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 +36 -0
- nat/plugins/a2a/__init__.py +14 -0
- nat/plugins/a2a/auth/__init__.py +15 -0
- nat/plugins/a2a/auth/credential_service.py +418 -0
- nat/plugins/a2a/client/__init__.py +14 -0
- nat/plugins/a2a/client/client_base.py +354 -0
- nat/plugins/a2a/client/client_config.py +72 -0
- nat/plugins/a2a/client/client_impl.py +324 -0
- nat/plugins/a2a/register.py +23 -0
- nat/plugins/a2a/server/__init__.py +14 -0
- nat/plugins/a2a/server/agent_executor_adapter.py +172 -0
- nat/plugins/a2a/server/front_end_config.py +131 -0
- nat/plugins/a2a/server/front_end_plugin.py +122 -0
- nat/plugins/a2a/server/front_end_plugin_worker.py +306 -0
- nat/plugins/a2a/server/oauth_middleware.py +121 -0
- nat/plugins/a2a/server/register_frontend.py +37 -0
- nvidia_nat_a2a-1.5.0a20251229.dist-info/METADATA +57 -0
- nvidia_nat_a2a-1.5.0a20251229.dist-info/RECORD +22 -0
- nvidia_nat_a2a-1.5.0a20251229.dist-info/WHEEL +5 -0
- nvidia_nat_a2a-1.5.0a20251229.dist-info/entry_points.txt +5 -0
- nvidia_nat_a2a-1.5.0a20251229.dist-info/licenses/LICENSE.md +201 -0
- nvidia_nat_a2a-1.5.0a20251229.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
import uvicorn
|
|
19
|
+
|
|
20
|
+
from nat.builder.front_end import FrontEndBase
|
|
21
|
+
from nat.builder.workflow_builder import WorkflowBuilder
|
|
22
|
+
from nat.plugins.a2a.server.front_end_config import A2AFrontEndConfig
|
|
23
|
+
from nat.plugins.a2a.server.front_end_plugin_worker import A2AFrontEndPluginWorker
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class A2AFrontEndPlugin(FrontEndBase[A2AFrontEndConfig]):
|
|
29
|
+
"""A2A front end plugin implementation.
|
|
30
|
+
|
|
31
|
+
Exposes NAT workflows as A2A-compliant remote agents that can be
|
|
32
|
+
discovered and invoked by other A2A agents and clients.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
async def run(self) -> None:
|
|
36
|
+
"""Run the A2A server.
|
|
37
|
+
|
|
38
|
+
This method:
|
|
39
|
+
1. Builds the workflow
|
|
40
|
+
2. Creates the agent card from configuration
|
|
41
|
+
3. Creates the agent executor adapter
|
|
42
|
+
4. Sets up the A2A server
|
|
43
|
+
5. Starts the server with uvicorn
|
|
44
|
+
"""
|
|
45
|
+
# Build the workflow
|
|
46
|
+
async with WorkflowBuilder.from_config(config=self.full_config) as builder:
|
|
47
|
+
workflow = await builder.build()
|
|
48
|
+
|
|
49
|
+
# Create worker instance
|
|
50
|
+
worker = self._get_worker_instance()
|
|
51
|
+
|
|
52
|
+
# Build agent card from configuration and workflow functions
|
|
53
|
+
agent_card = await worker.create_agent_card(workflow)
|
|
54
|
+
|
|
55
|
+
# Create agent executor adapter
|
|
56
|
+
agent_executor = worker.create_agent_executor(workflow, builder)
|
|
57
|
+
|
|
58
|
+
# Create A2A server
|
|
59
|
+
a2a_server = worker.create_a2a_server(agent_card, agent_executor)
|
|
60
|
+
|
|
61
|
+
# Start the server with proper cleanup
|
|
62
|
+
try:
|
|
63
|
+
logger.info(
|
|
64
|
+
"Starting A2A server '%s' at http://%s:%s",
|
|
65
|
+
self.front_end_config.name,
|
|
66
|
+
self.front_end_config.host,
|
|
67
|
+
self.front_end_config.port,
|
|
68
|
+
)
|
|
69
|
+
logger.info("Agent card available at: http://%s:%s/.well-known/agent-card.json",
|
|
70
|
+
self.front_end_config.host,
|
|
71
|
+
self.front_end_config.port)
|
|
72
|
+
|
|
73
|
+
# Build the ASGI app
|
|
74
|
+
app = a2a_server.build()
|
|
75
|
+
|
|
76
|
+
# Add OAuth2 validation middleware if configured
|
|
77
|
+
if self.front_end_config.server_auth:
|
|
78
|
+
from nat.plugins.a2a.server.oauth_middleware import OAuth2ValidationMiddleware
|
|
79
|
+
|
|
80
|
+
app.add_middleware(OAuth2ValidationMiddleware, config=self.front_end_config.server_auth)
|
|
81
|
+
logger.info(
|
|
82
|
+
"OAuth2 token validation enabled for A2A server (issuer=%s, scopes=%s)",
|
|
83
|
+
self.front_end_config.server_auth.issuer_url,
|
|
84
|
+
self.front_end_config.server_auth.scopes,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Run with uvicorn
|
|
88
|
+
config = uvicorn.Config(
|
|
89
|
+
app,
|
|
90
|
+
host=self.front_end_config.host,
|
|
91
|
+
port=self.front_end_config.port,
|
|
92
|
+
log_level=self.front_end_config.log_level.lower(),
|
|
93
|
+
)
|
|
94
|
+
server = uvicorn.Server(config)
|
|
95
|
+
await server.serve()
|
|
96
|
+
|
|
97
|
+
except KeyboardInterrupt:
|
|
98
|
+
logger.info("A2A server shutdown requested (Ctrl+C). Shutting down gracefully.")
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error("A2A server error: %s", e, exc_info=True)
|
|
101
|
+
raise
|
|
102
|
+
finally:
|
|
103
|
+
# Ensure cleanup of resources (httpx client)
|
|
104
|
+
await worker.cleanup()
|
|
105
|
+
logger.info("A2A server resources cleaned up")
|
|
106
|
+
|
|
107
|
+
def _get_worker_instance(self) -> A2AFrontEndPluginWorker:
|
|
108
|
+
"""Get an instance of the worker class.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
Worker instance configured with full config
|
|
112
|
+
"""
|
|
113
|
+
# Check if custom worker class is specified
|
|
114
|
+
if self.front_end_config.runner_class:
|
|
115
|
+
module_name, class_name = self.front_end_config.runner_class.rsplit(".", 1)
|
|
116
|
+
import importlib
|
|
117
|
+
module = importlib.import_module(module_name)
|
|
118
|
+
worker_class = getattr(module, class_name)
|
|
119
|
+
return worker_class(self.full_config)
|
|
120
|
+
|
|
121
|
+
# Use default worker
|
|
122
|
+
return A2AFrontEndPluginWorker(self.full_config)
|
|
@@ -0,0 +1,306 @@
|
|
|
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
|
+
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
import httpx
|
|
19
|
+
|
|
20
|
+
from a2a.server.apps import A2AStarletteApplication
|
|
21
|
+
from a2a.server.request_handlers import DefaultRequestHandler
|
|
22
|
+
from a2a.server.tasks import BasePushNotificationSender
|
|
23
|
+
from a2a.server.tasks import InMemoryPushNotificationConfigStore
|
|
24
|
+
from a2a.server.tasks import InMemoryTaskStore
|
|
25
|
+
from a2a.types import AgentCapabilities
|
|
26
|
+
from a2a.types import AgentCard
|
|
27
|
+
from a2a.types import AgentSkill
|
|
28
|
+
from a2a.types import SecurityScheme
|
|
29
|
+
from nat.builder.function import Function
|
|
30
|
+
from nat.builder.workflow import Workflow
|
|
31
|
+
from nat.builder.workflow_builder import WorkflowBuilder
|
|
32
|
+
from nat.data_models.config import Config
|
|
33
|
+
from nat.plugins.a2a.server.agent_executor_adapter import NATWorkflowAgentExecutor
|
|
34
|
+
from nat.plugins.a2a.server.front_end_config import A2AFrontEndConfig
|
|
35
|
+
from nat.runtime.session import SessionManager
|
|
36
|
+
|
|
37
|
+
logger = logging.getLogger(__name__)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class A2AFrontEndPluginWorker:
|
|
41
|
+
"""Worker that handles A2A server setup and configuration."""
|
|
42
|
+
|
|
43
|
+
def __init__(self, config: Config):
|
|
44
|
+
"""Initialize the A2A worker with configuration.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
config: The full NAT configuration
|
|
48
|
+
"""
|
|
49
|
+
self.full_config = config
|
|
50
|
+
self.front_end_config: A2AFrontEndConfig = config.general.front_end # type: ignore
|
|
51
|
+
|
|
52
|
+
# Max concurrency for handling A2A tasks (from configuration)
|
|
53
|
+
# This limits how many workflow invocations can run simultaneously
|
|
54
|
+
self.max_concurrency = self.front_end_config.max_concurrency
|
|
55
|
+
|
|
56
|
+
# HTTP client for push notifications (managed for cleanup)
|
|
57
|
+
self._httpx_client: httpx.AsyncClient | None = None
|
|
58
|
+
|
|
59
|
+
async def _get_all_functions(self, workflow: Workflow) -> dict[str, Function]:
|
|
60
|
+
"""Get all functions from the workflow.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
workflow: The NAT workflow
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Dict mapping function names to Function objects
|
|
67
|
+
"""
|
|
68
|
+
functions: dict[str, Function] = {}
|
|
69
|
+
|
|
70
|
+
# Extract all functions from the workflow
|
|
71
|
+
functions.update(workflow.functions)
|
|
72
|
+
for function_group in workflow.function_groups.values():
|
|
73
|
+
functions.update(await function_group.get_accessible_functions())
|
|
74
|
+
|
|
75
|
+
return functions
|
|
76
|
+
|
|
77
|
+
async def _generate_security_schemes(
|
|
78
|
+
self, server_auth_config) -> tuple[dict[str, SecurityScheme], list[dict[str, list[str]]]]:
|
|
79
|
+
"""Generate A2A security schemes from OAuth2ResourceServerConfig.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
server_auth_config: OAuth2ResourceServerConfig
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Tuple of (security_schemes dict, security requirements list)
|
|
86
|
+
"""
|
|
87
|
+
from a2a.types import AuthorizationCodeOAuthFlow
|
|
88
|
+
from a2a.types import OAuth2SecurityScheme
|
|
89
|
+
from a2a.types import OAuthFlows
|
|
90
|
+
|
|
91
|
+
# Resolve OAuth2 endpoints from configuration
|
|
92
|
+
auth_url, token_url = await self._resolve_oauth_endpoints(server_auth_config)
|
|
93
|
+
|
|
94
|
+
# Create scope descriptions
|
|
95
|
+
scope_descriptions = {scope: f"Permission: {scope}" for scope in server_auth_config.scopes}
|
|
96
|
+
|
|
97
|
+
# Build OAuth2 security scheme
|
|
98
|
+
security_schemes = {
|
|
99
|
+
"oauth2":
|
|
100
|
+
SecurityScheme(root=OAuth2SecurityScheme(
|
|
101
|
+
type="oauth2",
|
|
102
|
+
description="OAuth 2.0 authentication required to access this agent",
|
|
103
|
+
flows=OAuthFlows(authorizationCode=AuthorizationCodeOAuthFlow(
|
|
104
|
+
authorizationUrl=auth_url,
|
|
105
|
+
tokenUrl=token_url,
|
|
106
|
+
scopes=scope_descriptions,
|
|
107
|
+
)),
|
|
108
|
+
))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Security requirements (scopes needed)
|
|
112
|
+
security = [{"oauth2": server_auth_config.scopes}]
|
|
113
|
+
|
|
114
|
+
return security_schemes, security
|
|
115
|
+
|
|
116
|
+
async def _resolve_oauth_endpoints(self, server_auth_config) -> tuple[str, str]:
|
|
117
|
+
"""Resolve authorization and token URLs from OAuth2 configuration.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
server_auth_config: OAuth2ResourceServerConfig
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Tuple of (authorization_url, token_url)
|
|
124
|
+
"""
|
|
125
|
+
import httpx
|
|
126
|
+
|
|
127
|
+
# If discovery URL is provided, use OIDC discovery
|
|
128
|
+
if server_auth_config.discovery_url:
|
|
129
|
+
try:
|
|
130
|
+
async with httpx.AsyncClient() as client:
|
|
131
|
+
response = await client.get(server_auth_config.discovery_url, timeout=5.0)
|
|
132
|
+
response.raise_for_status()
|
|
133
|
+
metadata = response.json()
|
|
134
|
+
|
|
135
|
+
auth_url = metadata.get("authorization_endpoint")
|
|
136
|
+
token_url = metadata.get("token_endpoint")
|
|
137
|
+
|
|
138
|
+
if auth_url and token_url:
|
|
139
|
+
logger.info("Resolved OAuth endpoints via discovery: %s", server_auth_config.discovery_url)
|
|
140
|
+
return auth_url, token_url
|
|
141
|
+
except Exception as e:
|
|
142
|
+
logger.warning("Failed to discover OAuth endpoints: %s", e)
|
|
143
|
+
|
|
144
|
+
# Fallback: derive from issuer URL (common convention)
|
|
145
|
+
issuer = server_auth_config.issuer_url.rstrip("/")
|
|
146
|
+
auth_url = f"{issuer}/oauth/authorize"
|
|
147
|
+
token_url = f"{issuer}/oauth/token"
|
|
148
|
+
|
|
149
|
+
logger.info("Using derived OAuth endpoints from issuer: %s", issuer)
|
|
150
|
+
return auth_url, token_url
|
|
151
|
+
|
|
152
|
+
async def create_agent_card(self, workflow: Workflow) -> AgentCard:
|
|
153
|
+
"""Build AgentCard from configuration and workflow functions.
|
|
154
|
+
|
|
155
|
+
Skills are auto-generated from the workflow's functions, similar to how
|
|
156
|
+
MCP introspects and exposes functions as tools.
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
workflow: The NAT workflow to extract functions from
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
AgentCard with agent metadata, capabilities, and auto-generated skills
|
|
163
|
+
"""
|
|
164
|
+
config = self.front_end_config
|
|
165
|
+
|
|
166
|
+
# Build capabilities
|
|
167
|
+
capabilities = AgentCapabilities(
|
|
168
|
+
streaming=config.capabilities.streaming,
|
|
169
|
+
push_notifications=config.capabilities.push_notifications,
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
# Auto-generate skills from workflow functions
|
|
173
|
+
functions = await self._get_all_functions(workflow)
|
|
174
|
+
skills = []
|
|
175
|
+
|
|
176
|
+
for function_name, function in functions.items():
|
|
177
|
+
# Create skill from function metadata
|
|
178
|
+
skill_name = function_name.replace('_', ' ').replace('.', ' - ').title()
|
|
179
|
+
skill_description = function.description or f"Execute {function_name}"
|
|
180
|
+
|
|
181
|
+
skill = AgentSkill(
|
|
182
|
+
id=function_name,
|
|
183
|
+
name=skill_name,
|
|
184
|
+
description=skill_description,
|
|
185
|
+
tags=[], # Could be extended with function metadata
|
|
186
|
+
examples=[], # Could be extracted from function examples if available
|
|
187
|
+
)
|
|
188
|
+
skills.append(skill)
|
|
189
|
+
|
|
190
|
+
logger.info("Auto-generated %d skills from workflow functions", len(skills))
|
|
191
|
+
|
|
192
|
+
# Generate security schemes if server_auth is configured
|
|
193
|
+
security_schemes = None
|
|
194
|
+
security = None
|
|
195
|
+
|
|
196
|
+
if config.server_auth:
|
|
197
|
+
security_schemes, security = await self._generate_security_schemes(config.server_auth)
|
|
198
|
+
logger.info(
|
|
199
|
+
"Generated OAuth2 security schemes for agent (issuer=%s, scopes=%s)",
|
|
200
|
+
config.server_auth.issuer_url,
|
|
201
|
+
config.server_auth.scopes,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
# Build agent card
|
|
205
|
+
agent_url = f"http://{config.host}:{config.port}/"
|
|
206
|
+
agent_card = AgentCard(
|
|
207
|
+
name=config.name,
|
|
208
|
+
description=config.description,
|
|
209
|
+
url=agent_url,
|
|
210
|
+
version=config.version,
|
|
211
|
+
default_input_modes=config.default_input_modes,
|
|
212
|
+
default_output_modes=config.default_output_modes,
|
|
213
|
+
capabilities=capabilities,
|
|
214
|
+
skills=skills,
|
|
215
|
+
security_schemes=security_schemes,
|
|
216
|
+
security=security,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
logger.info("Created AgentCard for: %s v%s", config.name, config.version)
|
|
220
|
+
logger.info("Agent URL: %s", agent_url)
|
|
221
|
+
logger.info("Skills: %d", len(skills))
|
|
222
|
+
if security_schemes:
|
|
223
|
+
logger.info("Security: OAuth2 authentication required")
|
|
224
|
+
|
|
225
|
+
return agent_card
|
|
226
|
+
|
|
227
|
+
def create_agent_executor(self, workflow: Workflow, builder: WorkflowBuilder) -> NATWorkflowAgentExecutor:
|
|
228
|
+
"""Create agent executor adapter for the workflow.
|
|
229
|
+
|
|
230
|
+
This creates a SessionManager to handle concurrent A2A task requests,
|
|
231
|
+
similar to how FastAPI handles multiple HTTP requests.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
workflow: The NAT workflow to expose
|
|
235
|
+
builder: The workflow builder used to create the workflow
|
|
236
|
+
|
|
237
|
+
Returns:
|
|
238
|
+
NATWorkflowAgentExecutor that wraps the workflow with a SessionManager
|
|
239
|
+
"""
|
|
240
|
+
# Create SessionManager to handle concurrent requests with proper limits
|
|
241
|
+
session_manager = SessionManager(
|
|
242
|
+
config=self.full_config,
|
|
243
|
+
shared_builder=builder,
|
|
244
|
+
shared_workflow=workflow,
|
|
245
|
+
max_concurrency=self.max_concurrency,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
logger.info("Created SessionManager with max_concurrency=%d", self.max_concurrency)
|
|
249
|
+
|
|
250
|
+
return NATWorkflowAgentExecutor(session_manager)
|
|
251
|
+
|
|
252
|
+
def create_a2a_server(
|
|
253
|
+
self,
|
|
254
|
+
agent_card: AgentCard,
|
|
255
|
+
agent_executor: NATWorkflowAgentExecutor,
|
|
256
|
+
) -> A2AStarletteApplication:
|
|
257
|
+
"""Create A2A server with the agent executor.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
agent_card: The agent card describing the agent
|
|
261
|
+
agent_executor: The executor that handles task processing
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Configured A2A Starlette application
|
|
265
|
+
|
|
266
|
+
Note:
|
|
267
|
+
The httpx client is stored in self._httpx_client for lifecycle management.
|
|
268
|
+
Call cleanup() during server shutdown to properly close the client.
|
|
269
|
+
"""
|
|
270
|
+
# Create HTTP client for push notifications and store for cleanup
|
|
271
|
+
self._httpx_client = httpx.AsyncClient()
|
|
272
|
+
|
|
273
|
+
# Create push notification infrastructure
|
|
274
|
+
push_config_store = InMemoryPushNotificationConfigStore()
|
|
275
|
+
push_sender = BasePushNotificationSender(
|
|
276
|
+
httpx_client=self._httpx_client,
|
|
277
|
+
config_store=push_config_store,
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Create request handler
|
|
281
|
+
request_handler = DefaultRequestHandler(
|
|
282
|
+
agent_executor=agent_executor,
|
|
283
|
+
task_store=InMemoryTaskStore(),
|
|
284
|
+
push_config_store=push_config_store,
|
|
285
|
+
push_sender=push_sender,
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
# Create A2A server
|
|
289
|
+
server = A2AStarletteApplication(
|
|
290
|
+
agent_card=agent_card,
|
|
291
|
+
http_handler=request_handler,
|
|
292
|
+
)
|
|
293
|
+
|
|
294
|
+
logger.info("Created A2A server with DefaultRequestHandler")
|
|
295
|
+
|
|
296
|
+
return server
|
|
297
|
+
|
|
298
|
+
async def cleanup(self) -> None:
|
|
299
|
+
"""Clean up resources, particularly the httpx client.
|
|
300
|
+
|
|
301
|
+
This should be called during server shutdown to prevent connection leaks.
|
|
302
|
+
"""
|
|
303
|
+
if self._httpx_client is not None:
|
|
304
|
+
await self._httpx_client.aclose()
|
|
305
|
+
self._httpx_client = None
|
|
306
|
+
logger.info("Closed httpx client for push notifications")
|
|
@@ -0,0 +1,121 @@
|
|
|
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
|
+
"""OAuth 2.0 token validation middleware for A2A servers."""
|
|
16
|
+
|
|
17
|
+
import logging
|
|
18
|
+
|
|
19
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
20
|
+
from starlette.requests import Request
|
|
21
|
+
from starlette.responses import JSONResponse
|
|
22
|
+
|
|
23
|
+
from nat.authentication.credential_validator.bearer_token_validator import BearerTokenValidator
|
|
24
|
+
from nat.authentication.oauth2.oauth2_resource_server_config import OAuth2ResourceServerConfig
|
|
25
|
+
|
|
26
|
+
logger = logging.getLogger(__name__)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class OAuth2ValidationMiddleware(BaseHTTPMiddleware):
|
|
30
|
+
"""OAuth2 Bearer token validation middleware for A2A servers.
|
|
31
|
+
|
|
32
|
+
Validates Bearer tokens using NAT's BearerTokenValidator which supports:
|
|
33
|
+
- JWT validation via JWKS (RFC 7519)
|
|
34
|
+
- Opaque token validation via introspection (RFC 7662)
|
|
35
|
+
- OIDC discovery
|
|
36
|
+
- Scope and audience enforcement
|
|
37
|
+
|
|
38
|
+
The middleware allows public access to the agent card discovery endpoint
|
|
39
|
+
(/.well-known/agent.json) and validates all other A2A requests.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, app, config: OAuth2ResourceServerConfig):
|
|
43
|
+
"""Initialize OAuth2 validation middleware.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
app: Starlette application
|
|
47
|
+
config: OAuth2 resource server configuration
|
|
48
|
+
"""
|
|
49
|
+
super().__init__(app)
|
|
50
|
+
|
|
51
|
+
# Create validator using NAT's BearerTokenValidator
|
|
52
|
+
self.validator = BearerTokenValidator(
|
|
53
|
+
issuer=config.issuer_url,
|
|
54
|
+
audience=config.audience,
|
|
55
|
+
scopes=config.scopes,
|
|
56
|
+
jwks_uri=config.jwks_uri,
|
|
57
|
+
introspection_endpoint=config.introspection_endpoint,
|
|
58
|
+
discovery_url=config.discovery_url,
|
|
59
|
+
client_id=config.client_id,
|
|
60
|
+
client_secret=config.client_secret.get_secret_value() if config.client_secret else None,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
logger.info(
|
|
64
|
+
"OAuth2 validation middleware initialized (issuer=%s, scopes=%s, audience=%s)",
|
|
65
|
+
config.issuer_url,
|
|
66
|
+
config.scopes,
|
|
67
|
+
config.audience,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
async def dispatch(self, request: Request, call_next):
|
|
71
|
+
"""Validate OAuth2 Bearer token for all requests except agent card discovery.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
request: Incoming HTTP request
|
|
75
|
+
call_next: Next middleware/handler in chain
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
HTTP response (either error or result from next handler)
|
|
79
|
+
"""
|
|
80
|
+
# Public: Agent card discovery (per A2A spec)
|
|
81
|
+
if request.url.path == "/.well-known/agent-card.json":
|
|
82
|
+
logger.debug("Public access to agent card discovery")
|
|
83
|
+
return await call_next(request)
|
|
84
|
+
|
|
85
|
+
# Extract Bearer token
|
|
86
|
+
auth_header = request.headers.get("Authorization", "")
|
|
87
|
+
if not auth_header.startswith("Bearer "):
|
|
88
|
+
logger.warning("Missing or invalid Authorization header")
|
|
89
|
+
return JSONResponse({
|
|
90
|
+
"error": "unauthorized", "message": "Missing or invalid Bearer token"
|
|
91
|
+
},
|
|
92
|
+
status_code=401)
|
|
93
|
+
|
|
94
|
+
token = auth_header[7:] # Strip "Bearer "
|
|
95
|
+
|
|
96
|
+
# Validate token using NAT's validator
|
|
97
|
+
try:
|
|
98
|
+
result = await self.validator.verify(token)
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(f"Token validation error: {e}")
|
|
101
|
+
return JSONResponse({"error": "invalid_token", "message": "Token validation failed"}, status_code=403)
|
|
102
|
+
|
|
103
|
+
# Check if token is active
|
|
104
|
+
if not result.active:
|
|
105
|
+
logger.warning("Token is not active")
|
|
106
|
+
return JSONResponse({"error": "invalid_token", "message": "Token is not active"}, status_code=403)
|
|
107
|
+
|
|
108
|
+
# Attach token info to request state for potential use by handlers
|
|
109
|
+
request.state.oauth_user = result.subject
|
|
110
|
+
request.state.oauth_scopes = result.scopes or []
|
|
111
|
+
request.state.oauth_client_id = result.client_id
|
|
112
|
+
request.state.oauth_token_info = result
|
|
113
|
+
|
|
114
|
+
logger.debug(
|
|
115
|
+
"Token validated successfully (user=%s, scopes=%s, client=%s)",
|
|
116
|
+
result.subject,
|
|
117
|
+
result.scopes,
|
|
118
|
+
result.client_id,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
return await call_next(request)
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
"""Registration of A2A front end with NAT plugin system."""
|
|
16
|
+
|
|
17
|
+
from collections.abc import AsyncIterator
|
|
18
|
+
|
|
19
|
+
from nat.cli.register_workflow import register_front_end
|
|
20
|
+
from nat.data_models.config import Config
|
|
21
|
+
from nat.plugins.a2a.server.front_end_config import A2AFrontEndConfig
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@register_front_end(config_type=A2AFrontEndConfig)
|
|
25
|
+
async def register_a2a_front_end(_config: A2AFrontEndConfig, full_config: Config) -> AsyncIterator:
|
|
26
|
+
"""Register the A2A front end plugin.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
_config: The A2A front end configuration (unused, provided for registration)
|
|
30
|
+
full_config: The complete NAT configuration
|
|
31
|
+
|
|
32
|
+
Yields:
|
|
33
|
+
A2AFrontEndPlugin instance
|
|
34
|
+
"""
|
|
35
|
+
from nat.plugins.a2a.server.front_end_plugin import A2AFrontEndPlugin
|
|
36
|
+
|
|
37
|
+
yield A2AFrontEndPlugin(full_config=full_config)
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nvidia-nat-a2a
|
|
3
|
+
Version: 1.5.0a20251229
|
|
4
|
+
Summary: Subpackage for A2A Protocol integration in NeMo Agent Toolkit
|
|
5
|
+
Author: NVIDIA Corporation
|
|
6
|
+
Maintainer: NVIDIA Corporation
|
|
7
|
+
License: Apache-2.0
|
|
8
|
+
Project-URL: documentation, https://docs.nvidia.com/nemo/agent-toolkit/latest/
|
|
9
|
+
Project-URL: source, https://github.com/NVIDIA/NeMo-Agent-Toolkit
|
|
10
|
+
Keywords: ai,rag,agents,a2a
|
|
11
|
+
Classifier: Programming Language :: Python
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Python: <3.14,>=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE.md
|
|
18
|
+
Requires-Dist: nvidia-nat==v1.5.0a20251229
|
|
19
|
+
Requires-Dist: a2a-sdk~=0.3.20
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
<!--
|
|
23
|
+
SPDX-FileCopyrightText: Copyright (c) 2025, NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
24
|
+
SPDX-License-Identifier: Apache-2.0
|
|
25
|
+
|
|
26
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
27
|
+
you may not use this file except in compliance with the License.
|
|
28
|
+
You may obtain a copy of the License at
|
|
29
|
+
|
|
30
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
31
|
+
|
|
32
|
+
Unless required by applicable law or agreed to in writing, software
|
|
33
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
34
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
35
|
+
See the License for the specific language governing permissions and
|
|
36
|
+
limitations under the License.
|
|
37
|
+
-->
|
|
38
|
+
|
|
39
|
+

|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# NVIDIA NeMo Agent Toolkit A2A Subpackage
|
|
43
|
+
Subpackage for A2A Protocol integration in NeMo Agent toolkit.
|
|
44
|
+
|
|
45
|
+
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.
|
|
46
|
+
|
|
47
|
+
## Features
|
|
48
|
+
### Client
|
|
49
|
+
- Connect to remote A2A agents via HTTP with JSON-RPC transport
|
|
50
|
+
- Discover agent capabilities through Agent Cards
|
|
51
|
+
- Submit tasks to remote agents with async execution
|
|
52
|
+
|
|
53
|
+
### Server
|
|
54
|
+
- Serve A2A agents via HTTP with JSON-RPC transport
|
|
55
|
+
- Support for A2A agent executor pattern
|
|
56
|
+
|
|
57
|
+
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,22 @@
|
|
|
1
|
+
nat/meta/pypi.md,sha256=YkfjzZntzheoaBie5ZovnAwB78xxVqk9sblkZRZcdLU,1661
|
|
2
|
+
nat/plugins/a2a/__init__.py,sha256=GUJrgGtpvyMUCjUBvR3faAdv-tZzbU9W-izgx9aMEQg,680
|
|
3
|
+
nat/plugins/a2a/register.py,sha256=pUN1hbJ38M8GbdNcA0qQzJ1S-ZC91GnRGk_8SO_kTVg,853
|
|
4
|
+
nat/plugins/a2a/auth/__init__.py,sha256=iQFx1YrjFcepS7k8jp93A0IVOkFeNx_I35M6dIngoJA,726
|
|
5
|
+
nat/plugins/a2a/auth/credential_service.py,sha256=-_VdDF4YESaAtY1ONUiOL5z4aGDJZYVuhyhI9BZhuyI,15967
|
|
6
|
+
nat/plugins/a2a/client/__init__.py,sha256=GUJrgGtpvyMUCjUBvR3faAdv-tZzbU9W-izgx9aMEQg,680
|
|
7
|
+
nat/plugins/a2a/client/client_base.py,sha256=xShDZDFKa4R2XsY3yBMvM-eDaf_0cdE48XJzQ4WcEOw,13366
|
|
8
|
+
nat/plugins/a2a/client/client_config.py,sha256=KwWjymDg9GUfSYcIaBhcxph4Hu6IeTe414hrNUUo-6g,2875
|
|
9
|
+
nat/plugins/a2a/client/client_impl.py,sha256=CGAjiHr6EyWcnlSipmT8ixgjD4s8VbPRBPOZy2q_Sm0,12958
|
|
10
|
+
nat/plugins/a2a/server/__init__.py,sha256=GUJrgGtpvyMUCjUBvR3faAdv-tZzbU9W-izgx9aMEQg,680
|
|
11
|
+
nat/plugins/a2a/server/agent_executor_adapter.py,sha256=wvGXOb3FcV0_pYRv-yr-QzozjzXM909D49Dxm9199xI,7015
|
|
12
|
+
nat/plugins/a2a/server/front_end_config.py,sha256=Lg-qjDmC4fwrwnHNtSRl54pMpdwVnO06xhgbLt-aEZY,4902
|
|
13
|
+
nat/plugins/a2a/server/front_end_plugin.py,sha256=fX3Lagkd48snSiNo2IMTRpR-40WHUWQidpjKu8uQChY,4896
|
|
14
|
+
nat/plugins/a2a/server/front_end_plugin_worker.py,sha256=Ehdv6lyUcrWkfMq7YomD4NYFAusrtQ2JYj2HnkIqGhY,11696
|
|
15
|
+
nat/plugins/a2a/server/oauth_middleware.py,sha256=NvvIJSPB8wRui2eQlxr6AaNhN0JxdUQ1Ajr8Dnk0rnY,4751
|
|
16
|
+
nat/plugins/a2a/server/register_frontend.py,sha256=4TmpBcZF4x71c2xnWuketsygqHmU7D2hKA2bzO34TpU,1480
|
|
17
|
+
nvidia_nat_a2a-1.5.0a20251229.dist-info/licenses/LICENSE.md,sha256=QwcOLU5TJoTeUhuIXzhdCEEDDvorGiC6-3YTOl4TecE,11356
|
|
18
|
+
nvidia_nat_a2a-1.5.0a20251229.dist-info/METADATA,sha256=uHbXjyUGN9TADzyHyZgf4PZpkBHpV8uIX6euqTNhPFM,2438
|
|
19
|
+
nvidia_nat_a2a-1.5.0a20251229.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
20
|
+
nvidia_nat_a2a-1.5.0a20251229.dist-info/entry_points.txt,sha256=Lacvy6nXpDTv8dh8vKJ_QE8TobliVdhgABuw25t8fBg,145
|
|
21
|
+
nvidia_nat_a2a-1.5.0a20251229.dist-info/top_level.txt,sha256=8-CJ2cP6-f0ZReXe5Hzqp-5pvzzHz-5Ds5H2bGqh1-U,4
|
|
22
|
+
nvidia_nat_a2a-1.5.0a20251229.dist-info/RECORD,,
|