omnibase_infra 0.2.1__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.
Files changed (161) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/adapters/adapter_onex_tool_execution.py +451 -0
  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/cli/commands.py +1 -1
  8. omnibase_infra/configs/widget_mapping.yaml +176 -0
  9. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +5 -2
  10. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +5 -2
  11. omnibase_infra/enums/__init__.py +6 -0
  12. omnibase_infra/enums/enum_handler_error_type.py +10 -0
  13. omnibase_infra/enums/enum_handler_source_mode.py +72 -0
  14. omnibase_infra/enums/enum_kafka_acks.py +99 -0
  15. omnibase_infra/errors/error_compute_registry.py +4 -1
  16. omnibase_infra/errors/error_event_bus_registry.py +4 -1
  17. omnibase_infra/errors/error_infra.py +3 -1
  18. omnibase_infra/errors/error_policy_registry.py +4 -1
  19. omnibase_infra/event_bus/event_bus_kafka.py +1 -1
  20. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +59 -10
  21. omnibase_infra/handlers/__init__.py +8 -1
  22. omnibase_infra/handlers/handler_consul.py +7 -1
  23. omnibase_infra/handlers/handler_db.py +10 -3
  24. omnibase_infra/handlers/handler_graph.py +10 -5
  25. omnibase_infra/handlers/handler_http.py +8 -2
  26. omnibase_infra/handlers/handler_intent.py +387 -0
  27. omnibase_infra/handlers/handler_mcp.py +745 -63
  28. omnibase_infra/handlers/handler_vault.py +11 -5
  29. omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
  30. omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
  31. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +7 -0
  32. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +308 -4
  33. omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
  34. omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
  35. omnibase_infra/mixins/mixin_node_introspection.py +42 -7
  36. omnibase_infra/mixins/mixin_retry_execution.py +1 -1
  37. omnibase_infra/models/discovery/model_introspection_config.py +11 -0
  38. omnibase_infra/models/handlers/__init__.py +48 -5
  39. omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
  40. omnibase_infra/models/handlers/model_contract_discovery_result.py +6 -4
  41. omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
  42. omnibase_infra/models/handlers/model_handler_source_config.py +220 -0
  43. omnibase_infra/models/mcp/__init__.py +15 -0
  44. omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
  45. omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
  46. omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
  47. omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
  48. omnibase_infra/models/registration/model_node_capabilities.py +11 -0
  49. omnibase_infra/models/registration/model_node_introspection_event.py +9 -0
  50. omnibase_infra/models/runtime/model_handler_contract.py +25 -9
  51. omnibase_infra/models/runtime/model_loaded_handler.py +9 -0
  52. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
  53. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
  54. omnibase_infra/nodes/effects/contract.yaml +0 -5
  55. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
  56. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
  57. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
  58. omnibase_infra/nodes/node_registration_orchestrator/plugin.py +1 -1
  59. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
  60. omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +4 -3
  61. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
  62. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
  63. omnibase_infra/nodes/node_registration_storage_effect/node.py +4 -1
  64. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +47 -26
  65. omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
  66. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
  67. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +28 -20
  68. omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
  69. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
  70. omnibase_infra/plugins/plugin_compute_base.py +16 -2
  71. omnibase_infra/protocols/__init__.py +2 -0
  72. omnibase_infra/protocols/protocol_container_aware.py +200 -0
  73. omnibase_infra/protocols/protocol_event_projector.py +1 -1
  74. omnibase_infra/runtime/__init__.py +90 -1
  75. omnibase_infra/runtime/binding_config_resolver.py +102 -37
  76. omnibase_infra/runtime/constants_notification.py +75 -0
  77. omnibase_infra/runtime/contract_handler_discovery.py +6 -1
  78. omnibase_infra/runtime/handler_bootstrap_source.py +507 -0
  79. omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
  80. omnibase_infra/runtime/handler_contract_source.py +267 -186
  81. omnibase_infra/runtime/handler_identity.py +81 -0
  82. omnibase_infra/runtime/handler_plugin_loader.py +19 -2
  83. omnibase_infra/runtime/handler_registry.py +11 -3
  84. omnibase_infra/runtime/handler_source_resolver.py +326 -0
  85. omnibase_infra/runtime/mixin_semver_cache.py +25 -1
  86. omnibase_infra/runtime/mixins/__init__.py +7 -0
  87. omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
  88. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
  89. omnibase_infra/runtime/models/__init__.py +24 -0
  90. omnibase_infra/runtime/models/model_health_check_result.py +2 -1
  91. omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
  92. omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
  93. omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
  94. omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
  95. omnibase_infra/runtime/projector_plugin_loader.py +1 -1
  96. omnibase_infra/runtime/projector_shell.py +229 -1
  97. omnibase_infra/runtime/protocol_lifecycle_executor.py +6 -6
  98. omnibase_infra/runtime/protocols/__init__.py +10 -0
  99. omnibase_infra/runtime/registry/registry_protocol_binding.py +16 -15
  100. omnibase_infra/runtime/registry_contract_source.py +693 -0
  101. omnibase_infra/runtime/registry_policy.py +9 -326
  102. omnibase_infra/runtime/secret_resolver.py +4 -2
  103. omnibase_infra/runtime/service_kernel.py +11 -3
  104. omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
  105. omnibase_infra/runtime/service_runtime_host_process.py +589 -106
  106. omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
  107. omnibase_infra/runtime/transition_notification_publisher.py +764 -0
  108. omnibase_infra/runtime/util_container_wiring.py +6 -5
  109. omnibase_infra/runtime/util_wiring.py +17 -4
  110. omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
  111. omnibase_infra/services/__init__.py +21 -0
  112. omnibase_infra/services/corpus_capture.py +7 -1
  113. omnibase_infra/services/mcp/__init__.py +31 -0
  114. omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -0
  115. omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
  116. omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
  117. omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
  118. omnibase_infra/services/registry_api/__init__.py +40 -0
  119. omnibase_infra/services/registry_api/main.py +261 -0
  120. omnibase_infra/services/registry_api/models/__init__.py +66 -0
  121. omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
  122. omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
  123. omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
  124. omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
  125. omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
  126. omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
  127. omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
  128. omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
  129. omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
  130. omnibase_infra/services/registry_api/models/model_warning.py +49 -0
  131. omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
  132. omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
  133. omnibase_infra/services/registry_api/routes.py +371 -0
  134. omnibase_infra/services/registry_api/service.py +837 -0
  135. omnibase_infra/services/service_capability_query.py +4 -4
  136. omnibase_infra/services/service_health.py +3 -2
  137. omnibase_infra/services/service_timeout_emitter.py +20 -3
  138. omnibase_infra/services/service_timeout_scanner.py +7 -3
  139. omnibase_infra/services/session/__init__.py +56 -0
  140. omnibase_infra/services/session/config_consumer.py +120 -0
  141. omnibase_infra/services/session/config_store.py +139 -0
  142. omnibase_infra/services/session/consumer.py +1007 -0
  143. omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
  144. omnibase_infra/services/session/store.py +997 -0
  145. omnibase_infra/utils/__init__.py +19 -0
  146. omnibase_infra/utils/util_atomic_file.py +261 -0
  147. omnibase_infra/utils/util_db_transaction.py +239 -0
  148. omnibase_infra/utils/util_dsn_validation.py +1 -1
  149. omnibase_infra/utils/util_retry_optimistic.py +281 -0
  150. omnibase_infra/validation/__init__.py +3 -19
  151. omnibase_infra/validation/contracts/security.validation.yaml +114 -0
  152. omnibase_infra/validation/infra_validators.py +35 -24
  153. omnibase_infra/validation/validation_exemptions.yaml +140 -9
  154. omnibase_infra/validation/validator_chain_propagation.py +2 -2
  155. omnibase_infra/validation/validator_runtime_shape.py +1 -1
  156. omnibase_infra/validation/validator_security.py +473 -370
  157. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/METADATA +3 -3
  158. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/RECORD +161 -98
  159. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/WHEEL +0 -0
  160. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/entry_points.txt +0 -0
  161. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -43,6 +43,7 @@ Integration with Handlers:
