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.
- omnibase_infra/__init__.py +1 -1
- omnibase_infra/adapters/adapter_onex_tool_execution.py +446 -0
- omnibase_infra/cli/commands.py +1 -1
- omnibase_infra/configs/widget_mapping.yaml +176 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +4 -1
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +4 -1
- omnibase_infra/errors/error_compute_registry.py +4 -1
- omnibase_infra/errors/error_event_bus_registry.py +4 -1
- omnibase_infra/errors/error_infra.py +3 -1
- omnibase_infra/errors/error_policy_registry.py +4 -1
- omnibase_infra/handlers/handler_db.py +2 -1
- omnibase_infra/handlers/handler_graph.py +10 -5
- omnibase_infra/handlers/handler_mcp.py +736 -63
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
- omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
- omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +301 -4
- omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
- omnibase_infra/mixins/mixin_node_introspection.py +24 -7
- omnibase_infra/mixins/mixin_retry_execution.py +1 -1
- omnibase_infra/models/handlers/__init__.py +10 -0
- omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
- omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
- omnibase_infra/models/mcp/__init__.py +15 -0
- omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
- omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
- omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
- omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
- omnibase_infra/models/registration/model_node_capabilities.py +11 -0
- omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
- omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
- omnibase_infra/nodes/effects/contract.yaml +0 -5
- omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
- omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
- omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
- omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +46 -25
- omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
- omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
- omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +24 -19
- omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
- omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
- omnibase_infra/plugins/plugin_compute_base.py +16 -2
- omnibase_infra/protocols/protocol_event_projector.py +1 -1
- omnibase_infra/runtime/__init__.py +51 -1
- omnibase_infra/runtime/binding_config_resolver.py +102 -37
- omnibase_infra/runtime/constants_notification.py +75 -0
- omnibase_infra/runtime/contract_handler_discovery.py +6 -1
- omnibase_infra/runtime/handler_bootstrap_source.py +514 -0
- omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
- omnibase_infra/runtime/handler_contract_source.py +289 -167
- omnibase_infra/runtime/handler_plugin_loader.py +4 -2
- omnibase_infra/runtime/mixin_semver_cache.py +25 -1
- omnibase_infra/runtime/mixins/__init__.py +7 -0
- omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
- omnibase_infra/runtime/models/__init__.py +24 -0
- omnibase_infra/runtime/models/model_health_check_result.py +2 -1
- omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
- omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
- omnibase_infra/runtime/projector_plugin_loader.py +1 -1
- omnibase_infra/runtime/projector_shell.py +229 -1
- omnibase_infra/runtime/protocols/__init__.py +10 -0
- omnibase_infra/runtime/registry/registry_protocol_binding.py +3 -2
- omnibase_infra/runtime/registry_policy.py +9 -326
- omnibase_infra/runtime/secret_resolver.py +4 -2
- omnibase_infra/runtime/service_kernel.py +10 -2
- omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
- omnibase_infra/runtime/service_runtime_host_process.py +225 -15
- omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
- omnibase_infra/runtime/transition_notification_publisher.py +764 -0
- omnibase_infra/runtime/util_container_wiring.py +6 -5
- omnibase_infra/runtime/util_wiring.py +5 -1
- omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
- omnibase_infra/services/mcp/__init__.py +31 -0
- omnibase_infra/services/mcp/mcp_server_lifecycle.py +443 -0
- omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
- omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
- omnibase_infra/services/registry_api/__init__.py +40 -0
- omnibase_infra/services/registry_api/main.py +243 -0
- omnibase_infra/services/registry_api/models/__init__.py +66 -0
- omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
- omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
- omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
- omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
- omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
- omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
- omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
- omnibase_infra/services/registry_api/models/model_warning.py +49 -0
- omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
- omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
- omnibase_infra/services/registry_api/routes.py +371 -0
- omnibase_infra/services/registry_api/service.py +846 -0
- omnibase_infra/services/service_capability_query.py +4 -4
- omnibase_infra/services/service_health.py +3 -2
- omnibase_infra/services/service_timeout_emitter.py +13 -2
- omnibase_infra/utils/util_dsn_validation.py +1 -1
- omnibase_infra/validation/__init__.py +3 -19
- omnibase_infra/validation/contracts/security.validation.yaml +114 -0
- omnibase_infra/validation/infra_validators.py +35 -24
- omnibase_infra/validation/validation_exemptions.yaml +113 -9
- omnibase_infra/validation/validator_chain_propagation.py +2 -2
- omnibase_infra/validation/validator_runtime_shape.py +1 -1
- omnibase_infra/validation/validator_security.py +473 -370
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/METADATA +2 -2
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/RECORD +116 -74
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/entry_points.txt +0 -0
- {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
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
#
|
|
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 =
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
]
|