omnibase_infra 0.2.1__py3-none-any.whl → 0.2.2__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 (116) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/adapters/adapter_onex_tool_execution.py +446 -0
  3. omnibase_infra/cli/commands.py +1 -1
  4. omnibase_infra/configs/widget_mapping.yaml +176 -0
  5. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +4 -1
  6. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +4 -1
  7. omnibase_infra/errors/error_compute_registry.py +4 -1
  8. omnibase_infra/errors/error_event_bus_registry.py +4 -1
  9. omnibase_infra/errors/error_infra.py +3 -1
  10. omnibase_infra/errors/error_policy_registry.py +4 -1
  11. omnibase_infra/handlers/handler_db.py +2 -1
  12. omnibase_infra/handlers/handler_graph.py +10 -5
  13. omnibase_infra/handlers/handler_mcp.py +736 -63
  14. omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
  15. omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
  16. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +301 -4
  17. omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
  18. omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
  19. omnibase_infra/mixins/mixin_node_introspection.py +24 -7
  20. omnibase_infra/mixins/mixin_retry_execution.py +1 -1
  21. omnibase_infra/models/handlers/__init__.py +10 -0
  22. omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
  23. omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
  24. omnibase_infra/models/mcp/__init__.py +15 -0
  25. omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
  26. omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
  27. omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
  28. omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
  29. omnibase_infra/models/registration/model_node_capabilities.py +11 -0
  30. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
  31. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
  32. omnibase_infra/nodes/effects/contract.yaml +0 -5
  33. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
  34. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
  35. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
  36. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
  37. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
  38. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
  39. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +46 -25
  40. omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
  41. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
  42. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +24 -19
  43. omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
  44. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
  45. omnibase_infra/plugins/plugin_compute_base.py +16 -2
  46. omnibase_infra/protocols/protocol_event_projector.py +1 -1
  47. omnibase_infra/runtime/__init__.py +51 -1
  48. omnibase_infra/runtime/binding_config_resolver.py +102 -37
  49. omnibase_infra/runtime/constants_notification.py +75 -0
  50. omnibase_infra/runtime/contract_handler_discovery.py +6 -1
  51. omnibase_infra/runtime/handler_bootstrap_source.py +514 -0
  52. omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
  53. omnibase_infra/runtime/handler_contract_source.py +289 -167
  54. omnibase_infra/runtime/handler_plugin_loader.py +4 -2
  55. omnibase_infra/runtime/mixin_semver_cache.py +25 -1
  56. omnibase_infra/runtime/mixins/__init__.py +7 -0
  57. omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
  58. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
  59. omnibase_infra/runtime/models/__init__.py +24 -0
  60. omnibase_infra/runtime/models/model_health_check_result.py +2 -1
  61. omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
  62. omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
  63. omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
  64. omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
  65. omnibase_infra/runtime/projector_plugin_loader.py +1 -1
  66. omnibase_infra/runtime/projector_shell.py +229 -1
  67. omnibase_infra/runtime/protocols/__init__.py +10 -0
  68. omnibase_infra/runtime/registry/registry_protocol_binding.py +3 -2
  69. omnibase_infra/runtime/registry_policy.py +9 -326
  70. omnibase_infra/runtime/secret_resolver.py +4 -2
  71. omnibase_infra/runtime/service_kernel.py +10 -2
  72. omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
  73. omnibase_infra/runtime/service_runtime_host_process.py +225 -15
  74. omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
  75. omnibase_infra/runtime/transition_notification_publisher.py +764 -0
  76. omnibase_infra/runtime/util_container_wiring.py +6 -5
  77. omnibase_infra/runtime/util_wiring.py +5 -1
  78. omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
  79. omnibase_infra/services/mcp/__init__.py +31 -0
  80. omnibase_infra/services/mcp/mcp_server_lifecycle.py +443 -0
  81. omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
  82. omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
  83. omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
  84. omnibase_infra/services/registry_api/__init__.py +40 -0
  85. omnibase_infra/services/registry_api/main.py +243 -0
  86. omnibase_infra/services/registry_api/models/__init__.py +66 -0
  87. omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
  88. omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
  89. omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
  90. omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
  91. omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
  92. omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
  93. omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
  94. omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
  95. omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
  96. omnibase_infra/services/registry_api/models/model_warning.py +49 -0
  97. omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
  98. omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
  99. omnibase_infra/services/registry_api/routes.py +371 -0
  100. omnibase_infra/services/registry_api/service.py +846 -0
  101. omnibase_infra/services/service_capability_query.py +4 -4
  102. omnibase_infra/services/service_health.py +3 -2
  103. omnibase_infra/services/service_timeout_emitter.py +13 -2
  104. omnibase_infra/utils/util_dsn_validation.py +1 -1
  105. omnibase_infra/validation/__init__.py +3 -19
  106. omnibase_infra/validation/contracts/security.validation.yaml +114 -0
  107. omnibase_infra/validation/infra_validators.py +35 -24
  108. omnibase_infra/validation/validation_exemptions.yaml +113 -9
  109. omnibase_infra/validation/validator_chain_propagation.py +2 -2
  110. omnibase_infra/validation/validator_runtime_shape.py +1 -1
  111. omnibase_infra/validation/validator_security.py +473 -370
  112. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/METADATA +2 -2
  113. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/RECORD +116 -74
  114. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/WHEEL +0 -0
  115. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/entry_points.txt +0 -0
  116. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/licenses/LICENSE +0 -0