43
43
  from __future__ import annotations
44
44
 
45
45
  import asyncio
46
+ import importlib
46
47
  import json
47
48
  import logging
48
49
  from collections.abc import Awaitable, Callable
@@ -52,7 +53,11 @@ from uuid import UUID, uuid4
52
53
 
53
54
  from pydantic import BaseModel
54
55
 
55
- from omnibase_infra.enums import EnumInfraTransportType
56
+ from omnibase_infra.enums import (
57
+ EnumHandlerSourceMode,
58
+ EnumHandlerTypeCategory,
59
+ EnumInfraTransportType,
60
+ )
56
61
  from omnibase_infra.errors import (
57
62
  EnvelopeValidationError,
58
63
  ModelInfraErrorContext,
@@ -79,13 +84,27 @@ if TYPE_CHECKING:
79
84
  from omnibase_infra.idempotency.protocol_idempotency_store import (
80
85
  ProtocolIdempotencyStore,
81
86
  )
87
+ from omnibase_infra.models.handlers import ModelHandlerSourceConfig
82
88
  from omnibase_infra.nodes.architecture_validator import ProtocolArchitectureRule
89
+ from omnibase_infra.protocols import ProtocolContainerAware
83
90
  from omnibase_infra.runtime.contract_handler_discovery import (
84
91
  ContractHandlerDiscovery,
85
92
  )
86
- from omnibase_spi.protocols.handlers.protocol_handler import ProtocolHandler
87
93
 
94
+ # Imports for PluginLoaderContractSource adapter class
95
+ from omnibase_infra.models.errors import ModelHandlerValidationError
96
+ from omnibase_infra.models.handlers import (
97
+ LiteralHandlerKind,
98
+ ModelContractDiscoveryResult,
99
+ ModelHandlerDescriptor,
100
+ )
88
101
  from omnibase_infra.models.types import JsonDict
102
+ from omnibase_infra.runtime.handler_identity import (
103
+ HANDLER_IDENTITY_PREFIX,
104
+ handler_identity,
105
+ )
106
+ from omnibase_infra.runtime.handler_plugin_loader import HandlerPluginLoader
107
+ from omnibase_infra.runtime.protocol_contract_source import ProtocolContractSource
89
108
 
90
109
  # Expose wire_default_handlers as wire_handlers for test patching compatibility
91
110
  # Tests patch "omnibase_infra.runtime.service_runtime_host_process.wire_handlers"
@@ -93,6 +112,22 @@ wire_handlers = wire_default_handlers
93
112
 
94
113
  logger = logging.getLogger(__name__)
95
114
 
115
+ # Mapping from EnumHandlerTypeCategory to LiteralHandlerKind for descriptor creation.
116
+ # COMPUTE and EFFECT map directly to their string values.
117
+ # NONDETERMINISTIC_COMPUTE maps to "compute" because it is architecturally pure
118
+ # (no I/O) even though it may produce different results between runs.
119
+ # "effect" is used as the fallback for any unknown types as the safer option
120
+ # (effect handlers have stricter policy envelopes for I/O operations).
121
+ _HANDLER_TYPE_TO_KIND: dict[EnumHandlerTypeCategory, LiteralHandlerKind] = {
122
+ EnumHandlerTypeCategory.COMPUTE: "compute",
123
+ EnumHandlerTypeCategory.EFFECT: "effect",
124
+ EnumHandlerTypeCategory.NONDETERMINISTIC_COMPUTE: "compute",
125
+ }
126
+
127
+ # Default handler kind for unknown handler types. "effect" is the safe default
128
+ # because effect handlers have stricter policy envelopes for I/O operations.
129
+ _DEFAULT_HANDLER_KIND: LiteralHandlerKind = "effect"
130
+
96
131
  # Default configuration values
97
132
  DEFAULT_INPUT_TOPIC = "requests"
98
133
  DEFAULT_OUTPUT_TOPIC = "responses"
@@ -124,6 +159,152 @@ DEFAULT_DRAIN_TIMEOUT_SECONDS: float = parse_env_float(
124
159
  )
125
160
 
126
161
 
162
+ class PluginLoaderContractSource(ProtocolContractSource):
163
+ """Adapter that uses HandlerPluginLoader for contract discovery.
164
+
165
+ This adapter implements ProtocolContractSource using HandlerPluginLoader,
166
+ which uses the simpler contract schema (handler_name, handler_class,
167
+ handler_type, capability_tags) rather than the full ONEX contract schema.
168
+
169
+ This class wraps the HandlerPluginLoader to conform to the ProtocolContractSource
170
+ interface expected by HandlerSourceResolver, enabling plugin-based handler
171
+ discovery within the unified handler source resolution framework.
172
+
173
+ Attributes:
174
+ _contract_paths: List of filesystem paths to scan for handler contracts.
175
+ _plugin_loader: The underlying HandlerPluginLoader instance.
176
+
177
+ Example:
178
+ ```python
179
+ from pathlib import Path
180
+ source = PluginLoaderContractSource(
181
+ contract_paths=[Path("/etc/onex/handlers")]
182
+ )
183
+ result = await source.discover_handlers()
184
+ for descriptor in result.descriptors:
185
+ print(f"Found handler: {descriptor.name}")
186
+ ```
187
+
188
+ .. versionadded:: 0.7.0
189
+ Extracted from _resolve_handler_descriptors() method for better
190
+ testability and code organization.
191
+ """
192
+
193
+ def __init__(
194
+ self,
195
+ contract_paths: list[Path],
196
+ allowed_namespaces: tuple[str, ...] | None = None,
197
+ ) -> None:
198
+ """Initialize the contract source with paths to scan.
199
+
200
+ Args:
201
+ contract_paths: List of filesystem paths containing handler contracts.
202
+ allowed_namespaces: Optional tuple of allowed module namespaces for
203
+ handler class imports. If None, all namespaces are allowed.
204
+ """
205
+ self._contract_paths = contract_paths
206
+ self._allowed_namespaces = allowed_namespaces
207
+ self._plugin_loader = HandlerPluginLoader(
208
+ allowed_namespaces=list(allowed_namespaces) if allowed_namespaces else None
209
+ )
210
+
211
+ @property
212
+ def source_type(self) -> str:
213
+ """Return the source type identifier.
214
+
215
+ Returns:
216
+ str: Always "CONTRACT" for this filesystem-based source.
217
+ """
218
+ return "CONTRACT"
219
+
220
+ async def discover_handlers(self) -> ModelContractDiscoveryResult:
221
+ """Discover handlers using HandlerPluginLoader.
222
+
223
+ Scans all configured contract paths and loads handler contracts using
224
+ the HandlerPluginLoader. Each discovered handler is converted to a
225
+ ModelHandlerDescriptor for use by the handler resolution framework.
226
+
227
+ Returns:
228
+ ModelContractDiscoveryResult: Container with discovered descriptors
229
+ and any validation errors encountered during discovery.
230
+
231
+ Note:
232
+ This method uses graceful degradation - if a single contract path
233
+ fails to load, discovery continues with remaining paths and the
234
+ error is logged but not raised.
235
+ """
236
+ # NOTE: ModelContractDiscoveryResult.model_rebuild() is called at module-level
237
+ # in handler_source_resolver.py and handler_contract_source.py to resolve
238
+ # forward references. No need to call it here - see those modules for rationale.
239
+
240
+ descriptors: list[ModelHandlerDescriptor] = []
241
+ validation_errors: list[ModelHandlerValidationError] = []
242
+
243
+ for path in self._contract_paths:
244
+ path_obj = Path(path) if isinstance(path, str) else path
245
+ if not path_obj.exists():
246
+ logger.warning(
247
+ "Contract path does not exist, skipping: %s",
248
+ path_obj,
249
+ )
250
+ continue
251
+
252
+ try:
253
+ # Use plugin loader to discover handlers with simpler schema
254
+ loaded_handlers = self._plugin_loader.load_from_directory(
255
+ directory=path_obj,
256
+ )
257
+
258
+ # Convert ModelLoadedHandler to ModelHandlerDescriptor
259
+ for loaded in loaded_handlers:
260
+ # Map EnumHandlerTypeCategory to LiteralHandlerKind.
261
+ # handler_type is required on ModelLoadedHandler, so this always
262
+ # provides a valid value. The mapping handles COMPUTE, EFFECT,
263
+ # and NONDETERMINISTIC_COMPUTE. Falls back to "effect" for any
264
+ # unknown types as the safer option (stricter policy envelope).
265
+ handler_kind = _HANDLER_TYPE_TO_KIND.get(
266
+ loaded.handler_type, _DEFAULT_HANDLER_KIND
267
+ )
268
+
269
+ descriptor = ModelHandlerDescriptor(
270
+ # NOTE: Uses handler_identity() for consistent ID generation.
271
+ # In HYBRID mode, HandlerSourceResolver compares handler_id values to
272
+ # determine which handler wins when both sources provide the same handler.
273
+ # Contract handlers need matching IDs to override their bootstrap equivalents.
274
+ #
275
+ # The "proto." prefix is a **protocol identity namespace**, NOT a source
276
+ # indicator. Both bootstrap and contract sources use this prefix via the
277
+ # shared handler_identity() helper. This enables per-handler identity
278
+ # matching regardless of which source discovered the handler.
279
+ #
280
+ # See: HandlerSourceResolver._resolve_hybrid() for resolution logic.
281
+ # See: handler_identity.py for the shared helper function.
282
+ handler_id=handler_identity(loaded.protocol_type),
283
+ name=loaded.handler_name,
284
+ version=loaded.handler_version,
285
+ handler_kind=handler_kind,
286
+ input_model="omnibase_infra.models.types.JsonDict",
287
+ output_model="omnibase_core.models.dispatch.ModelHandlerOutput",
288
+ description=f"Handler: {loaded.handler_name}",
289
+ handler_class=loaded.handler_class,
290
+ contract_path=str(loaded.contract_path),
291
+ )
292
+ descriptors.append(descriptor)
293
+
294
+ except Exception as e:
295
+ logger.warning(
296
+ "Failed to load handlers from path %s: %s",
297
+ path_obj,
298
+ e,
299
+ )
300
+ # Continue with other paths (graceful degradation)
301
+
302
+ return ModelContractDiscoveryResult(
303
+ descriptors=descriptors,
304
+ validation_errors=validation_errors,
305
+ )
306
+
307
+
127
308
  class RuntimeHostProcess:
128
309
  """Runtime host process that owns event bus and coordinates handlers.
129
310
 
@@ -231,7 +412,7 @@ class RuntimeHostProcess:
231
412
 
232
413
  Purpose:
233
414
  Provides the registry that maps handler_type strings (e.g., "http", "db")
234
- to their corresponding ProtocolHandler classes. The registry is queried
415
+ to their corresponding ProtocolContainerAware classes. The registry is queried
235
416
  during start() to instantiate and initialize all registered handlers.
236
417
 
237
418
  Resolution Order:
@@ -244,10 +425,13 @@ class RuntimeHostProcess:
244
425
  the container and pass it to RuntimeHostProcess:
245
426
 
246
427
  ```python
247
- container = ModelONEXContainer()
248
- wire_infrastructure_services(container)
249
- registry = container.service_registry.resolve_service(RegistryProtocolBinding)
250
- process = RuntimeHostProcess(handler_registry=registry)
428
+ async def create_runtime() -> RuntimeHostProcess:
429
+ container = ModelONEXContainer()
430
+ await wire_infrastructure_services(container)
431
+ registry = await container.service_registry.resolve_service(
432
+ RegistryProtocolBinding
433
+ )
434
+ return RuntimeHostProcess(handler_registry=registry)
251
435
  ```
252
436
 
253
437
  This follows ONEX container-based DI patterns for better testability
@@ -469,12 +653,17 @@ class RuntimeHostProcess:
469
653
 
470
654
  # Handler registry (handler_type -> handler instance)
471
655
  # This will be populated from the singleton registry during start()
472
- self._handlers: dict[str, ProtocolHandler] = {}
656
+ self._handlers: dict[str, ProtocolContainerAware] = {}
473
657
 
474
658
  # Track failed handler instantiations (handler_type -> error message)
475
659
  # Used by health_check() to report degraded state
476
660
  self._failed_handlers: dict[str, str] = {}
477
661
 
662
+ # Handler descriptors (handler_type -> descriptor with contract_config)
663
+ # Stored during registration for use during handler initialization
664
+ # Enables contract config to be passed to handlers via initialize()
665
+ self._handler_descriptors: dict[str, ModelHandlerDescriptor] = {}
666
+
478
667
  # Pending message tracking for graceful shutdown (OMN-756)
479
668
  # Tracks count of in-flight messages currently being processed
480
669
  self._pending_message_count: int = 0
@@ -777,7 +966,7 @@ class RuntimeHostProcess:
777
966
  " - Look for: AMBIGUOUS_CONTRACT (HANDLER_LOADER_040)\n\n"
778
967
  " 6. If using wire_handlers() manually:\n"
779
968
  " - Ensure wire_handlers() is called before RuntimeHostProcess.start()\n"
780
- " - Check that handlers implement ProtocolHandler interface\n\n"
969
+ " - Check that handlers implement ProtocolContainerAware interface\n\n"
781
970
  " 7. Docker/container environment:\n"
782
971
  " - Verify volume mounts include handler contract directories\n"
783
972
  " - Check ONEX_CONTRACTS_DIR is set in docker-compose.yml/Dockerfile\n"
@@ -966,115 +1155,365 @@ class RuntimeHostProcess:
966
1155
 
967
1156
  logger.info("RuntimeHostProcess stopped successfully")
968
1157
 
969
- async def _discover_or_wire_handlers(self) -> None:
970
- """Discover handlers from contracts or wire default handlers.
1158
+ def _load_handler_source_config(self) -> ModelHandlerSourceConfig:
1159
+ """Load handler source configuration from runtime config.
971
1160
 
972
- This method implements the handler discovery/wiring step (Step 3) of the
973
- start() sequence. It supports two modes:
1161
+ Loads the handler source mode configuration that controls how handlers
1162
+ are discovered (BOOTSTRAP, CONTRACT, or HYBRID mode).
974
1163
 
975
- Contract-Based Discovery (OMN-1133):
976
- If contract_paths were provided at init, uses ContractHandlerDiscovery
977
- to auto-discover and register handlers from the specified paths.
1164
+ Config Keys:
1165
+ handler_source_mode: "bootstrap" | "contract" | "hybrid" (default: "hybrid")
1166
+ bootstrap_expires_at: ISO-8601 datetime string (optional, UTC required)
978
1167
 
979
- Discovery errors are logged but do not block startup, enabling
980
- graceful degradation where some handlers can be registered even
981
- if others fail to load.
1168
+ Returns:
1169
+ ModelHandlerSourceConfig with validated settings.
982
1170
 
983
- Default Handler Wiring (Fallback):
984
- If no contract_paths were provided, falls back to wire_default_handlers()
985
- which registers the standard set of handlers (HTTP, DB, Consul, Vault).
1171
+ Note:
1172
+ If no configuration is provided, defaults to HYBRID mode with no
1173
+ bootstrap expiry (bootstrap handlers always available as fallback).
986
1174
 
987
- The discovery/wiring step registers handler CLASSES with the handler registry.
988
- The subsequent _populate_handlers_from_registry() step instantiates and
989
- initializes these handler classes.
1175
+ .. versionadded:: 0.7.0
1176
+ Part of OMN-1095 handler source mode integration.
990
1177
  """
991
- if self._contract_paths:
992
- # Contract-based handler discovery (OMN-1133)
993
- await self._discover_handlers_from_contracts()
994
- else:
995
- # Fallback to default handler wiring (existing behavior)
996
- wire_handlers()
1178
+ # Deferred imports: avoid circular dependencies at module load time
1179
+ # and reduce import overhead when this method is not called.
1180
+ from datetime import datetime
997
1181
 
998
- async def _discover_handlers_from_contracts(self) -> None:
999
- """Discover and register handlers from contract files.
1182
+ from pydantic import ValidationError
1000
1183
 
1001
- This method implements contract-based handler discovery as part of OMN-1133.
1002
- It creates a ContractHandlerDiscovery service, discovers handlers from the
1003
- configured contract_paths, and registers them with the handler registry.
1184
+ from omnibase_infra.models.handlers import ModelHandlerSourceConfig
1004
1185
 
1005
- Error Handling:
1006
- Discovery errors are logged but do not block startup. This enables
1007
- graceful degradation where some handlers can be registered even if
1008
- others fail to load.
1186
+ config = self._config or {}
1187
+ handler_source_config = config.get("handler_source", {})
1009
1188
 
1010
- The discovery service tracks:
1011
- - handlers_discovered: Number of handlers found in contracts
1012
- - handlers_registered: Number successfully registered
1013
- - errors: List of individual discovery/registration failures
1189
+ if isinstance(handler_source_config, dict):
1190
+ mode_str = handler_source_config.get(
1191
+ "mode", EnumHandlerSourceMode.HYBRID.value
1192
+ )
1193
+ expires_at_str = handler_source_config.get("bootstrap_expires_at")
1194
+ allow_override_raw = handler_source_config.get(
1195
+ "allow_bootstrap_override", False
1196
+ )
1014
1197
 
1015
- Related:
1016
- - ContractHandlerDiscovery: Discovery service implementation
1017
- - HandlerPluginLoader: Contract file parsing and validation
1018
- - ModelDiscoveryResult: Result model with error tracking
1198
+ # Parse mode
1199
+ try:
1200
+ mode = EnumHandlerSourceMode(mode_str)
1201
+ except ValueError:
1202
+ logger.warning(
1203
+ "Invalid handler_source_mode, defaulting to HYBRID",
1204
+ extra={"invalid_value": mode_str},
1205
+ )
1206
+ mode = EnumHandlerSourceMode.HYBRID
1207
+
1208
+ # Parse expiry datetime
1209
+ expires_at = None
1210
+ if expires_at_str:
1211
+ try:
1212
+ expires_at = datetime.fromisoformat(str(expires_at_str))
1213
+ except ValueError:
1214
+ logger.warning(
1215
+ "Invalid bootstrap_expires_at format, ignoring",
1216
+ extra={"invalid_value": expires_at_str},
1217
+ )
1218
+
1219
+ # Construct config with validation - catch naive datetime errors
1220
+ # Note: allow_bootstrap_override coercion handled by Pydantic field validator
1221
+ try:
1222
+ return ModelHandlerSourceConfig(
1223
+ handler_source_mode=mode,
1224
+ bootstrap_expires_at=expires_at,
1225
+ allow_bootstrap_override=allow_override_raw,
1226
+ )
1227
+ except ValidationError as e:
1228
+ # Check if error is due to naive datetime (no timezone info)
1229
+ error_messages = [err.get("msg", "") for err in e.errors()]
1230
+ if any("timezone-aware" in msg for msg in error_messages):
1231
+ logger.warning(
1232
+ "bootstrap_expires_at must be timezone-aware (UTC recommended). "
1233
+ "Naive datetime provided - falling back to no expiry. "
1234
+ "Use ISO format with timezone: '2026-02-01T00:00:00+00:00' "
1235
+ "or '2026-02-01T00:00:00Z'",
1236
+ extra={
1237
+ "invalid_value": expires_at_str,
1238
+ "parsed_datetime": str(expires_at) if expires_at else None,
1239
+ },
1240
+ )
1241
+ # Fall back to config without expiry
1242
+ return ModelHandlerSourceConfig(
1243
+ handler_source_mode=mode,
1244
+ bootstrap_expires_at=None,
1245
+ allow_bootstrap_override=allow_override_raw,
1246
+ )
1247
+ # Re-raise other validation errors
1248
+ raise
1249
+
1250
+ # Default: HYBRID mode with no expiry
1251
+ return ModelHandlerSourceConfig(
1252
+ handler_source_mode=EnumHandlerSourceMode.HYBRID
1253
+ )
1254
+
1255
+ async def _resolve_handler_descriptors(self) -> list[ModelHandlerDescriptor]:
1256
+ """Resolve handler descriptors using the configured source mode.
1257
+
1258
+ Uses HandlerSourceResolver to discover handlers based on the configured
1259
+ mode (BOOTSTRAP, CONTRACT, or HYBRID). This replaces the previous
1260
+ sequential discovery logic with a unified, mode-driven approach.
1261
+
1262
+ Resolution Modes:
1263
+ - BOOTSTRAP: Only hardcoded bootstrap handlers
1264
+ - CONTRACT: Only filesystem contract-discovered handlers
1265
+ - HYBRID: Contract handlers win per-identity, bootstrap as fallback
1266
+
1267
+ Returns:
1268
+ List of resolved handler descriptors.
1269
+
1270
+ Raises:
1271
+ RuntimeHostError: If validation errors occur and fail-fast is enabled.
1272
+
1273
+ .. versionadded:: 0.7.0
1274
+ Part of OMN-1095 handler source mode integration.
1019
1275
  """
1020
- from omnibase_infra.runtime.contract_handler_discovery import (
1021
- ContractHandlerDiscovery,
1276
+ from omnibase_infra.runtime.handler_bootstrap_source import (
1277
+ HandlerBootstrapSource,
1022
1278
  )
1023
- from omnibase_infra.runtime.handler_plugin_loader import HandlerPluginLoader
1279
+ from omnibase_infra.runtime.handler_source_resolver import HandlerSourceResolver
1280
+
1281
+ source_config = self._load_handler_source_config()
1024
1282
 
1025
1283
  logger.info(
1026
- "Starting contract-based handler discovery",
1284
+ "Resolving handlers with source mode",
1027
1285
  extra={
1028
- "contract_paths": [str(p) for p in self._contract_paths],
1029
- "path_count": len(self._contract_paths),
1286
+ "mode": source_config.handler_source_mode.value,
1287
+ "effective_mode": source_config.effective_mode.value,
1288
+ "bootstrap_expires_at": str(source_config.bootstrap_expires_at)
1289
+ if source_config.bootstrap_expires_at
1290
+ else None,
1291
+ "is_bootstrap_expired": source_config.is_bootstrap_expired,
1030
1292
  },
1031
1293
  )
1032
1294
 
1033
- # Create handler discovery service if not already created
1034
- # Uses the handler_registry from init or falls back to singleton
1035
- handler_registry = await self._get_handler_registry()
1295
+ # Create bootstrap source
1296
+ bootstrap_source = HandlerBootstrapSource()
1036
1297
 
1037
- self._handler_discovery = ContractHandlerDiscovery(
1038
- plugin_loader=HandlerPluginLoader(),
1039
- handler_registry=handler_registry,
1298
+ # Contract source needs paths - use configured paths or default
1299
+ # If no contract_paths provided, reuse bootstrap_source as placeholder
1300
+ if self._contract_paths:
1301
+ # Use PluginLoaderContractSource which uses the simpler contract schema
1302
+ # compatible with test contracts (handler_name, handler_class, handler_type)
1303
+ contract_source: ProtocolContractSource = PluginLoaderContractSource(
1304
+ contract_paths=self._contract_paths,
1305
+ )
1306
+ else:
1307
+ # No contract paths provided
1308
+ if source_config.effective_mode == EnumHandlerSourceMode.CONTRACT:
1309
+ # CONTRACT mode REQUIRES contract_paths - fail fast
1310
+ raise ProtocolConfigurationError(
1311
+ "CONTRACT mode requires contract_paths to be provided. "
1312
+ "Either provide contract_paths or use HYBRID/BOOTSTRAP mode.",
1313
+ context=ModelInfraErrorContext.with_correlation(
1314
+ transport_type=EnumInfraTransportType.RUNTIME,
1315
+ operation="resolve_handler_descriptors",
1316
+ ),
1317
+ )
1318
+ # BOOTSTRAP or HYBRID mode without contract_paths - use bootstrap as fallback
1319
+ #
1320
+ # HYBRID MODE NOTE: When HYBRID mode is configured but no contract_paths
1321
+ # are provided, we reuse bootstrap_source for both the bootstrap_source
1322
+ # and contract_source parameters of HandlerSourceResolver. This means
1323
+ # discover_handlers() will be called twice on the same instance:
1324
+ # 1. Once as the "contract source" (returns bootstrap handlers)
1325
+ # 2. Once as the "bootstrap source" (returns same bootstrap handlers)
1326
+ #
1327
+ # This is intentional: HYBRID semantics require consulting both sources,
1328
+ # and with no contracts available, bootstrap provides all handlers.
1329
+ # The HandlerSourceResolver's HYBRID merge logic (contract wins per-identity,
1330
+ # bootstrap as fallback) produces the correct result since both sources
1331
+ # return identical handlers. The outcome is functionally equivalent to
1332
+ # BOOTSTRAP mode but maintains HYBRID logging/metrics for observability.
1333
+ #
1334
+ # DO NOT "optimize" this to skip the second call - it would break
1335
+ # metrics expectations (contract_handler_count would not be logged)
1336
+ # and change HYBRID mode semantics. See test_bootstrap_source_integration.py
1337
+ # test_bootstrap_source_called_during_start() for the verification test.
1338
+ logger.debug(
1339
+ "HYBRID mode: No contract_paths provided, using bootstrap source "
1340
+ "as fallback for contract source",
1341
+ extra={
1342
+ "mode": source_config.effective_mode.value,
1343
+ "behavior": "bootstrap_source_reused",
1344
+ },
1345
+ )
1346
+ contract_source = bootstrap_source
1347
+
1348
+ # Create resolver with the effective mode (handles expiry enforcement)
1349
+ resolver = HandlerSourceResolver(
1350
+ bootstrap_source=bootstrap_source,
1351
+ contract_source=contract_source,
1352
+ mode=source_config.effective_mode,
1353
+ allow_bootstrap_override=source_config.allow_bootstrap_override,
1040
1354
  )
1041
1355
 
1042
- # Discover and register handlers from contract paths
1043
- discovery_result = await self._handler_discovery.discover_and_register(
1044
- contract_paths=self._contract_paths,
1356
+ # Resolve handlers
1357
+ result = await resolver.resolve_handlers()
1358
+
1359
+ # Log resolution results
1360
+ logger.info(
1361
+ "Handler resolution completed",
1362
+ extra={
1363
+ "descriptor_count": len(result.descriptors),
1364
+ "validation_error_count": len(result.validation_errors),
1365
+ "mode": source_config.effective_mode.value,
1366
+ },
1045
1367
  )
1046
1368
 
1047
- # Log discovery results
1048
- if discovery_result.has_errors:
1369
+ # Log validation errors but continue with valid descriptors (graceful degradation)
1370
+ # This allows the runtime to start with bootstrap handlers even if some contracts fail
1371
+ if result.validation_errors:
1372
+ error_summary = "; ".join(
1373
+ f"{e.handler_identity.handler_id or 'unknown'}: {e.message}"
1374
+ for e in result.validation_errors[:5] # Show first 5
1375
+ )
1376
+ if len(result.validation_errors) > 5:
1377
+ error_summary += f" ... and {len(result.validation_errors) - 5} more"
1378
+
1049
1379
  logger.warning(
1050
- "Handler discovery completed with errors",
1380
+ "Handler resolution completed with validation errors (continuing with valid handlers)",
1051
1381
  extra={
1052
- "handlers_discovered": discovery_result.handlers_discovered,
1053
- "handlers_registered": discovery_result.handlers_registered,
1054
- "error_count": len(discovery_result.errors),
1382
+ "error_count": len(result.validation_errors),
1383
+ "valid_descriptor_count": len(result.descriptors),
1384
+ "error_summary": error_summary,
1055
1385
  },
1056
1386
  )
1057
- # Log individual errors for debugging
1058
- for error in discovery_result.errors:
1059
- logger.error(
1060
- "Handler discovery error: %s",
1061
- error.message,
1387
+
1388
+ return list(result.descriptors)
1389
+
1390
+ async def _discover_or_wire_handlers(self) -> None:
1391
+ """Discover and register handlers for the runtime.
1392
+
1393
+ This method implements the handler discovery/wiring step (Step 3) of the
1394
+ start() sequence. It uses HandlerSourceResolver to discover handlers
1395
+ based on the configured source mode.
1396
+
1397
+ Handler Source Modes (OMN-1095):
1398
+ - BOOTSTRAP: Only hardcoded bootstrap handlers (fast, no filesystem I/O)
1399
+ - CONTRACT: Only filesystem contract-discovered handlers
1400
+ - HYBRID: Contract handlers win per-identity, bootstrap as fallback
1401
+
1402
+ The mode is configured via runtime config:
1403
+ handler_source:
1404
+ mode: "hybrid" # bootstrap|contract|hybrid
1405
+ bootstrap_expires_at: "2026-02-01T00:00:00Z" # Optional, UTC
1406
+
1407
+ The discovery/wiring step registers handler CLASSES with the handler registry.
1408
+ The subsequent _populate_handlers_from_registry() step instantiates and
1409
+ initializes these handler classes.
1410
+
1411
+ .. versionchanged:: 0.7.0
1412
+ Replaced sequential bootstrap+contract discovery with unified
1413
+ HandlerSourceResolver-based resolution (OMN-1095).
1414
+ """
1415
+ # Resolve handlers using configured source mode
1416
+ descriptors = await self._resolve_handler_descriptors()
1417
+
1418
+ # Get handler registry for registration
1419
+ handler_registry = await self._get_handler_registry()
1420
+
1421
+ registered_count = 0
1422
+ error_count = 0
1423
+
1424
+ for descriptor in descriptors:
1425
+ try:
1426
+ # Extract protocol type from handler_id
1427
+ # Handler IDs use "proto." prefix for identity matching (e.g., "proto.consul" -> "consul")
1428
+ # Contract handlers also use this prefix for HYBRID mode resolution
1429
+ # removeprefix() is a no-op if prefix doesn't exist, so handlers without prefix keep their name as-is
1430
+ protocol_type = descriptor.handler_id.removeprefix(
1431
+ f"{HANDLER_IDENTITY_PREFIX}."
1432
+ )
1433
+
1434
+ # Import the handler class from fully qualified path
1435
+ handler_class_path = descriptor.handler_class
1436
+ if handler_class_path is None:
1437
+ logger.warning(
1438
+ "Handler descriptor missing handler_class, skipping",
1439
+ extra={
1440
+ "handler_id": descriptor.handler_id,
1441
+ "handler_name": descriptor.name,
1442
+ },
1443
+ )
1444
+ error_count += 1
1445
+ continue
1446
+
1447
+ # Import class using rsplit pattern
1448
+ if "." not in handler_class_path:
1449
+ logger.error(
1450
+ "Invalid handler class path (must be fully qualified): %s",
1451
+ handler_class_path,
1452
+ extra={"handler_id": descriptor.handler_id},
1453
+ )
1454
+ error_count += 1
1455
+ continue
1456
+
1457
+ module_path, class_name = handler_class_path.rsplit(".", 1)
1458
+ module = importlib.import_module(module_path)
1459
+ handler_cls = getattr(module, class_name)
1460
+
1461
+ # Verify handler_cls is actually a class before registration
1462
+ if not isinstance(handler_cls, type):
1463
+ logger.error(
1464
+ "Handler class path does not resolve to a class type",
1465
+ extra={
1466
+ "handler_id": descriptor.handler_id,
1467
+ "handler_class_path": handler_class_path,
1468
+ "resolved_type": type(handler_cls).__name__,
1469
+ },
1470
+ )
1471
+ error_count += 1
1472
+ continue
1473
+
1474
+ # Register with handler registry
1475
+ handler_registry.register(protocol_type, handler_cls)
1476
+
1477
+ # Store descriptor for later use during initialization
1478
+ self._handler_descriptors[protocol_type] = descriptor
1479
+
1480
+ registered_count += 1
1481
+ logger.debug(
1482
+ "Registered handler from descriptor",
1062
1483
  extra={
1063
- "error_code": error.error_code,
1064
- "handler_name": error.handler_name,
1065
- "contract_path": str(error.contract_path)
1066
- if error.contract_path
1067
- else None,
1484
+ "handler_id": descriptor.handler_id,
1485
+ "protocol_type": protocol_type,
1486
+ "handler_class": handler_class_path,
1068
1487
  },
1069
1488
  )
1070
- else:
1071
- logger.info(
1072
- "Handler discovery completed successfully",
1073
- extra={
1074
- "handlers_discovered": discovery_result.handlers_discovered,
1075
- "handlers_registered": discovery_result.handlers_registered,
1076
- },
1077
- )
1489
+
1490
+ except (ImportError, AttributeError):
1491
+ logger.exception(
1492
+ "Failed to import handler",
1493
+ extra={
1494
+ "handler_id": descriptor.handler_id,
1495
+ "handler_class": descriptor.handler_class,
1496
+ },
1497
+ )
1498
+ error_count += 1
1499
+ except Exception:
1500
+ logger.exception(
1501
+ "Unexpected error registering handler",
1502
+ extra={
1503
+ "handler_id": descriptor.handler_id,
1504
+ "handler_class": descriptor.handler_class,
1505
+ },
1506
+ )
1507
+ error_count += 1
1508
+
1509
+ logger.info(
1510
+ "Handler discovery completed",
1511
+ extra={
1512
+ "registered_count": registered_count,
1513
+ "error_count": error_count,
1514
+ "total_descriptors": len(descriptors),
1515
+ },
1516
+ )
1078
1517
 
1079
1518
  async def _populate_handlers_from_registry(self) -> None:
1080
1519
  """Populate self._handlers from handler registry (container or singleton).
@@ -1112,6 +1551,10 @@ class RuntimeHostProcess:
1112
1551
  },
