agent-nebula-secureagent 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.
@@ -0,0 +1,46 @@
1
+ """Public API for Agent Nebula SecureAgent."""
2
+ from agent_nebula_platform.skills import SkillInvocation, SkillResult
3
+
4
+ from .a2ui import create_memory_a2ui_session_service
5
+ from .discovery import DiscoveredAgent, DiscoveredSkill
6
+ from .execution_trace import execution_trace
7
+ from .llm import LlmCostEstimator, LlmUsageObserver, LlmUsageRecord
8
+ from .logging import (
9
+ SecureAgentLoggingConfiguration,
10
+ configure_console_logger,
11
+ create_utc_formatter,
12
+ create_uvicorn_log_config,
13
+ load_logging_configuration,
14
+ )
15
+ from .exceptions import (
16
+ SecureAgentConfigurationError,
17
+ SecureAgentError,
18
+ SecureAgentProtocolError,
19
+ SecureAgentSkillRegistrationError,
20
+ )
21
+ from .protocol import ProtocolRole, ProtocolType
22
+ from .secure_agent import SecureAgent
23
+
24
+ __all__ = [
25
+ "create_memory_a2ui_session_service",
26
+ "DiscoveredAgent",
27
+ "DiscoveredSkill",
28
+ "execution_trace",
29
+ "SecureAgentLoggingConfiguration",
30
+ "configure_console_logger",
31
+ "create_utc_formatter",
32
+ "create_uvicorn_log_config",
33
+ "load_logging_configuration",
34
+ "LlmCostEstimator",
35
+ "LlmUsageObserver",
36
+ "LlmUsageRecord",
37
+ "ProtocolRole",
38
+ "ProtocolType",
39
+ "SecureAgent",
40
+ "SecureAgentConfigurationError",
41
+ "SecureAgentError",
42
+ "SecureAgentProtocolError",
43
+ "SecureAgentSkillRegistrationError",
44
+ "SkillInvocation",
45
+ "SkillResult",
46
+ ]
@@ -0,0 +1,42 @@
1
+ # Agent Nebula SecureAgent — A2A Frozen Requirements
2
+
3
+ This directory is the application-facing A2A facade. All code here MUST follow these requirements.
4
+
5
+ ## Frozen ownership
6
+
7
+ 1. `a2a-sdk` owns standard A2A models, execution, client operations, tasks, artifacts, streaming, cancellation, push notifications, and protocol bindings.
8
+ 2. `agent_nebula_platform.a2a` is the only Agent Nebula Platform A2A dependency allowed here.
9
+ 3. Code in this directory MUST NOT import `agent_nebula_platform.protocols.*` or `agent_nebula_secureagent.protocols.*`.
10
+ 4. SecureAgent adds only configuration-based construction, native `AgentExecutor` registration, lifecycle convenience, and delegation to Platform A2A integration.
11
+ 5. Public A2A boundaries use native `a2a-sdk` types. Do not create SecureAgent Message, Task, Artifact, RequestContext, EventQueue, or Client wrappers.
12
+ 6. Agent Card is authoritative for skills, interfaces, extensions, and security.
13
+ 7. Nebula-specific extension semantics come only from `agent_nebula_platform.a2a.extensions`.
14
+ 8. A2UI does not belong in this directory or in the SecureAgent A2A facade.
15
+ 9. No LLM-dependent protocol behavior is allowed.
16
+ 10. Configuration, credentials, secrets, and mutable runtime state remain external.
17
+
18
+ ## Frozen structure
19
+
20
+ ```text
21
+ a2a/
22
+ ├── README.md
23
+ ├── __init__.py
24
+ ├── client.py
25
+ └── server.py
26
+ ```
27
+
28
+ Do not add adapters, services, factories, interfaces, models, or compatibility layers without changing this contract first.
29
+
30
+ ## Coding standards
31
+
32
+ - Keep the facade thin and direct.
33
+ - Prefer native SDK types in signatures and return values.
34
+ - Do not duplicate Platform configuration, security, or extension logic.
35
+ - Do not import legacy A2A implementation paths.
36
+ - Do not add A2UI helpers.
37
+ - Do not convert native SDK objects to dictionaries merely to hide the SDK.
38
+ - Use explicit type hints and explicit imports.
39
+ - Keep exports minimal.
40
+ - Comments explain Agent Nebula-specific behavior, not Python syntax.
41
+ - Fail explicitly on invalid or ambiguous configuration.
42
+ - Tests validate the public boundary.
@@ -0,0 +1,5 @@
1
+ """SecureAgent A2A facade built only on Platform A2A and the official SDK."""
2
+ from .client import SecureAgentA2AClient
3
+ from .server import A2AExecutableSkill, SecureAgentA2AServer
4
+
5
+ __all__ = ["A2AExecutableSkill", "SecureAgentA2AClient", "SecureAgentA2AServer"]
@@ -0,0 +1,58 @@
1
+ """SecureAgent construction of native A2A clients."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+
6
+ from a2a.client import Client
7
+ from a2a.types import AgentCard
8
+ from agent_nebula_platform.a2a.client import A2AClient
9
+ from agent_nebula_platform.a2a.config import A2AClientConfigurationLoader
10
+ from agent_nebula_platform.a2a.security import (
11
+ A2AClientSecurityInterceptor,
12
+ A2AClientSecurityResolver,
13
+ )
14
+ from agent_nebula_platform.a2a.security.client import (
15
+ A2AApiKeyCredentialProvider,
16
+ A2ADiscoveredOAuth2CredentialProvider,
17
+ A2AHttpCredentialProvider,
18
+ A2AMutualTlsCredentialProvider,
19
+ A2AOidcCredentialProvider,
20
+ load_static_credentials,
21
+ )
22
+
23
+
24
+ class SecureAgentA2AClient:
25
+ """Apply external configuration and expose native ``a2a-sdk`` clients."""
26
+
27
+ def __init__(self, configuration_file: Path) -> None:
28
+ configuration = A2AClientConfigurationLoader().load(configuration_file)
29
+ credentials = load_static_credentials(configuration)
30
+ providers = [
31
+ A2AApiKeyCredentialProvider(credentials),
32
+ A2AHttpCredentialProvider(credentials),
33
+ A2AOidcCredentialProvider(credentials),
34
+ A2AMutualTlsCredentialProvider(
35
+ identity_configured=configuration.tls.certificate_file is not None
36
+ ),
37
+ ]
38
+ if configuration.oauth is not None:
39
+ providers.append(
40
+ A2ADiscoveredOAuth2CredentialProvider(
41
+ configuration=configuration.oauth,
42
+ tls=configuration.tls,
43
+ timeout_seconds=configuration.timeout_seconds,
44
+ )
45
+ )
46
+ self._security = A2AClientSecurityInterceptor(
47
+ A2AClientSecurityResolver(tuple(providers))
48
+ )
49
+ self._client = A2AClient(configuration, security=self._security)
50
+
51
+ async def resolve_agent_card(self, location: str) -> AgentCard:
52
+ return await self._client.resolve_agent_card(location)
53
+
54
+ async def connect(self, location: str) -> Client:
55
+ return await self._client.connect(location)
56
+
57
+ async def close(self) -> None:
58
+ await self._security.close()
@@ -0,0 +1,177 @@
1
+ """SecureAgent composition of a native A2A server."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Mapping
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+
8
+ import httpx
9
+ from typing import Any
10
+
11
+ from a2a.server.agent_execution import AgentExecutor, RequestContext
12
+ from a2a.server.events import EventQueue
13
+ from a2a.server.request_handlers import DefaultRequestHandler
14
+ from a2a.server.tasks import InMemoryTaskStore
15
+ from a2a.server.tasks.base_push_notification_sender import BasePushNotificationSender
16
+ from a2a.server.tasks.inmemory_push_notification_config_store import (
17
+ InMemoryPushNotificationConfigStore,
18
+ )
19
+
20
+ from agent_nebula_platform.a2a.runtime import A2APlatformRuntimeComposer
21
+ from agent_nebula_platform.a2a.config import (
22
+ A2AListenerConfigurationLoader,
23
+ A2AServerConfigurationService,
24
+ )
25
+ from agent_nebula_platform.a2a.extensions import (
26
+ ensure_a2ui_extension,
27
+ ensure_schema_contract_extension,
28
+ ensure_skill_selection_extension,
29
+ selected_skill_id,
30
+ validate_executable_skills,
31
+ )
32
+ from agent_nebula_platform.a2a.security import (
33
+ A2AAuthorizedRequestHandler,
34
+ A2AServerSecurityFactory,
35
+ )
36
+ from agent_nebula_platform.a2a.security.server import A2AConfiguredSecurityLoader
37
+ from agent_nebula_platform.a2ui.configuration import A2UIConfiguration
38
+
39
+
40
+
41
+ @dataclass(frozen=True, slots=True)
42
+ class A2AExecutableSkill:
43
+ """Bind one advertised skill identifier to its native SDK executor."""
44
+
45
+ skill_id: str
46
+ executor: AgentExecutor
47
+ schema_contract_id: str | None = None
48
+ input_schema: Mapping[str, Any] | None = None
49
+ output_schema: Mapping[str, Any] | None = None
50
+
51
+
52
+ class _SkillDispatcher(AgentExecutor):
53
+ """Dispatch only the Nebula skill-selection extension; execution stays native."""
54
+
55
+ def __init__(self, skills: tuple[A2AExecutableSkill, ...]) -> None:
56
+ self._skills = {skill.skill_id: skill.executor for skill in skills}
57
+ self._task_skills: dict[str, str] = {}
58
+
59
+ async def execute(self, context: RequestContext, event_queue: EventQueue) -> None:
60
+ skill_id = selected_skill_id(
61
+ metadata=context.metadata,
62
+ requested_extensions=context.requested_extensions,
63
+ )
64
+ if skill_id is None:
65
+ if len(self._skills) != 1:
66
+ raise ValueError(
67
+ "Multi-skill A2A agent requires the Nebula skill-selection extension"
68
+ )
69
+ skill_id = next(iter(self._skills))
70
+ executor = self._skills.get(skill_id)
71
+ if executor is None:
72
+ raise ValueError(f"Selected A2A skill is not registered: {skill_id}")
73
+
74
+ # Authentication and skill authorization are complete before a2a-sdk
75
+ # invokes this dispatcher. This class only routes the selected skill to
76
+ # its application AgentExecutor.
77
+
78
+ if context.task_id is not None:
79
+ self._task_skills[context.task_id] = skill_id
80
+ await executor.execute(context, event_queue)
81
+
82
+ async def cancel(self, context: RequestContext, event_queue: EventQueue) -> None:
83
+ skill_id = self._task_skills.get(context.task_id or "")
84
+ if skill_id is None and len(self._skills) == 1:
85
+ skill_id = next(iter(self._skills))
86
+ if skill_id is None:
87
+ raise ValueError("Cannot route cancellation without the selected skill")
88
+ try:
89
+ await self._skills[skill_id].cancel(context, event_queue)
90
+ finally:
91
+ if context.task_id is not None:
92
+ self._task_skills.pop(context.task_id, None)
93
+
94
+
95
+ class SecureAgentA2AServer:
96
+ """Compose Platform A2A configuration/security with native SDK execution."""
97
+
98
+ def __init__(
99
+ self,
100
+ *,
101
+ configuration_file: Path,
102
+ skills: tuple[A2AExecutableSkill, ...],
103
+ application_config: Path,
104
+ nebula_config: Path | None,
105
+ a2ui_config: Path | None,
106
+ ) -> None:
107
+ server = A2AServerConfigurationService().load(configuration_file)
108
+ listener = A2AListenerConfigurationLoader().load(configuration_file)
109
+ validate_executable_skills(server.agent_card, tuple(skill.skill_id for skill in skills))
110
+ ensure_skill_selection_extension(server.agent_card, required=False)
111
+ ensure_schema_contract_extension(server.agent_card, skills)
112
+ if a2ui_config is not None:
113
+ a2ui = A2UIConfiguration.load(a2ui_config)
114
+ if a2ui.enabled:
115
+ ensure_a2ui_extension(
116
+ server.agent_card,
117
+ supported_catalog_ids=a2ui.supported_catalog_ids,
118
+ accepts_inline_catalogs=a2ui.accepts_inline_catalogs,
119
+ )
120
+
121
+ authenticators = A2AConfiguredSecurityLoader().load(
122
+ configuration_file,
123
+ mtls_transport_enforced=listener.tls.require_client_certificate,
124
+ )
125
+ self._security = A2AServerSecurityFactory().create(
126
+ agent_card=server.agent_card,
127
+ trust_bundle_file=listener.trust_bundle_file,
128
+ authenticators=authenticators,
129
+ )
130
+ self._push_http_client: httpx.AsyncClient | None = None
131
+ push_config_store = None
132
+ push_sender = None
133
+ if server.agent_card.capabilities.push_notifications:
134
+ push_config_store = InMemoryPushNotificationConfigStore()
135
+ self._push_http_client = httpx.AsyncClient()
136
+ push_sender = BasePushNotificationSender(
137
+ self._push_http_client,
138
+ push_config_store,
139
+ )
140
+
141
+ native_handler = DefaultRequestHandler(
142
+ agent_executor=_SkillDispatcher(skills),
143
+ task_store=InMemoryTaskStore(),
144
+ agent_card=server.agent_card,
145
+ push_config_store=push_config_store,
146
+ push_sender=push_sender,
147
+ extended_agent_card=server.extended_agent_card,
148
+ )
149
+ handler = A2AAuthorizedRequestHandler(
150
+ inner=native_handler,
151
+ agent_card=server.agent_card,
152
+ security=self._security.service,
153
+ )
154
+ # Keep native a2a-sdk request handling, but host it inside the existing
155
+ # Platform Runtime. The Runtime is required for restricted configuration
156
+ # paths and for onboarding, health, metrics, telemetry, state and lifecycle.
157
+ self._runtime = A2APlatformRuntimeComposer().compose(
158
+ application_config=application_config,
159
+ server_config=configuration_file,
160
+ nebula_config=nebula_config,
161
+ server_configuration=server,
162
+ listener=listener,
163
+ request_handler=handler,
164
+ security=self._security,
165
+ )
166
+
167
+ @property
168
+ def application(self):
169
+ return self._runtime.app
170
+
171
+ def run(self) -> None:
172
+ self._runtime.run()
173
+
174
+ async def close(self) -> None:
175
+ await self._runtime.close()
176
+ if self._push_http_client is not None:
177
+ await self._push_http_client.aclose()
@@ -0,0 +1,50 @@
1
+ """Public SecureAgent A2UI client/runtime facade.
2
+
3
+ Applications import A2UI runtime types from SecureAgent rather than depending on
4
+ Agent Nebula Platform directly. Platform remains the implementation owner; this
5
+ module freezes the application-facing names needed by generic A2UI clients such
6
+ as Agent Explorer and orchestrators.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from agent_nebula_platform.a2ui import (
12
+ A2UIActionDispatcher,
13
+ A2UIActionRecord,
14
+ A2UIComponentPolicy,
15
+ A2UIMessage,
16
+ A2UIProtocolPolicy,
17
+ A2UISessionService,
18
+ A2UISessionSnapshot,
19
+ A2UISurface,
20
+ A2UISurfaceFrame,
21
+ A2UISurfaceMode,
22
+ A2UITaskReference,
23
+ MemoryA2UISessionStore,
24
+ )
25
+ from agent_nebula_platform.platform.events import NullEventSink
26
+
27
+
28
+ def create_memory_a2ui_session_service() -> A2UISessionService:
29
+ """Create an in-memory A2UI session service through the SecureAgent API."""
30
+
31
+ return A2UISessionService(
32
+ store=MemoryA2UISessionStore(),
33
+ events=NullEventSink(),
34
+ )
35
+
36
+
37
+ __all__ = [
38
+ "A2UIActionDispatcher",
39
+ "A2UIActionRecord",
40
+ "A2UIComponentPolicy",
41
+ "A2UIMessage",
42
+ "A2UIProtocolPolicy",
43
+ "A2UISessionService",
44
+ "A2UISessionSnapshot",
45
+ "A2UISurface",
46
+ "A2UISurfaceFrame",
47
+ "A2UISurfaceMode",
48
+ "A2UITaskReference",
49
+ "create_memory_a2ui_session_service",
50
+ ]
@@ -0,0 +1,156 @@
1
+ """Stable developer-facing Agent Nebula discovery models."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping, Sequence
6
+ from dataclasses import dataclass, field
7
+ from typing import Any
8
+
9
+
10
+ @dataclass(frozen=True, slots=True)
11
+ class DiscoveredSkill:
12
+ """One skill advertised by a provider returned from Agent Nebula discovery."""
13
+
14
+ skill_id: str
15
+ name: str | None = None
16
+ description: str | None = None
17
+ required_scopes: tuple[str, ...] = ()
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class DiscoveredAgent:
22
+ """Protocol-neutral discovery result; remote protocol facts remain non-authoritative."""
23
+
24
+ agent_id: str
25
+ display_name: str
26
+ external_agent_id: str | None = None
27
+ provider_id: str | None = None
28
+ provider_name: str | None = None
29
+ category: str | None = None
30
+ tags: tuple[str, ...] = ()
31
+ trust_status: str | None = None
32
+ lifecycle_status: str | None = None
33
+ agent_card_url: str | None = None
34
+ a2a_endpoint: str | None = None
35
+ protocols: tuple[str, ...] = ()
36
+ skills: tuple[DiscoveredSkill, ...] = ()
37
+ raw_document: Mapping[str, Any] = field(default_factory=dict)
38
+
39
+ @property
40
+ def invokable(self) -> bool:
41
+ """Return whether discovery supplied a location from which to resolve the Agent Card."""
42
+ return bool(self.agent_card_url or self.a2a_endpoint)
43
+
44
+ def require_agent_card_location(self) -> str:
45
+ """Return discovery bootstrap data without treating it as invocation authority.
46
+
47
+ An explicit Agent Card URL is preferred. ``a2a_endpoint`` remains only as
48
+ a compatibility fallback for registries that publish a base location but
49
+ not an Agent Card URL. Platform resolves the authoritative Agent Card
50
+ before selecting the actual A2A interface or security mechanism.
51
+ """
52
+ location = self.agent_card_url or self.a2a_endpoint
53
+ if not location:
54
+ raise ValueError("Discovered agent does not expose an Agent Card location")
55
+ return location
56
+
57
+
58
+ class DiscoveryNormalizer:
59
+ """Normalize Registry response envelopes without coupling to one API revision."""
60
+
61
+ def normalize(self, payload: Mapping[str, Any] | Sequence[Any]) -> tuple[DiscoveredAgent, ...]:
62
+ return tuple(
63
+ agent
64
+ for record in self._records(payload)
65
+ if (agent := self._agent(record)) is not None
66
+ )
67
+
68
+ @staticmethod
69
+ def _records(payload: Mapping[str, Any] | Sequence[Any]) -> tuple[Mapping[str, Any], ...]:
70
+ if isinstance(payload, Sequence) and not isinstance(payload, (str, bytes, bytearray)):
71
+ return tuple(item for item in payload if isinstance(item, Mapping))
72
+ if not isinstance(payload, Mapping):
73
+ return ()
74
+ for key in ("items", "agents", "results"):
75
+ values = payload.get(key)
76
+ if isinstance(values, Sequence) and not isinstance(values, (str, bytes, bytearray)):
77
+ return tuple(item for item in values if isinstance(item, Mapping))
78
+ return (payload,) if any(key in payload for key in ("agent_id", "id", "external_agent_id")) else ()
79
+
80
+ @classmethod
81
+ def _agent(cls, record: Mapping[str, Any]) -> DiscoveredAgent | None:
82
+ document = cls._document(record)
83
+ endpoints = cls._first_mapping(record.get("endpoints"), document.get("endpoints"))
84
+ provider = cls._mapping(record.get("provider"))
85
+ agent_id = cls._first_string(record.get("agent_id"), record.get("id"), record.get("external_agent_id"), document.get("id"))
86
+ if agent_id is None:
87
+ return None
88
+ a2a_endpoint = cls._first_string(record.get("a2a_endpoint"), endpoints.get("primary"), endpoints.get("a2a"), document.get("url"))
89
+ categories = cls._strings(record.get("categories"))
90
+ return DiscoveredAgent(
91
+ agent_id=agent_id,
92
+ display_name=cls._first_string(record.get("display_name"), record.get("name"), document.get("name")) or agent_id,
93
+ external_agent_id=cls._first_string(record.get("external_agent_id"), document.get("id")),
94
+ provider_id=cls._first_string(record.get("provider_id"), provider.get("provider_id"), provider.get("id")),
95
+ provider_name=cls._first_string(record.get("provider_name"), provider.get("display_name"), provider.get("name")),
96
+ category=cls._first_string(record.get("category"), document.get("category")) or (categories[0] if categories else None),
97
+ tags=cls._strings(record.get("tags") or document.get("tags")),
98
+ trust_status=cls._first_string(record.get("trust_status"), record.get("trust")),
99
+ lifecycle_status=cls._first_string(record.get("lifecycle_status"), record.get("status")),
100
+ agent_card_url=cls._first_string(record.get("agent_card_url"), record.get("agent_card_uri"), endpoints.get("agent_card"), document.get("agent_card_url")),
101
+ a2a_endpoint=a2a_endpoint,
102
+ protocols=cls._strings(record.get("protocols") or document.get("protocols")),
103
+ skills=cls._skills(record, document),
104
+ raw_document=dict(record),
105
+ )
106
+
107
+ @classmethod
108
+ def _skills(cls, record: Mapping[str, Any], document: Mapping[str, Any]) -> tuple[DiscoveredSkill, ...]:
109
+ values = document.get("skills") or record.get("skills")
110
+ if not isinstance(values, Sequence) or isinstance(values, (str, bytes, bytearray)):
111
+ return ()
112
+ result: list[DiscoveredSkill] = []
113
+ for value in values:
114
+ if not isinstance(value, Mapping):
115
+ continue
116
+ skill_id = cls._first_string(value.get("skill_id"), value.get("id"))
117
+ if skill_id:
118
+ result.append(DiscoveredSkill(
119
+ skill_id=skill_id,
120
+ name=cls._first_string(value.get("name")),
121
+ description=cls._first_string(value.get("description")),
122
+ required_scopes=cls._strings(value.get("required_scopes")),
123
+ ))
124
+ return tuple(result)
125
+
126
+ @classmethod
127
+ def _document(cls, record: Mapping[str, Any]) -> Mapping[str, Any]:
128
+ return cls._first_mapping(
129
+ record.get("discovery_document"), record.get("agent_card_document"),
130
+ record.get("agent_card"), record.get("document"), record.get("card")
131
+ )
132
+
133
+ @staticmethod
134
+ def _mapping(value: Any) -> Mapping[str, Any]:
135
+ return value if isinstance(value, Mapping) else {}
136
+
137
+ @classmethod
138
+ def _first_mapping(cls, *values: Any) -> Mapping[str, Any]:
139
+ for value in values:
140
+ mapped = cls._mapping(value)
141
+ if mapped:
142
+ return mapped
143
+ return {}
144
+
145
+ @staticmethod
146
+ def _first_string(*values: Any) -> str | None:
147
+ for value in values:
148
+ if isinstance(value, str) and value.strip():
149
+ return value.strip()
150
+ return None
151
+
152
+ @staticmethod
153
+ def _strings(value: Any) -> tuple[str, ...]:
154
+ if not isinstance(value, Sequence) or isinstance(value, (str, bytes, bytearray)):
155
+ return ()
156
+ return tuple(dict.fromkeys(item.strip() for item in value if isinstance(item, str) and item.strip()))
@@ -0,0 +1,17 @@
1
+ """Stable exceptions exposed by the SecureAgent developer API."""
2
+
3
+
4
+ class SecureAgentError(Exception):
5
+ """Base exception for failures reported through the SecureAgent facade."""
6
+
7
+
8
+ class SecureAgentConfigurationError(SecureAgentError):
9
+ """Raised when facade or protocol configuration is invalid."""
10
+
11
+
12
+ class SecureAgentSkillRegistrationError(SecureAgentError):
13
+ """Raised when a business skill cannot be registered safely."""
14
+
15
+
16
+ class SecureAgentProtocolError(SecureAgentError):
17
+ """Raised when a requested protocol capability is unavailable or invalid."""
@@ -0,0 +1,82 @@
1
+ """Public SecureAgent facade for correlated Platform execution tracing.
2
+
3
+ SecureAgent deliberately exposes only the lifecycle boundary needed by an
4
+ application host. Agent Nebula Platform remains responsible for creating the
5
+ low-level, transport/security-aware events generated by discovery, OAuth,
6
+ DPoP, Agent Card resolution, and A2A operations. The application remains
7
+ responsible for choosing the execution identifier and deciding how the
8
+ already-sanitized events are persisted or rendered.
9
+
10
+ Keeping this context manager in SecureAgent prevents application code from
11
+ importing Platform implementation primitives directly while preserving one
12
+ correlation context across all nested Platform operations.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ from collections.abc import Callable, Iterator, Mapping
18
+ from contextlib import contextmanager
19
+ from typing import Any
20
+
21
+ from agent_nebula_platform.platform.execution_trace import (
22
+ activate_execution_trace,
23
+ reset_execution_trace,
24
+ )
25
+
26
+ ExecutionTraceRecorder = Callable[[Mapping[str, Any]], None]
27
+
28
+
29
+ @contextmanager
30
+ def execution_trace(
31
+ execution_id: str,
32
+ *,
33
+ recorder: ExecutionTraceRecorder,
34
+ ) -> Iterator[None]:
35
+ """Correlate nested SecureAgent/Platform operations with one host execution.
36
+
37
+ This function provides the public execution-trace boundary for applications
38
+ built on SecureAgent. While the context is active, Platform operations can:
39
+
40
+ * attach the same execution identifier to supported outbound requests;
41
+ * emit semantic events for discovery, Agent Card, OAuth/DPoP, and A2A work;
42
+ * redact sensitive security material before an event reaches ``recorder``;
43
+ * forward those sanitized events to the host without coupling Platform to
44
+ the host's persistence, workflow, or user-interface implementation.
45
+
46
+ SecureAgent does not create or persist the execution identifier and does
47
+ not perform a second interpretation of Platform events. The caller owns the
48
+ trace lifecycle and supplies the recorder used by its observability model.
49
+
50
+ Args:
51
+ execution_id: Host-owned correlation identifier shared by all nested
52
+ Platform operations executed inside this context. Leading/trailing
53
+ whitespace is ignored and a blank identifier is rejected.
54
+ recorder: Host callback that receives sanitized Platform event
55
+ mappings. Each event is copied before forwarding so host code cannot
56
+ mutate the Platform-owned event object.
57
+
58
+ Raises:
59
+ ValueError: If ``execution_id`` is empty or contains only whitespace.
60
+
61
+ Notes:
62
+ The previous Platform context is restored in ``finally``. This makes
63
+ nesting safe and prevents a failed workflow from leaking its execution
64
+ correlation into later requests handled by the same process/context.
65
+ """
66
+
67
+ normalized_execution_id = execution_id.strip()
68
+ if not normalized_execution_id:
69
+ raise ValueError("execution_id must not be blank")
70
+
71
+ def forward(event: dict[str, Any]) -> None:
72
+ # Platform has already applied its security redaction boundary. Copying
73
+ # here protects the Platform-owned event object from host-side mutation.
74
+ recorder(dict(event))
75
+
76
+ token = activate_execution_trace(normalized_execution_id, recorder=forward)
77
+ try:
78
+ yield
79
+ finally:
80
+ # Always restore the prior ContextVar token, including exception paths.
81
+ # This is required for nested traces and long-lived async server workers.
82
+ reset_execution_trace(token)
@@ -0,0 +1,13 @@
1
+ """Public LLM observability facade for SecureAgent applications.
2
+
3
+ Applications can record provider-reported LLM usage without importing Agent
4
+ Nebula Platform implementation modules directly.
5
+ """
6
+
7
+ from agent_nebula_platform.platform.llm import (
8
+ LlmCostEstimator,
9
+ LlmUsageObserver,
10
+ LlmUsageRecord,
11
+ )
12
+
13
+ __all__ = ["LlmCostEstimator", "LlmUsageObserver", "LlmUsageRecord"]