omnibase_infra 0.2.2__py3-none-any.whl → 0.2.3__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.
- omnibase_infra/__init__.py +1 -1
- omnibase_infra/adapters/adapter_onex_tool_execution.py +6 -1
- omnibase_infra/capabilities/__init__.py +15 -0
- omnibase_infra/capabilities/capability_inference_rules.py +211 -0
- omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
- omnibase_infra/capabilities/intent_type_extractor.py +160 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +1 -1
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +1 -1
- omnibase_infra/enums/__init__.py +6 -0
- omnibase_infra/enums/enum_handler_error_type.py +10 -0
- omnibase_infra/enums/enum_handler_source_mode.py +72 -0
- omnibase_infra/enums/enum_kafka_acks.py +99 -0
- omnibase_infra/event_bus/event_bus_kafka.py +1 -1
- omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +59 -10
- omnibase_infra/handlers/__init__.py +8 -1
- omnibase_infra/handlers/handler_consul.py +7 -1
- omnibase_infra/handlers/handler_db.py +8 -2
- omnibase_infra/handlers/handler_http.py +8 -2
- omnibase_infra/handlers/handler_intent.py +387 -0
- omnibase_infra/handlers/handler_mcp.py +10 -1
- omnibase_infra/handlers/handler_vault.py +11 -5
- omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +7 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +7 -0
- omnibase_infra/mixins/mixin_node_introspection.py +18 -0
- omnibase_infra/models/discovery/model_introspection_config.py +11 -0
- omnibase_infra/models/handlers/__init__.py +38 -5
- omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +4 -4
- omnibase_infra/models/handlers/model_contract_discovery_result.py +6 -4
- omnibase_infra/models/handlers/model_handler_source_config.py +220 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +9 -0
- omnibase_infra/models/runtime/model_handler_contract.py +25 -9
- omnibase_infra/models/runtime/model_loaded_handler.py +9 -0
- omnibase_infra/nodes/node_registration_orchestrator/plugin.py +1 -1
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +7 -7
- omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +4 -3
- omnibase_infra/nodes/node_registration_storage_effect/node.py +4 -1
- omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +1 -1
- omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +4 -1
- omnibase_infra/protocols/__init__.py +2 -0
- omnibase_infra/protocols/protocol_container_aware.py +200 -0
- omnibase_infra/runtime/__init__.py +39 -0
- omnibase_infra/runtime/handler_bootstrap_source.py +26 -33
- omnibase_infra/runtime/handler_contract_config_loader.py +1 -1
- omnibase_infra/runtime/handler_contract_source.py +10 -51
- omnibase_infra/runtime/handler_identity.py +81 -0
- omnibase_infra/runtime/handler_plugin_loader.py +15 -0
- omnibase_infra/runtime/handler_registry.py +11 -3
- omnibase_infra/runtime/handler_source_resolver.py +326 -0
- omnibase_infra/runtime/protocol_lifecycle_executor.py +6 -6
- omnibase_infra/runtime/registry/registry_protocol_binding.py +13 -13
- omnibase_infra/runtime/registry_contract_source.py +693 -0
- omnibase_infra/runtime/service_kernel.py +1 -1
- omnibase_infra/runtime/service_runtime_host_process.py +463 -190
- omnibase_infra/runtime/util_wiring.py +12 -3
- omnibase_infra/services/__init__.py +21 -0
- omnibase_infra/services/corpus_capture.py +7 -1
- omnibase_infra/services/mcp/mcp_server_lifecycle.py +9 -3
- omnibase_infra/services/registry_api/main.py +31 -13
- omnibase_infra/services/registry_api/service.py +10 -19
- omnibase_infra/services/service_timeout_emitter.py +7 -1
- omnibase_infra/services/service_timeout_scanner.py +7 -3
- omnibase_infra/services/session/__init__.py +56 -0
- omnibase_infra/services/session/config_consumer.py +120 -0
- omnibase_infra/services/session/config_store.py +139 -0
- omnibase_infra/services/session/consumer.py +1007 -0
- omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
- omnibase_infra/services/session/store.py +997 -0
- omnibase_infra/utils/__init__.py +19 -0
- omnibase_infra/utils/util_atomic_file.py +261 -0
- omnibase_infra/utils/util_db_transaction.py +239 -0
- omnibase_infra/utils/util_retry_optimistic.py +281 -0
- omnibase_infra/validation/validation_exemptions.yaml +27 -0
- {omnibase_infra-0.2.2.dist-info → omnibase_infra-0.2.3.dist-info}/METADATA +3 -3
- {omnibase_infra-0.2.2.dist-info → omnibase_infra-0.2.3.dist-info}/RECORD +77 -56
- {omnibase_infra-0.2.2.dist-info → omnibase_infra-0.2.3.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.2.dist-info → omnibase_infra-0.2.3.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.2.2.dist-info → omnibase_infra-0.2.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Intent Handler - Temporary demo wiring for intent graph operations.
|
|
4
|
+
|
|
5
|
+
Wraps HandlerGraph to provide intent-specific graph operations for the demo.
|
|
6
|
+
This is temporary hardcoded routing that will be replaced by contract-driven
|
|
7
|
+
handler routing in production.
|
|
8
|
+
|
|
9
|
+
Supported Operations:
|
|
10
|
+
- intent.store: Store an intent as a graph node with label "Intent"
|
|
11
|
+
- intent.query_session: Query intents by session_id property
|
|
12
|
+
- intent.query_distribution: Get intent count/statistics
|
|
13
|
+
|
|
14
|
+
Note:
|
|
15
|
+
This is TEMPORARY demo wiring. Keep it simple and focused on the demo use case.
|
|
16
|
+
Production implementation should use contract-driven handler routing.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# TODO(OMN-1515): Remove demo wiring after intent routing is contract-driven
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import logging
|
|
24
|
+
from uuid import UUID, uuid4
|
|
25
|
+
|
|
26
|
+
from omnibase_core.container import ModelONEXContainer
|
|
27
|
+
from omnibase_core.types import JsonType
|
|
28
|
+
from omnibase_infra.enums import EnumInfraTransportType
|
|
29
|
+
from omnibase_infra.errors import (
|
|
30
|
+
ModelInfraErrorContext,
|
|
31
|
+
RuntimeHostError,
|
|
32
|
+
)
|
|
33
|
+
from omnibase_infra.handlers.handler_graph import HandlerGraph
|
|
34
|
+
from omnibase_infra.mixins import MixinEnvelopeExtraction
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
HANDLER_ID_INTENT: str = "intent-handler"
|
|
39
|
+
_SUPPORTED_OPERATIONS: frozenset[str] = frozenset(
|
|
40
|
+
{
|
|
41
|
+
"intent.store",
|
|
42
|
+
"intent.query_session",
|
|
43
|
+
"intent.query_distribution",
|
|
44
|
+
}
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class HandlerIntent(MixinEnvelopeExtraction): # DEMO ONLY
|
|
49
|
+
"""Intent handler wrapping HandlerGraph for intent-specific operations.
|
|
50
|
+
|
|
51
|
+
This handler provides a simplified interface for storing and querying
|
|
52
|
+
intents in the graph database. It wraps HandlerGraph and translates
|
|
53
|
+
intent-specific operations to graph operations.
|
|
54
|
+
|
|
55
|
+
Note:
|
|
56
|
+
This is temporary demo wiring. The handler assumes HandlerGraph
|
|
57
|
+
is already initialized and passed via config during initialize().
|
|
58
|
+
|
|
59
|
+
Idempotency:
|
|
60
|
+
- intent.store: NOT idempotent (creates new node each call)
|
|
61
|
+
- intent.query_session: Idempotent (read-only query)
|
|
62
|
+
- intent.query_distribution: Idempotent (read-only aggregation)
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, container: ModelONEXContainer) -> None:
|
|
66
|
+
"""Initialize HandlerIntent with ONEX container for dependency injection.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
container: ONEX container for dependency injection.
|
|
70
|
+
"""
|
|
71
|
+
self._container = container
|
|
72
|
+
self._graph_handler: HandlerGraph | None = None
|
|
73
|
+
self._initialized: bool = False
|
|
74
|
+
|
|
75
|
+
async def initialize(self, config: dict[str, object]) -> None:
|
|
76
|
+
"""Initialize the intent handler.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
config: Configuration dict containing:
|
|
80
|
+
- graph_handler: Pre-initialized HandlerGraph instance (required)
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
RuntimeHostError: If graph_handler is missing or invalid.
|
|
84
|
+
"""
|
|
85
|
+
init_correlation_id = uuid4()
|
|
86
|
+
|
|
87
|
+
logger.info(
|
|
88
|
+
"Initializing %s",
|
|
89
|
+
self.__class__.__name__,
|
|
90
|
+
extra={
|
|
91
|
+
"handler": self.__class__.__name__,
|
|
92
|
+
"correlation_id": str(init_correlation_id),
|
|
93
|
+
},
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
graph_handler = config.get("graph_handler")
|
|
97
|
+
if not isinstance(graph_handler, HandlerGraph):
|
|
98
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
99
|
+
transport_type=EnumInfraTransportType.GRAPH,
|
|
100
|
+
operation="initialize",
|
|
101
|
+
target_name="intent_handler",
|
|
102
|
+
correlation_id=init_correlation_id,
|
|
103
|
+
)
|
|
104
|
+
raise RuntimeHostError(
|
|
105
|
+
"Missing or invalid 'graph_handler' in config - "
|
|
106
|
+
"must be an initialized HandlerGraph instance",
|
|
107
|
+
context=ctx,
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
self._graph_handler = graph_handler
|
|
111
|
+
self._initialized = True
|
|
112
|
+
|
|
113
|
+
logger.info(
|
|
114
|
+
"%s initialized successfully",
|
|
115
|
+
self.__class__.__name__,
|
|
116
|
+
extra={
|
|
117
|
+
"handler": self.__class__.__name__,
|
|
118
|
+
"correlation_id": str(init_correlation_id),
|
|
119
|
+
},
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
async def shutdown(self) -> None:
|
|
123
|
+
"""Shutdown the intent handler.
|
|
124
|
+
|
|
125
|
+
Note:
|
|
126
|
+
This handler does not own the graph handler, so we do not
|
|
127
|
+
shut it down here. The caller is responsible for managing
|
|
128
|
+
the graph handler lifecycle.
|
|
129
|
+
"""
|
|
130
|
+
self._graph_handler = None
|
|
131
|
+
self._initialized = False
|
|
132
|
+
logger.info("HandlerIntent shutdown complete")
|
|
133
|
+
|
|
134
|
+
async def execute(self, envelope: dict[str, object]) -> dict[str, object]:
|
|
135
|
+
"""Execute intent operation from envelope.
|
|
136
|
+
|
|
137
|
+
Args:
|
|
138
|
+
envelope: Request envelope containing:
|
|
139
|
+
- operation: Intent operation (intent.store, intent.query_session, etc.)
|
|
140
|
+
- payload: dict with operation-specific parameters
|
|
141
|
+
- correlation_id: Optional correlation ID for tracing
|
|
142
|
+
- envelope_id: Optional envelope ID for causality tracking
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
dict containing operation result with:
|
|
146
|
+
- success: bool indicating operation success
|
|
147
|
+
- data: Operation-specific result data
|
|
148
|
+
- correlation_id: UUID string for tracing
|
|
149
|
+
|
|
150
|
+
Raises:
|
|
151
|
+
RuntimeHostError: If handler not initialized or invalid input.
|
|
152
|
+
"""
|
|
153
|
+
correlation_id = self._extract_correlation_id(envelope)
|
|
154
|
+
|
|
155
|
+
if not self._initialized or self._graph_handler is None:
|
|
156
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
157
|
+
transport_type=EnumInfraTransportType.GRAPH,
|
|
158
|
+
operation="execute",
|
|
159
|
+
target_name="intent_handler",
|
|
160
|
+
correlation_id=correlation_id,
|
|
161
|
+
)
|
|
162
|
+
raise RuntimeHostError(
|
|
163
|
+
"HandlerIntent not initialized. Call initialize() first.",
|
|
164
|
+
context=ctx,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
operation = envelope.get("operation")
|
|
168
|
+
if not isinstance(operation, str):
|
|
169
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
170
|
+
transport_type=EnumInfraTransportType.GRAPH,
|
|
171
|
+
operation="execute",
|
|
172
|
+
target_name="intent_handler",
|
|
173
|
+
correlation_id=correlation_id,
|
|
174
|
+
)
|
|
175
|
+
raise RuntimeHostError(
|
|
176
|
+
"Missing or invalid 'operation' in envelope",
|
|
177
|
+
context=ctx,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
if operation not in _SUPPORTED_OPERATIONS:
|
|
181
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
182
|
+
transport_type=EnumInfraTransportType.GRAPH,
|
|
183
|
+
operation=operation,
|
|
184
|
+
target_name="intent_handler",
|
|
185
|
+
correlation_id=correlation_id,
|
|
186
|
+
)
|
|
187
|
+
raise RuntimeHostError(
|
|
188
|
+
f"Operation '{operation}' not supported. "
|
|
189
|
+
f"Available: {', '.join(sorted(_SUPPORTED_OPERATIONS))}",
|
|
190
|
+
context=ctx,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
payload = envelope.get("payload")
|
|
194
|
+
if not isinstance(payload, dict):
|
|
195
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
196
|
+
transport_type=EnumInfraTransportType.GRAPH,
|
|
197
|
+
operation=operation,
|
|
198
|
+
target_name="intent_handler",
|
|
199
|
+
correlation_id=correlation_id,
|
|
200
|
+
)
|
|
201
|
+
raise RuntimeHostError(
|
|
202
|
+
"Missing or invalid 'payload' in envelope",
|
|
203
|
+
context=ctx,
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Route to appropriate handler method
|
|
207
|
+
if operation == "intent.store":
|
|
208
|
+
return await self._store_intent(payload, correlation_id)
|
|
209
|
+
elif operation == "intent.query_session":
|
|
210
|
+
return await self._query_session(payload, correlation_id)
|
|
211
|
+
else: # intent.query_distribution
|
|
212
|
+
return await self._query_distribution(correlation_id)
|
|
213
|
+
|
|
214
|
+
async def _store_intent(
|
|
215
|
+
self, payload: dict[str, object], correlation_id: UUID
|
|
216
|
+
) -> dict[str, object]:
|
|
217
|
+
"""Store an intent as a graph node.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
payload: Intent data to store. Should contain:
|
|
221
|
+
- intent_type: Type of intent (required)
|
|
222
|
+
- session_id: Session identifier (optional)
|
|
223
|
+
- Additional properties as needed
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
dict with created node details.
|
|
227
|
+
"""
|
|
228
|
+
# Note: _graph_handler is guaranteed non-None by execute() validation
|
|
229
|
+
assert self._graph_handler is not None # Type narrowing for mypy
|
|
230
|
+
|
|
231
|
+
# Extract intent properties - use JsonType for graph compatibility
|
|
232
|
+
properties: dict[str, JsonType] = {
|
|
233
|
+
"correlation_id": str(correlation_id),
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Copy all payload properties to node properties
|
|
237
|
+
for key, value in payload.items():
|
|
238
|
+
# Convert non-primitive types to strings for graph storage
|
|
239
|
+
# NOTE: Using tuple form for isinstance to avoid union validator flag
|
|
240
|
+
if isinstance(value, (str, int, float, bool)) or value is None:
|
|
241
|
+
properties[key] = value
|
|
242
|
+
else:
|
|
243
|
+
properties[key] = str(value)
|
|
244
|
+
|
|
245
|
+
# Create the intent node
|
|
246
|
+
node = await self._graph_handler.create_node(
|
|
247
|
+
labels=["Intent"],
|
|
248
|
+
properties=properties,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
return {
|
|
252
|
+
"success": True,
|
|
253
|
+
"data": {
|
|
254
|
+
"node_id": node.id,
|
|
255
|
+
"element_id": node.element_id,
|
|
256
|
+
"labels": node.labels,
|
|
257
|
+
"properties": node.properties,
|
|
258
|
+
},
|
|
259
|
+
"correlation_id": str(correlation_id),
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async def _query_session(
|
|
263
|
+
self, payload: dict[str, object], correlation_id: UUID
|
|
264
|
+
) -> dict[str, object]:
|
|
265
|
+
"""Query intents by session_id.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
payload: Query parameters. Should contain:
|
|
269
|
+
- session_id: Session identifier to filter by (required)
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
dict with matching intent nodes.
|
|
273
|
+
"""
|
|
274
|
+
# Note: _graph_handler is guaranteed non-None by execute() validation
|
|
275
|
+
assert self._graph_handler is not None # Type narrowing for mypy
|
|
276
|
+
|
|
277
|
+
session_id = payload.get("session_id")
|
|
278
|
+
if not session_id:
|
|
279
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
280
|
+
transport_type=EnumInfraTransportType.GRAPH,
|
|
281
|
+
operation="intent.query_session",
|
|
282
|
+
target_name="intent_handler",
|
|
283
|
+
correlation_id=correlation_id,
|
|
284
|
+
)
|
|
285
|
+
raise RuntimeHostError(
|
|
286
|
+
"Missing 'session_id' in payload",
|
|
287
|
+
context=ctx,
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
# Query intents by session_id
|
|
291
|
+
# SECURITY: Using parameterized query ($session_id) to prevent Cypher injection
|
|
292
|
+
query = """
|
|
293
|
+
MATCH (i:Intent {session_id: $session_id})
|
|
294
|
+
RETURN i, elementId(i) as eid, id(i) as nid
|
|
295
|
+
ORDER BY i.created_at DESC
|
|
296
|
+
"""
|
|
297
|
+
|
|
298
|
+
result = await self._graph_handler.execute_query(
|
|
299
|
+
query=query,
|
|
300
|
+
parameters={"session_id": str(session_id)},
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
# Transform records to intent data
|
|
304
|
+
intents = []
|
|
305
|
+
for record in result.records:
|
|
306
|
+
node = record.get("i")
|
|
307
|
+
if node:
|
|
308
|
+
intents.append(
|
|
309
|
+
{
|
|
310
|
+
"node_id": str(record.get("nid", "")),
|
|
311
|
+
"element_id": str(record.get("eid", "")),
|
|
312
|
+
"properties": dict(node) if isinstance(node, dict) else {},
|
|
313
|
+
}
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
"success": True,
|
|
318
|
+
"data": {
|
|
319
|
+
"session_id": str(session_id),
|
|
320
|
+
"intents": intents,
|
|
321
|
+
"count": len(intents),
|
|
322
|
+
},
|
|
323
|
+
"correlation_id": str(correlation_id),
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async def _query_distribution(self, correlation_id: UUID) -> dict[str, object]:
|
|
327
|
+
"""Query intent distribution/statistics.
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
dict with intent statistics including counts by intent_type.
|
|
331
|
+
"""
|
|
332
|
+
# Note: _graph_handler is guaranteed non-None by execute() validation
|
|
333
|
+
assert self._graph_handler is not None # Type narrowing for mypy
|
|
334
|
+
|
|
335
|
+
# Query total count
|
|
336
|
+
count_query = "MATCH (i:Intent) RETURN count(i) as total"
|
|
337
|
+
count_result = await self._graph_handler.execute_query(query=count_query)
|
|
338
|
+
|
|
339
|
+
total_count = 0
|
|
340
|
+
if count_result.records:
|
|
341
|
+
raw_total = count_result.records[0].get("total", 0)
|
|
342
|
+
total_count = int(raw_total) if isinstance(raw_total, int | float) else 0
|
|
343
|
+
|
|
344
|
+
# Query distribution by intent_type
|
|
345
|
+
distribution_query = """
|
|
346
|
+
MATCH (i:Intent)
|
|
347
|
+
RETURN i.intent_type as intent_type, count(i) as count
|
|
348
|
+
ORDER BY count DESC
|
|
349
|
+
"""
|
|
350
|
+
distribution_result = await self._graph_handler.execute_query(
|
|
351
|
+
query=distribution_query
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
# Build distribution dict
|
|
355
|
+
distribution: dict[str, int] = {}
|
|
356
|
+
for record in distribution_result.records:
|
|
357
|
+
intent_type = record.get("intent_type")
|
|
358
|
+
raw_count = record.get("count", 0)
|
|
359
|
+
if intent_type:
|
|
360
|
+
count_val = int(raw_count) if isinstance(raw_count, int | float) else 0
|
|
361
|
+
distribution[str(intent_type)] = count_val
|
|
362
|
+
|
|
363
|
+
return {
|
|
364
|
+
"success": True,
|
|
365
|
+
"data": {
|
|
366
|
+
"total_count": total_count,
|
|
367
|
+
"distribution": distribution,
|
|
368
|
+
},
|
|
369
|
+
"correlation_id": str(correlation_id),
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
def describe(self) -> dict[str, object]:
|
|
373
|
+
"""Return handler metadata and capabilities.
|
|
374
|
+
|
|
375
|
+
Returns:
|
|
376
|
+
dict containing handler information.
|
|
377
|
+
"""
|
|
378
|
+
return {
|
|
379
|
+
"handler_id": HANDLER_ID_INTENT,
|
|
380
|
+
"handler_type": "intent_handler",
|
|
381
|
+
"supported_operations": sorted(_SUPPORTED_OPERATIONS),
|
|
382
|
+
"initialized": self._initialized,
|
|
383
|
+
"version": "0.1.0-demo",
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
__all__: list[str] = ["HandlerIntent", "HANDLER_ID_INTENT"]
|
|
@@ -569,7 +569,16 @@ class HandlerMCP(MixinEnvelopeExtraction, MixinAsyncCircuitBreaker):
|
|
|
569
569
|
# - Resource leaks from partial initialization
|
|
570
570
|
try:
|
|
571
571
|
# Create and start MCPServerLifecycle for tool discovery
|
|
572
|
-
|
|
572
|
+
# Container is required for lifecycle initialization
|
|
573
|
+
if self._container is None:
|
|
574
|
+
raise ValueError(
|
|
575
|
+
"Container required for MCPServerLifecycle initialization"
|
|
576
|
+
)
|
|
577
|
+
self._lifecycle = MCPServerLifecycle(
|
|
578
|
+
container=self._container,
|
|
579
|
+
config=server_config,
|
|
580
|
+
bus=None,
|
|
581
|
+
)
|
|
573
582
|
await self._lifecycle.start()
|
|
574
583
|
|
|
575
584
|
# Update MCP registry and executor references from lifecycle
|
|
@@ -27,6 +27,7 @@ from uuid import uuid4
|
|
|
27
27
|
|
|
28
28
|
import hvac
|
|
29
29
|
|
|
30
|
+
from omnibase_core.container import ModelONEXContainer
|
|
30
31
|
from omnibase_core.models.dispatch import ModelHandlerOutput
|
|
31
32
|
from omnibase_infra.enums import (
|
|
32
33
|
EnumHandlerType,
|
|
@@ -122,13 +123,18 @@ class HandlerVault(
|
|
|
122
123
|
- MixinVaultToken: Token management and renewal
|
|
123
124
|
"""
|
|
124
125
|
|
|
125
|
-
def __init__(self) -> None:
|
|
126
|
-
"""Initialize HandlerVault
|
|
126
|
+
def __init__(self, container: ModelONEXContainer) -> None:
|
|
127
|
+
"""Initialize HandlerVault with ONEX container for dependency injection.
|
|
127
128
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
Args:
|
|
130
|
+
container: ONEX container for dependency injection.
|
|
131
|
+
|
|
132
|
+
Note:
|
|
133
|
+
Circuit breaker is initialized during initialize() call when
|
|
134
|
+
configuration is available. The mixin's _init_circuit_breaker() method
|
|
135
|
+
is called there with the actual config values.
|
|
131
136
|
"""
|
|
137
|
+
self._container = container
|
|
132
138
|
self._client: hvac.Client | None = None
|
|
133
139
|
self._config: ModelVaultHandlerConfig | None = None
|
|
134
140
|
self._initialized: bool = False
|
|
@@ -48,6 +48,7 @@ from uuid import UUID, uuid4
|
|
|
48
48
|
# Import asyncpg at module level to avoid redundant imports inside methods
|
|
49
49
|
import asyncpg
|
|
50
50
|
|
|
51
|
+
from omnibase_core.container import ModelONEXContainer
|
|
51
52
|
from omnibase_core.enums.enum_node_kind import EnumNodeKind
|
|
52
53
|
from omnibase_infra.enums import EnumInfraTransportType
|
|
53
54
|
from omnibase_infra.errors import (
|
|
@@ -152,7 +153,10 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
|
|
|
152
153
|
handler_type: Returns "postgresql" identifier.
|
|
153
154
|
|
|
154
155
|
Example:
|
|
156
|
+
>>> from omnibase_core.container import ModelONEXContainer
|
|
157
|
+
>>> container = ModelONEXContainer(...)
|
|
155
158
|
>>> handler = HandlerRegistrationStoragePostgres(
|
|
159
|
+
... container=container,
|
|
156
160
|
... postgres_adapter=postgres_adapter,
|
|
157
161
|
... circuit_breaker_config={"threshold": 5, "reset_timeout": 30.0},
|
|
158
162
|
... )
|
|
@@ -161,6 +165,7 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
|
|
|
161
165
|
|
|
162
166
|
def __init__(
|
|
163
167
|
self,
|
|
168
|
+
container: ModelONEXContainer,
|
|
164
169
|
postgres_adapter: ProtocolPostgresAdapter | None = None,
|
|
165
170
|
dsn: str | None = None,
|
|
166
171
|
host: str = "localhost",
|
|
@@ -178,6 +183,7 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
|
|
|
178
183
|
"""Initialize HandlerRegistrationStoragePostgres.
|
|
179
184
|
|
|
180
185
|
Args:
|
|
186
|
+
container: ONEX dependency injection container (required).
|
|
181
187
|
postgres_adapter: Optional existing PostgreSQL adapter (ProtocolPostgresAdapter).
|
|
182
188
|
If not provided, a new asyncpg connection pool will be created.
|
|
183
189
|
dsn: Optional PostgreSQL connection DSN (overrides host/port/etc).
|
|
@@ -197,6 +203,7 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
|
|
|
197
203
|
table on first connection. Default is False. Production deployments
|
|
198
204
|
should use database migrations instead of auto-creation.
|
|
199
205
|
"""
|
|
206
|
+
self._container = container
|
|
200
207
|
# Normalize circuit breaker config to ModelCircuitBreakerConfig
|
|
201
208
|
if isinstance(circuit_breaker_config, ModelCircuitBreakerConfig):
|
|
202
209
|
cb_config = circuit_breaker_config
|
|
@@ -30,6 +30,7 @@ from uuid import NAMESPACE_DNS, UUID, uuid4, uuid5
|
|
|
30
30
|
|
|
31
31
|
import consul
|
|
32
32
|
|
|
33
|
+
from omnibase_core.container import ModelONEXContainer
|
|
33
34
|
from omnibase_infra.enums import EnumInfraTransportType
|
|
34
35
|
from omnibase_infra.errors import (
|
|
35
36
|
InfraConnectionError,
|
|
@@ -91,7 +92,10 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
|
|
|
91
92
|
handler_type: Returns "consul" identifier.
|
|
92
93
|
|
|
93
94
|
Example:
|
|
95
|
+
>>> from unittest.mock import MagicMock
|
|
96
|
+
>>> container = MagicMock(spec=ModelONEXContainer)
|
|
94
97
|
>>> handler = HandlerServiceDiscoveryConsul(
|
|
98
|
+
... container=container,
|
|
95
99
|
... consul_client=consul_client,
|
|
96
100
|
... circuit_breaker_config=ModelCircuitBreakerConfig(threshold=5),
|
|
97
101
|
... )
|
|
@@ -100,6 +104,7 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
|
|
|
100
104
|
|
|
101
105
|
def __init__(
|
|
102
106
|
self,
|
|
107
|
+
container: ModelONEXContainer,
|
|
103
108
|
consul_client: ProtocolConsulClient | None = None,
|
|
104
109
|
consul_host: str = "localhost",
|
|
105
110
|
consul_port: int = 8500,
|
|
@@ -114,6 +119,7 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
|
|
|
114
119
|
"""Initialize HandlerServiceDiscoveryConsul.
|
|
115
120
|
|
|
116
121
|
Args:
|
|
122
|
+
container: ONEX container for dependency injection and service resolution.
|
|
117
123
|
consul_client: Optional existing Consul client (ProtocolConsulClient).
|
|
118
124
|
If not provided, a new python-consul client will be created.
|
|
119
125
|
consul_host: Consul server hostname (default: "localhost").
|
|
@@ -129,6 +135,7 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
|
|
|
129
135
|
max_workers: Thread pool max workers (default: 10).
|
|
130
136
|
timeout_seconds: Operation timeout in seconds (default: 30.0).
|
|
131
137
|
"""
|
|
138
|
+
self._container = container
|
|
132
139
|
# Parse circuit breaker configuration using ModelCircuitBreakerConfig
|
|
133
140
|
if isinstance(circuit_breaker_config, ModelCircuitBreakerConfig):
|
|
134
141
|
cb_config = circuit_breaker_config
|
|
@@ -209,6 +209,7 @@ from uuid import UUID, uuid4
|
|
|
209
209
|
from omnibase_core.enums import EnumNodeKind
|
|
210
210
|
from omnibase_core.models.events.model_event_envelope import ModelEventEnvelope
|
|
211
211
|
from omnibase_core.models.primitives.model_semver import ModelSemVer
|
|
212
|
+
from omnibase_infra.capabilities import ContractCapabilityExtractor
|
|
212
213
|
from omnibase_infra.enums import EnumInfraTransportType, EnumIntrospectionReason
|
|
213
214
|
from omnibase_infra.errors import ModelInfraErrorContext, ProtocolConfigurationError
|
|
214
215
|
from omnibase_infra.models.discovery import (
|
|
@@ -228,6 +229,7 @@ from omnibase_infra.models.registration.model_node_introspection_event import (
|
|
|
228
229
|
)
|
|
229
230
|
|
|
230
231
|
if TYPE_CHECKING:
|
|
232
|
+
from omnibase_core.models.contracts import ModelContractBase
|
|
231
233
|
from omnibase_core.protocols.event_bus.protocol_event_bus import ProtocolEventBus
|
|
232
234
|
from omnibase_core.protocols.event_bus.protocol_event_message import (
|
|
233
235
|
ProtocolEventMessage,
|
|
@@ -246,6 +248,9 @@ PERF_THRESHOLD_DISCOVER_CAPABILITIES_MS = 30.0
|
|
|
246
248
|
PERF_THRESHOLD_GET_INTROSPECTION_DATA_MS = 50.0
|
|
247
249
|
PERF_THRESHOLD_CACHE_HIT_MS = 1.0
|
|
248
250
|
|
|
251
|
+
# Module-level capability extractor instance (stateless, can be shared)
|
|
252
|
+
_CAPABILITY_EXTRACTOR = ContractCapabilityExtractor()
|
|
253
|
+
|
|
249
254
|
|
|
250
255
|
class PerformanceMetricsCacheDict(TypedDict, total=False):
|
|
251
256
|
"""TypedDict for JSON-serialized ModelIntrospectionPerformanceMetrics.
|
|
@@ -458,6 +463,7 @@ class MixinNodeIntrospection:
|
|
|
458
463
|
_introspection_event_bus: ProtocolEventBus | None
|
|
459
464
|
_introspection_version: str
|
|
460
465
|
_introspection_start_time: float | None
|
|
466
|
+
_introspection_contract: ModelContractBase | None
|
|
461
467
|
|
|
462
468
|
# Capability discovery configuration
|
|
463
469
|
_introspection_operation_keywords: frozenset[str]
|
|
@@ -650,6 +656,9 @@ class MixinNodeIntrospection:
|
|
|
650
656
|
self._heartbeat_topic = config.heartbeat_topic
|
|
651
657
|
self._request_introspection_topic = config.request_introspection_topic
|
|
652
658
|
|
|
659
|
+
# Contract for capability extraction (may be None for legacy nodes)
|
|
660
|
+
self._introspection_contract = config.contract
|
|
661
|
+
|
|
653
662
|
# State
|
|
654
663
|
self._introspection_cache = None
|
|
655
664
|
self._introspection_cached_at = None
|
|
@@ -1338,6 +1347,14 @@ class MixinNodeIntrospection:
|
|
|
1338
1347
|
# Fallback to 1.0.0 if version parsing fails
|
|
1339
1348
|
node_version = ModelSemVer(major=1, minor=0, patch=0)
|
|
1340
1349
|
|
|
1350
|
+
# Extract contract capabilities if contract is available
|
|
1351
|
+
# This is automatic and non-skippable when contract is provided
|
|
1352
|
+
contract_capabilities = None
|
|
1353
|
+
if self._introspection_contract is not None:
|
|
1354
|
+
contract_capabilities = _CAPABILITY_EXTRACTOR.extract(
|
|
1355
|
+
self._introspection_contract
|
|
1356
|
+
)
|
|
1357
|
+
|
|
1341
1358
|
# Create event with performance metrics (metrics is already Pydantic model)
|
|
1342
1359
|
event = ModelNodeIntrospectionEvent(
|
|
1343
1360
|
node_id=node_id_uuid,
|
|
@@ -1345,6 +1362,7 @@ class MixinNodeIntrospection:
|
|
|
1345
1362
|
node_version=node_version,
|
|
1346
1363
|
declared_capabilities=ModelNodeCapabilities(),
|
|
1347
1364
|
discovered_capabilities=discovered_capabilities,
|
|
1365
|
+
contract_capabilities=contract_capabilities,
|
|
1348
1366
|
endpoints=endpoints,
|
|
1349
1367
|
current_state=current_state,
|
|
1350
1368
|
reason=EnumIntrospectionReason.HEARTBEAT, # cache_refresh maps to heartbeat
|
|
@@ -27,6 +27,7 @@ from uuid import UUID
|
|
|
27
27
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
28
28
|
|
|
29
29
|
from omnibase_core.enums import EnumNodeKind
|
|
30
|
+
from omnibase_core.models.contracts import ModelContractBase
|
|
30
31
|
|
|
31
32
|
if TYPE_CHECKING:
|
|
32
33
|
from omnibase_core.protocols.event_bus.protocol_event_bus import ProtocolEventBus
|
|
@@ -86,6 +87,9 @@ class ModelIntrospectionConfig(BaseModel):
|
|
|
86
87
|
request_introspection_topic: Topic for receiving introspection requests.
|
|
87
88
|
Defaults to "node.request_introspection". ONEX topics (onex.*)
|
|
88
89
|
require version suffix (.v1, .v2, etc.).
|
|
90
|
+
contract: Optional typed contract model for capability extraction.
|
|
91
|
+
When provided, MixinNodeIntrospection extracts contract_capabilities
|
|
92
|
+
using ContractCapabilityExtractor. None for legacy nodes.
|
|
89
93
|
|
|
90
94
|
Example:
|
|
91
95
|
```python
|
|
@@ -185,6 +189,13 @@ class ModelIntrospectionConfig(BaseModel):
|
|
|
185
189
|
"ONEX topics (onex.*) require version suffix (.v1, .v2, etc.).",
|
|
186
190
|
)
|
|
187
191
|
|
|
192
|
+
contract: ModelContractBase | None = Field(
|
|
193
|
+
default=None,
|
|
194
|
+
description="Typed contract model for capability extraction. "
|
|
195
|
+
"When provided, MixinNodeIntrospection will extract contract_capabilities "
|
|
196
|
+
"using ContractCapabilityExtractor. None for legacy nodes without contracts.",
|
|
197
|
+
)
|
|
198
|
+
|
|
188
199
|
@field_validator("node_type", mode="before")
|
|
189
200
|
@classmethod
|
|
190
201
|
def validate_node_type(cls, v: object) -> EnumNodeKind:
|
|
@@ -16,12 +16,21 @@ and error reporting in ONEX handlers.
|
|
|
16
16
|
Added ModelBootstrapHandlerDescriptor for OMN-1087 bootstrap handler
|
|
17
17
|
validation with required handler_class field.
|
|
18
18
|
|
|
19
|
+
.. versionchanged:: 0.7.0
|
|
20
|
+
Added ModelHandlerSourceConfig for OMN-1095 handler source mode
|
|
21
|
+
configuration with production hardening features.
|
|
22
|
+
|
|
19
23
|
Note:
|
|
20
|
-
ModelContractDiscoveryResult uses a forward reference to
|
|
21
|
-
|
|
22
|
-
reference is resolved via model_rebuild() in
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
ModelContractDiscoveryResult uses a forward reference to ModelHandlerValidationError
|
|
25
|
+
to avoid circular imports between models.handlers and models.errors packages.
|
|
26
|
+
The forward reference is resolved via model_rebuild() calls in runtime modules
|
|
27
|
+
that import ModelHandlerValidationError (e.g., handler_contract_source.py,
|
|
28
|
+
handler_bootstrap_source.py, registry_contract_source.py). Each module calls
|
|
29
|
+
model_rebuild() after importing both the model and the forward-referenced type.
|
|
30
|
+
This pattern is required because:
|
|
31
|
+
1. models.errors imports ModelHandlerIdentifier from models.handlers
|
|
32
|
+
2. models.handlers cannot import from models.errors at module level (circular)
|
|
33
|
+
3. model_rebuild() is idempotent, so multiple calls are harmless
|
|
25
34
|
"""
|
|
26
35
|
|
|
27
36
|
from omnibase_infra.models.handlers.model_bootstrap_handler_descriptor import (
|
|
@@ -37,6 +46,9 @@ from omnibase_infra.models.handlers.model_handler_descriptor import (
|
|
|
37
46
|
from omnibase_infra.models.handlers.model_handler_identifier import (
|
|
38
47
|
ModelHandlerIdentifier,
|
|
39
48
|
)
|
|
49
|
+
from omnibase_infra.models.handlers.model_handler_source_config import (
|
|
50
|
+
ModelHandlerSourceConfig,
|
|
51
|
+
)
|
|
40
52
|
|
|
41
53
|
__all__ = [
|
|
42
54
|
"LiteralHandlerKind",
|
|
@@ -44,4 +56,25 @@ __all__ = [
|
|
|
44
56
|
"ModelContractDiscoveryResult",
|
|
45
57
|
"ModelHandlerDescriptor",
|
|
46
58
|
"ModelHandlerIdentifier",
|
|
59
|
+
"ModelHandlerSourceConfig",
|
|
47
60
|
]
|
|
61
|
+
|
|
62
|
+
# =============================================================================
|
|
63
|
+
# Forward Reference Resolution
|
|
64
|
+
# =============================================================================
|
|
65
|
+
# ModelContractDiscoveryResult uses TYPE_CHECKING to defer import of
|
|
66
|
+
# ModelHandlerValidationError to avoid circular imports:
|
|
67
|
+
# - models.errors imports ModelHandlerIdentifier from models.handlers
|
|
68
|
+
# - models.handlers cannot import ModelHandlerValidationError at module level
|
|
69
|
+
#
|
|
70
|
+
# The forward reference is resolved via model_rebuild() in runtime modules that
|
|
71
|
+
# import ModelHandlerValidationError (e.g., handler_contract_source.py,
|
|
72
|
+
# handler_bootstrap_source.py, registry_contract_source.py, handler_source_resolver.py).
|
|
73
|
+
# Each module calls model_rebuild() at module level after importing both the model
|
|
74
|
+
# and the forward-referenced type. This is safe because model_rebuild() is idempotent.
|
|
75
|
+
#
|
|
76
|
+
# Why NOT here at module level:
|
|
77
|
+
# - Circular import: models.handlers.__init__ -> models.errors.__init__
|
|
78
|
+
# -> model_handler_validation_error.py -> models.handlers (for identifier)
|
|
79
|
+
# - Runtime modules load after model packages, avoiding this cycle
|
|
80
|
+
# =============================================================================
|