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