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
@@ -0,0 +1,603 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Handler Contract Configuration Loader.
4
+
5
+ This module provides utilities for loading handler configuration from contract
6
+ YAML files. It supports both relative and absolute paths and extracts handler-
7
+ specific configuration for use during handler initialization.
8
+
9
+ Part of the bootstrap handler contract infrastructure.
10
+
11
+ The loader validates:
12
+ - Contract file existence
13
+ - YAML syntax validity
14
+ - Required contract structure (must be a dict)
15
+
16
+ Contract File Structure:
17
+ Handler contracts follow this schema (see contracts/handlers/*/handler_contract.yaml):
18
+
19
+ ```yaml
20
+ name: handler-consul
21
+ handler_class: omnibase_infra.handlers.handler_consul.HandlerConsul
22
+ handler_type: effect
23
+ tags:
24
+ - consul
25
+ - service-discovery
26
+ security:
27
+ trusted_namespace: omnibase_infra.handlers
28
+ audit_logging: true
29
+ requires_authentication: false # optional
30
+ ```
31
+
32
+ Security:
33
+ - Uses yaml.safe_load() to prevent arbitrary code execution
34
+ - Contract files are treated as trusted configuration (see CLAUDE.md security patterns)
35
+
36
+ See Also:
37
+ - HandlerBootstrapSource: Uses this loader for bootstrap handler contracts
38
+ - handler_plugin_loader.py: Related handler loading infrastructure
39
+ - docs/patterns/handler_plugin_loader.md: Security documentation
40
+
41
+ .. versionadded:: 0.6.5
42
+ Created for bootstrap handler contract loading.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import logging
48
+ from pathlib import Path
49
+ from typing import TYPE_CHECKING
50
+
51
+ import yaml
52
+
53
+ from omnibase_infra.errors import ProtocolConfigurationError
54
+ from omnibase_infra.models.errors.model_infra_error_context import (
55
+ ModelInfraErrorContext,
56
+ )
57
+
58
+ if TYPE_CHECKING:
59
+ from omnibase_core.types import JsonType
60
+
61
+ logger = logging.getLogger(__name__)
62
+
63
+ # Maximum contract file size (10 MB) - matches handler_plugin_loader.py
64
+ MAX_CONTRACT_SIZE_BYTES = 10 * 1024 * 1024
65
+
66
+
67
+ def load_handler_contract_config(
68
+ contract_path: str | Path | None,
69
+ handler_id: str,
70
+ ) -> dict[str, JsonType]:
71
+ """Load handler configuration from contract YAML file.
72
+
73
+ Reads and parses a handler contract YAML file, returning the parsed
74
+ dictionary for further processing. The contract path can be either
75
+ absolute or relative (resolved against common base paths).
76
+
77
+ Args:
78
+ contract_path: Path to handler_contract.yaml (relative or absolute).
79
+ If None, raises ProtocolConfigurationError.
80
+ handler_id: Handler identifier for error messages and logging.
81
+
82
+ Returns:
83
+ Dict containing the parsed contract YAML content.
84
+
85
+ Raises:
86
+ ProtocolConfigurationError: If any of the following conditions occur:
87
+ - contract_path is None
88
+ - File not found
89
+ - Permission denied (accessing, reading metadata, or reading content)
90
+ - OS error (path too long, invalid characters, etc.)
91
+ - File too large (exceeds MAX_CONTRACT_SIZE_BYTES)
92
+ - YAML syntax error
93
+ - Contract is not a dict
94
+
95
+ Example:
96
+ >>> contract = load_handler_contract_config(
97
+ ... "contracts/handlers/consul/handler_contract.yaml",
98
+ ... "proto.consul",
99
+ ... )
100
+ >>> contract["name"]
101
+ 'handler-consul'
102
+ """
103
+ if contract_path is None:
104
+ raise ProtocolConfigurationError(
105
+ f"Handler {handler_id} has no contract_path configured",
106
+ context=ModelInfraErrorContext.with_correlation(
107
+ operation="load_handler_contract",
108
+ target_name=handler_id,
109
+ ),
110
+ )
111
+
112
+ path = Path(contract_path)
113
+
114
+ # Resolve relative paths against common base directories
115
+ if not path.is_absolute():
116
+ resolved_path = _resolve_contract_path(path)
117
+ if resolved_path is None:
118
+ raise ProtocolConfigurationError(
119
+ f"Contract file not found: {contract_path}",
120
+ context=ModelInfraErrorContext.with_correlation(
121
+ operation="load_handler_contract",
122
+ target_name=handler_id,
123
+ ),
124
+ )
125
+ path = resolved_path
126
+
127
+ # Check file existence with permission error handling
128
+ try:
129
+ file_exists = path.exists()
130
+ except PermissionError as e:
131
+ raise ProtocolConfigurationError(
132
+ f"Permission denied accessing contract file: {contract_path}",
133
+ context=ModelInfraErrorContext.with_correlation(
134
+ operation="load_handler_contract",
135
+ target_name=handler_id,
136
+ ),
137
+ ) from e
138
+ except OSError as e:
139
+ raise ProtocolConfigurationError(
140
+ f"OS error accessing contract file: {contract_path} ({e})",
141
+ context=ModelInfraErrorContext.with_correlation(
142
+ operation="load_handler_contract",
143
+ target_name=handler_id,
144
+ ),
145
+ ) from e
146
+
147
+ if not file_exists:
148
+ raise ProtocolConfigurationError(
149
+ f"Contract file not found: {contract_path}",
150
+ context=ModelInfraErrorContext.with_correlation(
151
+ operation="load_handler_contract",
152
+ target_name=handler_id,
153
+ ),
154
+ )
155
+
156
+ # Check file size before reading (security: prevent memory exhaustion)
157
+ try:
158
+ file_size = path.stat().st_size
159
+ except PermissionError as e:
160
+ raise ProtocolConfigurationError(
161
+ f"Permission denied reading contract file metadata: {contract_path}",
162
+ context=ModelInfraErrorContext.with_correlation(
163
+ operation="load_handler_contract",
164
+ target_name=handler_id,
165
+ ),
166
+ ) from e
167
+ except OSError as e:
168
+ raise ProtocolConfigurationError(
169
+ f"OS error reading contract file metadata: {contract_path} ({e})",
170
+ context=ModelInfraErrorContext.with_correlation(
171
+ operation="load_handler_contract",
172
+ target_name=handler_id,
173
+ ),
174
+ ) from e
175
+
176
+ if file_size > MAX_CONTRACT_SIZE_BYTES:
177
+ raise ProtocolConfigurationError(
178
+ f"Contract file too large: {file_size} bytes (max {MAX_CONTRACT_SIZE_BYTES})",
179
+ context=ModelInfraErrorContext.with_correlation(
180
+ operation="load_handler_contract",
181
+ target_name=handler_id,
182
+ ),
183
+ )
184
+
185
+ try:
186
+ with path.open() as f:
187
+ contract = yaml.safe_load(f)
188
+ except FileNotFoundError as e:
189
+ # Race condition: file was deleted between exists() check and open()
190
+ raise ProtocolConfigurationError(
191
+ f"Contract file not found (deleted after check): {contract_path}",
192
+ context=ModelInfraErrorContext.with_correlation(
193
+ operation="load_handler_contract",
194
+ target_name=handler_id,
195
+ ),
196
+ ) from e
197
+ except PermissionError as e:
198
+ raise ProtocolConfigurationError(
199
+ f"Permission denied reading contract file: {contract_path}",
200
+ context=ModelInfraErrorContext.with_correlation(
201
+ operation="load_handler_contract",
202
+ target_name=handler_id,
203
+ ),
204
+ ) from e
205
+ except yaml.YAMLError as e:
206
+ raise ProtocolConfigurationError(
207
+ f"Invalid YAML in contract file: {contract_path}: {e}",
208
+ context=ModelInfraErrorContext.with_correlation(
209
+ operation="load_handler_contract",
210
+ target_name=handler_id,
211
+ ),
212
+ ) from e
213
+ except OSError as e:
214
+ # Catch other I/O errors (disk full, I/O error, etc.)
215
+ raise ProtocolConfigurationError(
216
+ f"I/O error reading contract file: {contract_path}: {e}",
217
+ context=ModelInfraErrorContext.with_correlation(
218
+ operation="load_handler_contract",
219
+ target_name=handler_id,
220
+ ),
221
+ ) from e
222
+
223
+ if not isinstance(contract, dict):
224
+ raise ProtocolConfigurationError(
225
+ f"Contract must be a dict, got {type(contract).__name__}",
226
+ context=ModelInfraErrorContext.with_correlation(
227
+ operation="load_handler_contract",
228
+ target_name=handler_id,
229
+ ),
230
+ )
231
+
232
+ logger.debug(
233
+ "Loaded handler contract",
234
+ extra={
235
+ "handler_id": handler_id,
236
+ "contract_path": str(path),
237
+ "contract_name": contract.get("name"),
238
+ },
239
+ )
240
+
241
+ return contract
242
+
243
+
244
+ def _resolve_contract_path(relative_path: Path) -> Path | None:
245
+ """Resolve a relative contract path against common base directories.
246
+
247
+ Tries multiple base directories to find the contract file:
248
+ 1. Current working directory
249
+ 2. Repository root (for contracts/handlers/*/handler_contract.yaml)
250
+ 3. Source directory (for src/omnibase_infra/contracts/handlers/*)
251
+
252
+ Path Resolution Reference:
253
+ This file: src/omnibase_infra/runtime/handler_contract_config_loader.py
254
+ - .parent = runtime/
255
+ - .parent.parent = omnibase_infra/
256
+ - .parent.parent.parent = src/
257
+ - .parent.parent.parent.parent = repo root (contains contracts/)
258
+
259
+ Args:
260
+ relative_path: Relative path to the contract file.
261
+
262
+ Returns:
263
+ Resolved absolute path if the file exists in any of the searched
264
+ directories. Returns None if:
265
+ - The file does not exist in any searched directory
266
+ - Permission errors prevent checking all directories
267
+ - OS errors (path too long, invalid characters) occur for all paths
268
+
269
+ Note:
270
+ Permission errors and symlink resolution issues are logged but do not
271
+ cause failures - the function continues to the next base path. Only
272
+ returns None after exhausting all possible base directories.
273
+ """
274
+ # Calculate paths with clear semantics based on this file's location:
275
+ # This file: src/omnibase_infra/runtime/handler_contract_config_loader.py
276
+ this_file = Path(__file__)
277
+ runtime_dir = this_file.parent # src/omnibase_infra/runtime/
278
+ infra_pkg_dir = runtime_dir.parent # src/omnibase_infra/
279
+ src_dir = infra_pkg_dir.parent # src/
280
+ repo_root = src_dir.parent # repository root (contains contracts/)
281
+
282
+ # Base directories to try, in order of preference
283
+ base_paths = [
284
+ Path.cwd(), # Current working directory (most specific)
285
+ repo_root, # For contracts/handlers/*/handler_contract.yaml
286
+ src_dir, # For src/omnibase_infra/contracts/handlers/*/handler_contract.yaml
287
+ ]
288
+
289
+ for base in base_paths:
290
+ full_path = base / relative_path
291
+ try:
292
+ if full_path.exists():
293
+ # Resolve symlinks and normalize the path
294
+ try:
295
+ return full_path.resolve(strict=True)
296
+ except OSError as resolve_error:
297
+ # Symlink resolution failed (broken symlink, etc.)
298
+ # Fall back to non-strict resolution
299
+ logger.debug(
300
+ "Symlink resolution failed, using non-strict resolution",
301
+ extra={
302
+ "path": str(full_path),
303
+ "error": str(resolve_error),
304
+ },
305
+ )
306
+ return full_path.resolve(strict=False)
307
+ except PermissionError:
308
+ logger.warning(
309
+ "Permission denied checking contract path",
310
+ extra={"path": str(full_path), "base": str(base)},
311
+ )
312
+ continue
313
+ except OSError as e:
314
+ # Handle other OS-level errors (e.g., path too long, invalid characters)
315
+ logger.warning(
316
+ "OS error checking contract path",
317
+ extra={"path": str(full_path), "error": str(e)},
318
+ )
319
+ continue
320
+
321
+ return None
322
+
323
+
324
+ def _validate_required_contract_fields(
325
+ contract: dict[str, JsonType],
326
+ handler_type: str,
327
+ ) -> None:
328
+ """Validate required fields are present in contract.
329
+
330
+ Performs fail-fast validation for required contract fields. Raises
331
+ ProtocolConfigurationError immediately if any required field is missing.
332
+
333
+ Required Fields:
334
+ - name: Handler name (REQUIRED)
335
+ - handler_class: Fully qualified class path (REQUIRED)
336
+ - handler_type OR descriptor.handler_kind: At least one (REQUIRED)
337
+
338
+ Args:
339
+ contract: Parsed contract dict to validate.
340
+ handler_type: Handler type identifier for error context.
341
+
342
+ Raises:
343
+ ProtocolConfigurationError: If any required field is missing, with
344
+ clear message indicating which field and proper error context.
345
+ """
346
+ # Validate 'name' field
347
+ if "name" not in contract:
348
+ raise ProtocolConfigurationError(
349
+ f"Missing required field 'name' in handler contract for '{handler_type}'",
350
+ context=ModelInfraErrorContext.with_correlation(
351
+ operation="extract_handler_config",
352
+ target_name=handler_type,
353
+ ),
354
+ )
355
+
356
+ # Validate 'handler_class' field
357
+ if "handler_class" not in contract:
358
+ raise ProtocolConfigurationError(
359
+ f"Missing required field 'handler_class' in handler contract for '{handler_type}'",
360
+ context=ModelInfraErrorContext.with_correlation(
361
+ operation="extract_handler_config",
362
+ target_name=handler_type,
363
+ ),
364
+ )
365
+
366
+ # Validate handler kind: must have either 'handler_type' or 'descriptor.handler_kind'
367
+ has_handler_type = "handler_type" in contract
368
+ descriptor = contract.get("descriptor", {})
369
+ has_descriptor_handler_kind = (
370
+ isinstance(descriptor, dict) and "handler_kind" in descriptor
371
+ )
372
+
373
+ if not has_handler_type and not has_descriptor_handler_kind:
374
+ raise ProtocolConfigurationError(
375
+ f"Missing required field 'handler_type' or 'descriptor.handler_kind' "
376
+ f"in handler contract for '{handler_type}'",
377
+ context=ModelInfraErrorContext.with_correlation(
378
+ operation="extract_handler_config",
379
+ target_name=handler_type,
380
+ ),
381
+ )
382
+
383
+
384
+ def extract_handler_config(
385
+ contract: dict[str, JsonType],
386
+ handler_type: str,
387
+ *,
388
+ require_basic_fields: bool = True,
389
+ ) -> dict[str, JsonType]:
390
+ """Extract handler-specific configuration from parsed contract.
391
+
392
+ Extracts configuration values from both basic and rich contract structures
393
+ and flattens them into a dict suitable for handler initialization.
394
+
395
+ Fail-Fast Validation:
396
+ When require_basic_fields=True (default), validates that required fields
397
+ are present and raises ProtocolConfigurationError immediately if missing.
398
+
399
+ Required fields for basic contracts:
400
+ - name: REQUIRED
401
+ - handler_class: REQUIRED
402
+ - handler_type OR descriptor.handler_kind: At least one REQUIRED
403
+
404
+ Supports Two Contract Formats:
405
+
406
+ Basic Contract Structure (contracts/handlers/*/handler_contract.yaml):
407
+ - name: Handler name (e.g., "handler-consul") [REQUIRED]
408
+ - handler_class: Fully qualified class path [REQUIRED]
409
+ - handler_type: Handler kind (effect, compute, etc.) [REQUIRED*]
410
+ - tags: List of discovery tags
411
+ - security: Security metadata dict
412
+ - trusted_namespace: Required trusted import namespace
413
+ - audit_logging: Whether to enable audit logging
414
+ - requires_authentication: Whether auth is required (optional)
415
+
416
+ *handler_type is required unless descriptor.handler_kind is provided.
417
+
418
+ Rich Contract Structure (src/omnibase_infra/contracts/handlers/*/handler_contract.yaml):
419
+ - handler_id: Unique identifier (e.g., "effect.mcp.handler")
420
+ - name: Handler name [REQUIRED]
421
+ - version: Semantic version
422
+ - descriptor: Handler descriptor with timeout, retry, circuit breaker
423
+ - handler_kind: Handler behavioral type [REQUIRED*]
424
+ - timeout_ms: Timeout in milliseconds
425
+ - retry_policy: Retry configuration
426
+ - circuit_breaker: Circuit breaker configuration
427
+ - metadata: Additional metadata
428
+ - transport: Transport configuration
429
+ - default_host: Default bind host
430
+ - default_port: Default port
431
+ - default_path: Default URL path
432
+ - stateless: Whether handler is stateless
433
+ - json_response: Whether to use JSON responses
434
+ - security: Security configuration
435
+ - tool_access: Tool access control
436
+ - max_tools: Maximum number of tools
437
+
438
+ *handler_kind is required unless handler_type is provided.
439
+
440
+ Args:
441
+ contract: Parsed contract dict from load_handler_contract_config().
442
+ handler_type: Handler type identifier (e.g., "consul", "db") for
443
+ logging and context.
444
+ require_basic_fields: If True (default), validates required fields are
445
+ present and raises ProtocolConfigurationError if missing. Set to
446
+ False for rich contracts that may have different requirements or
447
+ during migration.
448
+
449
+ Returns:
450
+ Flat dict with extracted configuration values suitable for
451
+ handler.initialize() or similar configuration methods:
452
+ - name: Handler name
453
+ - handler_class: Fully qualified class path
454
+ - handler_kind: Handler behavioral type
455
+ - tags: List of tags
456
+ - trusted_namespace: Security namespace
457
+ - audit_logging: Audit logging flag
458
+ - requires_authentication: Auth requirement flag
459
+ - host: Transport default host (rich contracts)
460
+ - port: Transport default port (rich contracts)
461
+ - path: Transport default path (rich contracts)
462
+ - stateless: Transport stateless flag (rich contracts)
463
+ - json_response: Transport JSON response flag (rich contracts)
464
+ - timeout_seconds: Timeout in seconds (rich contracts)
465
+ - max_tools: Maximum tools for MCP (rich contracts)
466
+
467
+ Raises:
468
+ ProtocolConfigurationError: If require_basic_fields=True and any of:
469
+ - 'name' field is missing
470
+ - 'handler_class' field is missing
471
+ - Neither 'handler_type' nor 'descriptor.handler_kind' is present
472
+
473
+ Example:
474
+ >>> # Basic contract
475
+ >>> contract = {
476
+ ... "name": "handler-consul",
477
+ ... "handler_class": "omnibase_infra.handlers.handler_consul.HandlerConsul",
478
+ ... "handler_type": "effect",
479
+ ... "tags": ["consul", "service-discovery"],
480
+ ... "security": {
481
+ ... "trusted_namespace": "omnibase_infra.handlers",
482
+ ... "audit_logging": True,
483
+ ... },
484
+ ... }
485
+ >>> config = extract_handler_config(contract, "consul")
486
+ >>> config["name"]
487
+ 'handler-consul'
488
+ >>> config["audit_logging"]
489
+ True
490
+
491
+ >>> # Rich contract with transport
492
+ >>> rich_contract = {
493
+ ... "name": "MCP Handler",
494
+ ... "descriptor": {"handler_kind": "effect", "timeout_ms": 30000},
495
+ ... "metadata": {
496
+ ... "transport": {"default_host": "0.0.0.0", "default_port": 8090},
497
+ ... "security": {"tool_access": {"max_tools": 100}},
498
+ ... },
499
+ ... }
500
+ >>> config = extract_handler_config(rich_contract, "mcp")
501
+ >>> config["port"]
502
+ 8090
503
+ >>> config["max_tools"]
504
+ 100
505
+
506
+ >>> # Missing required field raises error
507
+ >>> bad_contract = {"handler_class": "some.Handler"}
508
+ >>> extract_handler_config(bad_contract, "test") # doctest: +SKIP
509
+ Traceback (most recent call last):
510
+ ...
511
+ ProtocolConfigurationError: Missing required field 'name' in handler contract
512
+ """
513
+ # Fail-fast validation for required fields
514
+ if require_basic_fields:
515
+ _validate_required_contract_fields(contract, handler_type)
516
+
517
+ config: dict[str, JsonType] = {}
518
+
519
+ # Extract top-level fields
520
+ if "name" in contract:
521
+ config["name"] = contract["name"]
522
+
523
+ if "handler_class" in contract:
524
+ config["handler_class"] = contract["handler_class"]
525
+
526
+ # Handler kind can be in handler_type (basic) or descriptor.handler_kind (rich)
527
+ if "handler_type" in contract:
528
+ config["handler_kind"] = contract["handler_type"]
529
+
530
+ if "tags" in contract:
531
+ config["tags"] = contract["tags"]
532
+
533
+ # Extract descriptor configuration (rich contracts)
534
+ descriptor = contract.get("descriptor", {})
535
+ if isinstance(descriptor, dict):
536
+ # Handler kind from descriptor (rich contracts)
537
+ if "handler_kind" in descriptor and "handler_kind" not in config:
538
+ config["handler_kind"] = descriptor["handler_kind"]
539
+
540
+ # Timeout configuration (convert ms to seconds)
541
+ if "timeout_ms" in descriptor:
542
+ timeout_ms = descriptor["timeout_ms"]
543
+ if isinstance(timeout_ms, (int, float)):
544
+ config["timeout_seconds"] = timeout_ms / 1000.0
545
+
546
+ # Extract security configuration (basic contracts - top level)
547
+ security = contract.get("security", {})
548
+ if isinstance(security, dict):
549
+ if "trusted_namespace" in security:
550
+ config["trusted_namespace"] = security["trusted_namespace"]
551
+
552
+ if "audit_logging" in security:
553
+ config["audit_logging"] = security["audit_logging"]
554
+
555
+ if "requires_authentication" in security:
556
+ config["requires_authentication"] = security["requires_authentication"]
557
+
558
+ # Extract metadata configuration (rich contracts)
559
+ metadata = contract.get("metadata", {})
560
+ if isinstance(metadata, dict):
561
+ # Transport configuration
562
+ transport = metadata.get("transport", {})
563
+ if isinstance(transport, dict):
564
+ if "default_host" in transport:
565
+ config["host"] = transport["default_host"]
566
+
567
+ if "default_port" in transport:
568
+ config["port"] = transport["default_port"]
569
+
570
+ if "default_path" in transport:
571
+ config["path"] = transport["default_path"]
572
+
573
+ if "stateless" in transport:
574
+ config["stateless"] = transport["stateless"]
575
+
576
+ if "json_response" in transport:
577
+ config["json_response"] = transport["json_response"]
578
+
579
+ # Security configuration (rich contracts - in metadata.security)
580
+ metadata_security = metadata.get("security", {})
581
+ if isinstance(metadata_security, dict):
582
+ # Tool access configuration (for MCP)
583
+ tool_access = metadata_security.get("tool_access", {})
584
+ if isinstance(tool_access, dict):
585
+ if "max_tools" in tool_access:
586
+ config["max_tools"] = tool_access["max_tools"]
587
+
588
+ logger.debug(
589
+ "Extracted handler config from contract",
590
+ extra={
591
+ "handler_type": handler_type,
592
+ "config_keys": list(config.keys()),
593
+ },
594
+ )
595
+
596
+ return config
597
+
598
+
599
+ __all__ = [
600
+ "MAX_CONTRACT_SIZE_BYTES",
601
+ "extract_handler_config",
602
+ "load_handler_contract_config",
603
+ ]