omnibase_infra 0.2.2__py3-none-any.whl → 0.2.4__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.
Files changed (79) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/adapters/adapter_onex_tool_execution.py +6 -1
  3. omnibase_infra/capabilities/__init__.py +15 -0
  4. omnibase_infra/capabilities/capability_inference_rules.py +211 -0
  5. omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
  6. omnibase_infra/capabilities/intent_type_extractor.py +160 -0
  7. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +1 -1
  8. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +1 -1
  9. omnibase_infra/enums/__init__.py +6 -0
  10. omnibase_infra/enums/enum_handler_error_type.py +10 -0
  11. omnibase_infra/enums/enum_handler_source_mode.py +72 -0
  12. omnibase_infra/enums/enum_kafka_acks.py +99 -0
  13. omnibase_infra/event_bus/event_bus_kafka.py +1 -1
  14. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +59 -10
  15. omnibase_infra/handlers/__init__.py +8 -1
  16. omnibase_infra/handlers/handler_consul.py +7 -1
  17. omnibase_infra/handlers/handler_db.py +8 -2
  18. omnibase_infra/handlers/handler_graph.py +860 -4
  19. omnibase_infra/handlers/handler_http.py +8 -2
  20. omnibase_infra/handlers/handler_intent.py +387 -0
  21. omnibase_infra/handlers/handler_mcp.py +10 -1
  22. omnibase_infra/handlers/handler_vault.py +11 -5
  23. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +7 -0
  24. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +7 -0
  25. omnibase_infra/mixins/mixin_node_introspection.py +18 -0
  26. omnibase_infra/models/discovery/model_introspection_config.py +11 -0
  27. omnibase_infra/models/handlers/__init__.py +38 -5
  28. omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +4 -4
  29. omnibase_infra/models/handlers/model_contract_discovery_result.py +6 -4
  30. omnibase_infra/models/handlers/model_handler_source_config.py +220 -0
  31. omnibase_infra/models/registration/model_node_introspection_event.py +9 -0
  32. omnibase_infra/models/runtime/model_handler_contract.py +25 -9
  33. omnibase_infra/models/runtime/model_loaded_handler.py +9 -0
  34. omnibase_infra/nodes/node_registration_orchestrator/plugin.py +1 -1
  35. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +7 -7
  36. omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +4 -3
  37. omnibase_infra/nodes/node_registration_storage_effect/node.py +4 -1
  38. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +1 -1
  39. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +4 -1
  40. omnibase_infra/protocols/__init__.py +2 -0
  41. omnibase_infra/protocols/protocol_container_aware.py +200 -0
  42. omnibase_infra/runtime/__init__.py +39 -0
  43. omnibase_infra/runtime/handler_bootstrap_source.py +26 -33
  44. omnibase_infra/runtime/handler_contract_config_loader.py +1 -1
  45. omnibase_infra/runtime/handler_contract_source.py +10 -51
  46. omnibase_infra/runtime/handler_identity.py +81 -0
  47. omnibase_infra/runtime/handler_plugin_loader.py +15 -0
  48. omnibase_infra/runtime/handler_registry.py +11 -3
  49. omnibase_infra/runtime/handler_source_resolver.py +326 -0
  50. omnibase_infra/runtime/protocol_lifecycle_executor.py +6 -6
  51. omnibase_infra/runtime/registry/registry_protocol_binding.py +13 -13
  52. omnibase_infra/runtime/registry_contract_source.py +693 -0
  53. omnibase_infra/runtime/service_kernel.py +1 -1
  54. omnibase_infra/runtime/service_runtime_host_process.py +463 -190
  55. omnibase_infra/runtime/util_wiring.py +12 -3
  56. omnibase_infra/services/__init__.py +21 -0
  57. omnibase_infra/services/corpus_capture.py +7 -1
  58. omnibase_infra/services/mcp/mcp_server_lifecycle.py +9 -3
  59. omnibase_infra/services/registry_api/main.py +31 -13
  60. omnibase_infra/services/registry_api/service.py +10 -19
  61. omnibase_infra/services/service_timeout_emitter.py +7 -1
  62. omnibase_infra/services/service_timeout_scanner.py +7 -3
  63. omnibase_infra/services/session/__init__.py +56 -0
  64. omnibase_infra/services/session/config_consumer.py +120 -0
  65. omnibase_infra/services/session/config_store.py +139 -0
  66. omnibase_infra/services/session/consumer.py +1007 -0
  67. omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
  68. omnibase_infra/services/session/store.py +997 -0
  69. omnibase_infra/utils/__init__.py +19 -0
  70. omnibase_infra/utils/util_atomic_file.py +261 -0
  71. omnibase_infra/utils/util_db_transaction.py +239 -0
  72. omnibase_infra/utils/util_retry_optimistic.py +281 -0
  73. omnibase_infra/validation/__init__.py +16 -0
  74. omnibase_infra/validation/validation_exemptions.yaml +27 -0
  75. {omnibase_infra-0.2.2.dist-info → omnibase_infra-0.2.4.dist-info}/METADATA +3 -3
  76. {omnibase_infra-0.2.2.dist-info → omnibase_infra-0.2.4.dist-info}/RECORD +79 -58
  77. {omnibase_infra-0.2.2.dist-info → omnibase_infra-0.2.4.dist-info}/WHEEL +0 -0
  78. {omnibase_infra-0.2.2.dist-info → omnibase_infra-0.2.4.dist-info}/entry_points.txt +0 -0
  79. {omnibase_infra-0.2.2.dist-info → omnibase_infra-0.2.4.dist-info}/licenses/LICENSE +0 -0
@@ -24,6 +24,7 @@ from uuid import UUID, uuid4
24
24
 
25
25
  import httpx
26
26
 
27
+ from omnibase_core.container import ModelONEXContainer
27
28
  from omnibase_core.models.dispatch import ModelHandlerOutput
28
29
  from omnibase_infra.enums import (
29
30
  EnumHandlerType,
@@ -114,8 +115,13 @@ class HandlerHttpRest(MixinEnvelopeExtraction):
114
115
  - Streaming body validation for chunked transfer encoding
115
116
  """
116
117
 
117
- def __init__(self) -> None:
118
- """Initialize HandlerHttpRest in uninitialized state."""
118
+ def __init__(self, container: ModelONEXContainer) -> None:
119
+ """Initialize HandlerHttpRest with ONEX container for dependency injection.
120
+
121
+ Args:
122
+ container: ONEX container for dependency injection.
123
+ """
124
+ self._container = container
119
125
  self._client: httpx.AsyncClient | None = None
120
126
  self._timeout: float = _DEFAULT_TIMEOUT_SECONDS
121
127
  self._max_request_size: int = _DEFAULT_MAX_REQUEST_SIZE
@@ -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
- self._lifecycle = MCPServerLifecycle(config=server_config, bus=None)
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 in uninitialized state.
126
+ def __init__(self, container: ModelONEXContainer) -> None:
127
+ """Initialize HandlerVault with ONEX container for dependency injection.
127
128
 
128
- Note: Circuit breaker is initialized during initialize() call when
129
- configuration is available. The mixin's _init_circuit_breaker() method
130
- is called there with the actual config values.
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: