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
@@ -31,40 +31,29 @@ from __future__ import annotations
31
31
  import logging
32
32
  import time
33
33
  from pathlib import Path
34
+ from typing import cast
34
35
 
35
36
  import yaml
36
37
  from pydantic import ValidationError
37
38
 
38
39
  from omnibase_core.models.contracts.model_handler_contract import ModelHandlerContract
39
40
  from omnibase_core.models.errors.model_onex_error import ModelOnexError
41
+ from omnibase_core.models.primitives import ModelSemVer
40
42
  from omnibase_infra.enums import EnumHandlerErrorType, EnumHandlerSourceType
41
43
  from omnibase_infra.models.errors import ModelHandlerValidationError
42
44
  from omnibase_infra.models.handlers import (
45
+ LiteralHandlerKind,
43
46
  ModelContractDiscoveryResult,
44
47
  ModelHandlerDescriptor,
45
48
  ModelHandlerIdentifier,
46
49
  )
47
50
  from omnibase_infra.runtime.protocol_contract_source import ProtocolContractSource
48
51
 
49
- # Rebuild ModelContractDiscoveryResult to resolve the forward reference
50
- # to ModelHandlerValidationError. This must happen after ModelHandlerValidationError
51
- # is imported to make the type available for Pydantic validation.
52
- #
53
- # Why this pattern is used:
54
- # ModelContractDiscoveryResult has a field typed as list[ModelHandlerValidationError].
55
- # ModelHandlerValidationError imports ModelHandlerIdentifier from models.handlers.
56
- # If ModelContractDiscoveryResult directly imported ModelHandlerValidationError,
57
- # it would cause a circular import because models.handlers.__init__.py imports
58
- # ModelContractDiscoveryResult.
59
- #
60
- # The solution:
61
- # 1. ModelContractDiscoveryResult uses TYPE_CHECKING to defer the import
62
- # 2. With PEP 563 (from __future__ import annotations), the annotation becomes
63
- # a string at runtime, avoiding the circular import
64
- # 3. model_rebuild() resolves the string annotation to the actual type after
65
- # both classes are defined
66
- #
67
- # This is tested in: tests/unit/runtime/test_handler_contract_source.py
52
+ # Forward Reference Resolution:
53
+ # ModelContractDiscoveryResult uses a forward reference to ModelHandlerValidationError.
54
+ # Since we import ModelHandlerValidationError above, we can call model_rebuild() here
55
+ # to resolve the forward reference. This call is idempotent - multiple calls are harmless.
56
+ # This ensures the model is fully defined before we create instances in discover_handlers().
68
57
  ModelContractDiscoveryResult.model_rebuild()
69
58
 
70
59
  logger = logging.getLogger(__name__)
@@ -72,10 +61,212 @@ logger = logging.getLogger(__name__)
72
61
  # File pattern for handler contracts
73
62
  HANDLER_CONTRACT_FILENAME = "handler_contract.yaml"
74
63
 
64
+
75
65
  # Maximum contract file size (10MB) to prevent memory exhaustion
76
66
  MAX_CONTRACT_SIZE = 10 * 1024 * 1024
77
67
 
78
68
 