1113
1552
  )
1114
1553
 
1554
+ # Get or create container once for all handlers to share
1555
+ # This ensures all handlers have access to the same DI container
1556
+ container = self._get_or_create_container()
1557
+
1115
1558
  for handler_type in registered_types:
1116
1559
  # Skip if handler is already registered (e.g., by tests or explicit registration)
1117
1560
  if handler_type in self._handlers:
@@ -1128,15 +1571,54 @@ class RuntimeHostProcess:
1128
1571
 
1129
1572
  try:
1130
1573
  # Get handler class from singleton registry
1131
- handler_cls: type[ProtocolHandler] = handler_registry.get(handler_type)
1574
+ handler_cls: type[ProtocolContainerAware] = handler_registry.get(
1575
+ handler_type
1576
+ )
1132
1577
 
1133
- # Instantiate the handler
1134
- handler_instance: ProtocolHandler = handler_cls()
1578
+ # Instantiate the handler with container for dependency injection
1579
+ # ProtocolContainerAware defines __init__(container: ModelONEXContainer)
1580
+ handler_instance: ProtocolContainerAware = handler_cls(
1581
+ container=container
1582
+ )
1135
1583
 
1136
1584
  # Call initialize() if the handler has this method
1137
1585
  # Handlers may require async initialization with config
