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
@@ -53,7 +53,11 @@ from uuid import UUID, uuid4
53
53
 
54
54
  from pydantic import BaseModel
55
55
 
56
- from omnibase_infra.enums import EnumInfraTransportType
56
+ from omnibase_infra.enums import (
57
+ EnumHandlerSourceMode,
58
+ EnumHandlerTypeCategory,
59
+ EnumInfraTransportType,
60
+ )
57
61
  from omnibase_infra.errors import (
58
62
  EnvelopeValidationError,
59
63
  ModelInfraErrorContext,
@@ -80,14 +84,27 @@ if TYPE_CHECKING:
80
84
  from omnibase_infra.idempotency.protocol_idempotency_store import (
81
85
  ProtocolIdempotencyStore,
82
86
  )
83
- from omnibase_infra.models.handlers import ModelHandlerDescriptor
87
+ from omnibase_infra.models.handlers import ModelHandlerSourceConfig
84
88
  from omnibase_infra.nodes.architecture_validator import ProtocolArchitectureRule
89
+ from omnibase_infra.protocols import ProtocolContainerAware
85
90
  from omnibase_infra.runtime.contract_handler_discovery import (
86
91
  ContractHandlerDiscovery,
87
92
  )
88
- from omnibase_spi.protocols.handlers.protocol_handler import ProtocolHandler
89
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
+ )
90
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
91
108
 
92
109
  # Expose wire_default_handlers as wire_handlers for test patching compatibility
93
110
  # Tests patch "omnibase_infra.runtime.service_runtime_host_process.wire_handlers"
@@ -95,6 +112,22 @@ wire_handlers = wire_default_handlers
95
112
 
96
113
  logger = logging.getLogger(__name__)
97
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
+
98
131
  # Default configuration values
99
132
  DEFAULT_INPUT_TOPIC = "requests"
100
133
  DEFAULT_OUTPUT_TOPIC = "responses"
@@ -126,6 +159,152 @@ DEFAULT_DRAIN_TIMEOUT_SECONDS: float = parse_env_float(
126
159
  )
127
160
 
128
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
+
129
308
  class RuntimeHostProcess:
130
309
  """Runtime host process that owns event bus and coordinates handlers.
131
310
 
@@ -233,7 +412,7 @@ class RuntimeHostProcess:
233
412
 
234
413
  Purpose:
235
414
  Provides the registry that maps handler_type strings (e.g., "http", "db")
236
- to their corresponding ProtocolHandler classes. The registry is queried
415
+ to their corresponding ProtocolContainerAware classes. The registry is queried
237
416
  during start() to instantiate and initialize all registered handlers.
238
417
 
239
418
  Resolution Order:
@@ -474,7 +653,7 @@ class RuntimeHostProcess:
474
653
 
475
654
  # Handler registry (handler_type -> handler instance)
476
655
  # This will be populated from the singleton registry during start()
477
- self._handlers: dict[str, ProtocolHandler] = {}
656
+ self._handlers: dict[str, ProtocolContainerAware] = {}
478
657
 
479
658
  # Track failed handler instantiations (handler_type -> error message)
480
659
  # Used by health_check() to report degraded state
@@ -787,7 +966,7 @@ class RuntimeHostProcess:
787
966
  " - Look for: AMBIGUOUS_CONTRACT (HANDLER_LOADER_040)\n\n"
788
967
  " 6. If using wire_handlers() manually:\n"
789
968
  " - Ensure wire_handlers() is called before RuntimeHostProcess.start()\n"
790
- " - Check that handlers implement ProtocolHandler interface\n\n"
969
+ " - Check that handlers implement ProtocolContainerAware interface\n\n"
791
970
  " 7. Docker/container environment:\n"
792
971
  " - Verify volume mounts include handler contract directories\n"
793
972
  " - Check ONEX_CONTRACTS_DIR is set in docker-compose.yml/Dockerfile\n"
@@ -976,183 +1155,287 @@ class RuntimeHostProcess:
976
1155
 
977
1156
  logger.info("RuntimeHostProcess stopped successfully")
978
1157
 
979
- async def _discover_or_wire_handlers(self) -> None:
980
- """Discover and register handlers for the runtime.
1158
+ def _load_handler_source_config(self) -> ModelHandlerSourceConfig:
1159
+ """Load handler source configuration from runtime config.
981
1160
 
982
- This method implements the handler discovery/wiring step (Step 3) of the
983
- start() sequence.
1161
+ Loads the handler source mode configuration that controls how handlers
1162
+ are discovered (BOOTSTRAP, CONTRACT, or HYBRID mode).
984
1163
 
985
- Bootstrap Handlers (OMN-1087):
986
- Bootstrap handlers (Consul, DB, HTTP, Vault) are ALWAYS registered first
987
- via HandlerBootstrapSource. These core infrastructure handlers are
988
- essential for the ONEX runtime and are loaded from descriptor-based
989
- definitions rather than filesystem contracts.
1164
+ Config Keys:
1165
+ handler_source_mode: "bootstrap" | "contract" | "hybrid" (default: "hybrid")
1166
+ bootstrap_expires_at: ISO-8601 datetime string (optional, UTC required)
990
1167
 
991
- Contract-Based Discovery (OMN-1133):
992
- If contract_paths were provided at init, uses ContractHandlerDiscovery
993
- to auto-discover and register additional handlers from the specified paths.
1168
+ Returns:
1169
+ ModelHandlerSourceConfig with validated settings.
994
1170
 
995
- Discovery errors are logged but do not block startup, enabling
996
- graceful degradation where some handlers can be registered even
997
- if others fail to load.
1171
+ Note:
1172
+ If no configuration is provided, defaults to HYBRID mode with no
1173
+ bootstrap expiry (bootstrap handlers always available as fallback).
998
1174
 
999
- The discovery/wiring step registers handler CLASSES with the handler registry.
1000
- The subsequent _populate_handlers_from_registry() step instantiates and
1001
- initializes these handler classes.
1175
+ .. versionadded:: 0.7.0
1176
+ Part of OMN-1095 handler source mode integration.
1002
1177
  """