69
+ # =============================================================================
70
+ # Module-Level Helper Functions
71
+ # =============================================================================
72
+ #
73
+ # These functions are extracted from HandlerContractSource to reduce method count
74
+ # while maintaining the same functionality. They are pure functions that operate
75
+ # on their inputs without requiring instance state.
76
+ # =============================================================================
77
+
78
+
79
+ def _sanitize_path_for_logging(path: Path) -> str:
80
+ """Sanitize a file path for safe inclusion in logs and error messages.
81
+
82
+ In production environments, full paths may leak sensitive information
83
+ about directory structure. This function returns only the filename and
84
+ parent directory to provide context without exposing full paths.
85
+
86
+ Args:
87
+ path: The full path to sanitize.
88
+
89
+ Returns:
90
+ Sanitized path string showing only parent/filename.
91
+ For example: "/home/user/code/handlers/handler_contract.yaml"
92
+ becomes "handlers/handler_contract.yaml".
93
+ """
94
+ # Return parent directory name + filename for context
95
+ # This provides enough info for debugging without full path exposure
96
+ try:
97
+ return str(Path(path.parent.name) / path.name)
98
+ except (ValueError, AttributeError):
99
+ # Fallback to just filename if parent extraction fails
100
+ return path.name
101
+
102
+
103
+ def _create_parse_error(
104
+ contract_path: Path,
105
+ error: yaml.YAMLError,
106
+ ) -> ModelHandlerValidationError:
107
+ """Create a validation error for YAML parse failures.
108
+
109
+ Args:
110
+ contract_path: Path to the failing contract file.
111
+ error: The YAML parsing error.
112
+
113
+ Returns:
114
+ ModelHandlerValidationError with parse error details.
115
+ """
116
+ handler_identity = ModelHandlerIdentifier.from_handler_id(
117
+ f"unknown@{contract_path.name}"
118
+ )
119
+
120
+ return ModelHandlerValidationError(
121
+ error_type=EnumHandlerErrorType.CONTRACT_PARSE_ERROR,
122
+ rule_id="CONTRACT-001",
123
+ handler_identity=handler_identity,
124
+ source_type=EnumHandlerSourceType.CONTRACT,
125
+ message=f"Failed to parse YAML in {_sanitize_path_for_logging(contract_path)}: {error}",
126
+ remediation_hint="Check YAML syntax and ensure proper indentation",
127
+ file_path=str(contract_path),
128
+ )
129
+
130
+
131
+ def _create_validation_error(
132
+ contract_path: Path,
133
+ error: ValidationError,
134
+ ) -> ModelHandlerValidationError:
135
+ """Create a validation error for contract validation failures.
136
+
137
+ Args:
138
+ contract_path: Path to the failing contract file.
139
+ error: The Pydantic validation error.
140
+
141
+ Returns:
142
+ ModelHandlerValidationError with validation details.
143
+ """
144
+ handler_identity = ModelHandlerIdentifier.from_handler_id(
145
+ f"unknown@{contract_path.name}"
146
+ )
147
+
148
+ # Extract first error detail for remediation hint
149
+ error_details = error.errors()
150
+ if error_details:
151
+ first_error = error_details[0]
152
+ field_loc = " -> ".join(str(x) for x in first_error.get("loc", ()))
153
+ error_msg = str(first_error.get("msg", "validation failed"))
154
+ else:
155
+ field_loc = "unknown"
156
+ error_msg = "validation failed"
157
+
158
+ return ModelHandlerValidationError(
159
+ error_type=EnumHandlerErrorType.CONTRACT_VALIDATION_ERROR,
160
+ rule_id="CONTRACT-002",
161
+ handler_identity=handler_identity,
162
+ source_type=EnumHandlerSourceType.CONTRACT,
163
+ message=f"Contract validation failed in {_sanitize_path_for_logging(contract_path)}: {error_msg} at {field_loc}",
164
+ remediation_hint=f"Check the '{field_loc}' field in the contract",
165
+ file_path=str(contract_path),
166
+ )
167
+
168
+
169
+ def _create_size_limit_error(
170
+ contract_path: Path,
171
+ file_size: int,
172
+ ) -> ModelHandlerValidationError:
173
+ """Create a validation error for file size limit violations.
174
+
175
+ Args:
176
+ contract_path: Path to the oversized contract file.
177
+ file_size: The actual file size in bytes.
178
+
179
+ Returns:
180
+ ModelHandlerValidationError with size limit details.
181
+ """
182
+ handler_identity = ModelHandlerIdentifier.from_handler_id(
183
+ f"unknown@{contract_path.name}"
184
+ )
185
+
186
+ return ModelHandlerValidationError(
187
+ error_type=EnumHandlerErrorType.CONTRACT_VALIDATION_ERROR,
188
+ rule_id="CONTRACT-003",
189
+ handler_identity=handler_identity,
190
+ source_type=EnumHandlerSourceType.CONTRACT,
191
+ message=(
192
+ f"Contract file {_sanitize_path_for_logging(contract_path)} exceeds size limit: "
193
+ f"{file_size} bytes (max: {MAX_CONTRACT_SIZE} bytes)"
194
+ ),
195
+ remediation_hint=(
196
+ f"Reduce contract file size to under {MAX_CONTRACT_SIZE // (1024 * 1024)}MB. "
197
+ "Consider splitting into multiple contracts if needed."
198
+ ),
199
+ file_path=str(contract_path),
200
+ )
201
+
202
+
203
+ def _create_io_error(
204
+ contract_path: Path,
205
+ error: OSError,
206
+ ) -> ModelHandlerValidationError:
207
+ """Create a validation error for I/O failures.
208
+
209
+ Args:
210
+ contract_path: Path to the contract file that failed to read.
211
+ error: The I/O error encountered.
212
+
213
+ Returns:
214
+ ModelHandlerValidationError with I/O error details.
215
+ """
216
+ handler_identity = ModelHandlerIdentifier.from_handler_id(
217
+ f"unknown@{contract_path.name}"
218
+ )
219
+
220
+ # OSError.strerror may be None for some error types (e.g., custom subclasses),
221
+ # so use str(error) as a fallback to ensure we always have an error message
222
+ error_message = error.strerror or str(error)
223
+
224
+ return ModelHandlerValidationError(
225
+ error_type=EnumHandlerErrorType.CONTRACT_PARSE_ERROR,
226
+ rule_id="CONTRACT-004",
227
+ handler_identity=handler_identity,
228
+ source_type=EnumHandlerSourceType.CONTRACT,
229
+ message=f"Failed to read contract file: {error_message}",
230
+ remediation_hint="Check file permissions and ensure the file exists",
231
+ file_path=str(contract_path),
232
+ )
233
+
234
+
235
+ def _create_version_parse_error(
236
+ contract_path: Path,
237
+ error_message: str,
238
+ ) -> ModelHandlerValidationError:
239
+ """Create a validation error for version string parse failures.
240
+
241
+ Args:
242
+ contract_path: Path to the contract file with invalid version.
243
+ error_message: The error message describing the version parse failure.
244
+
245
+ Returns:
246
+ ModelHandlerValidationError with version parse error details.
247
+ """
248
+ handler_identity = ModelHandlerIdentifier.from_handler_id(
249
+ f"unknown@{contract_path.name}"
250
+ )
251
+
252
+ return ModelHandlerValidationError(
253
+ error_type=EnumHandlerErrorType.CONTRACT_VALIDATION_ERROR,
254
+ rule_id="CONTRACT-005",
255
+ handler_identity=handler_identity,
256
+ source_type=EnumHandlerSourceType.CONTRACT,
257
+ message=(
258
+ f"Invalid version string in contract "
259
+ f"{_sanitize_path_for_logging(contract_path)}: {error_message}"
260
+ ),
261
+ remediation_hint=(
262
+ "Ensure the 'version' field uses semantic versioning format "
263
+ "(e.g., '1.0.0', '2.1.3-beta.1'). "
264
+ "Version components must be non-negative integers."
265
+ ),
266
+ file_path=str(contract_path),
267
+ )
268
+
269
+
79
270
  # =============================================================================