@@ -37,6 +37,7 @@ from pydantic import ValidationError
37
37
 
38
38
  from omnibase_core.models.contracts.model_handler_contract import ModelHandlerContract
39
39
  from omnibase_core.models.errors.model_onex_error import ModelOnexError
40
+ from omnibase_core.models.primitives import ModelSemVer
40
41
  from omnibase_infra.enums import EnumHandlerErrorType, EnumHandlerSourceType
41
42
  from omnibase_infra.models.errors import ModelHandlerValidationError
42
43
  from omnibase_infra.models.handlers import (
@@ -46,11 +47,42 @@ from omnibase_infra.models.handlers import (
46
47
  )
47
48
  from omnibase_infra.runtime.protocol_contract_source import ProtocolContractSource
48
49
 
50
+ # =============================================================================
51
+ # Module-Level Model Rebuild Pattern (Immediate Execution)
52
+ # =============================================================================
53
+ #
54
+ # This module uses a simple MODULE-LEVEL model_rebuild() call. This differs from
55
+ # HandlerBootstrapSource which uses a deferred, thread-safe pattern.
56
+ #
57
+ # WHY IMMEDIATE (module-load) IS SAFE HERE:
58
+ # - HandlerContractSource is NOT imported through runtime.__init__.py
59
+ # - This module is imported explicitly by user code AFTER the runtime is
60
+ # initialized and all model dependencies are resolved
61
+ # - Python's import mechanism is inherently thread-safe for module-level code:
62
+ # the import lock ensures module initialization runs exactly once, even if
63
+ # multiple threads import the same module simultaneously
64
+ # - Therefore, no explicit thread-safety mechanism (locks) is needed
65
+ #
66
+ # WHY HANDLERBOOTSTRAPSOURCE NEEDS DEFERRED PATTERN:
67
+ # - HandlerBootstrapSource is imported during runtime bootstrap (via __init__.py)
68
+ # - At that point, ModelHandlerValidationError may not be fully resolved
69
+ # - Additionally, discover_handlers() may be called from multiple threads,
70
+ # requiring explicit synchronization for the rebuild call
71
+ #
72
+ # PATTERN COMPARISON:
73
+ # - HandlerContractSource: Immediate + module-level (this file)
74
+ # - HandlerBootstrapSource: Deferred + thread-safe (see that file for rationale)
75
+ #
76
+ # See Also:
77
+ # - handler_bootstrap_source.py lines 68-100 for the deferred pattern rationale
78
+ # - OMN-1087 for the ticket tracking this design decision
79
+ # =============================================================================
80
+ #
49
81
  # Rebuild ModelContractDiscoveryResult to resolve the forward reference
50
82
  # to ModelHandlerValidationError. This must happen after ModelHandlerValidationError
51
83
  # is imported to make the type available for Pydantic validation.
52
84
  #
53
- # Why this pattern is used:
85
+ # Why forward reference resolution is needed:
54
86
  # ModelContractDiscoveryResult has a field typed as list[ModelHandlerValidationError].
55
87
  # ModelHandlerValidationError imports ModelHandlerIdentifier from models.handlers.
56
88
  # If ModelContractDiscoveryResult directly imported ModelHandlerValidationError,
@@ -72,10 +104,212 @@ logger = logging.getLogger(__name__)
72
104
  # File pattern for handler contracts
73
105
  HANDLER_CONTRACT_FILENAME = "handler_contract.yaml"
74
106
 
107
+
75
108
  # Maximum contract file size (10MB) to prevent memory exhaustion
76
109
  MAX_CONTRACT_SIZE = 10 * 1024 * 1024
77
110
 
78
111
 
112
+ # =============================================================================
113
+ # Module-Level Helper Functions
114
+ # =============================================================================
115
+ #
116
+ # These functions are extracted from HandlerContractSource to reduce method count
117
+ # while maintaining the same functionality. They are pure functions that operate
118
+ # on their inputs without requiring instance state.
119
+ # =============================================================================
120
+
121
+
122
+ def _sanitize_path_for_logging(path: Path) -> str:
123
+ """Sanitize a file path for safe inclusion in logs and error messages.
124
+
125
+ In production environments, full paths may leak sensitive information
126
+ about directory structure. This function returns only the filename and
127
+ parent directory to provide context without exposing full paths.
128
+
129
+ Args:
130
+ path: The full path to sanitize.
131
+
132
+ Returns:
133
+ Sanitized path string showing only parent/filename.
134
+ For example: "/home/user/code/handlers/handler_contract.yaml"
135
+ becomes "handlers/handler_contract.yaml".
136
+ """
137
+ # Return parent directory name + filename for context
138
+ # This provides enough info for debugging without full path exposure
139
+ try:
140
+ return str(Path(path.parent.name) / path.name)
141
+ except (ValueError, AttributeError):
142
+ # Fallback to just filename if parent extraction fails
143
+ return path.name
144
+
145
+
146
+ def _create_parse_error(
147
+ contract_path: Path,
148
+ error: yaml.YAMLError,
149
+ ) -> ModelHandlerValidationError:
150
+ """Create a validation error for YAML parse failures.
151
+
152
+ Args:
153
+ contract_path: Path to the failing contract file.
154
+ error: The YAML parsing error.
155
+
156
+ Returns:
157
+ ModelHandlerValidationError with parse error details.
158
+ """
159
+ handler_identity = ModelHandlerIdentifier.from_handler_id(
160
+ f"unknown@{contract_path.name}"
161
+ )
162
+
163
+ return ModelHandlerValidationError(
164
+ error_type=EnumHandlerErrorType.CONTRACT_PARSE_ERROR,
165
+ rule_id="CONTRACT-001",
166
+ handler_identity=handler_identity,
167
+ source_type=EnumHandlerSourceType.CONTRACT,
168
+ message=f"Failed to parse YAML in {_sanitize_path_for_logging(contract_path)}: {error}",
169
+ remediation_hint="Check YAML syntax and ensure proper indentation",
170
+ file_path=str(contract_path),
171
+ )
172
+
173
+
174
+ def _create_validation_error(
175
+ contract_path: Path,
176
+ error: ValidationError,
177
+ ) -> ModelHandlerValidationError:
178
+ """Create a validation error for contract validation failures.
179
+
180
+ Args:
181
+ contract_path: Path to the failing contract file.
182
+ error: The Pydantic validation error.
183
+
184
+ Returns:
185
+ ModelHandlerValidationError with validation details.
186
+ """
187
+ handler_identity = ModelHandlerIdentifier.from_handler_id(
188
+ f"unknown@{contract_path.name}"
189
+ )
190
+
191
+ # Extract first error detail for remediation hint
192
+ error_details = error.errors()
193
+ if error_details:
194
+ first_error = error_details[0]
195
+ field_loc = " -> ".join(str(x) for x in first_error.get("loc", ()))
196
+ error_msg = str(first_error.get("msg", "validation failed"))
197
+ else:
198
+ field_loc = "unknown"
199
+ error_msg = "validation failed"
200
+
201
+ return ModelHandlerValidationError(
202
+ error_type=EnumHandlerErrorType.CONTRACT_VALIDATION_ERROR,
203
+ rule_id="CONTRACT-002",
204
+ handler_identity=handler_identity,
205
+ source_type=EnumHandlerSourceType.CONTRACT,
206
+ message=f"Contract validation failed in {_sanitize_path_for_logging(contract_path)}: {error_msg} at {field_loc}",
207
+ remediation_hint=f"Check the '{field_loc}' field in the contract",
208
+ file_path=str(contract_path),
209
+ )
210
+
211
+
212
+ def _create_size_limit_error(
213
+ contract_path: Path,
214
+ file_size: int,
215
+ ) -> ModelHandlerValidationError:
216
+ """Create a validation error for file size limit violations.
217
+
218
+ Args:
219
+ contract_path: Path to the oversized contract file.
220
+ file_size: The actual file size in bytes.
221
+
222
+ Returns:
223
+ ModelHandlerValidationError with size limit details.
224
+ """
225
+ handler_identity = ModelHandlerIdentifier.from_handler_id(
226
+ f"unknown@{contract_path.name}"
227
+ )
228
+
229
+ return ModelHandlerValidationError(
230
+ error_type=EnumHandlerErrorType.CONTRACT_VALIDATION_ERROR,
231
+ rule_id="CONTRACT-003",
232
+ handler_identity=handler_identity,
233
+ source_type=EnumHandlerSourceType.CONTRACT,
234
+ message=(
235
+ f"Contract file {_sanitize_path_for_logging(contract_path)} exceeds size limit: "
236
+ f"{file_size} bytes (max: {MAX_CONTRACT_SIZE} bytes)"
237
+ ),
238
+ remediation_hint=(
239
+ f"Reduce contract file size to under {MAX_CONTRACT_SIZE // (1024 * 1024)}MB. "
240
+ "Consider splitting into multiple contracts if needed."
241
+ ),
242
+ file_path=str(contract_path),
243
+ )
244
+
245
+
246
+ def _create_io_error(
247
+ contract_path: Path,
248
+ error: OSError,
249
+ ) -> ModelHandlerValidationError:
250
+ """Create a validation error for I/O failures.
251
+
252
+ Args:
253
+ contract_path: Path to the contract file that failed to read.
254
+ error: The I/O error encountered.
255
+
256
+ Returns:
257
+ ModelHandlerValidationError with I/O error details.
258
+ """
259
+ handler_identity = ModelHandlerIdentifier.from_handler_id(
260
+ f"unknown@{contract_path.name}"
261
+ )
262
+
263
+ # OSError.strerror may be None for some error types (e.g., custom subclasses),
264
+ # so use str(error) as a fallback to ensure we always have an error message
265
+ error_message = error.strerror or str(error)
266
+
267
+ return ModelHandlerValidationError(
268
+ error_type=EnumHandlerErrorType.CONTRACT_PARSE_ERROR,
269
+ rule_id="CONTRACT-004",
270
+ handler_identity=handler_identity,
271
+ source_type=EnumHandlerSourceType.CONTRACT,
272
+ message=f"Failed to read contract file: {error_message}",
273
+ remediation_hint="Check file permissions and ensure the file exists",
274
+ file_path=str(contract_path),
275
+ )
276
+
277
+
278
+ def _create_version_parse_error(
279
+ contract_path: Path,
280
+ error_message: str,
281
+ ) -> ModelHandlerValidationError:
282
+ """Create a validation error for version string parse failures.
283
+
284
+ Args:
285
+ contract_path: Path to the contract file with invalid version.
286
+ error_message: The error message describing the version parse failure.
287
+
288
+ Returns:
289
+ ModelHandlerValidationError with version parse error details.
290
+ """
291
+ handler_identity = ModelHandlerIdentifier.from_handler_id(
292
+ f"unknown@{contract_path.name}"
293
+ )
294
+
295
+ return ModelHandlerValidationError(
296
+ error_type=EnumHandlerErrorType.CONTRACT_VALIDATION_ERROR,
297
+ rule_id="CONTRACT-005",
298
+ handler_identity=handler_identity,
299
+ source_type=EnumHandlerSourceType.CONTRACT,
300
+ message=(
301
+ f"Invalid version string in contract "
302
+ f"{_sanitize_path_for_logging(contract_path)}: {error_message}"
303
+ ),
304
+ remediation_hint=(
305
+ "Ensure the 'version' field uses semantic versioning format "
306
+ "(e.g., '1.0.0', '2.1.3-beta.1'). "
307
+ "Version components must be non-negative integers."
308
+ ),
309
+ file_path=str(contract_path),
310
+ )
311
+
312
+
79
313
  # =============================================================================
80
314
  # HandlerContractSource Implementation
81
315
  # =============================================================================
@@ -165,29 +399,6 @@ class HandlerContractSource(ProtocolContractSource):
165
399
  """
166
400
  return "CONTRACT"
167
401
 
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
402
  async def discover_handlers(
192
403
  self,
193
404
  ) -> ModelContractDiscoveryResult:
@@ -307,7 +518,7 @@ class HandlerContractSource(ProtocolContractSource):
307
518
  },
308
519
  )
309
520
  except yaml.YAMLError as e:
310
- error = self._create_parse_error(contract_file, e)
521
+ error = _create_parse_error(contract_file, e)
311
522
  if not self._graceful_mode:
312
523
  raise ModelOnexError(
313
524
  f"Failed to parse YAML contract at {contract_file}: {e}",
@@ -315,7 +526,7 @@ class HandlerContractSource(ProtocolContractSource):
315
526
  ) from e
316
527
  logger.warning(
317
528
  "Failed to parse YAML contract in %s, continuing in graceful mode",
318
- self._sanitize_path_for_logging(contract_file),
529
+ _sanitize_path_for_logging(contract_file),
319
530
  extra={
320
531
  "contract_file": str(contract_file),
321
532
  "error_type": "yaml_parse_error",
@@ -325,7 +536,7 @@ class HandlerContractSource(ProtocolContractSource):
325
536
  )
326
537
  validation_errors.append(error)
327
538
  except ValidationError as e:
328
- error = self._create_validation_error(contract_file, e)
539
+ error = _create_validation_error(contract_file, e)
329
540
  if not self._graceful_mode:
330
541
  raise ModelOnexError(
331
542
  f"Contract validation failed at {contract_file}: {e}",
@@ -333,7 +544,7 @@ class HandlerContractSource(ProtocolContractSource):
333
544
  ) from e
334
545
  logger.warning(
335
546
  "Contract validation failed in %s, continuing in graceful mode",
336
- self._sanitize_path_for_logging(contract_file),
547
+ _sanitize_path_for_logging(contract_file),
337
548
  extra={
338
549
  "contract_file": str(contract_file),
339
550
  "error_type": "validation_error",
@@ -348,7 +559,9 @@ class HandlerContractSource(ProtocolContractSource):
348
559
  if not self._graceful_mode:
349
560
  raise
350
561
 
351
- # Only handle file size limit errors (HANDLER_SOURCE_005) gracefully
562
+ # Handle specific error codes gracefully:
563
+ # - HANDLER_SOURCE_005: File size limit exceeded
564
+ # - HANDLER_SOURCE_007: Invalid version string
352
565
  # Other ModelOnexError types should be re-raised as they may indicate
353
566
  # more serious issues (e.g., configuration errors, programming errors)
354
567
  # Defensive check: error_code should always exist on ModelOnexError,
@@ -362,13 +575,13 @@ class HandlerContractSource(ProtocolContractSource):
362
575
  file_size = contract_file.stat().st_size
363
576
  except OSError:
364
577
  file_size = 0 # File may have been deleted/changed
365
- error = self._create_size_limit_error(
578
+ error = _create_size_limit_error(
366
579
  contract_file,
367
580
  file_size,
368
581
  )
369
582
  logger.warning(
370
583
  "Contract file %s exceeds size limit, continuing in graceful mode",
371
- self._sanitize_path_for_logging(contract_file),
584
+ _sanitize_path_for_logging(contract_file),
372
585
  extra={
373
586
  "contract_file": str(contract_file),
374
587
  "error_type": "size_limit_error",
@@ -378,6 +591,26 @@ class HandlerContractSource(ProtocolContractSource):
378
591
  },
379
592
  )
380
593
  validation_errors.append(error)
594
+ elif error_code == "HANDLER_SOURCE_007":
595
+ # Invalid version string - extract version from error message
596
+ error = _create_version_parse_error(
597
+ contract_file,
598
+ str(e),
599
+ )
600
+ logger.warning(
601
+ "Contract file %s has invalid version string, "
602
+ "continuing in graceful mode",
603
+ _sanitize_path_for_logging(contract_file),
604
+ extra={
605
+ "contract_file": str(contract_file),
606
+ "error_type": "version_parse_error",
607
+ "error_code": error_code,
608
+ "error_message": str(e),
609
+ "graceful_mode": self._graceful_mode,
610
+ "paths_scanned": len(self._contract_paths),
611
+ },
612
+ )
613
+ validation_errors.append(error)
381
614
  else:
382
615
  # Re-raise unexpected ModelOnexError types even in graceful mode
383
616
  # These may indicate configuration or programming errors
@@ -389,10 +622,10 @@ class HandlerContractSource(ProtocolContractSource):
389
622
  f"Failed to read contract file at {contract_file}: {e}",
390
623
  error_code="HANDLER_SOURCE_006",
391
624
  ) from e
392
- error = self._create_io_error(contract_file, e)
625
+ error = _create_io_error(contract_file, e)
393
626
  logger.warning(
394
627
  "Failed to read contract file, continuing in graceful mode: %s",
395
- self._sanitize_path_for_logging(contract_file),
628
+ _sanitize_path_for_logging(contract_file),
396
629
  extra={
397
630
  "contract_file": str(contract_file),
398
631
  "error_type": "io_error",
@@ -446,7 +679,8 @@ class HandlerContractSource(ProtocolContractSource):
446
679
  ModelHandlerDescriptor created from the contract.
447
680
 
448
681
  Raises:
449
- ModelOnexError: If contract file exceeds MAX_CONTRACT_SIZE (10MB).
682
+ ModelOnexError: If contract file exceeds MAX_CONTRACT_SIZE (10MB),
683
+ or if the version string in the contract is invalid.
450
684
  yaml.YAMLError: If YAML parsing fails.
451
685
  ValidationError: If contract validation fails.
452
686
  """
@@ -477,150 +711,38 @@ class HandlerContractSource(ProtocolContractSource):
477
711
  # Validate against ModelHandlerContract
478
712
  contract = ModelHandlerContract.model_validate(raw_data)
479
713
 
714
+ # TODO [OMN-1420]: Extract handler_class from ModelHandlerContract
715
+ #
716
+ # handler_contract.yaml files include a `handler_class` field for dynamic import
717
+ # (e.g., "omnibase_infra.handlers.handler_consul.HandlerConsul"), but
718
+ # ModelHandlerContract from omnibase_core does not have this field yet.
719
+ #
720
+ # Once ModelHandlerContract is updated to include handler_class, this code
721
+ # should be changed from:
722
+ # handler_class=raw_data.get("handler_class")
723
+ # to:
724
+ # handler_class=contract.handler_class
725
+ #
726
+ # For now, extract directly from raw YAML data to support dynamic handler loading.
727
+ # See: https://linear.app/omninode/issue/OMN-1420
728
+ handler_class = (
729
+ raw_data.get("handler_class") if isinstance(raw_data, dict) else None
730
+ )
731
+
732
+ # Use contract_version directly - it's already a ModelSemVer from Pydantic validation
480
733
  # Transform to descriptor
481
734
  return ModelHandlerDescriptor(
482
735
  handler_id=contract.handler_id,
483
736
  name=contract.name,
484
- version=contract.version,
737
+ version=contract.contract_version,
485
738
  handler_kind=contract.descriptor.handler_kind,
486
739
  input_model=contract.input_model,
487
740
  output_model=contract.output_model,
488
741
  description=contract.description,
742
+ handler_class=handler_class,
489
743
  contract_path=str(contract_path),
490
744
  )
491
745
 
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
746
  def _log_discovery_results(
625
747
  self,
626
748
  discovered_count: int,
@@ -820,7 +820,8 @@ class HandlerPluginLoader(ProtocolHandlerPluginLoader):
820
820
  # Extract error code if available
821
821
  error_code: str | None = None
822
822
  if hasattr(e, "model") and hasattr(e.model, "context"):
823
- error_code = e.model.context.get("loader_error")
823
+ loader_error = e.model.context.get("loader_error")
824
+ error_code = str(loader_error) if loader_error is not None else None
824
825
 
825
826
  failed_handlers.append(
826
827
  ModelFailedPluginLoad(
@@ -1126,7 +1127,8 @@ class HandlerPluginLoader(ProtocolHandlerPluginLoader):
1126
1127
  # Extract error code if available
1127
1128
  error_code: str | None = None
1128
1129
  if hasattr(e, "model") and hasattr(e.model, "context"):
1129
- error_code = e.model.context.get("loader_error")
1130
+ loader_error = e.model.context.get("loader_error")
1131
+ error_code = str(loader_error) if loader_error is not None else None
1130
1132
 
1131
1133
  failed_handlers.append(
1132
1134
  ModelFailedPluginLoad(
@@ -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
  ]