1003
- # Register bootstrap handlers FIRST (OMN-1087)
1004
- # Bootstrap handlers are core infrastructure handlers that are always
1005
- # available, loaded from HandlerBootstrapSource descriptors.
1006
- await self._register_bootstrap_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
1007
1181
 
1008
- if self._contract_paths:
1009
- # Contract-based handler discovery (OMN-1133)
1010
- await self._discover_handlers_from_contracts()
1182
+ from pydantic import ValidationError
1011
1183
 
1012
- async def _discover_handlers_from_contracts(self) -> None:
1013
- """Discover and register handlers from contract files.
1184
+ from omnibase_infra.models.handlers import ModelHandlerSourceConfig
1014
1185
 
1015
- This method implements contract-based handler discovery as part of OMN-1133.
1016
- It creates a ContractHandlerDiscovery service, discovers handlers from the
1017
- configured contract_paths, and registers them with the handler registry.
1186
+ config = self._config or {}
1187
+ handler_source_config = config.get("handler_source", {})
1018
1188
 
1019
- Error Handling:
1020
- Discovery errors are logged but do not block startup. This enables
1021
- graceful degradation where some handlers can be registered even if
1022
- others fail to load.
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
+ )
1023
1197
 
1024
- The discovery service tracks:
1025
- - handlers_discovered: Number of handlers found in contracts
1026
- - handlers_registered: Number successfully registered
1027
- - errors: List of individual discovery/registration failures
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
1028
1207
 
1029
- Related:
1030
- - ContractHandlerDiscovery: Discovery service implementation
1031
- - HandlerPluginLoader: Contract file parsing and validation
1032
- - ModelDiscoveryResult: Result model with error tracking
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.
1033
1275
  """
1034
- from omnibase_infra.runtime.contract_handler_discovery import (
1035
- ContractHandlerDiscovery,
1276
+ from omnibase_infra.runtime.handler_bootstrap_source import (
1277
+ HandlerBootstrapSource,
1036
1278
  )
1037
- 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()
1038
1282
 
1039
1283
  logger.info(
1040
- "Starting contract-based handler discovery",
1284
+ "Resolving handlers with source mode",
1041
1285
  extra={
1042
- "contract_paths": [str(p) for p in self._contract_paths],
1043
- "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,
1044
1292
  },
1045
1293
  )
1046
1294
 
1047
- # Create handler discovery service if not already created
1048
- # Uses the handler_registry from init or falls back to singleton
1049
- handler_registry = await self._get_handler_registry()
1295
+ # Create bootstrap source
1296
+ bootstrap_source = HandlerBootstrapSource()
1050
1297
 
1051
- self._handler_discovery = ContractHandlerDiscovery(
1052
- plugin_loader=HandlerPluginLoader(),
1053
- 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,
1054
1354
  )
1055
1355
 
1056
- # Discover and register handlers from contract paths
1057
- discovery_result = await self._handler_discovery.discover_and_register(
1058
- 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
+ },
1059
1367
  )