80
271
  # HandlerContractSource Implementation
81
272
  # =============================================================================
@@ -165,29 +356,6 @@ class HandlerContractSource(ProtocolContractSource):
165
356
  """
166
357
  return "CONTRACT"
167
358
 
168
- def _sanitize_path_for_logging(self, path: Path) -> str:
169
- """Sanitize a file path for safe inclusion in logs and error messages.
170
-
171
- In production environments, full paths may leak sensitive information
172
- about directory structure. This method returns only the filename and
173
- parent directory to provide context without exposing full paths.
174
-
175
- Args:
176
- path: The full path to sanitize.
177
-
178
- Returns:
179
- Sanitized path string showing only parent/filename.
180
- For example: "/home/user/code/handlers/handler_contract.yaml"
181
- becomes "handlers/handler_contract.yaml".
182
- """
183
- # Return parent directory name + filename for context
184
- # This provides enough info for debugging without full path exposure
185
- try:
186
- return str(Path(path.parent.name) / path.name)
187
- except (ValueError, AttributeError):
188
- # Fallback to just filename if parent extraction fails
189
- return path.name
190
-
191
359
  async def discover_handlers(
192
360
  self,
193
361
  ) -> ModelContractDiscoveryResult:
@@ -307,7 +475,7 @@ class HandlerContractSource(ProtocolContractSource):
307
475
  },
308
476
  )
309
477
  except yaml.YAMLError as e:
310
- error = self._create_parse_error(contract_file, e)
478
+ error = _create_parse_error(contract_file, e)
311
479
  if not self._graceful_mode:
312
480
  raise ModelOnexError(
313
481
  f"Failed to parse YAML contract at {contract_file}: {e}",
@@ -315,7 +483,7 @@ class HandlerContractSource(ProtocolContractSource):
315
483
  ) from e
316
484
  logger.warning(
317
485
  "Failed to parse YAML contract in %s, continuing in graceful mode",
318
- self._sanitize_path_for_logging(contract_file),
486
+ _sanitize_path_for_logging(contract_file),
319
487
  extra={
320
488
  "contract_file": str(contract_file),
321
489
  "error_type": "yaml_parse_error",
@@ -325,7 +493,7 @@ class HandlerContractSource(ProtocolContractSource):
325
493
  )
326
494
  validation_errors.append(error)
327
495
  except ValidationError as e:
328
- error = self._create_validation_error(contract_file, e)
496
+ error = _create_validation_error(contract_file, e)
329
497
  if not self._graceful_mode:
330
498
  raise ModelOnexError(
331
499
  f"Contract validation failed at {contract_file}: {e}",
@@ -333,7 +501,7 @@ class HandlerContractSource(ProtocolContractSource):
333
501
  ) from e
334
502
  logger.warning(
335
503
  "Contract validation failed in %s, continuing in graceful mode",
336
- self._sanitize_path_for_logging(contract_file),
504
+ _sanitize_path_for_logging(contract_file),
337
505
  extra={
338
506
  "contract_file": str(contract_file),
339
507
  "error_type": "validation_error",
@@ -348,7 +516,9 @@ class HandlerContractSource(ProtocolContractSource):
348
516
  if not self._graceful_mode:
349
517
  raise
350
518
 
351
- # Only handle file size limit errors (HANDLER_SOURCE_005) gracefully
519
+ # Handle specific error codes gracefully:
520
+ # - HANDLER_SOURCE_005: File size limit exceeded
521
+ # - HANDLER_SOURCE_007: Invalid version string
352
522
  # Other ModelOnexError types should be re-raised as they may indicate
353
523
  # more serious issues (e.g., configuration errors, programming errors)
354
524
  # Defensive check: error_code should always exist on ModelOnexError,
@@ -362,13 +532,13 @@ class HandlerContractSource(ProtocolContractSource):
362
532
  file_size = contract_file.stat().st_size
363
533
  except OSError:
364
534
  file_size = 0 # File may have been deleted/changed
365
- error = self._create_size_limit_error(
535
+ error = _create_size_limit_error(
366
536
  contract_file,
367
537
  file_size,
368
538
  )
369
539
  logger.warning(
370
540
  "Contract file %s exceeds size limit, continuing in graceful mode",
371
- self._sanitize_path_for_logging(contract_file),
541
+ _sanitize_path_for_logging(contract_file),
372
542
  extra={
373
543
  "contract_file": str(contract_file),
374
544
  "error_type": "size_limit_error",
@@ -378,6 +548,26 @@ class HandlerContractSource(ProtocolContractSource):
378
548
  },
379
549
  )
380
550
  validation_errors.append(error)
551
+ elif error_code == "HANDLER_SOURCE_007":
552
+ # Invalid version string - extract version from error message
553
+ error = _create_version_parse_error(
554
+ contract_file,
555
+ str(e),
556
+ )
557
+ logger.warning(
558
+ "Contract file %s has invalid version string, "
559
+ "continuing in graceful mode",
560
+ _sanitize_path_for_logging(contract_file),
561
+ extra={
562
+ "contract_file": str(contract_file),
563
+ "error_type": "version_parse_error",
564
+ "error_code": error_code,
565
+ "error_message": str(e),
566
+ "graceful_mode": self._graceful_mode,
567
+ "paths_scanned": len(self._contract_paths),
568
+ },
569
+ )
570
+ validation_errors.append(error)
381
571
  else:
382
572
  # Re-raise unexpected ModelOnexError types even in graceful mode
383
573
  # These may indicate configuration or programming errors
@@ -389,10 +579,10 @@ class HandlerContractSource(ProtocolContractSource):
389
579
  f"Failed to read contract file at {contract_file}: {e}",
390
580
  error_code="HANDLER_SOURCE_006",
391
581
  ) from e
392
- error = self._create_io_error(contract_file, e)
582
+ error = _create_io_error(contract_file, e)
393
583
  logger.warning(
394
584
  "Failed to read contract file, continuing in graceful mode: %s",
395
- self._sanitize_path_for_logging(contract_file),
585
+ _sanitize_path_for_logging(contract_file),
396
586
  extra={
397
587
  "contract_file": str(contract_file),
398
588
  "error_type": "io_error",
@@ -446,7 +636,8 @@ class HandlerContractSource(ProtocolContractSource):
446
636
  ModelHandlerDescriptor created from the contract.
447
637
 
448
638
  Raises:
449
- ModelOnexError: If contract file exceeds MAX_CONTRACT_SIZE (10MB).
639
+ ModelOnexError: If contract file exceeds MAX_CONTRACT_SIZE (10MB),
640
+ or if the version string in the contract is invalid.
450
641
  yaml.YAMLError: If YAML parsing fails.
451
642
  ValidationError: If contract validation fails.
452
643
  """
