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
|
@@ -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
|
+
... "bootstrap.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
|
+
]
|