1060
1368
 
1061
- # Log discovery results
1062
- if discovery_result.has_errors:
1063
- logger.warning(
1064
- "Handler discovery completed with errors",
1065
- extra={
1066
- "handlers_discovered": discovery_result.handlers_discovered,
1067
- "handlers_registered": discovery_result.handlers_registered,
1068
- "error_count": len(discovery_result.errors),
1069
- },
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
1070
1375
  )
1071
- # Log individual errors for debugging
1072
- for error in discovery_result.errors:
1073
- logger.error(
1074
- "Handler discovery error: %s",
1075
- error.message,
1076
- extra={
1077
- "error_code": error.error_code,
1078
- "handler_name": error.handler_name,
1079
- "contract_path": str(error.contract_path)
1080
- if error.contract_path
1081
- else None,
1082
- },
1083
- )
1084
- else:
1085
- logger.info(
1086
- "Handler discovery completed successfully",
1376
+ if len(result.validation_errors) > 5:
1377
+ error_summary += f" ... and {len(result.validation_errors) - 5} more"
1378
+
1379
+ logger.warning(
1380
+ "Handler resolution completed with validation errors (continuing with valid handlers)",
1087
1381
  extra={
1088
- "handlers_discovered": discovery_result.handlers_discovered,
1089
- "handlers_registered": discovery_result.handlers_registered,
1382
+ "error_count": len(result.validation_errors),
1383
+ "valid_descriptor_count": len(result.descriptors),
1384
+ "error_summary": error_summary,
1090
1385
  },
1091
1386
  )
1092
1387
 
1093
- async def _register_bootstrap_handlers(self) -> None:
1094
- """Register core infrastructure handlers from HandlerBootstrapSource.
1388
+ return list(result.descriptors)
1095
1389
 
1096
- This method implements descriptor-based handler registration as part of
1097
- OMN-1087. It uses HandlerBootstrapSource to discover and register the
1098
- core infrastructure handlers (Consul, DB, HTTP, Vault) without requiring
1099
- contract.yaml files on the filesystem.
1390
+ async def _discover_or_wire_handlers(self) -> None:
1391
+ """Discover and register handlers for the runtime.
1100
1392
 
1101
- Bootstrap handlers are registered FIRST, before any contract-based or
1102
- default handler wiring. This ensures core infrastructure handlers are
1103
- always available regardless of how other handlers are discovered.
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.
1104
1396
 
1105
- Handler Registration Process:
1106
- 1. Create HandlerBootstrapSource instance
1107
- 2. Call discover_handlers() to get handler descriptors
1108
- 3. For each descriptor:
1109
- a. Extract protocol type from handler_id (e.g., "bootstrap.consul" -> "consul")
1110
- b. Import handler class from fully qualified class path
1111
- c. Register class with handler registry
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
1112
1401
 
1113
- Error Handling:
1114
- Individual handler failures are logged but do not block registration
1115
- of other handlers. This enables graceful degradation where some
1116
- bootstrap handlers can be registered even if others fail to import.
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
1117
1406
 
1118
- Related:
1119
- - HandlerBootstrapSource: Source that provides handler descriptors
1120
- - ModelHandlerDescriptor: Descriptor model with handler metadata
1121
- - _discover_or_wire_handlers: Caller that orchestrates all handler loading
1122
- """
1123
- from omnibase_infra.runtime.handler_bootstrap_source import (
1124
- SOURCE_TYPE_BOOTSTRAP,
1125
- HandlerBootstrapSource,
1126
- )
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.
1127
1410
 
1128
- logger.info(
1129
- "Starting bootstrap handler registration",
1130
- extra={"source_type": SOURCE_TYPE_BOOTSTRAP},
1131
- )
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()
1132
1417
 
1133
1418
  # Get handler registry for registration
1134
1419
  handler_registry = await self._get_handler_registry()
1135
1420
 
1136
- # Create bootstrap source and discover handlers
1137
- bootstrap_source = HandlerBootstrapSource()
1138
- discovery_result = await bootstrap_source.discover_handlers()
1139
-
1140
1421
  registered_count = 0
1141
1422
  error_count = 0
1142
1423
 
1143
- for descriptor in discovery_result.descriptors:
1424
+ for descriptor in descriptors:
1144
1425
  try:
1145
1426
  # Extract protocol type from handler_id
1146
- # Handler IDs are formatted as "bootstrap.{protocol_type}"
1147
- # e.g., "bootstrap.consul" -> "consul"
1148
- # removeprefix returns original string if prefix not found
1149
- protocol_type = descriptor.handler_id.removeprefix("bootstrap.")
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
+ )
1150
1433
 
1151
1434
  # Import the handler class from fully qualified path
1152
1435
  handler_class_path = descriptor.handler_class
1153
1436
  if handler_class_path is None:
1154
1437
  logger.warning(
1155
- "Bootstrap handler missing handler_class, skipping",
1438
+ "Handler descriptor missing handler_class, skipping",
1156
1439
  extra={
1157
1440
  "handler_id": descriptor.handler_id,
1158
1441
  "handler_name": descriptor.name,
@@ -1161,7 +1444,7 @@ class RuntimeHostProcess:
1161
1444
  error_count += 1
1162
1445
  continue
1163
1446
 
1164
- # Import class using rsplit pattern (same as ContractHandlerDiscovery)
1447
+ # Import class using rsplit pattern
1165
1448
  if "." not in handler_class_path:
1166
1449
  logger.error(
1167
1450
  "Invalid handler class path (must be fully qualified): %s",
@@ -1175,13 +1458,13 @@ class RuntimeHostProcess:
1175
1458
  module = importlib.import_module(module_path)
1176
1459
  handler_cls = getattr(module, class_name)
1177
1460
 
1178
- # Verify it's actually a class
1461
+ # Verify handler_cls is actually a class before registration
1179
1462
  if not isinstance(handler_cls, type):
1180
1463
  logger.error(
1181
- "Handler path does not resolve to a class: %s",
1182
- handler_class_path,
1464
+ "Handler class path does not resolve to a class type",
1183
1465
  extra={
1184
1466
  "handler_id": descriptor.handler_id,
1467
+ "handler_class_path": handler_class_path,
1185
1468
  "resolved_type": type(handler_cls).__name__,
1186
1469
  },
1187
1470
  )
@@ -1190,67 +1473,47 @@ class RuntimeHostProcess:
1190
1473
 
1191
1474
  # Register with handler registry
1192
1475
  handler_registry.register(protocol_type, handler_cls)
1193
- registered_count += 1
1194
1476
 
1195
- # Store descriptor for later use during handler initialization
1196
- # This enables passing contract_config to handler.initialize()
1477
+ # Store descriptor for later use during initialization
1197
1478
  self._handler_descriptors[protocol_type] = descriptor
1198
1479
 
1480
+ registered_count += 1
1199
1481
  logger.debug(
1200
- "Registered bootstrap handler: %s -> %s",
1201
- protocol_type,
1202
- handler_class_path,
1482
+ "Registered handler from descriptor",
1203
1483
  extra={
1204
1484
  "handler_id": descriptor.handler_id,
1205
1485
  "protocol_type": protocol_type,
1206
1486
  "handler_class": handler_class_path,
1207
- "has_contract_config": descriptor.contract_config is not None,
1208
- "source_type": SOURCE_TYPE_BOOTSTRAP,
1209
1487
  },
1210
1488
  )
1211
1489
 
1212
1490
  except (ImportError, AttributeError):
1213
- # Module or class import failed
1214
- error_count += 1
1215
1491
  logger.exception(
1216
- "Failed to import bootstrap handler",
1492
+ "Failed to import handler",
1217
1493
  extra={
1218
1494
  "handler_id": descriptor.handler_id,
1219
1495
  "handler_class": descriptor.handler_class,
1220
1496
  },
1221
1497
  )
1222
-
1223
- except Exception:
1224
- # Unexpected error - log but continue with other handlers
1225
1498
  error_count += 1
1499
+ except Exception:
1226
1500
  logger.exception(
1227
- "Unexpected error registering bootstrap handler",
1501
+ "Unexpected error registering handler",
1228
1502
  extra={
1229
1503
  "handler_id": descriptor.handler_id,
1230
1504
  "handler_class": descriptor.handler_class,
1231
1505
  },
1232
1506
  )
1507
+ error_count += 1
1233
1508
 
1234
- # Log summary
1235
- if error_count > 0:
1236
- logger.warning(
1237
- "Bootstrap handler registration completed with errors",
1238
- extra={
1239
- "registered_count": registered_count,
1240
- "error_count": error_count,
1241
- "total_descriptors": len(discovery_result.descriptors),
1242
- "source_type": SOURCE_TYPE_BOOTSTRAP,
1243
- },
1244
- )
1245
- else:
1246
- logger.info(
1247
- "Bootstrap handler registration completed successfully",
1248
- extra={
1249
- "registered_count": registered_count,
1250
- "total_descriptors": len(discovery_result.descriptors),
1251
- "source_type": SOURCE_TYPE_BOOTSTRAP,
1252
- },
1253
- )
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
+ )
1254
1517
 
1255
1518
  async def _populate_handlers_from_registry(self) -> None:
1256
1519
  """Populate self._handlers from handler registry (container or singleton).
@@ -1288,6 +1551,10 @@ class RuntimeHostProcess:
1288
1551
  },