@@ -477,150 +668,40 @@ class HandlerContractSource(ProtocolContractSource):
477
668
  # Validate against ModelHandlerContract
478
669
  contract = ModelHandlerContract.model_validate(raw_data)
479
670
 
671
+ # TODO [OMN-1420]: Extract handler_class from ModelHandlerContract
672
+ #
673
+ # handler_contract.yaml files include a `handler_class` field for dynamic import
674
+ # (e.g., "omnibase_infra.handlers.handler_consul.HandlerConsul"), but
675
+ # ModelHandlerContract from omnibase_core does not have this field yet.
676
+ #
677
+ # Once ModelHandlerContract is updated to include handler_class, this code
678
+ # should be changed from:
679
+ # handler_class=raw_data.get("handler_class")
680
+ # to:
681
+ # handler_class=contract.handler_class
682
+ #
683
+ # For now, extract directly from raw YAML data to support dynamic handler loading.
684
+ # See: https://linear.app/omninode/issue/OMN-1420
685
+ handler_class = (
686
+ raw_data.get("handler_class") if isinstance(raw_data, dict) else None
687
+ )
688
+
689
+ # Use contract_version directly - it's already a ModelSemVer from Pydantic validation
480
690
  # Transform to descriptor
481
691
  return ModelHandlerDescriptor(
482
692
  handler_id=contract.handler_id,
483
693
  name=contract.name,
484
- version=contract.version,
485
- handler_kind=contract.descriptor.handler_kind,
694
+ version=contract.contract_version,
695
+ handler_kind=cast(
696
+ "LiteralHandlerKind", contract.descriptor.node_archetype.value
697
+ ),
486
698
  input_model=contract.input_model,
487
699
  output_model=contract.output_model,
488
700
  description=contract.description,
701
+ handler_class=handler_class,
489
702
  contract_path=str(contract_path),
490
703
  )