1138
1586
  if hasattr(handler_instance, "initialize"):
1139
- await handler_instance.initialize(self._config)
1587
+ # Build effective config: contract config as base, runtime overrides on top
1588
+ # This enables contracts to provide handler-specific defaults while
1589
+ # allowing runtime/deploy-time customization without touching contracts
1590
+ effective_config: dict[str, object] = {}
1591
+ config_source = "runtime_only"
1592
+
1593
+ # Layer 1: Contract config as baseline (if descriptor exists with config)
1594
+ descriptor = self._handler_descriptors.get(handler_type)
1595
+ if descriptor and descriptor.contract_config:
1596
+ effective_config.update(descriptor.contract_config)
1597
+ config_source = "contract_only"
1598
+
1599
+ # Layer 2: Runtime config overrides
1600
+ # Runtime config takes precedence, enabling deploy-time customization
1601
+ if self._config:
1602
+ effective_config.update(self._config)
1603
+ if descriptor and descriptor.contract_config:
1604
+ config_source = "contract+runtime_override"
1605
+
1606
+ # Pass empty dict if no config, not None
1607
+ # Handlers expect dict interface (e.g., config.get("key"))
1608
+ await handler_instance.initialize(effective_config)
1609
+
1610
+ logger.debug(
1611
+ "Handler initialized with effective config",
1612
+ extra={
1613
+ "handler_type": handler_type,
1614
+ "config_source": config_source,
1615
+ "effective_config_keys": list(effective_config.keys()),
1616
+ "has_contract_config": bool(
1617
+ descriptor and descriptor.contract_config
1618
+ ),
1619
+ "has_runtime_config": bool(self._config),
1620
+ },
1621
+ )
1140
1622
 
