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
@@ -659,6 +659,20 @@ class HandlerPluginLoader(ProtocolHandlerPluginLoader):
659
659
  },
660
660
  )
661
661
 
662
+ # contract.handler_version is guaranteed non-None by model_validator
663
+ if contract.handler_version is None:
664
+ context = ModelInfraErrorContext.with_correlation(
665
+ correlation_id=correlation_id,
666
+ transport_type=EnumInfraTransportType.RUNTIME,
667
+ operation="load_from_contract",
668
+ )
669
+ raise ProtocolConfigurationError(
670
+ "handler_version should be set by model_validator",
671
+ context=context,
672
+ loader_error=EnumHandlerLoaderError.MISSING_REQUIRED_FIELDS.value,
673
+ contract_path=str(contract_path),
674
+ )
675
+
662
676
  return ModelLoadedHandler(
663
677
  handler_name=handler_name,
664
678
  protocol_type=protocol_type,
@@ -667,6 +681,7 @@ class HandlerPluginLoader(ProtocolHandlerPluginLoader):
667
681
  contract_path=resolved_contract_path,
668
682
  capability_tags=capability_tags,
669
683
  loaded_at=datetime.now(UTC),
684
+ handler_version=contract.handler_version,
670
685
  )
671
686
 
672
687
  def load_from_directory(
@@ -820,7 +835,8 @@ class HandlerPluginLoader(ProtocolHandlerPluginLoader):
820
835
  # Extract error code if available
821
836
  error_code: str | None = None
822
837
  if hasattr(e, "model") and hasattr(e.model, "context"):
823
- error_code = e.model.context.get("loader_error")
838
+ loader_error = e.model.context.get("loader_error")
839
+ error_code = str(loader_error) if loader_error is not None else None
824
840
 
825
841
  failed_handlers.append(
826
842
  ModelFailedPluginLoad(
@@ -1126,7 +1142,8 @@ class HandlerPluginLoader(ProtocolHandlerPluginLoader):
1126
1142
  # Extract error code if available
1127
1143
  error_code: str | None = None
1128
1144
  if hasattr(e, "model") and hasattr(e.model, "context"):
1129
- error_code = e.model.context.get("loader_error")
1145
+ loader_error = e.model.context.get("loader_error")
1146
+ error_code = str(loader_error) if loader_error is not None else None
1130
1147
 
1131
1148
  failed_handlers.append(
1132
1149
  ModelFailedPluginLoad(
@@ -74,7 +74,7 @@ from omnibase_infra.runtime.registry.registry_protocol_binding import (
74
74
 
75
75
  if TYPE_CHECKING:
76
76
  from omnibase_core.protocol.protocol_event_bus import ProtocolEventBus
77
- from omnibase_spi.protocols.handlers.protocol_handler import ProtocolHandler
77
+ from omnibase_infra.protocols import ProtocolContainerAware
78
78
 
79
79
  # =============================================================================
80
80
  # Handler Type Constants
@@ -115,6 +115,12 @@ HANDLER_TYPE_MCP: str = "mcp"
115
115
  The MCP handler exposes ONEX nodes as tools for AI agents via streamable HTTP.
116
116
  Supports tools/list and tools/call operations per the MCP specification."""
117
117
 
118
+ HANDLER_TYPE_GRAPH: str = "graph"
119
+ """Graph database (Memgraph/Neo4j) protocol handler type."""
120
+
121
+ HANDLER_TYPE_INTENT: str = "intent" # DEMO (OMN-1515)
122
+ """Intent storage and query handler type for demo wiring."""
123
+
118
124
 
119
125
  # =============================================================================
120
126
  # Event Bus Kind Constants
@@ -192,7 +198,7 @@ def get_event_bus_registry() -> RegistryEventBusBinding:
192
198
  # =============================================================================
193
199
 
194
200
 
195
- def get_handler_class(handler_type: str) -> type[ProtocolHandler]:
201
+ def get_handler_class(handler_type: str) -> type[ProtocolContainerAware]:
196
202
  """Get handler class for the given type from the singleton registry.
197
203
 
198
204
  Convenience function that wraps get_handler_registry().get().
@@ -275,7 +281,7 @@ def register_handlers_from_config(
275
281
 
276
282
  TODO(OMN-41): Implement full handler resolution:
277
283
  1. Use importlib to resolve protocol_class string to actual class
278
- 2. Validate class implements ProtocolHandler protocol
284
+ 2. Validate class implements ProtocolContainerAware protocol
279
285
  3. Register handler with runtime via get_handler_registry()
280
286
  4. Support handler instantiation options from config.options
281
287
  """
@@ -300,8 +306,10 @@ __all__: list[str] = [
300
306
  "HANDLER_TYPE_CONSUL",
301
307
  "HANDLER_TYPE_DATABASE",
302
308
  "HANDLER_TYPE_GRPC",
309
+ "HANDLER_TYPE_GRAPH",
303
310
  # Handler type constants
304
311
  "HANDLER_TYPE_HTTP",
312
+ "HANDLER_TYPE_INTENT",
305
313
  "HANDLER_TYPE_KAFKA",
306
314
  "HANDLER_TYPE_MCP",
307
315
  "HANDLER_TYPE_VALKEY",
@@ -0,0 +1,326 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Handler Source Resolver for Multi-Source Handler Discovery.
4
+
5
+ This module provides the HandlerSourceResolver class, which resolves handlers
6
+ from multiple sources (bootstrap, contract) based on the configured mode.
7
+
8
+ Part of OMN-1095: Handler Source Mode Hybrid Resolution.
9
+
10
+ Resolution Modes:
11
+ - BOOTSTRAP: Only use hardcoded bootstrap handlers.
12
+ - CONTRACT: Only use YAML contract-discovered handlers.
13
+ - HYBRID: Per-handler resolution with configurable precedence.
14
+
15
+ In HYBRID mode, the resolver performs per-handler identity resolution:
16
+ 1. Discovers handlers from both bootstrap and contract sources
17
+ 2. Builds a handler map keyed by handler_id
18
+ 3. Resolves conflicts based on allow_bootstrap_override:
19
+ - False (default): Contract handlers override bootstrap handlers
20
+ - True: Bootstrap handlers override contract handlers
21
+ 4. Non-conflicting handlers are included from both sources
22
+
23
+ See Also:
24
+ - EnumHandlerSourceMode: Defines the resolution modes
25
+ - HandlerBootstrapSource: Provides bootstrap handlers
26
+ - HandlerContractSource: Provides contract-discovered handlers
27
+ - ProtocolContractSource: Protocol for handler sources
28
+
29
+ .. versionadded:: 0.7.0
30
+ Created as part of OMN-1095 handler source mode hybrid resolution.
31
+ """
32
+
33
+ from __future__ import annotations
34
+
35
+ import logging
36
+ from typing import TYPE_CHECKING
37
+
38
+ from omnibase_infra.enums.enum_handler_source_mode import EnumHandlerSourceMode
39
+
40
+ if TYPE_CHECKING:
41
+ from omnibase_infra.runtime.protocol_contract_source import ProtocolContractSource
42
+
43
+ # Import models after TYPE_CHECKING to avoid circular imports
44
+ from omnibase_infra.models.errors import ModelHandlerValidationError
45
+ from omnibase_infra.models.handlers import (
46
+ ModelContractDiscoveryResult,
47
+ ModelHandlerDescriptor,
48
+ )
49
+
50
+ # Forward Reference Resolution:
51
+ # ModelContractDiscoveryResult uses a forward reference to ModelHandlerValidationError.
52
+ # Since we import ModelHandlerValidationError above, we can call model_rebuild() here
53
+ # to resolve the forward reference. This call is idempotent - multiple calls are harmless.
54
+ ModelContractDiscoveryResult.model_rebuild()
55
+
56
+ logger = logging.getLogger(__name__)
57
+
58
+
59
+ class HandlerSourceResolver:
60
+ """Resolver for multi-source handler discovery with configurable modes.
61
+
62
+ This class resolves handlers from bootstrap and contract sources based on
63
+ the configured mode. It supports three resolution strategies:
64
+
65
+ - BOOTSTRAP: Use only bootstrap handlers, ignore contracts.
66
+ - CONTRACT: Use only contract handlers, ignore bootstrap.
67
+ - HYBRID: Per-handler resolution with configurable precedence:
68
+ - allow_bootstrap_override=False (default): Contract handlers take
69
+ precedence over bootstrap handlers with the same handler_id.
70
+ - allow_bootstrap_override=True: Bootstrap handlers take precedence
71
+ over contract handlers with the same handler_id.
72
+ In both cases, handlers without conflicts are included from both sources.
73
+
74
+ Attributes:
75
+ mode: The configured resolution mode.
76
+ allow_bootstrap_override: If True, bootstrap handlers take precedence
77
+ in HYBRID mode. Default is False (contract precedence).
78
+
79
+ Example:
80
+ >>> resolver = HandlerSourceResolver(
81
+ ... bootstrap_source=bootstrap_source,
82
+ ... contract_source=contract_source,
83
+ ... mode=EnumHandlerSourceMode.HYBRID,
84
+ ... )
85
+ >>> result = await resolver.resolve_handlers()
86
+ >>> print(f"Discovered {len(result.descriptors)} handlers")
87
+
88
+ .. versionadded:: 0.7.0
89
+ Created as part of OMN-1095 handler source mode hybrid resolution.
90
+ """
91
+
92
+ def __init__(
93
+ self,
94
+ bootstrap_source: ProtocolContractSource,
95
+ contract_source: ProtocolContractSource,
96
+ mode: EnumHandlerSourceMode,
97
+ *,
98
+ allow_bootstrap_override: bool = False,
99
+ ) -> None:
100
+ """Initialize the handler source resolver.
101
+
102
+ Args:
103
+ bootstrap_source: Source for bootstrap handlers. Must implement
104
+ ProtocolContractSource with discover_handlers() method.
105
+ contract_source: Source for contract-discovered handlers. Must
106
+ implement ProtocolContractSource with discover_handlers() method.
107
+ mode: Resolution mode determining which sources are used and how
108
+ handlers are merged.
109
+ allow_bootstrap_override: If True, bootstrap handlers override
110
+ contract handlers with the same handler_id in HYBRID mode.
111
+ Default is False (contract handlers take precedence).
112
+ Has no effect in BOOTSTRAP or CONTRACT modes.
113
+ """
114
+ self._bootstrap_source = bootstrap_source
115
+ self._contract_source = contract_source
116
+ self._mode = mode
117
+ self._allow_bootstrap_override = allow_bootstrap_override
118
+
119
+ @property
120
+ def mode(self) -> EnumHandlerSourceMode:
121
+ """Get the configured resolution mode.
122
+
123
+ Returns:
124
+ EnumHandlerSourceMode: The mode used for handler resolution.
125
+ """
126
+ return self._mode
127
+
128
+ @property
129
+ def allow_bootstrap_override(self) -> bool:
130
+ """Get the bootstrap override configuration.
131
+
132
+ Returns:
133
+ bool: True if bootstrap handlers take precedence in HYBRID mode,
134
+ False if contract handlers take precedence (default).
135
+ """
136
+ return self._allow_bootstrap_override
137
+
138
+ async def resolve_handlers(self) -> ModelContractDiscoveryResult:
139
+ """Resolve handlers based on the configured mode.
140
+
141
+ Discovers handlers from the appropriate source(s) based on mode:
142
+ - BOOTSTRAP: Only queries bootstrap source
143
+ - CONTRACT: Only queries contract source
144
+ - HYBRID: Queries both sources and merges with contract precedence
145
+
146
+ Returns:
147
+ ModelContractDiscoveryResult: Container with discovered descriptors
148
+ and any validation errors from the queried source(s).
149
+ """
150
+ if self._mode == EnumHandlerSourceMode.BOOTSTRAP:
151
+ return await self._resolve_bootstrap()
152
+ elif self._mode == EnumHandlerSourceMode.CONTRACT:
153
+ return await self._resolve_contract()
154
+ else:
155
+ # HYBRID mode
156
+ return await self._resolve_hybrid()
157
+
158
+ async def _resolve_bootstrap(self) -> ModelContractDiscoveryResult:
159
+ """Resolve handlers using only the bootstrap source.
160
+
161
+ Returns:
162
+ ModelContractDiscoveryResult: Handlers from bootstrap source only.
163
+ """
164
+ result = await self._bootstrap_source.discover_handlers()
165
+
166
+ logger.info(
167
+ "Handler resolution completed (BOOTSTRAP mode)",
168
+ extra={
169
+ "mode": self._mode.value,
170
+ "bootstrap_handler_count": len(result.descriptors),
171
+ "resolved_handler_count": len(result.descriptors),
172
+ },
173
+ )
174
+
175
+ return result
176
+
177
+ async def _resolve_contract(self) -> ModelContractDiscoveryResult:
178
+ """Resolve handlers using only the contract source.
179
+
180
+ Returns:
181
+ ModelContractDiscoveryResult: Handlers from contract source only.
182
+ """
183
+ result = await self._contract_source.discover_handlers()
184
+
185
+ logger.info(
186
+ "Handler resolution completed (CONTRACT mode)",
187
+ extra={
188
+ "mode": self._mode.value,
189
+ "contract_handler_count": len(result.descriptors),
190
+ "resolved_handler_count": len(result.descriptors),
191
+ },
192
+ )
193
+
194
+ return result
195
+
196
+ async def _resolve_hybrid(self) -> ModelContractDiscoveryResult:
197
+ """Resolve handlers using both sources with configurable precedence.
198
+
199
+ In HYBRID mode:
200
+ 1. Discover handlers from both bootstrap and contract sources
201
+ 2. Build a handler map keyed by handler_id
202
+ 3. Resolve conflicts based on allow_bootstrap_override:
203
+ - False (default): Contract handlers override bootstrap handlers
204
+ - True: Bootstrap handlers override contract handlers
205
+ 4. Non-conflicting handlers are included from both sources
206
+
207
+ Returns:
208
+ ModelContractDiscoveryResult: Merged handlers with configured
209
+ precedence and combined validation errors from both sources.
210
+ """
211
+ # Get handlers from both sources
212
+ bootstrap_result = await self._bootstrap_source.discover_handlers()
213
+ contract_result = await self._contract_source.discover_handlers()
214
+
215
+ # Build lookup maps for both sources
216
+ bootstrap_by_id: dict[str, ModelHandlerDescriptor] = {
217
+ d.handler_id: d for d in bootstrap_result.descriptors
218
+ }
219
+ contract_by_id: dict[str, ModelHandlerDescriptor] = {
220
+ d.handler_id: d for d in contract_result.descriptors
221
+ }
222
+
223
+ # Determine which source takes precedence
224
+ if self._allow_bootstrap_override:
225
+ # Bootstrap wins conflicts: add bootstrap first, then contract fallbacks
226
+ primary_source = bootstrap_result.descriptors
227
+ primary_by_id = bootstrap_by_id
228
+ secondary_source = contract_result.descriptors
229
+ secondary_by_id = contract_by_id
230
+ primary_name = "bootstrap"
231
+ secondary_name = "contract"
232
+ else:
233
+ # Contract wins conflicts (default): add contract first, then bootstrap fallbacks
234
+ primary_source = contract_result.descriptors
235
+ primary_by_id = contract_by_id
236
+ secondary_source = bootstrap_result.descriptors
237
+ secondary_by_id = bootstrap_by_id
238
+ primary_name = "contract"
239
+ secondary_name = "bootstrap"
240
+
241
+ # Build handler map - primary source handlers first (they take precedence)
242
+ handlers_by_id: dict[str, ModelHandlerDescriptor] = {}
243
+
244
+ # Add primary handlers (they win conflicts)
245
+ for descriptor in primary_source:
246
+ handlers_by_id[descriptor.handler_id] = descriptor
247
+
248
+ # Log primary-only handlers (no secondary equivalent)
249
+ if descriptor.handler_id not in secondary_by_id:
250
+ logger.debug(
251
+ f"Adding {primary_name}-only handler (no {secondary_name} equivalent)",
252
+ extra={
253
+ "handler_id": descriptor.handler_id,
254
+ "handler_name": descriptor.name,
255
+ "source": primary_name,
256
+ },
257
+ )
258
+
259
+ # Add secondary handlers only if not already present (fallback)
260
+ fallback_count = 0
261
+ override_count = 0
262
+ for descriptor in secondary_source:
263
+ if descriptor.handler_id in handlers_by_id:
264
+ # Primary handler wins - this is an override
265
+ override_count += 1
266
+ primary_handler = handlers_by_id[descriptor.handler_id]
267
+ logger.debug(
268
+ f"{primary_name.capitalize()} handler overrides {secondary_name} handler",
269
+ extra={
270
+ "handler_id": descriptor.handler_id,
271
+ "primary_name": primary_handler.name,
272
+ "secondary_name": descriptor.name,
273
+ "primary_source": primary_name,
274
+ "secondary_source": secondary_name,
275
+ "contract_path": (
276
+ primary_handler.contract_path
277
+ if primary_name == "contract"
278
+ else descriptor.contract_path
279
+ ),
280
+ },
281
+ )
282
+ else:
283
+ # No primary handler with this ID - use secondary as fallback
284
+ handlers_by_id[descriptor.handler_id] = descriptor
285
+ fallback_count += 1
286
+ logger.debug(
287
+ f"Using {secondary_name} handler as fallback (no {primary_name} match)",
288
+ extra={
289
+ "handler_id": descriptor.handler_id,
290
+ "handler_name": descriptor.name,
291
+ "source": secondary_name,
292
+ },
293
+ )
294
+
295
+ # NOTE: Validation errors from bootstrap and contract sources are intentionally
296
+ # combined WITHOUT deduplication. During migration, the same error appearing from
297
+ # BOTH sources helps distinguish handler-level issues (error in both) from
298
+ # source-specific configuration problems (error in only one). This preserves
299
+ # diagnostic signal that would be lost if we deduplicated.
300
+ all_errors: list[ModelHandlerValidationError] = list(
301
+ bootstrap_result.validation_errors
302
+ ) + list(contract_result.validation_errors)
303
+
304
+ # Log structured counts for observability
305
+ logger.info(
306
+ "Handler resolution completed (HYBRID mode)",
307
+ extra={
308
+ "mode": self._mode.value,
309
+ "allow_bootstrap_override": self._allow_bootstrap_override,
310
+ "precedence": primary_name,
311
+ "contract_handler_count": len(contract_result.descriptors),
312
+ "bootstrap_handler_count": len(bootstrap_result.descriptors),
313
+ "fallback_handler_count": fallback_count,
314
+ "override_count": override_count,
315
+ "resolved_handler_count": len(handlers_by_id),
316
+ "validation_error_count": len(all_errors),
317
+ },
318
+ )
319
+
320
+ return ModelContractDiscoveryResult(
321
+ descriptors=list(handlers_by_id.values()),
322
+ validation_errors=all_errors,
323
+ )
324
+
325
+
326
+ __all__ = ["HandlerSourceResolver"]
@@ -23,7 +23,11 @@ from collections.abc import Callable
23
23
 
24
24
  from omnibase_core.models.errors import ModelOnexError
25
25
  from omnibase_core.models.primitives import ModelSemVer
26
+ from omnibase_infra.enums import EnumInfraTransportType
26
27
  from omnibase_infra.errors import ProtocolConfigurationError
28
+ from omnibase_infra.models.errors.model_infra_error_context import (
29
+ ModelInfraErrorContext,
30
+ )
27
31
  from omnibase_infra.runtime.util_version import normalize_version
28
32
 
29
33
 
@@ -98,10 +102,15 @@ class MixinSemverCache:
98
102
  """
99
103
  with cls._semver_cache_lock:
100
104
  if cls._semver_cache is not None:
105
+ context = ModelInfraErrorContext.with_correlation(
106
+ transport_type=EnumInfraTransportType.RUNTIME,
107
+ operation="configure_semver_cache",
108
+ )
101
109
  raise ProtocolConfigurationError(
102
110
  "Cannot reconfigure semver cache after first use. "
103
111
  "Set SEMVER_CACHE_SIZE before creating any "
104
- "registry instances, or use _reset_semver_cache() for testing."
112
+ "registry instances, or use _reset_semver_cache() for testing.",
113
+ context=context,
105
114
  )
106
115
  cls.SEMVER_CACHE_SIZE = maxsize
107
116
 
@@ -219,14 +228,24 @@ class MixinSemverCache:
219
228
  try:
220
229
  return ModelSemVer.parse(normalized_version)
221
230
  except ModelOnexError as e:
231
+ context = ModelInfraErrorContext.with_correlation(
232
+ transport_type=EnumInfraTransportType.RUNTIME,
233
+ operation="parse_semver",
234
+ )
222
235
  raise ProtocolConfigurationError(
223
236
  str(e),
224
237
  version=normalized_version,
238
+ context=context,
225
239
  ) from e
226
240
  except ValueError as e:
241
+ context = ModelInfraErrorContext.with_correlation(
242
+ transport_type=EnumInfraTransportType.RUNTIME,
243
+ operation="parse_semver",
244
+ )
227
245
  raise ProtocolConfigurationError(
228
246
  str(e),
229
247
  version=normalized_version,
248
+ context=context,
230
249
  ) from e
231
250
 
232
251
  def _parse_semver_impl(version: str) -> ModelSemVer:
@@ -248,9 +267,14 @@ class MixinSemverCache:
248
267
  try:
249
268
  normalized = normalize_version(version)
250
269
  except ValueError as e:
270
+ context = ModelInfraErrorContext.with_correlation(
271
+ transport_type=EnumInfraTransportType.RUNTIME,
272
+ operation="normalize_version",
273
+ )
251
274
  raise ProtocolConfigurationError(
252
275
  str(e),
253
276
  version=version,
277
+ context=context,
254
278
  ) from e
255
279
 
256
280
  # Now call the cached function with the NORMALIZED version
@@ -6,12 +6,19 @@ This module provides mixins for runtime components such as projectors.
6
6
 
7
7
  Exports:
8
8
  - MixinProjectorSqlOperations: SQL execution methods for projector implementations
9
+ - MixinProjectorNotificationPublishing: Notification publishing for projector implementations
9
10
  """
10
11
 
12
+ from omnibase_infra.runtime.mixins.mixin_projector_notification_publishing import (
13
+ MixinProjectorNotificationPublishing,
14
+ ProtocolProjectorNotificationContext,
15
+ )
11
16
  from omnibase_infra.runtime.mixins.mixin_projector_sql_operations import (
12
17
  MixinProjectorSqlOperations,
13
18
  )
14
19
 
15
20
  __all__: list[str] = [
21
+ "MixinProjectorNotificationPublishing",
16
22
  "MixinProjectorSqlOperations",
23
+ "ProtocolProjectorNotificationContext",
17
24
  ]