491
704
 
492
- def _create_parse_error(
493
- self,
494
- contract_path: Path,
495
- error: yaml.YAMLError,
496
- ) -> ModelHandlerValidationError:
497
- """Create a validation error for YAML parse failures.
498
-
499
- Args:
500
- contract_path: Path to the failing contract file.
501
- error: The YAML parsing error.
502
-
503
- Returns:
504
- ModelHandlerValidationError with parse error details.
505
- """
506
- handler_identity = ModelHandlerIdentifier.from_handler_id(
507
- f"unknown@{contract_path.name}"
508
- )
509
-
510
- return ModelHandlerValidationError(
511
- error_type=EnumHandlerErrorType.CONTRACT_PARSE_ERROR,
512
- rule_id="CONTRACT-001",
513
- handler_identity=handler_identity,
514
- source_type=EnumHandlerSourceType.CONTRACT,
515
- message=f"Failed to parse YAML in {self._sanitize_path_for_logging(contract_path)}: {error}",
516
- remediation_hint="Check YAML syntax and ensure proper indentation",
517
- file_path=str(contract_path),
518
- )
519
-
520
- def _create_validation_error(
521
- self,
522
- contract_path: Path,
523
- error: ValidationError,
524
- ) -> ModelHandlerValidationError:
525
- """Create a validation error for contract validation failures.
526
-
527
- Args:
528
- contract_path: Path to the failing contract file.
529
- error: The Pydantic validation error.
530
-
531
- Returns:
532
- ModelHandlerValidationError with validation details.
533
- """
534
- handler_identity = ModelHandlerIdentifier.from_handler_id(
535
- f"unknown@{contract_path.name}"
536
- )
537
-
538
- # Extract first error detail for remediation hint
539
- error_details = error.errors()
540
- if error_details:
541
- first_error = error_details[0]
542
- field_loc = " -> ".join(str(x) for x in first_error.get("loc", ()))
543
- error_msg = str(first_error.get("msg", "validation failed"))
544
- else:
545
- field_loc = "unknown"
546
- error_msg = "validation failed"
547
-
548
- return ModelHandlerValidationError(
549
- error_type=EnumHandlerErrorType.CONTRACT_VALIDATION_ERROR,
550
- rule_id="CONTRACT-002",
551
- handler_identity=handler_identity,
552
- source_type=EnumHandlerSourceType.CONTRACT,
553
- message=f"Contract validation failed in {self._sanitize_path_for_logging(contract_path)}: {error_msg} at {field_loc}",
554
- remediation_hint=f"Check the '{field_loc}' field in the contract",
555
- file_path=str(contract_path),
556
- )
557
-
558
- def _create_size_limit_error(
559
- self,
560
- contract_path: Path,
561
- file_size: int,
562
- ) -> ModelHandlerValidationError:
563
- """Create a validation error for file size limit violations.
564
-
565
- Args:
566
- contract_path: Path to the oversized contract file.
567
- file_size: The actual file size in bytes.
568
-
569
- Returns:
570
- ModelHandlerValidationError with size limit details.
571
- """
572
- handler_identity = ModelHandlerIdentifier.from_handler_id(
573
- f"unknown@{contract_path.name}"
574
- )
575
-
576
- return ModelHandlerValidationError(
577
- error_type=EnumHandlerErrorType.CONTRACT_VALIDATION_ERROR,
578
- rule_id="CONTRACT-003",
579
- handler_identity=handler_identity,
580
- source_type=EnumHandlerSourceType.CONTRACT,
581
- message=(
582
- f"Contract file {self._sanitize_path_for_logging(contract_path)} exceeds size limit: "
583
- f"{file_size} bytes (max: {MAX_CONTRACT_SIZE} bytes)"
584
- ),
585
- remediation_hint=(
586
- f"Reduce contract file size to under {MAX_CONTRACT_SIZE // (1024 * 1024)}MB. "
587
- "Consider splitting into multiple contracts if needed."
588
- ),
589
- file_path=str(contract_path),
590
- )
591
-
592
- def _create_io_error(
593
- self,
594
- contract_path: Path,
595
- error: OSError,
596
- ) -> ModelHandlerValidationError:
597
- """Create a validation error for I/O failures.
598
-
599
- Args:
600
- contract_path: Path to the contract file that failed to read.
601
- error: The I/O error encountered.
602
-
603
- Returns:
604
- ModelHandlerValidationError with I/O error details.
605
- """
606
- handler_identity = ModelHandlerIdentifier.from_handler_id(
607
- f"unknown@{contract_path.name}"
608
- )
609
-
610
- # OSError.strerror may be None for some error types (e.g., custom subclasses),
611
- # so use str(error) as a fallback to ensure we always have an error message
612
- error_message = error.strerror or str(error)
613
-
614
- return ModelHandlerValidationError(
615
- error_type=EnumHandlerErrorType.CONTRACT_PARSE_ERROR,
616
- rule_id="CONTRACT-004",
617
- handler_identity=handler_identity,
618
- source_type=EnumHandlerSourceType.CONTRACT,
619
- message=f"Failed to read contract file: {error_message}",
620
- remediation_hint="Check file permissions and ensure the file exists",
621
- file_path=str(contract_path),
622
- )
623
-
624
705
  def _log_discovery_results(
625
706
  self,
626
707
  discovered_count: int,
@@ -0,0 +1,81 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Handler Identity Utilities for HYBRID Mode Resolution.
4
+
5
+ This module provides the `handler_identity()` function used by both bootstrap
6
+ and contract sources to generate consistent handler IDs. This enables per-handler
7
+ identity matching in HYBRID mode.
8
+
9
+ The Problem:
10
+ Prior to this module, contract-discovered handlers used a "bootstrap." prefix
11
+ for handler_id to enable HYBRID mode identity matching. This was semantically
12
+ confusing because "bootstrap" reads like "where it came from," not "what it is."
13
+
14
+ The Solution:
15
+ A neutral "proto." prefix that indicates this is a **protocol identity namespace**,
16
+ not a source indicator. Both HandlerBootstrapSource and PluginLoaderContractSource
17
+ use this shared helper to generate consistent IDs.
18
+
19
+ Example:
20
+ >>> from omnibase_infra.runtime.handler_identity import handler_identity
21
+ >>> handler_identity("consul")
22
+ 'proto.consul'
23
+ >>> handler_identity("http")
24
+ 'proto.http'
25
+
26
+ See Also:
27
+ - HandlerSourceResolver._resolve_hybrid(): Resolution logic that compares handler_id
28
+ - HandlerBootstrapSource: Uses this to generate bootstrap handler IDs
29
+ - PluginLoaderContractSource: Uses this for contract-discovered handlers
30
+
31
+ Part of OMN-1095: Handler Source Mode Feature Flag / Bootstrap Contract Hybrid.
32
+
33
+ .. versionadded:: 0.7.0
34
+ Introduced to fix handler ID namespace confusion.
35
+ """
36
+
37
+ from __future__ import annotations
38
+
39
+ # Prefix used for handler identity in HYBRID mode resolution.
40
+ # This is a protocol namespace, NOT a source indicator.
41
+ # Both bootstrap and contract sources use this prefix.
42
+ HANDLER_IDENTITY_PREFIX = "proto"
43
+
44
+
45
+ def handler_identity(protocol_type: str) -> str:
46
+ """Generate stable handler identity for HYBRID mode resolution.
47
+
48
+ Both bootstrap and contract sources use this to generate consistent IDs,
49
+ enabling per-handler identity matching in HYBRID mode. When both sources
50
+ provide a handler with the same identity, the resolver applies precedence
51
+ rules (contract wins by default, or bootstrap wins if allow_bootstrap_override=True).
52
+
53
+ The "proto." prefix indicates this is a **protocol identity namespace**, not
54
+ a source origin indicator. Contract-discovered handlers use this prefix
55
+ specifically so they can be compared against bootstrap-discovered handlers
56
+ with the same protocol_type.
57
+
58
+ Args:
59
+ protocol_type: The protocol type (e.g., "consul", "http", "db", "vault", "mcp").
60
+
61
+ Returns:
62
+ Stable handler identity string (e.g., "proto.consul", "proto.http").
63
+
64
+ Example:
65
+ >>> handler_identity("consul")
66
+ 'proto.consul'
67
+ >>> handler_identity("http")
68
+ 'proto.http'
69
+
70
+ See Also:
71
+ HandlerSourceResolver._resolve_hybrid() for resolution logic that uses
72
+ these identities to determine which handler wins when both sources
73
+ provide handlers with the same identity.
74
+ """
75
+ return f"{HANDLER_IDENTITY_PREFIX}.{protocol_type}"
76
+
77
+
78
+ __all__ = [
79
+ "HANDLER_IDENTITY_PREFIX",
80
+ "handler_identity",
81
+ ]