1141
1623
  # Store the handler instance for routing
1142
1624
  self._handlers[handler_type] = handler_instance
@@ -1702,12 +2184,14 @@ class RuntimeHostProcess:
1702
2184
  "no_handlers_registered": no_handlers_registered,
1703
2185
  }
1704
2186
 
1705
- def register_handler(self, handler_type: str, handler: ProtocolHandler) -> None:
2187
+ def register_handler(
2188
+ self, handler_type: str, handler: ProtocolContainerAware
2189
+ ) -> None:
1706
2190
  """Register a handler for a specific type.
1707
2191
 
1708
2192
  Args:
1709
2193
  handler_type: Protocol type identifier (e.g., "http", "db").
1710
- handler: Handler instance implementing the ProtocolHandler protocol.
2194
+ handler: Handler instance implementing the ProtocolContainerAware protocol.
1711
2195
  """
1712
2196
  self._handlers[handler_type] = handler
1713
2197
  logger.debug(
@@ -1718,7 +2202,7 @@ class RuntimeHostProcess:
1718
2202
  },
1719
2203
  )
1720
2204
 
1721
- def get_handler(self, handler_type: str) -> ProtocolHandler | None:
2205
+ def get_handler(self, handler_type: str) -> ProtocolContainerAware | None:
1722
2206
  """Get handler for type, returns None if not registered.
1723
2207
 
1724
2208
  Args:
@@ -1805,7 +2289,7 @@ class RuntimeHostProcess:
1805
2289
  # after validation in _populate_handlers_from_registry). We validate the
1806
2290
  # handler CLASSES from the registry, not handler instances.
1807
2291
  handler_registry = await self._get_handler_registry()
1808
- handler_classes: list[type[ProtocolHandler]] = []
2292
+ handler_classes: list[type[ProtocolContainerAware]] = []
1809
2293
  for handler_type in handler_registry.list_protocols():
1810
2294
  try:
1811
2295
  handler_cls = handler_registry.get(handler_type)
@@ -1876,29 +2360,28 @@ class RuntimeHostProcess:
1876
2360
  )
1877
2361
 
1878
2362
  def _get_or_create_container(self) -> ModelONEXContainer:
1879
- """Get the injected container or create a new one.
2363
+ """Get the injected container or create and cache a new one.
1880
2364
 
1881
2365
  Returns:
1882
- ModelONEXContainer instance for architecture validation.
2366
+ ModelONEXContainer instance for dependency injection.
1883
2367
 
1884
2368
  Note:
1885
- If no container was provided at init, a new container is created.
1886
- This container provides basic infrastructure for node execution
1887
- but may not have all services wired.
2369
+ If no container was provided at init, a new container is created
2370
+ and cached in self._container. This ensures all handlers share
2371
+ the same container instance. The container provides basic
2372
+ infrastructure for node execution but may not have all services wired.
1888
2373
  """
1889
2374
  if self._container is not None:
1890
2375
  return self._container
1891
2376
 
1892
- # Create container for validation
2377
+ # Create container and cache it for reuse
1893
2378
  from omnibase_core.models.container.model_onex_container import (
1894
2379
  ModelONEXContainer,
1895
2380
  )
1896
2381
 
1897
- logger.debug(
1898
- "Creating container for architecture validation "
1899
- "(no container provided at init)"
1900
- )
1901
- return ModelONEXContainer()
2382
+ logger.debug("Creating and caching container (no container provided at init)")
2383
+ self._container = ModelONEXContainer()
2384
+ return self._container
1902
2385
 
1903
2386
  # =========================================================================
1904
2387
  # Idempotency Guard Methods (OMN-945)