1289
1552
  )
1290
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
+
1291
1558
  for handler_type in registered_types:
1292
1559
  # Skip if handler is already registered (e.g., by tests or explicit registration)
1293
1560
  if handler_type in self._handlers:
@@ -1304,10 +1571,15 @@ class RuntimeHostProcess:
1304
1571
 
1305
1572
  try:
1306
1573
  # Get handler class from singleton registry
1307
- handler_cls: type[ProtocolHandler] = handler_registry.get(handler_type)
1574
+ handler_cls: type[ProtocolContainerAware] = handler_registry.get(
1575
+ handler_type
1576
+ )
1308
1577
 
1309
- # Instantiate the handler
1310
- 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
+ )
1311
1583
 
1312
1584
  # Call initialize() if the handler has this method
1313
1585
  # Handlers may require async initialization with config
@@ -1912,12 +2184,14 @@ class RuntimeHostProcess:
1912
2184
  "no_handlers_registered": no_handlers_registered,
1913
2185
  }
1914
2186
 
1915
- def register_handler(self, handler_type: str, handler: ProtocolHandler) -> None:
2187
+ def register_handler(
2188
+ self, handler_type: str, handler: ProtocolContainerAware
2189
+ ) -> None:
1916
2190
  """Register a handler for a specific type.
1917
2191
 
1918
2192
  Args:
1919
2193
  handler_type: Protocol type identifier (e.g., "http", "db").
1920
- handler: Handler instance implementing the ProtocolHandler protocol.
2194
+ handler: Handler instance implementing the ProtocolContainerAware protocol.
1921
2195
  """
