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.
- omnibase_infra/__init__.py +1 -1
- omnibase_infra/adapters/adapter_onex_tool_execution.py +451 -0
- omnibase_infra/capabilities/__init__.py +15 -0
- omnibase_infra/capabilities/capability_inference_rules.py +211 -0
- omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
- omnibase_infra/capabilities/intent_type_extractor.py +160 -0
- omnibase_infra/cli/commands.py +1 -1
- omnibase_infra/configs/widget_mapping.yaml +176 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +5 -2
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +5 -2
- omnibase_infra/enums/__init__.py +6 -0
- omnibase_infra/enums/enum_handler_error_type.py +10 -0
- omnibase_infra/enums/enum_handler_source_mode.py +72 -0
- omnibase_infra/enums/enum_kafka_acks.py +99 -0
- omnibase_infra/errors/error_compute_registry.py +4 -1
- omnibase_infra/errors/error_event_bus_registry.py +4 -1
- omnibase_infra/errors/error_infra.py +3 -1
- omnibase_infra/errors/error_policy_registry.py +4 -1
- omnibase_infra/event_bus/event_bus_kafka.py +1 -1
- omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +59 -10
- omnibase_infra/handlers/__init__.py +8 -1
- omnibase_infra/handlers/handler_consul.py +7 -1
- omnibase_infra/handlers/handler_db.py +10 -3
- omnibase_infra/handlers/handler_graph.py +10 -5
- omnibase_infra/handlers/handler_http.py +8 -2
- omnibase_infra/handlers/handler_intent.py +387 -0
- omnibase_infra/handlers/handler_mcp.py +745 -63
- omnibase_infra/handlers/handler_vault.py +11 -5
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
- omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
- omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +7 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +308 -4
- omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
- omnibase_infra/mixins/mixin_node_introspection.py +42 -7
- omnibase_infra/mixins/mixin_retry_execution.py +1 -1
- omnibase_infra/models/discovery/model_introspection_config.py +11 -0
- omnibase_infra/models/handlers/__init__.py +48 -5
- omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
- omnibase_infra/models/handlers/model_contract_discovery_result.py +6 -4
- omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
- omnibase_infra/models/handlers/model_handler_source_config.py +220 -0
- omnibase_infra/models/mcp/__init__.py +15 -0
- omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
- omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
- omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
- omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
- omnibase_infra/models/registration/model_node_capabilities.py +11 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +9 -0
- omnibase_infra/models/runtime/model_handler_contract.py +25 -9
- omnibase_infra/models/runtime/model_loaded_handler.py +9 -0
- omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
- omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
- omnibase_infra/nodes/effects/contract.yaml +0 -5
- omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
- omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
- omnibase_infra/nodes/node_registration_orchestrator/plugin.py +1 -1
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
- omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +4 -3
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
- omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
- omnibase_infra/nodes/node_registration_storage_effect/node.py +4 -1
- omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +47 -26
- omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
- omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
- omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +28 -20
- omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
- omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
- omnibase_infra/plugins/plugin_compute_base.py +16 -2
- omnibase_infra/protocols/__init__.py +2 -0
- omnibase_infra/protocols/protocol_container_aware.py +200 -0
- omnibase_infra/protocols/protocol_event_projector.py +1 -1
- omnibase_infra/runtime/__init__.py +90 -1
- omnibase_infra/runtime/binding_config_resolver.py +102 -37
- omnibase_infra/runtime/constants_notification.py +75 -0
- omnibase_infra/runtime/contract_handler_discovery.py +6 -1
- omnibase_infra/runtime/handler_bootstrap_source.py +507 -0
- omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
- omnibase_infra/runtime/handler_contract_source.py +267 -186
- omnibase_infra/runtime/handler_identity.py +81 -0
- omnibase_infra/runtime/handler_plugin_loader.py +19 -2
- omnibase_infra/runtime/handler_registry.py +11 -3
- omnibase_infra/runtime/handler_source_resolver.py +326 -0
- omnibase_infra/runtime/mixin_semver_cache.py +25 -1
- omnibase_infra/runtime/mixins/__init__.py +7 -0
- omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
- omnibase_infra/runtime/models/__init__.py +24 -0
- omnibase_infra/runtime/models/model_health_check_result.py +2 -1
- omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
- omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
- omnibase_infra/runtime/projector_plugin_loader.py +1 -1
- omnibase_infra/runtime/projector_shell.py +229 -1
- omnibase_infra/runtime/protocol_lifecycle_executor.py +6 -6
- omnibase_infra/runtime/protocols/__init__.py +10 -0
- omnibase_infra/runtime/registry/registry_protocol_binding.py +16 -15
- omnibase_infra/runtime/registry_contract_source.py +693 -0
- omnibase_infra/runtime/registry_policy.py +9 -326
- omnibase_infra/runtime/secret_resolver.py +4 -2
- omnibase_infra/runtime/service_kernel.py +11 -3
- omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
- omnibase_infra/runtime/service_runtime_host_process.py +589 -106
- omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
- omnibase_infra/runtime/transition_notification_publisher.py +764 -0
- omnibase_infra/runtime/util_container_wiring.py +6 -5
- omnibase_infra/runtime/util_wiring.py +17 -4
- omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
- omnibase_infra/services/__init__.py +21 -0
- omnibase_infra/services/corpus_capture.py +7 -1
- omnibase_infra/services/mcp/__init__.py +31 -0
- omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -0
- omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
- omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
- omnibase_infra/services/registry_api/__init__.py +40 -0
- omnibase_infra/services/registry_api/main.py +261 -0
- omnibase_infra/services/registry_api/models/__init__.py +66 -0
- omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
- omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
- omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
- omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
- omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
- omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
- omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
- omnibase_infra/services/registry_api/models/model_warning.py +49 -0
- omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
- omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
- omnibase_infra/services/registry_api/routes.py +371 -0
- omnibase_infra/services/registry_api/service.py +837 -0
- omnibase_infra/services/service_capability_query.py +4 -4
- omnibase_infra/services/service_health.py +3 -2
- omnibase_infra/services/service_timeout_emitter.py +20 -3
- omnibase_infra/services/service_timeout_scanner.py +7 -3
- omnibase_infra/services/session/__init__.py +56 -0
- omnibase_infra/services/session/config_consumer.py +120 -0
- omnibase_infra/services/session/config_store.py +139 -0
- omnibase_infra/services/session/consumer.py +1007 -0
- omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
- omnibase_infra/services/session/store.py +997 -0
- omnibase_infra/utils/__init__.py +19 -0
- omnibase_infra/utils/util_atomic_file.py +261 -0
- omnibase_infra/utils/util_db_transaction.py +239 -0
- omnibase_infra/utils/util_dsn_validation.py +1 -1
- omnibase_infra/utils/util_retry_optimistic.py +281 -0
- omnibase_infra/validation/__init__.py +3 -19
- omnibase_infra/validation/contracts/security.validation.yaml +114 -0
- omnibase_infra/validation/infra_validators.py +35 -24
- omnibase_infra/validation/validation_exemptions.yaml +140 -9
- omnibase_infra/validation/validator_chain_propagation.py +2 -2
- omnibase_infra/validation/validator_runtime_shape.py +1 -1
- omnibase_infra/validation/validator_security.py +473 -370
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/METADATA +3 -3
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/RECORD +161 -98
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/entry_points.txt +0 -0
- {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
|
|
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
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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,
|
|
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
|
|
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
|
-
|
|
970
|
-
"""
|
|
1158
|
+
def _load_handler_source_config(self) -> ModelHandlerSourceConfig:
|
|
1159
|
+
"""Load handler source configuration from runtime config.
|
|
971
1160
|
|
|
972
|
-
|
|
973
|
-
|
|
1161
|
+
Loads the handler source mode configuration that controls how handlers
|
|
1162
|
+
are discovered (BOOTSTRAP, CONTRACT, or HYBRID mode).
|
|
974
1163
|
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
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
|
-
|
|
980
|
-
|
|
981
|
-
if others fail to load.
|
|
1168
|
+
Returns:
|
|
1169
|
+
ModelHandlerSourceConfig with validated settings.
|
|
982
1170
|
|
|
983
|
-
|
|
984
|
-
If no
|
|
985
|
-
|
|
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
|
-
|
|
988
|
-
|
|
989
|
-
initializes these handler classes.
|
|
1175
|
+
.. versionadded:: 0.7.0
|
|
1176
|
+
Part of OMN-1095 handler source mode integration.
|
|
990
1177
|
"""
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
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
|
-
|
|
999
|
-
"""Discover and register handlers from contract files.
|
|
1182
|
+
from pydantic import ValidationError
|
|
1000
1183
|
|
|
1001
|
-
|
|
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
|
-
|
|
1006
|
-
|
|
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
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
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
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
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.
|
|
1021
|
-
|
|
1276
|
+
from omnibase_infra.runtime.handler_bootstrap_source import (
|
|
1277
|
+
HandlerBootstrapSource,
|
|
1022
1278
|
)
|
|
1023
|
-
from omnibase_infra.runtime.
|
|
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
|
-
"
|
|
1284
|
+
"Resolving handlers with source mode",
|
|
1027
1285
|
extra={
|
|
1028
|
-
"
|
|
1029
|
-
"
|
|
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
|
|
1034
|
-
|
|
1035
|
-
handler_registry = await self._get_handler_registry()
|
|
1295
|
+
# Create bootstrap source
|
|
1296
|
+
bootstrap_source = HandlerBootstrapSource()
|
|
1036
1297
|
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
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
|
-
#
|
|
1043
|
-
|
|
1044
|
-
|
|
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
|
|
1048
|
-
if
|
|
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
|
|
1380
|
+
"Handler resolution completed with validation errors (continuing with valid handlers)",
|
|
1051
1381
|
extra={
|
|
1052
|
-
"
|
|
1053
|
-
"
|
|
1054
|
-
"
|
|
1382
|
+
"error_count": len(result.validation_errors),
|
|
1383
|
+
"valid_descriptor_count": len(result.descriptors),
|
|
1384
|
+
"error_summary": error_summary,
|
|
1055
1385
|
},
|
|
1056
1386
|
)
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
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
|
-
"
|
|
1064
|
-
"
|
|
1065
|
-
"
|
|
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
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
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[
|
|
1574
|
+
handler_cls: type[ProtocolContainerAware] = handler_registry.get(
|
|
1575
|
+
handler_type
|
|
1576
|
+
)
|
|
1132
1577
|
|
|
1133
|
-
# Instantiate the handler
|
|
1134
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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) ->
|
|
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[
|
|
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
|
|
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
|
-
|
|
1887
|
-
|
|
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
|
|
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
|
-
|
|
1899
|
-
|
|
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)
|