solace-agent-mesh 1.6.0__py3-none-any.whl → 1.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of solace-agent-mesh might be problematic. Click here for more details.
- solace_agent_mesh/agent/protocol/event_handlers.py +173 -30
- solace_agent_mesh/agent/proxies/base/component.py +35 -4
- solace_agent_mesh/agent/sac/app.py +54 -7
- solace_agent_mesh/agent/sac/component.py +84 -73
- solace_agent_mesh/agent/sac/task_execution_context.py +46 -0
- solace_agent_mesh/assets/docs/404.html +3 -3
- solace_agent_mesh/assets/docs/assets/js/{e3d9abda.2b916f9e.js → e3d9abda.6b9493d0.js} +1 -1
- solace_agent_mesh/assets/docs/assets/js/{main.20feee82.js → main.b12eac43.js} +2 -2
- solace_agent_mesh/assets/docs/assets/js/{runtime~main.0d198646.js → runtime~main.e268214e.js} +1 -1
- solace_agent_mesh/assets/docs/docs/documentation/components/agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/artifact-management/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/audio-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/data-analysis-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/embeds/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/builtin-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/cli/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/gateways/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/orchestrator/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/plugins/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/components/proxies/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deploying/debugging/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deploying/deployment-options/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deploying/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/deploying/observability/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/create-gateways/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-python-tools/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/creating-service-providers/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/evaluations/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/structure/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/bedrock-agents/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/custom-agent/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/event-mesh-gateway/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mcp-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/mongodb-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rag-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/rest-gateway/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/slack-integration/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/developing/tutorials/sql-database/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/installation/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/rbac-setup-guide/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/enterprise/single-sign-on/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/architecture/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/introduction/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/getting-started/try-agent-mesh/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/configurations/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/installation/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/large_language_models/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/installing-and-configuring/run-project/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-gateway-upgrade-to-0.3.0/index.html +3 -3
- solace_agent_mesh/assets/docs/docs/documentation/migrations/a2a-upgrade/a2a-technical-migration-map/index.html +3 -3
- solace_agent_mesh/assets/docs/lunr-index-1761248203150.json +1 -0
- solace_agent_mesh/assets/docs/lunr-index.json +1 -1
- solace_agent_mesh/assets/docs/search-doc-1761248203150.json +1 -0
- solace_agent_mesh/assets/docs/search-doc.json +1 -1
- solace_agent_mesh/cli/__init__.py +1 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-B32noGmR.js +342 -0
- solace_agent_mesh/client/webui/frontend/static/index.html +1 -1
- solace_agent_mesh/common/a2a/protocol.py +78 -0
- solace_agent_mesh/common/sac/sam_component_base.py +383 -4
- solace_agent_mesh/gateway/base/app.py +15 -0
- solace_agent_mesh/gateway/base/component.py +104 -38
- solace_agent_mesh/gateway/http_sse/component.py +1 -1
- solace_agent_mesh/gateway/http_sse/main.py +2 -2
- solace_agent_mesh/gateway/http_sse/routers/users.py +47 -1
- {solace_agent_mesh-1.6.0.dist-info → solace_agent_mesh-1.6.1.dist-info}/METADATA +1 -1
- {solace_agent_mesh-1.6.0.dist-info → solace_agent_mesh-1.6.1.dist-info}/RECORD +76 -76
- solace_agent_mesh/assets/docs/lunr-index-1761165361160.json +0 -1
- solace_agent_mesh/assets/docs/search-doc-1761165361160.json +0 -1
- solace_agent_mesh/client/webui/frontend/static/assets/main-BGTaW0uv.js +0 -342
- /solace_agent_mesh/assets/docs/assets/js/{main.20feee82.js.LICENSE.txt → main.b12eac43.js.LICENSE.txt} +0 -0
- {solace_agent_mesh-1.6.0.dist-info → solace_agent_mesh-1.6.1.dist-info}/WHEEL +0 -0
- {solace_agent_mesh-1.6.0.dist-info → solace_agent_mesh-1.6.1.dist-info}/entry_points.txt +0 -0
- {solace_agent_mesh-1.6.0.dist-info → solace_agent_mesh-1.6.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<link rel="icon" type="image/svg+xml" href="/assets/favicon-BLgzUch9.ico" />
|
|
6
6
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
7
|
<title>Solace Agent Mesh</title>
|
|
8
|
-
<script type="module" crossorigin src="/assets/main-
|
|
8
|
+
<script type="module" crossorigin src="/assets/main-B32noGmR.js"></script>
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/vendor-BEmvJSYz.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/client-CaY59VuC.js">
|
|
11
11
|
<link rel="stylesheet" crossorigin href="/assets/main-DHJKSW1S.css">
|
|
@@ -197,6 +197,84 @@ def get_sam_events_subscription_topic(namespace: str, category: str) -> str:
|
|
|
197
197
|
return f"{namespace.rstrip('/')}/sam/events/{category}/>"
|
|
198
198
|
|
|
199
199
|
|
|
200
|
+
def get_trust_card_topic(namespace: str, component_type: str, component_id: str) -> str:
|
|
201
|
+
"""
|
|
202
|
+
Returns the topic for publishing a Trust Card.
|
|
203
|
+
|
|
204
|
+
IMPORTANT: The component_id parameter MUST be the exact broker client-username
|
|
205
|
+
that the component uses to authenticate with the Solace broker. This is critical
|
|
206
|
+
for trust verification - trust cards are validated against the actual broker
|
|
207
|
+
authentication identity.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
namespace: SAM namespace
|
|
211
|
+
component_type: Type of component ("gateway", "agent", etc.)
|
|
212
|
+
component_id: MUST be the broker client-username (from broker_username config).
|
|
213
|
+
DO NOT use arbitrary IDs like agent_name or gateway_id unless they
|
|
214
|
+
match the broker_username exactly.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Topic string: {namespace}/a2a/v1/trust/{component_type}/{component_id}
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
ValueError: If any parameter is empty
|
|
221
|
+
|
|
222
|
+
Security Note:
|
|
223
|
+
Trust card verification relies on matching the topic component_id with the
|
|
224
|
+
authenticated broker client-username. Using a different value breaks the
|
|
225
|
+
security model and trust chain verification.
|
|
226
|
+
"""
|
|
227
|
+
if not namespace:
|
|
228
|
+
raise ValueError("Namespace cannot be empty.")
|
|
229
|
+
if not component_type:
|
|
230
|
+
raise ValueError("Component type cannot be empty.")
|
|
231
|
+
if not component_id:
|
|
232
|
+
raise ValueError("Component ID cannot be empty.")
|
|
233
|
+
return f"{get_a2a_base_topic(namespace)}/trust/{component_type}/{component_id}"
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def get_trust_card_subscription_topic(namespace: str, component_type: Optional[str] = None) -> str:
|
|
237
|
+
"""
|
|
238
|
+
Returns subscription pattern for Trust Cards.
|
|
239
|
+
|
|
240
|
+
Args:
|
|
241
|
+
namespace: SAM namespace
|
|
242
|
+
component_type: Optional - subscribe to specific type, or None for all types
|
|
243
|
+
|
|
244
|
+
Returns:
|
|
245
|
+
Subscription pattern
|
|
246
|
+
"""
|
|
247
|
+
if not namespace:
|
|
248
|
+
raise ValueError("Namespace cannot be empty.")
|
|
249
|
+
|
|
250
|
+
if component_type:
|
|
251
|
+
return f"{get_a2a_base_topic(namespace)}/trust/{component_type}/*"
|
|
252
|
+
else:
|
|
253
|
+
return f"{get_a2a_base_topic(namespace)}/trust/*/*"
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def extract_trust_card_info_from_topic(topic: str) -> tuple[str, str]:
|
|
257
|
+
"""
|
|
258
|
+
Extracts component type and ID from trust card topic.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
topic: Trust card topic
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Tuple of (component_type, component_id)
|
|
265
|
+
|
|
266
|
+
Raises:
|
|
267
|
+
ValueError: If topic format is invalid
|
|
268
|
+
"""
|
|
269
|
+
parts = topic.split('/')
|
|
270
|
+
if len(parts) < 6 or parts[1] != 'a2a' or parts[2] != 'v1' or parts[3] != 'trust':
|
|
271
|
+
raise ValueError(f"Invalid trust card topic format: {topic}")
|
|
272
|
+
|
|
273
|
+
component_type = parts[4]
|
|
274
|
+
component_id = parts[5]
|
|
275
|
+
return component_type, component_id
|
|
276
|
+
|
|
277
|
+
|
|
200
278
|
def subscription_to_regex(subscription: str) -> str:
|
|
201
279
|
"""Converts a Solace topic subscription string to a regex pattern."""
|
|
202
280
|
# Escape regex special characters except for Solace wildcards
|
|
@@ -6,7 +6,8 @@ import logging
|
|
|
6
6
|
import abc
|
|
7
7
|
import asyncio
|
|
8
8
|
import threading
|
|
9
|
-
|
|
9
|
+
import functools
|
|
10
|
+
from typing import Any, Optional
|
|
10
11
|
|
|
11
12
|
from solace_ai_connector.components.component_base import ComponentBase
|
|
12
13
|
|
|
@@ -15,6 +16,7 @@ from ..utils.message_utils import validate_message_size
|
|
|
15
16
|
|
|
16
17
|
log = logging.getLogger(__name__)
|
|
17
18
|
|
|
19
|
+
|
|
18
20
|
class SamComponentBase(ComponentBase, abc.ABC):
|
|
19
21
|
"""
|
|
20
22
|
Abstract base class for high-level SAM components (Agents, Gateways).
|
|
@@ -54,8 +56,294 @@ class SamComponentBase(ComponentBase, abc.ABC):
|
|
|
54
56
|
|
|
55
57
|
self._async_loop: asyncio.AbstractEventLoop | None = None
|
|
56
58
|
self._async_thread: threading.Thread | None = None
|
|
59
|
+
|
|
60
|
+
# Timer callback registry
|
|
61
|
+
self._timer_callbacks: dict[str, Any] = {}
|
|
62
|
+
self._timer_callbacks_lock = threading.Lock()
|
|
63
|
+
|
|
64
|
+
# Trust Manager integration (enterprise feature) - initialized as part of _late_init
|
|
65
|
+
self.trust_manager: Optional[Any] = None
|
|
66
|
+
|
|
57
67
|
log.info("%s SamComponentBase initialized successfully.", self.log_identifier)
|
|
58
68
|
|
|
69
|
+
def add_timer(
|
|
70
|
+
self,
|
|
71
|
+
delay_ms: int,
|
|
72
|
+
timer_id: str,
|
|
73
|
+
interval_ms: int = 0,
|
|
74
|
+
callback: Optional[Any] = None,
|
|
75
|
+
):
|
|
76
|
+
"""
|
|
77
|
+
Add a timer with optional callback.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
delay_ms: Initial delay in milliseconds
|
|
81
|
+
timer_id: Unique timer identifier
|
|
82
|
+
interval_ms: Repeat interval in milliseconds (0 for one-shot)
|
|
83
|
+
callback: Optional callback function to invoke when timer fires.
|
|
84
|
+
If provided, callback will be invoked when timer event occurs.
|
|
85
|
+
Callback receives timer_data dict as argument.
|
|
86
|
+
Callback should be thread-safe or schedule work appropriately.
|
|
87
|
+
"""
|
|
88
|
+
# Register callback if provided
|
|
89
|
+
if callback:
|
|
90
|
+
with self._timer_callbacks_lock:
|
|
91
|
+
if timer_id in self._timer_callbacks:
|
|
92
|
+
log.warning(
|
|
93
|
+
"%s Timer ID '%s' already has a registered callback. Overwriting.",
|
|
94
|
+
self.log_identifier,
|
|
95
|
+
timer_id,
|
|
96
|
+
)
|
|
97
|
+
self._timer_callbacks[timer_id] = callback
|
|
98
|
+
log.debug(
|
|
99
|
+
"%s Registered callback for timer: %s",
|
|
100
|
+
self.log_identifier,
|
|
101
|
+
timer_id,
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Call parent implementation to actually create the timer
|
|
105
|
+
super().add_timer(delay_ms=delay_ms, timer_id=timer_id, interval_ms=interval_ms)
|
|
106
|
+
|
|
107
|
+
def cancel_timer(self, timer_id: str):
|
|
108
|
+
"""
|
|
109
|
+
Cancel a timer and remove its callback if registered.
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
timer_id: Timer identifier to cancel
|
|
113
|
+
"""
|
|
114
|
+
# Remove callback registration
|
|
115
|
+
with self._timer_callbacks_lock:
|
|
116
|
+
if timer_id in self._timer_callbacks:
|
|
117
|
+
del self._timer_callbacks[timer_id]
|
|
118
|
+
log.debug(
|
|
119
|
+
"%s Unregistered callback for timer: %s",
|
|
120
|
+
self.log_identifier,
|
|
121
|
+
timer_id,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Call parent implementation to actually cancel the timer
|
|
125
|
+
super().cancel_timer(timer_id)
|
|
126
|
+
|
|
127
|
+
def process_event(self, event):
|
|
128
|
+
"""
|
|
129
|
+
Process incoming events by routing to appropriate handlers.
|
|
130
|
+
|
|
131
|
+
This base implementation handles MESSAGE and TIMER events:
|
|
132
|
+
- MESSAGE events are routed to _handle_message() abstract method
|
|
133
|
+
- TIMER events are routed to registered callbacks
|
|
134
|
+
- Other events are passed to parent class
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
event: Event object from SAC framework
|
|
138
|
+
"""
|
|
139
|
+
from solace_ai_connector.common.event import Event, EventType
|
|
140
|
+
from solace_ai_connector.common.message import Message as SolaceMessage
|
|
141
|
+
|
|
142
|
+
if event.event_type == EventType.MESSAGE:
|
|
143
|
+
message: SolaceMessage = event.data
|
|
144
|
+
topic = message.get_topic()
|
|
145
|
+
|
|
146
|
+
if not topic:
|
|
147
|
+
log.warning(
|
|
148
|
+
"%s Received message without topic. Ignoring.",
|
|
149
|
+
self.log_identifier,
|
|
150
|
+
)
|
|
151
|
+
try:
|
|
152
|
+
message.call_negative_acknowledgements()
|
|
153
|
+
except Exception as nack_e:
|
|
154
|
+
log.error(
|
|
155
|
+
"%s Failed to NACK message without topic: %s",
|
|
156
|
+
self.log_identifier,
|
|
157
|
+
nack_e,
|
|
158
|
+
)
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
# Delegate to abstract method implemented by subclass
|
|
163
|
+
self._handle_message(message, topic)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
log.error(
|
|
166
|
+
"%s Error in _handle_message for topic %s: %s",
|
|
167
|
+
self.log_identifier,
|
|
168
|
+
topic,
|
|
169
|
+
e,
|
|
170
|
+
exc_info=True,
|
|
171
|
+
)
|
|
172
|
+
try:
|
|
173
|
+
message.call_negative_acknowledgements()
|
|
174
|
+
except Exception as nack_e:
|
|
175
|
+
log.error(
|
|
176
|
+
"%s Failed to NACK message after error: %s",
|
|
177
|
+
self.log_identifier,
|
|
178
|
+
nack_e,
|
|
179
|
+
)
|
|
180
|
+
self.handle_error(e, event)
|
|
181
|
+
|
|
182
|
+
elif event.event_type == EventType.TIMER:
|
|
183
|
+
# Handle timer events via callback registry
|
|
184
|
+
timer_data = event.data
|
|
185
|
+
timer_id = timer_data.get("timer_id")
|
|
186
|
+
|
|
187
|
+
if not timer_id:
|
|
188
|
+
log.warning(
|
|
189
|
+
"%s Timer event missing timer_id: %s",
|
|
190
|
+
self.log_identifier,
|
|
191
|
+
timer_data,
|
|
192
|
+
)
|
|
193
|
+
return
|
|
194
|
+
|
|
195
|
+
# Look up registered callback
|
|
196
|
+
with self._timer_callbacks_lock:
|
|
197
|
+
callback = self._timer_callbacks.get(timer_id)
|
|
198
|
+
|
|
199
|
+
if callback:
|
|
200
|
+
try:
|
|
201
|
+
log.debug(
|
|
202
|
+
"%s Invoking registered callback for timer: %s",
|
|
203
|
+
self.log_identifier,
|
|
204
|
+
timer_id,
|
|
205
|
+
)
|
|
206
|
+
callback(timer_data)
|
|
207
|
+
except Exception as e:
|
|
208
|
+
log.error(
|
|
209
|
+
"%s Error in timer callback for %s: %s",
|
|
210
|
+
self.log_identifier,
|
|
211
|
+
timer_id,
|
|
212
|
+
e,
|
|
213
|
+
exc_info=True,
|
|
214
|
+
)
|
|
215
|
+
else:
|
|
216
|
+
log.warning(
|
|
217
|
+
"%s No callback registered for timer: %s. Timer event ignored.",
|
|
218
|
+
self.log_identifier,
|
|
219
|
+
timer_id,
|
|
220
|
+
)
|
|
221
|
+
else:
|
|
222
|
+
# Pass other event types to parent class
|
|
223
|
+
super().process_event(event)
|
|
224
|
+
|
|
225
|
+
def _handle_message(self, message, topic: str) -> None:
|
|
226
|
+
"""
|
|
227
|
+
Handle an incoming message by routing to async handler.
|
|
228
|
+
|
|
229
|
+
This base implementation schedules async processing on the component's
|
|
230
|
+
event loop. Subclasses can override this for custom sync handling,
|
|
231
|
+
or implement _handle_message_async() for async handling.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
message: The Solace message (SolaceMessage instance)
|
|
235
|
+
topic: The topic the message was received on
|
|
236
|
+
"""
|
|
237
|
+
loop = self.get_async_loop()
|
|
238
|
+
if loop and loop.is_running():
|
|
239
|
+
# Schedule async processing
|
|
240
|
+
coro = self._handle_message_async(message, topic)
|
|
241
|
+
future = asyncio.run_coroutine_threadsafe(coro, loop)
|
|
242
|
+
future.add_done_callback(
|
|
243
|
+
functools.partial(self._handle_async_message_completion, topic=topic)
|
|
244
|
+
)
|
|
245
|
+
else:
|
|
246
|
+
log.error(
|
|
247
|
+
"%s Async loop not available. Cannot process message on topic: %s",
|
|
248
|
+
self.log_identifier,
|
|
249
|
+
topic,
|
|
250
|
+
)
|
|
251
|
+
raise RuntimeError("Async loop not available for message processing")
|
|
252
|
+
|
|
253
|
+
def _handle_async_message_completion(self, future: asyncio.Future, topic: str):
|
|
254
|
+
"""Callback to handle completion of async message processing."""
|
|
255
|
+
try:
|
|
256
|
+
if future.cancelled():
|
|
257
|
+
log.warning(
|
|
258
|
+
"%s Message processing for topic %s was cancelled.",
|
|
259
|
+
self.log_identifier,
|
|
260
|
+
topic,
|
|
261
|
+
)
|
|
262
|
+
elif future.done():
|
|
263
|
+
exception = future.exception()
|
|
264
|
+
if exception is not None:
|
|
265
|
+
log.error(
|
|
266
|
+
"%s Message processing for topic %s failed: %s",
|
|
267
|
+
self.log_identifier,
|
|
268
|
+
topic,
|
|
269
|
+
exception,
|
|
270
|
+
exc_info=exception,
|
|
271
|
+
)
|
|
272
|
+
else:
|
|
273
|
+
# Handle successful completion
|
|
274
|
+
try:
|
|
275
|
+
_ = future.result()
|
|
276
|
+
log.debug(
|
|
277
|
+
"%s Message processing for topic %s completed successfully.",
|
|
278
|
+
self.log_identifier,
|
|
279
|
+
topic,
|
|
280
|
+
)
|
|
281
|
+
# Optional: Process the result if needed
|
|
282
|
+
# self._process_successful_result(result, topic)
|
|
283
|
+
except Exception as result_exception:
|
|
284
|
+
# This catches exceptions that might occur when getting the result
|
|
285
|
+
log.error(
|
|
286
|
+
"%s Error retrieving result for topic %s: %s",
|
|
287
|
+
self.log_identifier,
|
|
288
|
+
topic,
|
|
289
|
+
result_exception,
|
|
290
|
+
exc_info=result_exception,
|
|
291
|
+
)
|
|
292
|
+
else:
|
|
293
|
+
# This case shouldn't normally occur in a completion callback,
|
|
294
|
+
# but it's good defensive programming
|
|
295
|
+
log.warning(
|
|
296
|
+
"%s Future for topic %s is not done in completion handler.",
|
|
297
|
+
self.log_identifier,
|
|
298
|
+
topic,
|
|
299
|
+
)
|
|
300
|
+
except Exception as e:
|
|
301
|
+
log.error(
|
|
302
|
+
"%s Error in async message completion handler for topic %s: %s",
|
|
303
|
+
self.log_identifier,
|
|
304
|
+
topic,
|
|
305
|
+
e,
|
|
306
|
+
exc_info=True,
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
@abc.abstractmethod
|
|
310
|
+
async def _handle_message_async(self, message, topic: str) -> None:
|
|
311
|
+
"""
|
|
312
|
+
Async handler for incoming messages.
|
|
313
|
+
|
|
314
|
+
Subclasses must implement this to process messages asynchronously.
|
|
315
|
+
This runs on the component's dedicated async event loop.
|
|
316
|
+
|
|
317
|
+
Args:
|
|
318
|
+
message: The Solace message (SolaceMessage instance)
|
|
319
|
+
topic: The topic the message was received on
|
|
320
|
+
"""
|
|
321
|
+
pass
|
|
322
|
+
|
|
323
|
+
def _late_init(self):
|
|
324
|
+
"""Late initialization hook called after the component is fully set up."""
|
|
325
|
+
|
|
326
|
+
# Setup the Trust Manager if present (enterprise feature)
|
|
327
|
+
# NOTE: The Trust Manager should use component.get_broker_username() to retrieve
|
|
328
|
+
# the actual broker client-username for trust card topic construction. This is
|
|
329
|
+
# critical because trust cards MUST be published on topics that match the actual
|
|
330
|
+
# authentication identity (client-username) used to connect to the broker.
|
|
331
|
+
try:
|
|
332
|
+
from solace_agent_mesh_enterprise.common.trust import (
|
|
333
|
+
initialize_trust_manager,
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
trust_config = self.get_config("trust_manager")
|
|
337
|
+
if trust_config and trust_config.get("enabled", False):
|
|
338
|
+
self.trust_manager = initialize_trust_manager(self)
|
|
339
|
+
log.info("%s Enterprise Trust Manager initialized", self.log_identifier)
|
|
340
|
+
except ImportError:
|
|
341
|
+
log.debug("%s Enterprise Trust Manager not available", self.log_identifier)
|
|
342
|
+
except Exception as e:
|
|
343
|
+
log.error(
|
|
344
|
+
"%s Failed to initialize Trust Manager: %s", self.log_identifier, e
|
|
345
|
+
)
|
|
346
|
+
|
|
59
347
|
def publish_a2a_message(
|
|
60
348
|
self, payload: dict, topic: str, user_properties: dict | None = None
|
|
61
349
|
):
|
|
@@ -197,6 +485,10 @@ class SamComponentBase(ComponentBase, abc.ABC):
|
|
|
197
485
|
def run(self):
|
|
198
486
|
"""Starts the component's dedicated async thread."""
|
|
199
487
|
log.info("%s Starting SamComponentBase run method.", self.log_identifier)
|
|
488
|
+
|
|
489
|
+
# Do all initialization that needs to be done after we are fully setup
|
|
490
|
+
self._late_init()
|
|
491
|
+
|
|
200
492
|
if not self._async_thread or not self._async_thread.is_alive():
|
|
201
493
|
self._async_thread = threading.Thread(
|
|
202
494
|
target=self._run_async_operations,
|
|
@@ -256,14 +548,101 @@ class SamComponentBase(ComponentBase, abc.ABC):
|
|
|
256
548
|
"""Returns the dedicated asyncio event loop for this component's async tasks."""
|
|
257
549
|
return self._async_loop
|
|
258
550
|
|
|
551
|
+
def get_broker_username(self) -> Optional[str]:
|
|
552
|
+
"""
|
|
553
|
+
Returns the broker username (client-username) that this component uses
|
|
554
|
+
to authenticate with the Solace broker.
|
|
555
|
+
|
|
556
|
+
This is critical for trust card publishing and verification, as the
|
|
557
|
+
trust card topic must match the actual authentication identity.
|
|
558
|
+
|
|
559
|
+
Returns:
|
|
560
|
+
The broker username if available, None otherwise.
|
|
561
|
+
"""
|
|
562
|
+
try:
|
|
563
|
+
app = self.get_app()
|
|
564
|
+
if app and hasattr(app, "app_info"):
|
|
565
|
+
broker_config = app.app_info.get("broker", {})
|
|
566
|
+
broker_username = broker_config.get("broker_username")
|
|
567
|
+
if broker_username:
|
|
568
|
+
log.debug(
|
|
569
|
+
"%s Retrieved broker username: %s",
|
|
570
|
+
self.log_identifier,
|
|
571
|
+
broker_username,
|
|
572
|
+
)
|
|
573
|
+
return broker_username
|
|
574
|
+
else:
|
|
575
|
+
log.warning(
|
|
576
|
+
"%s Broker username not found in broker configuration",
|
|
577
|
+
self.log_identifier,
|
|
578
|
+
)
|
|
579
|
+
else:
|
|
580
|
+
log.warning(
|
|
581
|
+
"%s Unable to access app or app_info to retrieve broker username",
|
|
582
|
+
self.log_identifier,
|
|
583
|
+
)
|
|
584
|
+
except Exception as e:
|
|
585
|
+
log.error(
|
|
586
|
+
"%s Error retrieving broker username: %s",
|
|
587
|
+
self.log_identifier,
|
|
588
|
+
e,
|
|
589
|
+
exc_info=True,
|
|
590
|
+
)
|
|
591
|
+
return None
|
|
592
|
+
|
|
259
593
|
@abc.abstractmethod
|
|
260
|
-
|
|
594
|
+
def _get_component_id(self) -> str:
|
|
595
|
+
"""
|
|
596
|
+
Returns unique identifier for this component instance.
|
|
597
|
+
Must be implemented by subclasses.
|
|
598
|
+
|
|
599
|
+
Returns:
|
|
600
|
+
Unique component identifier (e.g., agent_name, gateway_id)
|
|
261
601
|
"""
|
|
262
|
-
|
|
263
|
-
|
|
602
|
+
pass
|
|
603
|
+
|
|
604
|
+
@abc.abstractmethod
|
|
605
|
+
def _get_component_type(self) -> str:
|
|
606
|
+
"""
|
|
607
|
+
Returns component type string.
|
|
608
|
+
Must be implemented by subclasses.
|
|
609
|
+
|
|
610
|
+
Returns:
|
|
611
|
+
Component type ("gateway", "agent", etc.)
|
|
264
612
|
"""
|
|
265
613
|
pass
|
|
266
614
|
|
|
615
|
+
async def _async_setup_and_run(self) -> None:
|
|
616
|
+
"""
|
|
617
|
+
Base async setup that initializes Trust Manager if present.
|
|
618
|
+
Subclasses should override and call super() first, then add their logic.
|
|
619
|
+
"""
|
|
620
|
+
# Initialize Trust Manager if present (ENTERPRISE FEATURE)
|
|
621
|
+
if self.trust_manager:
|
|
622
|
+
try:
|
|
623
|
+
log.info(
|
|
624
|
+
"%s Initializing Trust Manager with periodic publishing...",
|
|
625
|
+
self.log_identifier,
|
|
626
|
+
)
|
|
627
|
+
# Pass event loop and add_timer method to Trust Manager
|
|
628
|
+
await self.trust_manager.initialize(
|
|
629
|
+
add_timer_callback=self.add_timer,
|
|
630
|
+
event_loop=self.get_async_loop(),
|
|
631
|
+
)
|
|
632
|
+
log.info(
|
|
633
|
+
"%s Trust Manager initialized successfully", self.log_identifier
|
|
634
|
+
)
|
|
635
|
+
except Exception as e:
|
|
636
|
+
log.error(
|
|
637
|
+
"%s Failed to initialize Trust Manager: %s",
|
|
638
|
+
self.log_identifier,
|
|
639
|
+
e,
|
|
640
|
+
exc_info=True,
|
|
641
|
+
)
|
|
642
|
+
# Trust Manager failure should not prevent component startup
|
|
643
|
+
# Set to None to disable trust manager for this session
|
|
644
|
+
self.trust_manager = None
|
|
645
|
+
|
|
267
646
|
@abc.abstractmethod
|
|
268
647
|
def _pre_async_cleanup(self) -> None:
|
|
269
648
|
"""
|
|
@@ -19,6 +19,7 @@ from ...common.a2a import (
|
|
|
19
19
|
|
|
20
20
|
log = logging.getLogger(__name__)
|
|
21
21
|
|
|
22
|
+
|
|
22
23
|
class BaseGatewayComponent(ComponentBase):
|
|
23
24
|
pass
|
|
24
25
|
|
|
@@ -244,6 +245,20 @@ class BaseGatewayApp(App):
|
|
|
244
245
|
)
|
|
245
246
|
},
|
|
246
247
|
]
|
|
248
|
+
|
|
249
|
+
# Add trust card subscription if trust manager is enabled
|
|
250
|
+
trust_config = resolved_app_config_block.get("trust_manager")
|
|
251
|
+
if trust_config and trust_config.get("enabled", False):
|
|
252
|
+
from ...common.a2a.protocol import get_trust_card_subscription_topic
|
|
253
|
+
|
|
254
|
+
trust_card_topic = get_trust_card_subscription_topic(self.namespace)
|
|
255
|
+
subscriptions.append({"topic": trust_card_topic})
|
|
256
|
+
log.info(
|
|
257
|
+
"Trust Manager enabled for gateway '%s', added trust card subscription: %s",
|
|
258
|
+
self.gateway_id,
|
|
259
|
+
trust_card_topic,
|
|
260
|
+
)
|
|
261
|
+
|
|
247
262
|
log.info(
|
|
248
263
|
"Generated Solace subscriptions for gateway '%s': %s",
|
|
249
264
|
self.gateway_id,
|