agent-nebula-platform 5.0.0__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.
- agent_nebula_platform/__init__.py +77 -0
- agent_nebula_platform/a2a/README.md +60 -0
- agent_nebula_platform/a2a/__init__.py +8 -0
- agent_nebula_platform/a2a/client.py +105 -0
- agent_nebula_platform/a2a/config/__init__.py +4 -0
- agent_nebula_platform/a2a/config/client.py +292 -0
- agent_nebula_platform/a2a/config/server.py +303 -0
- agent_nebula_platform/a2a/extensions/__init__.py +6 -0
- agent_nebula_platform/a2a/extensions/a2ui.py +91 -0
- agent_nebula_platform/a2a/extensions/health.py +49 -0
- agent_nebula_platform/a2a/extensions/schema_contract.py +79 -0
- agent_nebula_platform/a2a/extensions/skill_selection.py +100 -0
- agent_nebula_platform/a2a/runtime.py +248 -0
- agent_nebula_platform/a2a/security/__init__.py +5 -0
- agent_nebula_platform/a2a/security/client.py +426 -0
- agent_nebula_platform/a2a/security/request_handler.py +118 -0
- agent_nebula_platform/a2a/security/server.py +726 -0
- agent_nebula_platform/a2a/server.py +239 -0
- agent_nebula_platform/a2ui/__init__.py +83 -0
- agent_nebula_platform/a2ui/actions/__init__.py +8 -0
- agent_nebula_platform/a2ui/actions/service.py +40 -0
- agent_nebula_platform/a2ui/composition.py +164 -0
- agent_nebula_platform/a2ui/configuration.py +110 -0
- agent_nebula_platform/a2ui/data_model/__init__.py +7 -0
- agent_nebula_platform/a2ui/data_model/service.py +24 -0
- agent_nebula_platform/a2ui/events/__init__.py +7 -0
- agent_nebula_platform/a2ui/events/models.py +19 -0
- agent_nebula_platform/a2ui/models.py +162 -0
- agent_nebula_platform/a2ui/persistence/__init__.py +13 -0
- agent_nebula_platform/a2ui/plugin.py +74 -0
- agent_nebula_platform/a2ui/service.py +68 -0
- agent_nebula_platform/a2ui/session/__init__.py +25 -0
- agent_nebula_platform/a2ui/session/models.py +96 -0
- agent_nebula_platform/a2ui/session/plugin.py +49 -0
- agent_nebula_platform/a2ui/session/service.py +249 -0
- agent_nebula_platform/a2ui/session/store.py +167 -0
- agent_nebula_platform/a2ui/surfaces/__init__.py +9 -0
- agent_nebula_platform/a2ui/surfaces/models.py +13 -0
- agent_nebula_platform/a2ui/surfaces/service.py +55 -0
- agent_nebula_platform/a2ui/validation.py +325 -0
- agent_nebula_platform/integrations/__init__.py +1 -0
- agent_nebula_platform/integrations/nebula/__init__.py +26 -0
- agent_nebula_platform/integrations/nebula/configuration.py +125 -0
- agent_nebula_platform/integrations/nebula/connector.py +146 -0
- agent_nebula_platform/integrations/nebula/discovery_service.py +45 -0
- agent_nebula_platform/integrations/nebula/onboarding/__init__.py +16 -0
- agent_nebula_platform/integrations/nebula/onboarding/models.py +27 -0
- agent_nebula_platform/integrations/nebula/onboarding/payloads.py +154 -0
- agent_nebula_platform/integrations/nebula/onboarding/plugin.py +104 -0
- agent_nebula_platform/integrations/nebula/registration.py +30 -0
- agent_nebula_platform/integrations/nebula/transport.py +69 -0
- agent_nebula_platform/observability/__init__.py +5 -0
- agent_nebula_platform/observability/logging.py +45 -0
- agent_nebula_platform/platform/__init__.py +1 -0
- agent_nebula_platform/platform/capabilities/__init__.py +17 -0
- agent_nebula_platform/platform/capabilities/catalog.py +185 -0
- agent_nebula_platform/platform/configuration/__init__.py +26 -0
- agent_nebula_platform/platform/configuration/application.py +91 -0
- agent_nebula_platform/platform/configuration/home.py +50 -0
- agent_nebula_platform/platform/configuration/ini.py +66 -0
- agent_nebula_platform/platform/configuration/layered.py +58 -0
- agent_nebula_platform/platform/configuration/paths.py +74 -0
- agent_nebula_platform/platform/configuration/ports.py +56 -0
- agent_nebula_platform/platform/configuration/sources.py +139 -0
- agent_nebula_platform/platform/context.py +21 -0
- agent_nebula_platform/platform/discovery.py +141 -0
- agent_nebula_platform/platform/errors.py +36 -0
- agent_nebula_platform/platform/events.py +53 -0
- agent_nebula_platform/platform/execution_trace.py +138 -0
- agent_nebula_platform/platform/health/__init__.py +15 -0
- agent_nebula_platform/platform/health/models.py +42 -0
- agent_nebula_platform/platform/health/publication.py +96 -0
- agent_nebula_platform/platform/health/service.py +69 -0
- agent_nebula_platform/platform/lifecycle/__init__.py +1 -0
- agent_nebula_platform/platform/lifecycle/agent_server_plugin.py +101 -0
- agent_nebula_platform/platform/lifecycle/fastapi.py +50 -0
- agent_nebula_platform/platform/lifecycle/plugin.py +63 -0
- agent_nebula_platform/platform/llm/__init__.py +5 -0
- agent_nebula_platform/platform/llm/observability.py +192 -0
- agent_nebula_platform/platform/metrics/__init__.py +13 -0
- agent_nebula_platform/platform/metrics/exporters.py +23 -0
- agent_nebula_platform/platform/metrics/models.py +26 -0
- agent_nebula_platform/platform/metrics/plugin.py +45 -0
- agent_nebula_platform/platform/metrics/registry.py +91 -0
- agent_nebula_platform/platform/plugins/__init__.py +14 -0
- agent_nebula_platform/platform/plugins/base.py +82 -0
- agent_nebula_platform/platform/plugins/registry.py +79 -0
- agent_nebula_platform/platform/presets/__init__.py +45 -0
- agent_nebula_platform/platform/presets/application.py +691 -0
- agent_nebula_platform/platform/presets/common.py +68 -0
- agent_nebula_platform/platform/presets/composition.py +256 -0
- agent_nebula_platform/platform/presets/embedded/__init__.py +7 -0
- agent_nebula_platform/platform/presets/embedded/composition.py +32 -0
- agent_nebula_platform/platform/presets/logging.py +28 -0
- agent_nebula_platform/platform/presets/models.py +174 -0
- agent_nebula_platform/platform/presets/standalone/__init__.py +25 -0
- agent_nebula_platform/platform/presets/standalone/application.py +50 -0
- agent_nebula_platform/platform/presets/standalone/composition.py +91 -0
- agent_nebula_platform/platform/presets/standalone/settings.py +280 -0
- agent_nebula_platform/platform/retry/__init__.py +11 -0
- agent_nebula_platform/platform/retry/executor.py +39 -0
- agent_nebula_platform/platform/retry/policy.py +66 -0
- agent_nebula_platform/platform/runtime.py +219 -0
- agent_nebula_platform/platform/scheduler/__init__.py +11 -0
- agent_nebula_platform/platform/scheduler/models.py +48 -0
- agent_nebula_platform/platform/scheduler/plugin.py +58 -0
- agent_nebula_platform/platform/scheduler/service.py +108 -0
- agent_nebula_platform/platform/services.py +43 -0
- agent_nebula_platform/platform/state.py +91 -0
- agent_nebula_platform/platform/telemetry/__init__.py +14 -0
- agent_nebula_platform/platform/telemetry/events.py +33 -0
- agent_nebula_platform/platform/telemetry/plugin.py +47 -0
- agent_nebula_platform/platform/telemetry/service.py +26 -0
- agent_nebula_platform/platform/telemetry/sinks.py +53 -0
- agent_nebula_platform/platform/tls.py +41 -0
- agent_nebula_platform/platform/transport/__init__.py +1 -0
- agent_nebula_platform/platform/transport/fastapi/__init__.py +12 -0
- agent_nebula_platform/platform/transport/fastapi/app.py +211 -0
- agent_nebula_platform/platform/transport/fastapi/lifespan.py +33 -0
- agent_nebula_platform/platform/transport/fastapi/server.py +225 -0
- agent_nebula_platform/platform/transport/http/__init__.py +7 -0
- agent_nebula_platform/platform/transport/http/request_target.py +66 -0
- agent_nebula_platform/platform/transport/http_client/__init__.py +8 -0
- agent_nebula_platform/platform/transport/http_client/factory.py +50 -0
- agent_nebula_platform/platform/transport/rest/__init__.py +7 -0
- agent_nebula_platform/platform/transport/rest/adapter.py +37 -0
- agent_nebula_platform/security/__init__.py +1 -0
- agent_nebula_platform/security/oauth/__init__.py +1 -0
- agent_nebula_platform/security/oauth/client/__init__.py +29 -0
- agent_nebula_platform/security/oauth/client/cache.py +35 -0
- agent_nebula_platform/security/oauth/client/httpx_transport.py +95 -0
- agent_nebula_platform/security/oauth/client/identity.py +187 -0
- agent_nebula_platform/security/oauth/client/models.py +36 -0
- agent_nebula_platform/security/oauth/client/plugin.py +37 -0
- agent_nebula_platform/security/oauth/client/ports.py +60 -0
- agent_nebula_platform/security/oauth/client/protected_http.py +164 -0
- agent_nebula_platform/security/oauth/client/service.py +324 -0
- agent_nebula_platform/security/oauth/metadata/__init__.py +24 -0
- agent_nebula_platform/security/oauth/metadata/authorization_server.py +121 -0
- agent_nebula_platform/security/oauth/metadata/builders.py +148 -0
- agent_nebula_platform/security/oauth/metadata/factory.py +36 -0
- agent_nebula_platform/security/oauth/metadata/metadata.py +52 -0
- agent_nebula_platform/security/oauth/protected_resource/__init__.py +19 -0
- agent_nebula_platform/security/oauth/protected_resource/factory.py +136 -0
- agent_nebula_platform/security/oauth/protected_resource/models.py +30 -0
- agent_nebula_platform/security/oauth/protected_resource/plugin.py +41 -0
- agent_nebula_platform/security/oauth/protected_resource/policy.py +26 -0
- agent_nebula_platform/security/oauth/protected_resource/ports.py +19 -0
- agent_nebula_platform/security/oauth/protected_resource/routes.py +94 -0
- agent_nebula_platform/security/oauth/protected_resource/service.py +79 -0
- agent_nebula_platform/security/oauth/registration/README.md +18 -0
- agent_nebula_platform/security/oauth/registration/__init__.py +7 -0
- agent_nebula_platform/security/oauth/registration/plugin.py +219 -0
- agent_nebula_platform/security/oauth/registration/transport.py +65 -0
- agent_nebula_platform/skills/__init__.py +14 -0
- agent_nebula_platform/skills/models.py +50 -0
- agent_nebula_platform/skills/plugin.py +89 -0
- agent_nebula_platform/skills/registry.py +53 -0
- agent_nebula_platform/skills/service.py +122 -0
- agent_nebula_platform-5.0.0.dist-info/METADATA +14 -0
- agent_nebula_platform-5.0.0.dist-info/RECORD +163 -0
- agent_nebula_platform-5.0.0.dist-info/WHEEL +4 -0
- agent_nebula_platform-5.0.0.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Public API for the pluggable Agent Runtime integration layer."""
|
|
2
|
+
|
|
3
|
+
from .a2ui.session import (
|
|
4
|
+
A2UIActionRecord,
|
|
5
|
+
A2UISessionService,
|
|
6
|
+
A2UISessionSnapshot,
|
|
7
|
+
A2UISurfaceFrame,
|
|
8
|
+
A2UISurfaceMode,
|
|
9
|
+
A2UITaskReference,
|
|
10
|
+
)
|
|
11
|
+
from .platform.capabilities import (
|
|
12
|
+
RuntimeCapability,
|
|
13
|
+
capability_catalog,
|
|
14
|
+
require_available,
|
|
15
|
+
)
|
|
16
|
+
from .platform.context import RuntimeContext
|
|
17
|
+
from .platform.events import (
|
|
18
|
+
EventSink,
|
|
19
|
+
NullEventSink,
|
|
20
|
+
PythonLoggingEventSink,
|
|
21
|
+
RuntimeEvent,
|
|
22
|
+
)
|
|
23
|
+
from .platform.execution_trace import (
|
|
24
|
+
EXECUTION_ID_HEADER,
|
|
25
|
+
SECURITY_TRACE_HEADER,
|
|
26
|
+
activate_execution_trace,
|
|
27
|
+
current_execution_trace,
|
|
28
|
+
reset_execution_trace,
|
|
29
|
+
)
|
|
30
|
+
from .platform.health import HealthStatus, RuntimeHealthService
|
|
31
|
+
from .platform.llm import LlmCostEstimator, LlmUsageObserver, LlmUsageRecord
|
|
32
|
+
from .platform.metrics import MetricsRegistry, MetricsSnapshot
|
|
33
|
+
from .platform.runtime import AgentRuntime
|
|
34
|
+
from .platform.scheduler import JobTriggerType, RuntimeScheduler, ScheduledJob
|
|
35
|
+
from .platform.services import RuntimeServiceRegistry
|
|
36
|
+
from .platform.state import JsonFileStateStore, MemoryStateStore, RuntimeStateStore
|
|
37
|
+
from .platform.telemetry import TelemetryEvent, TelemetryService, TelemetrySeverity
|
|
38
|
+
|
|
39
|
+
__all__ = [
|
|
40
|
+
"EXECUTION_ID_HEADER",
|
|
41
|
+
"SECURITY_TRACE_HEADER",
|
|
42
|
+
"A2UIActionRecord",
|
|
43
|
+
"A2UISessionService",
|
|
44
|
+
"A2UISessionSnapshot",
|
|
45
|
+
"A2UISurfaceFrame",
|
|
46
|
+
"A2UISurfaceMode",
|
|
47
|
+
"A2UITaskReference",
|
|
48
|
+
"AgentRuntime",
|
|
49
|
+
"EventSink",
|
|
50
|
+
"HealthStatus",
|
|
51
|
+
"JobTriggerType",
|
|
52
|
+
"JsonFileStateStore",
|
|
53
|
+
"MemoryStateStore",
|
|
54
|
+
"LlmCostEstimator",
|
|
55
|
+
"LlmUsageObserver",
|
|
56
|
+
"LlmUsageRecord",
|
|
57
|
+
"MetricsRegistry",
|
|
58
|
+
"MetricsSnapshot",
|
|
59
|
+
"NullEventSink",
|
|
60
|
+
"PythonLoggingEventSink",
|
|
61
|
+
"RuntimeCapability",
|
|
62
|
+
"RuntimeContext",
|
|
63
|
+
"RuntimeEvent",
|
|
64
|
+
"RuntimeHealthService",
|
|
65
|
+
"RuntimeScheduler",
|
|
66
|
+
"RuntimeServiceRegistry",
|
|
67
|
+
"RuntimeStateStore",
|
|
68
|
+
"ScheduledJob",
|
|
69
|
+
"TelemetryEvent",
|
|
70
|
+
"TelemetryService",
|
|
71
|
+
"TelemetrySeverity",
|
|
72
|
+
"activate_execution_trace",
|
|
73
|
+
"capability_catalog",
|
|
74
|
+
"current_execution_trace",
|
|
75
|
+
"require_available",
|
|
76
|
+
"reset_execution_trace",
|
|
77
|
+
]
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Agent Nebula Platform — A2A Frozen Requirements
|
|
2
|
+
|
|
3
|
+
This directory is the clean Agent Nebula integration boundary on top of the official `a2a-sdk`. **All code changes under this directory MUST follow this document.**
|
|
4
|
+
|
|
5
|
+
## Frozen ownership
|
|
6
|
+
|
|
7
|
+
1. `a2a-sdk` is authoritative for standard A2A protocol functionality.
|
|
8
|
+
2. This package contains only Agent Nebula functionality required on top of `a2a-sdk`: configuration integration, security integration, and Nebula-specific extension semantics.
|
|
9
|
+
3. Do not duplicate or wrap standard `a2a-sdk` models or operations, including AgentCard, AgentSkill, Message, Task, Artifact, AgentExecutor, RequestContext, EventQueue, Client operations, streaming, cancellation, push notifications, or protocol bindings.
|
|
10
|
+
4. Public boundaries use and return native `a2a-sdk` types. Do not convert native models to dictionaries merely to hide the SDK.
|
|
11
|
+
5. Agent Card is authoritative for interfaces, skills, capabilities, extensions, and security declarations.
|
|
12
|
+
6. A2A extensions use the standard A2A extension mechanism. Nebula code implements only Nebula-specific extension semantics.
|
|
13
|
+
7. A2UI protocol functionality does not belong in Platform. No A2UI surface, component, action, session, result-mapping, or rendering implementation may be added here.
|
|
14
|
+
8. No LLM-dependent protocol behavior is allowed.
|
|
15
|
+
9. Configuration, secrets, credentials, and mutable runtime state remain external to source code.
|
|
16
|
+
10. Legacy compatibility code stays outside this directory and must not be imported by this directory.
|
|
17
|
+
|
|
18
|
+
## Frozen structure
|
|
19
|
+
|
|
20
|
+
```text
|
|
21
|
+
a2a/
|
|
22
|
+
├── README.md
|
|
23
|
+
├── __init__.py
|
|
24
|
+
├── client.py
|
|
25
|
+
├── server.py
|
|
26
|
+
├── config/
|
|
27
|
+
│ ├── __init__.py
|
|
28
|
+
│ ├── client.py
|
|
29
|
+
│ └── server.py
|
|
30
|
+
├── security/
|
|
31
|
+
│ ├── __init__.py
|
|
32
|
+
│ ├── client.py
|
|
33
|
+
│ └── server.py
|
|
34
|
+
└── extensions/
|
|
35
|
+
├── __init__.py
|
|
36
|
+
├── health.py
|
|
37
|
+
├── schema_contract.py
|
|
38
|
+
└── skill_selection.py
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Do not add `models`, `services`, `adapters`, `interfaces`, `factories`, `plugins`, `execution`, or similar layering without first changing this frozen contract.
|
|
42
|
+
|
|
43
|
+
## Coding standards
|
|
44
|
+
|
|
45
|
+
- Keep modules small in responsibility and code paths direct.
|
|
46
|
+
- Prefer composition of official SDK objects over Agent Nebula wrappers.
|
|
47
|
+
- Use explicit type hints and native SDK types at public boundaries.
|
|
48
|
+
- Do not use wildcard imports. Keep package exports minimal and explicit.
|
|
49
|
+
- Do not duplicate constants or protocol definitions.
|
|
50
|
+
- Do not add speculative abstractions for hypothetical future implementations.
|
|
51
|
+
- Do not add compatibility aliases or dead migration code to this tree.
|
|
52
|
+
- Comments explain Agent Nebula-specific behavior or standards boundaries, not Python syntax.
|
|
53
|
+
- Configuration errors must fail explicitly; do not silently invent defaults for protocol/security facts advertised by an Agent Card.
|
|
54
|
+
- Security must integrate through official A2A SDK extension points such as client interceptors and server call-context builders.
|
|
55
|
+
- New functionality requires tests at the public boundary.
|
|
56
|
+
- A patch is not complete until formatting/static checks, imports, and targeted tests pass. Runtime success must not be claimed unless runtime execution occurred.
|
|
57
|
+
|
|
58
|
+
## Change test
|
|
59
|
+
|
|
60
|
+
Before adding code here, ask: **Would this functionality exist without Agent Nebula configuration, security, or a Nebula-specific extension?** If yes, it belongs to `a2a-sdk` or another standard library, not here.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Agent Nebula integration layer for the official A2A SDK.
|
|
2
|
+
|
|
3
|
+
Keep this package initializer side-effect free. Configuration, security and
|
|
4
|
+
runtime modules import one another through explicit submodules; eagerly
|
|
5
|
+
importing the server here creates cycles while Platform presets are loading.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
__all__: list[str] = []
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Native a2a-sdk client construction with Agent Nebula configuration and security."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Callable
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
7
|
+
import httpx
|
|
8
|
+
from a2a.client import A2ACardResolver, Client, ClientConfig, ClientFactory
|
|
9
|
+
from a2a.types import AgentCard
|
|
10
|
+
from a2a.utils.constants import TransportProtocol
|
|
11
|
+
from .config.client import A2AClientConfiguration
|
|
12
|
+
from .security.client import A2AClientSecurityInterceptor
|
|
13
|
+
|
|
14
|
+
class A2AClient:
|
|
15
|
+
"""Resolve Agent Cards and return native ``a2a-sdk`` clients."""
|
|
16
|
+
def __init__(self, configuration: A2AClientConfiguration, *, security: A2AClientSecurityInterceptor | None = None) -> None:
|
|
17
|
+
self.configuration = configuration
|
|
18
|
+
self.security = security
|
|
19
|
+
|
|
20
|
+
async def resolve_agent_card(self, location: str) -> AgentCard:
|
|
21
|
+
base_url, card_path = self._resolution_target(location)
|
|
22
|
+
async with self._http_client() as http_client:
|
|
23
|
+
return await A2ACardResolver(http_client, base_url).get_agent_card(relative_card_path=card_path)
|
|
24
|
+
|
|
25
|
+
async def connect(self, location: str) -> Client:
|
|
26
|
+
return self.create(await self.resolve_agent_card(location))
|
|
27
|
+
|
|
28
|
+
def create(self, agent_card: AgentCard) -> Client:
|
|
29
|
+
interceptors = [self.security] if self.security is not None else []
|
|
30
|
+
config = ClientConfig(
|
|
31
|
+
streaming=self.configuration.streaming,
|
|
32
|
+
polling=self.configuration.polling,
|
|
33
|
+
httpx_client=self._http_client(),
|
|
34
|
+
grpc_channel_factory=self._grpc_channel_factory(),
|
|
35
|
+
supported_protocol_bindings=list(self.configuration.supported_protocol_bindings),
|
|
36
|
+
use_client_preference=self.configuration.use_client_preference,
|
|
37
|
+
accepted_output_modes=list(self.configuration.accepted_output_modes),
|
|
38
|
+
)
|
|
39
|
+
return ClientFactory(config).create(agent_card, interceptors)
|
|
40
|
+
|
|
41
|
+
def _resolution_target(self, location: str) -> tuple[str, str]:
|
|
42
|
+
path = self.configuration.agent_card_path or "/.well-known/agent-card.json"
|
|
43
|
+
parsed = urlsplit(location)
|
|
44
|
+
if parsed.path.endswith(path):
|
|
45
|
+
base_path = parsed.path[:-len(path)].rstrip("/")
|
|
46
|
+
return urlunsplit((parsed.scheme, parsed.netloc, base_path, "", "")), path
|
|
47
|
+
return location, path
|
|
48
|
+
|
|
49
|
+
def _http_client(self) -> httpx.AsyncClient:
|
|
50
|
+
tls = self.configuration.tls
|
|
51
|
+
verify: bool | str = True
|
|
52
|
+
cert = None
|
|
53
|
+
if tls.trust_bundle is not None:
|
|
54
|
+
verify = str(tls.trust_bundle)
|
|
55
|
+
if tls.certificate_file is not None and tls.private_key_file is not None:
|
|
56
|
+
cert = (str(tls.certificate_file), str(tls.private_key_file))
|
|
57
|
+
return httpx.AsyncClient(verify=verify, cert=cert, timeout=self.configuration.timeout_seconds)
|
|
58
|
+
|
|
59
|
+
def _grpc_channel_factory(self) -> Callable[[str], object] | None:
|
|
60
|
+
"""Create the gRPC channel factory required by the official A2A SDK."""
|
|
61
|
+
if TransportProtocol.GRPC not in self.configuration.supported_protocol_bindings:
|
|
62
|
+
return None
|
|
63
|
+
try:
|
|
64
|
+
import grpc
|
|
65
|
+
except ImportError as exc:
|
|
66
|
+
raise RuntimeError(
|
|
67
|
+
'A2A gRPC client support requires the Platform "a2a-grpc" extra'
|
|
68
|
+
) from exc
|
|
69
|
+
|
|
70
|
+
tls = self.configuration.tls
|
|
71
|
+
if not tls.enabled:
|
|
72
|
+
def create_insecure_channel(target: str) -> object:
|
|
73
|
+
authority = target.removeprefix("grpc://").removeprefix("grpcs://")
|
|
74
|
+
return grpc.aio.insecure_channel(authority)
|
|
75
|
+
|
|
76
|
+
return create_insecure_channel
|
|
77
|
+
|
|
78
|
+
root_certificates = (
|
|
79
|
+
self._read_bytes(tls.trust_bundle) if tls.trust_bundle is not None else None
|
|
80
|
+
)
|
|
81
|
+
private_key = (
|
|
82
|
+
self._read_bytes(tls.private_key_file)
|
|
83
|
+
if tls.private_key_file is not None
|
|
84
|
+
else None
|
|
85
|
+
)
|
|
86
|
+
certificate_chain = (
|
|
87
|
+
self._read_bytes(tls.certificate_file)
|
|
88
|
+
if tls.certificate_file is not None
|
|
89
|
+
else None
|
|
90
|
+
)
|
|
91
|
+
credentials = grpc.ssl_channel_credentials(
|
|
92
|
+
root_certificates=root_certificates,
|
|
93
|
+
private_key=private_key,
|
|
94
|
+
certificate_chain=certificate_chain,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
def create_secure_channel(target: str) -> object:
|
|
98
|
+
authority = target.removeprefix("grpcs://").removeprefix("grpc://")
|
|
99
|
+
return grpc.aio.secure_channel(authority, credentials)
|
|
100
|
+
|
|
101
|
+
return create_secure_channel
|
|
102
|
+
|
|
103
|
+
@staticmethod
|
|
104
|
+
def _read_bytes(path: Path) -> bytes:
|
|
105
|
+
return path.read_bytes()
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
"""A2A configuration integration."""
|
|
2
|
+
from .client import A2AClientConfiguration, A2AClientConfigurationLoader
|
|
3
|
+
from .server import A2AListenerConfiguration, A2AListenerConfigurationLoader, A2AServerConfiguration, A2AServerConfigurationService, A2AServerTlsConfiguration
|
|
4
|
+
__all__ = ["A2AClientConfiguration", "A2AClientConfigurationLoader", "A2AListenerConfiguration", "A2AListenerConfigurationLoader", "A2AServerConfiguration", "A2AServerConfigurationService", "A2AServerTlsConfiguration"]
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"""Typed loading and validation for the dedicated A2A client configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tomllib
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from a2a.utils.constants import TransportProtocol
|
|
11
|
+
|
|
12
|
+
from agent_nebula_platform.platform.errors import ConfigurationError
|
|
13
|
+
|
|
14
|
+
_SUPPORTED_BINDINGS = {
|
|
15
|
+
TransportProtocol.JSONRPC,
|
|
16
|
+
TransportProtocol.HTTP_JSON,
|
|
17
|
+
TransportProtocol.GRPC,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class A2AClientTlsConfiguration:
|
|
23
|
+
"""TLS mode and material used by outbound A2A transports and Agent Card discovery."""
|
|
24
|
+
|
|
25
|
+
enabled: bool = False
|
|
26
|
+
trust_bundle: Path | None = None
|
|
27
|
+
certificate_file: Path | None = None
|
|
28
|
+
private_key_file: Path | None = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True, slots=True)
|
|
32
|
+
class A2AClientCredentialConfiguration:
|
|
33
|
+
"""Local secret material keyed by arbitrary remote Agent Card scheme name."""
|
|
34
|
+
|
|
35
|
+
scheme_name: str
|
|
36
|
+
secret_file: Path
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True, slots=True)
|
|
40
|
+
class A2AClientOAuthConfiguration:
|
|
41
|
+
"""Locally provisioned OAuth client identity used for discovered OAuth2 schemes."""
|
|
42
|
+
|
|
43
|
+
client_id_file: Path
|
|
44
|
+
authentication_private_key_file: Path
|
|
45
|
+
dpop_private_key_file: Path
|
|
46
|
+
key_id: str = "a2a-oauth-client"
|
|
47
|
+
algorithm: str = "ES256"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass(frozen=True, slots=True)
|
|
51
|
+
class A2AClientConfiguration:
|
|
52
|
+
"""Platform-owned settings mapped to the official A2A ``ClientConfig``."""
|
|
53
|
+
|
|
54
|
+
streaming: bool
|
|
55
|
+
polling: bool
|
|
56
|
+
supported_protocol_bindings: tuple[str, ...]
|
|
57
|
+
use_client_preference: bool
|
|
58
|
+
accepted_output_modes: tuple[str, ...]
|
|
59
|
+
timeout_seconds: float
|
|
60
|
+
agent_card_path: str | None
|
|
61
|
+
tls: A2AClientTlsConfiguration
|
|
62
|
+
credentials: tuple[A2AClientCredentialConfiguration, ...] = ()
|
|
63
|
+
oauth: A2AClientOAuthConfiguration | None = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class A2AClientConfigurationLoader:
|
|
67
|
+
"""Load ``a2aclient.conf`` without interpreting remote Agent Card security."""
|
|
68
|
+
|
|
69
|
+
def load(self, path: Path) -> A2AClientConfiguration:
|
|
70
|
+
"""Parse and validate one dedicated A2A client TOML configuration file."""
|
|
71
|
+
|
|
72
|
+
configuration_path = path.expanduser().resolve()
|
|
73
|
+
if not configuration_path.is_file():
|
|
74
|
+
raise ConfigurationError(
|
|
75
|
+
"A2A client configuration file does not exist",
|
|
76
|
+
details={"path": str(configuration_path)},
|
|
77
|
+
)
|
|
78
|
+
try:
|
|
79
|
+
with configuration_path.open("rb") as stream:
|
|
80
|
+
document = tomllib.load(stream)
|
|
81
|
+
except (OSError, tomllib.TOMLDecodeError) as exc:
|
|
82
|
+
raise ConfigurationError(
|
|
83
|
+
"A2A client configuration file is invalid",
|
|
84
|
+
details={"path": str(configuration_path)},
|
|
85
|
+
) from exc
|
|
86
|
+
|
|
87
|
+
client = self._require_table(document, "client")
|
|
88
|
+
tls = self._require_table(client, "tls", parent="client")
|
|
89
|
+
bindings = tuple(self._string_list(client, "supported_protocol_bindings"))
|
|
90
|
+
if not bindings:
|
|
91
|
+
raise ConfigurationError(
|
|
92
|
+
"A2A client must configure at least one supported protocol binding"
|
|
93
|
+
)
|
|
94
|
+
unknown = sorted(set(bindings) - _SUPPORTED_BINDINGS)
|
|
95
|
+
if unknown:
|
|
96
|
+
raise ConfigurationError(
|
|
97
|
+
"A2A client configuration contains unsupported protocol bindings",
|
|
98
|
+
details={"bindings": unknown},
|
|
99
|
+
)
|
|
100
|
+
if len(set(bindings)) != len(bindings):
|
|
101
|
+
raise ConfigurationError("A2A client protocol bindings must be unique")
|
|
102
|
+
|
|
103
|
+
timeout = self._number(client, "timeout_seconds", default=30.0)
|
|
104
|
+
if timeout <= 0:
|
|
105
|
+
raise ConfigurationError(
|
|
106
|
+
"A2A client timeout_seconds must be greater than zero"
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
tls_enabled = self._boolean(tls, "enabled", default=False)
|
|
110
|
+
trust_bundle = self._optional_path(
|
|
111
|
+
tls, "trust_bundle", configuration_path.parent
|
|
112
|
+
)
|
|
113
|
+
certificate_file = self._optional_path(
|
|
114
|
+
tls, "certificate_file", configuration_path.parent
|
|
115
|
+
)
|
|
116
|
+
private_key_file = self._optional_path(
|
|
117
|
+
tls, "private_key_file", configuration_path.parent
|
|
118
|
+
)
|
|
119
|
+
if (certificate_file is None) != (private_key_file is None):
|
|
120
|
+
raise ConfigurationError(
|
|
121
|
+
"A2A client TLS certificate_file and private_key_file must be configured together"
|
|
122
|
+
)
|
|
123
|
+
# A trust bundle controls certificate verification for outbound HTTPS
|
|
124
|
+
# (Agent Card, OAuth metadata/token endpoints, and HTTP A2A bindings)
|
|
125
|
+
# even when explicit client TLS/mTLS mode is disabled. Requiring
|
|
126
|
+
# ``enabled = true`` for a CA bundle made ordinary HTTPS callers either
|
|
127
|
+
# reject valid private PKI configuration or fall back to disabled
|
|
128
|
+
# verification. Client certificate material remains explicit mTLS
|
|
129
|
+
# configuration and therefore still requires TLS mode to be enabled.
|
|
130
|
+
if not tls_enabled and (certificate_file is not None or private_key_file is not None):
|
|
131
|
+
raise ConfigurationError(
|
|
132
|
+
"A2A client certificate material requires [client.tls].enabled = true"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
credentials_table = client.get("credentials", {})
|
|
136
|
+
if not isinstance(credentials_table, dict):
|
|
137
|
+
raise ConfigurationError("A2A client [client.credentials] must be a table")
|
|
138
|
+
credentials: list[A2AClientCredentialConfiguration] = []
|
|
139
|
+
for scheme_name, entry in credentials_table.items():
|
|
140
|
+
if (
|
|
141
|
+
not isinstance(scheme_name, str)
|
|
142
|
+
or not scheme_name.strip()
|
|
143
|
+
or not isinstance(entry, dict)
|
|
144
|
+
):
|
|
145
|
+
raise ConfigurationError(
|
|
146
|
+
"A2A client credential entries must be named tables"
|
|
147
|
+
)
|
|
148
|
+
secret_file = self._required_path(
|
|
149
|
+
entry, "secret_file", configuration_path.parent
|
|
150
|
+
)
|
|
151
|
+
credentials.append(
|
|
152
|
+
A2AClientCredentialConfiguration(
|
|
153
|
+
scheme_name=scheme_name.strip(), secret_file=secret_file
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
oauth_table = client.get("oauth")
|
|
158
|
+
oauth = None
|
|
159
|
+
if oauth_table is not None:
|
|
160
|
+
if not isinstance(oauth_table, dict):
|
|
161
|
+
raise ConfigurationError("A2A client [client.oauth] must be a table")
|
|
162
|
+
oauth = A2AClientOAuthConfiguration(
|
|
163
|
+
client_id_file=self._destination_path(
|
|
164
|
+
oauth_table, "client_id_file", configuration_path.parent
|
|
165
|
+
),
|
|
166
|
+
authentication_private_key_file=self._required_path(
|
|
167
|
+
oauth_table,
|
|
168
|
+
"authentication_private_key_file",
|
|
169
|
+
configuration_path.parent,
|
|
170
|
+
),
|
|
171
|
+
dpop_private_key_file=self._required_path(
|
|
172
|
+
oauth_table, "dpop_private_key_file", configuration_path.parent
|
|
173
|
+
),
|
|
174
|
+
key_id=str(oauth_table.get("key_id", "a2a-oauth-client")).strip(),
|
|
175
|
+
algorithm=str(oauth_table.get("algorithm", "ES256")).strip(),
|
|
176
|
+
)
|
|
177
|
+
if not oauth.key_id or not oauth.algorithm:
|
|
178
|
+
raise ConfigurationError(
|
|
179
|
+
"A2A client OAuth key_id and algorithm must be non-empty"
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
agent_card_path = client.get("agent_card_path")
|
|
183
|
+
if agent_card_path is not None:
|
|
184
|
+
if not isinstance(agent_card_path, str) or not agent_card_path.strip():
|
|
185
|
+
raise ConfigurationError(
|
|
186
|
+
"A2A client agent_card_path must be a non-empty string"
|
|
187
|
+
)
|
|
188
|
+
agent_card_path = agent_card_path.strip()
|
|
189
|
+
|
|
190
|
+
return A2AClientConfiguration(
|
|
191
|
+
streaming=self._boolean(client, "streaming", default=True),
|
|
192
|
+
polling=self._boolean(client, "polling", default=False),
|
|
193
|
+
supported_protocol_bindings=bindings,
|
|
194
|
+
use_client_preference=self._boolean(
|
|
195
|
+
client, "use_client_preference", default=False
|
|
196
|
+
),
|
|
197
|
+
accepted_output_modes=tuple(
|
|
198
|
+
self._string_list(client, "accepted_output_modes")
|
|
199
|
+
),
|
|
200
|
+
timeout_seconds=float(timeout),
|
|
201
|
+
agent_card_path=agent_card_path,
|
|
202
|
+
tls=A2AClientTlsConfiguration(
|
|
203
|
+
enabled=tls_enabled,
|
|
204
|
+
trust_bundle=trust_bundle,
|
|
205
|
+
certificate_file=certificate_file,
|
|
206
|
+
private_key_file=private_key_file,
|
|
207
|
+
),
|
|
208
|
+
credentials=tuple(credentials),
|
|
209
|
+
oauth=oauth,
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
@staticmethod
|
|
213
|
+
def _require_table(
|
|
214
|
+
document: dict[str, Any], name: str, *, parent: str | None = None
|
|
215
|
+
) -> dict[str, Any]:
|
|
216
|
+
value = document.get(name)
|
|
217
|
+
if not isinstance(value, dict):
|
|
218
|
+
location = f"[{parent}.{name}]" if parent else f"[{name}]"
|
|
219
|
+
raise ConfigurationError(f"A2A client configuration requires {location}")
|
|
220
|
+
return value
|
|
221
|
+
|
|
222
|
+
@staticmethod
|
|
223
|
+
def _boolean(document: dict[str, Any], name: str, *, default: bool) -> bool:
|
|
224
|
+
value = document.get(name, default)
|
|
225
|
+
if not isinstance(value, bool):
|
|
226
|
+
raise ConfigurationError(f"A2A client {name} must be a boolean")
|
|
227
|
+
return value
|
|
228
|
+
|
|
229
|
+
@staticmethod
|
|
230
|
+
def _number(document: dict[str, Any], name: str, *, default: float) -> float:
|
|
231
|
+
value = document.get(name, default)
|
|
232
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
233
|
+
raise ConfigurationError(f"A2A client {name} must be numeric")
|
|
234
|
+
return float(value)
|
|
235
|
+
|
|
236
|
+
@staticmethod
|
|
237
|
+
def _string_list(document: dict[str, Any], name: str) -> list[str]:
|
|
238
|
+
value = document.get(name, [])
|
|
239
|
+
if not isinstance(value, list) or any(
|
|
240
|
+
not isinstance(item, str) or not item.strip() for item in value
|
|
241
|
+
):
|
|
242
|
+
raise ConfigurationError(
|
|
243
|
+
f"A2A client {name} must be a list of non-empty strings"
|
|
244
|
+
)
|
|
245
|
+
return [item.strip() for item in value]
|
|
246
|
+
|
|
247
|
+
@staticmethod
|
|
248
|
+
def _destination_path(document: dict[str, Any], name: str, base: Path) -> Path:
|
|
249
|
+
"""Resolve an OAuth output path without requiring the file to exist yet."""
|
|
250
|
+
value = document.get(name)
|
|
251
|
+
if not isinstance(value, str) or not value.strip():
|
|
252
|
+
raise ConfigurationError(
|
|
253
|
+
f"A2A client OAuth {name} must be a non-empty path"
|
|
254
|
+
)
|
|
255
|
+
path = Path(value).expanduser()
|
|
256
|
+
if not path.is_absolute():
|
|
257
|
+
path = base / path
|
|
258
|
+
path = path.resolve()
|
|
259
|
+
if not path.parent.is_dir():
|
|
260
|
+
raise ConfigurationError(
|
|
261
|
+
f"A2A client OAuth {name} parent directory does not exist",
|
|
262
|
+
details={"path": str(path), "parent": str(path.parent)},
|
|
263
|
+
)
|
|
264
|
+
return path
|
|
265
|
+
|
|
266
|
+
@staticmethod
|
|
267
|
+
def _required_path(document: dict[str, Any], name: str, base: Path) -> Path:
|
|
268
|
+
value = document.get(name)
|
|
269
|
+
if not isinstance(value, str) or not value.strip():
|
|
270
|
+
raise ConfigurationError(f"A2A client TLS {name} is required")
|
|
271
|
+
return A2AClientConfigurationLoader._resolve_existing_path(value, name, base)
|
|
272
|
+
|
|
273
|
+
@staticmethod
|
|
274
|
+
def _optional_path(document: dict[str, Any], name: str, base: Path) -> Path | None:
|
|
275
|
+
value = document.get(name)
|
|
276
|
+
if value is None:
|
|
277
|
+
return None
|
|
278
|
+
if not isinstance(value, str) or not value.strip():
|
|
279
|
+
raise ConfigurationError(f"A2A client TLS {name} must be a non-empty path")
|
|
280
|
+
return A2AClientConfigurationLoader._resolve_existing_path(value, name, base)
|
|
281
|
+
|
|
282
|
+
@staticmethod
|
|
283
|
+
def _resolve_existing_path(value: str, name: str, base: Path) -> Path:
|
|
284
|
+
path = Path(value).expanduser()
|
|
285
|
+
if not path.is_absolute():
|
|
286
|
+
path = base / path
|
|
287
|
+
path = path.resolve()
|
|
288
|
+
if not path.is_file():
|
|
289
|
+
raise ConfigurationError(
|
|
290
|
+
f"A2A client TLS {name} does not exist", details={"path": str(path)}
|
|
291
|
+
)
|
|
292
|
+
return path
|