1922
2196
  self._handlers[handler_type] = handler
1923
2197
  logger.debug(
@@ -1928,7 +2202,7 @@ class RuntimeHostProcess:
1928
2202
  },
1929
2203
  )
1930
2204
 
1931
- def get_handler(self, handler_type: str) -> ProtocolHandler | None:
2205
+ def get_handler(self, handler_type: str) -> ProtocolContainerAware | None:
1932
2206
  """Get handler for type, returns None if not registered.
1933
2207
 
1934
2208
  Args:
@@ -2015,7 +2289,7 @@ class RuntimeHostProcess:
2015
2289
  # after validation in _populate_handlers_from_registry). We validate the
2016
2290
  # handler CLASSES from the registry, not handler instances.
2017
2291
  handler_registry = await self._get_handler_registry()
2018
- handler_classes: list[type[ProtocolHandler]] = []
2292
+ handler_classes: list[type[ProtocolContainerAware]] = []
2019
2293
  for handler_type in handler_registry.list_protocols():
2020
2294
  try:
2021
2295
  handler_cls = handler_registry.get(handler_type)
@@ -2086,29 +2360,28 @@ class RuntimeHostProcess:
2086
2360
  )
2087
2361
 
2088
2362
  def _get_or_create_container(self) -> ModelONEXContainer:
2089
- """Get the injected container or create a new one.
2363
+ """Get the injected container or create and cache a new one.
2090
2364
 
2091
2365
  Returns:
2092
- ModelONEXContainer instance for architecture validation.
2366
+ ModelONEXContainer instance for dependency injection.
2093
2367
 
2094
2368
  Note:
2095
- If no container was provided at init, a new container is created.
2096
- This container provides basic infrastructure for node execution
2097
- 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.
2098
2373
  """
2099
2374
  if self._container is not None:
2100
2375
  return self._container
2101
2376
 
2102
- # Create container for validation
2377
+ # Create container and cache it for reuse
2103
2378
  from omnibase_core.models.container.model_onex_container import (
2104
2379
  ModelONEXContainer,
2105
2380
  )
2106
2381
 
2107
- logger.debug(
2108
- "Creating container for architecture validation "
2109
- "(no container provided at init)"
2110
- )
2111
- return ModelONEXContainer()
2382
+ logger.debug("Creating and caching container (no container provided at init)")
2383
+ self._container = ModelONEXContainer()
2384
+ return self._container
2112
2385
 
2113
2386
  # =========================================================================
2114
2387
  # Idempotency Guard Methods (OMN-945)