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
omnibase_infra/__init__.py
CHANGED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""ONEX Tool Execution Adapter - Bridges MCP tool calls to ONEX orchestrator execution.
|
|
4
|
+
|
|
5
|
+
This adapter handles the execution of MCP tool invocations by:
|
|
6
|
+
1. Validating input arguments against the tool's input schema
|
|
7
|
+
2. Building an ONEX envelope with the input payload
|
|
8
|
+
3. Dispatching to the orchestrator endpoint via HTTP
|
|
9
|
+
4. Transforming the response to MCP format
|
|
10
|
+
|
|
11
|
+
Routing:
|
|
12
|
+
The adapter uses the tool definition's endpoint or service_id to locate
|
|
13
|
+
the target orchestrator. It supports both direct HTTP dispatch and
|
|
14
|
+
service discovery via Consul.
|
|
15
|
+
|
|
16
|
+
Timeout Handling:
|
|
17
|
+
Each tool definition includes a timeout_seconds value. The adapter
|
|
18
|
+
enforces this timeout when dispatching to the orchestrator, raising
|
|
19
|
+
InfraTimeoutError if exceeded.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import logging
|
|
26
|
+
from typing import TYPE_CHECKING
|
|
27
|
+
from uuid import UUID, uuid4
|
|
28
|
+
|
|
29
|
+
import httpx
|
|
30
|
+
|
|
31
|
+
from omnibase_core.container import ModelONEXContainer
|
|
32
|
+
from omnibase_infra.enums import EnumInfraTransportType
|
|
33
|
+
from omnibase_infra.errors import (
|
|
34
|
+
InfraConnectionError,
|
|
35
|
+
InfraTimeoutError,
|
|
36
|
+
InfraUnavailableError,
|
|
37
|
+
ModelInfraErrorContext,
|
|
38
|
+
ModelTimeoutErrorContext,
|
|
39
|
+
)
|
|
40
|
+
from omnibase_infra.mixins import MixinAsyncCircuitBreaker
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
43
|
+
from omnibase_infra.models.mcp.model_mcp_tool_definition import (
|
|
44
|
+
ModelMCPToolDefinition,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AdapterONEXToolExecution(MixinAsyncCircuitBreaker):
|
|
51
|
+
"""Bridges MCP tool calls to ONEX orchestrator execution.
|
|
52
|
+
|
|
53
|
+
This adapter handles the dispatch of MCP tool invocations to the
|
|
54
|
+
appropriate ONEX orchestrator node. It supports:
|
|
55
|
+
- Direct HTTP dispatch to orchestrator endpoint
|
|
56
|
+
- Input validation against JSON Schema
|
|
57
|
+
- Timeout enforcement
|
|
58
|
+
- Error transformation to MCP format
|
|
59
|
+
- Circuit breaker protection for external HTTP calls
|
|
60
|
+
|
|
61
|
+
Attributes:
|
|
62
|
+
_container: ONEX container for dependency injection.
|
|
63
|
+
_http_client: HTTP client for orchestrator dispatch.
|
|
64
|
+
_default_timeout: Default timeout if tool definition doesn't specify one.
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
>>> adapter = AdapterONEXToolExecution(container=container)
|
|
68
|
+
>>> result = await adapter.execute(
|
|
69
|
+
... tool=tool_definition,
|
|
70
|
+
... arguments={"input_data": "test"},
|
|
71
|
+
... correlation_id=uuid4(),
|
|
72
|
+
... )
|
|
73
|
+
>>> print(result["success"])
|
|
74
|
+
True
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(
|
|
78
|
+
self,
|
|
79
|
+
container: ModelONEXContainer,
|
|
80
|
+
http_client: httpx.AsyncClient | None = None,
|
|
81
|
+
default_timeout: float = 30.0,
|
|
82
|
+
circuit_breaker_threshold: int = 5,
|
|
83
|
+
circuit_breaker_reset_timeout: float = 60.0,
|
|
84
|
+
) -> None:
|
|
85
|
+
"""Initialize the execution adapter.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
container: ONEX container for dependency injection.
|
|
89
|
+
http_client: Optional HTTP client. If not provided, one will be
|
|
90
|
+
created during execute() calls.
|
|
91
|
+
default_timeout: Default timeout in seconds for orchestrator calls.
|
|
92
|
+
circuit_breaker_threshold: Max failures before opening circuit (default: 5).
|
|
93
|
+
circuit_breaker_reset_timeout: Seconds before automatic reset (default: 60.0).
|
|
94
|
+
"""
|
|
95
|
+
self._container = container
|
|
96
|
+
self._http_client = http_client
|
|
97
|
+
self._default_timeout = default_timeout
|
|
98
|
+
self._owns_client = http_client is None
|
|
99
|
+
|
|
100
|
+
# Initialize circuit breaker for HTTP dispatch resilience
|
|
101
|
+
self._init_circuit_breaker(
|
|
102
|
+
threshold=circuit_breaker_threshold,
|
|
103
|
+
reset_timeout=circuit_breaker_reset_timeout,
|
|
104
|
+
service_name="onex-tool-execution",
|
|
105
|
+
transport_type=EnumInfraTransportType.HTTP,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
logger.debug(
|
|
109
|
+
"AdapterONEXToolExecution initialized",
|
|
110
|
+
extra={
|
|
111
|
+
"default_timeout": default_timeout,
|
|
112
|
+
"circuit_breaker_threshold": circuit_breaker_threshold,
|
|
113
|
+
"circuit_breaker_reset_timeout": circuit_breaker_reset_timeout,
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
async def execute(
|
|
118
|
+
self,
|
|
119
|
+
tool: ModelMCPToolDefinition,
|
|
120
|
+
arguments: dict[str, object],
|
|
121
|
+
correlation_id: UUID,
|
|
122
|
+
) -> dict[str, object]:
|
|
123
|
+
"""Execute an MCP tool call by dispatching to the ONEX orchestrator.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
tool: Tool definition containing endpoint, timeout, and metadata.
|
|
127
|
+
arguments: Input arguments from the MCP tool call.
|
|
128
|
+
correlation_id: Correlation ID for tracing.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Dictionary with execution result:
|
|
132
|
+
- success: True if execution succeeded
|
|
133
|
+
- result: Orchestrator response (if successful)
|
|
134
|
+
- error: Error message (if failed)
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
InfraUnavailableError: If tool endpoint is not configured.
|
|
138
|
+
InfraTimeoutError: If execution times out.
|
|
139
|
+
InfraConnectionError: If connection to orchestrator fails.
|
|
140
|
+
"""
|
|
141
|
+
logger.info(
|
|
142
|
+
"Executing MCP tool",
|
|
143
|
+
extra={
|
|
144
|
+
"tool_name": tool.name,
|
|
145
|
+
"correlation_id": str(correlation_id),
|
|
146
|
+
},
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# Validate endpoint
|
|
150
|
+
endpoint = tool.endpoint
|
|
151
|
+
if not endpoint:
|
|
152
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
153
|
+
correlation_id=correlation_id,
|
|
154
|
+
transport_type=EnumInfraTransportType.HTTP,
|
|
155
|
+
operation="execute_tool",
|
|
156
|
+
target_name=tool.name,
|
|
157
|
+
)
|
|
158
|
+
raise InfraUnavailableError(
|
|
159
|
+
f"Tool '{tool.name}' has no endpoint configured",
|
|
160
|
+
context=ctx,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
# Build envelope payload
|
|
164
|
+
envelope = self._build_envelope(tool, arguments, correlation_id)
|
|
165
|
+
|
|
166
|
+
# Determine timeout
|
|
167
|
+
timeout = tool.timeout_seconds or self._default_timeout
|
|
168
|
+
|
|
169
|
+
# Check circuit breaker before dispatch
|
|
170
|
+
try:
|
|
171
|
+
async with self._circuit_breaker_lock:
|
|
172
|
+
await self._check_circuit_breaker(
|
|
173
|
+
operation="execute_tool",
|
|
174
|
+
correlation_id=correlation_id,
|
|
175
|
+
)
|
|
176
|
+
except InfraUnavailableError:
|
|
177
|
+
logger.warning(
|
|
178
|
+
"MCP tool execution blocked - circuit breaker open",
|
|
179
|
+
extra={
|
|
180
|
+
"tool_name": tool.name,
|
|
181
|
+
"correlation_id": str(correlation_id),
|
|
182
|
+
},
|
|
183
|
+
)
|
|
184
|
+
return {
|
|
185
|
+
"success": False,
|
|
186
|
+
"error": "Service temporarily unavailable - circuit breaker open",
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# Dispatch to orchestrator
|
|
190
|
+
try:
|
|
191
|
+
result = await self._http_dispatch(
|
|
192
|
+
endpoint=endpoint,
|
|
193
|
+
envelope=envelope,
|
|
194
|
+
timeout=timeout,
|
|
195
|
+
correlation_id=correlation_id,
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
# Record success to reset circuit breaker
|
|
199
|
+
async with self._circuit_breaker_lock:
|
|
200
|
+
await self._reset_circuit_breaker()
|
|
201
|
+
|
|
202
|
+
logger.info(
|
|
203
|
+
"MCP tool execution succeeded",
|
|
204
|
+
extra={
|
|
205
|
+
"tool_name": tool.name,
|
|
206
|
+
"correlation_id": str(correlation_id),
|
|
207
|
+
},
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
"success": True,
|
|
212
|
+
"result": result,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
except InfraTimeoutError:
|
|
216
|
+
# Record failure to potentially open circuit breaker
|
|
217
|
+
async with self._circuit_breaker_lock:
|
|
218
|
+
await self._record_circuit_failure(
|
|
219
|
+
operation="execute_tool",
|
|
220
|
+
correlation_id=correlation_id,
|
|
221
|
+
)
|
|
222
|
+
logger.warning(
|
|
223
|
+
"MCP tool execution timed out",
|
|
224
|
+
extra={
|
|
225
|
+
"tool_name": tool.name,
|
|
226
|
+
"timeout": timeout,
|
|
227
|
+
"correlation_id": str(correlation_id),
|
|
228
|
+
},
|
|
229
|
+
)
|
|
230
|
+
return {
|
|
231
|
+
"success": False,
|
|
232
|
+
"error": f"Tool execution timed out after {timeout} seconds",
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
except InfraConnectionError as e:
|
|
236
|
+
# Record failure to potentially open circuit breaker
|
|
237
|
+
async with self._circuit_breaker_lock:
|
|
238
|
+
await self._record_circuit_failure(
|
|
239
|
+
operation="execute_tool",
|
|
240
|
+
correlation_id=correlation_id,
|
|
241
|
+
)
|
|
242
|
+
logger.warning(
|
|
243
|
+
"MCP tool execution failed - connection error",
|
|
244
|
+
extra={
|
|
245
|
+
"tool_name": tool.name,
|
|
246
|
+
"error": str(e),
|
|
247
|
+
"correlation_id": str(correlation_id),
|
|
248
|
+
},
|
|
249
|
+
)
|
|
250
|
+
return {
|
|
251
|
+
"success": False,
|
|
252
|
+
"error": f"Connection error: {e}",
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
except Exception as e:
|
|
256
|
+
# Record failure to potentially open circuit breaker
|
|
257
|
+
async with self._circuit_breaker_lock:
|
|
258
|
+
await self._record_circuit_failure(
|
|
259
|
+
operation="execute_tool",
|
|
260
|
+
correlation_id=correlation_id,
|
|
261
|
+
)
|
|
262
|
+
logger.exception(
|
|
263
|
+
"MCP tool execution failed - unexpected error",
|
|
264
|
+
extra={
|
|
265
|
+
"tool_name": tool.name,
|
|
266
|
+
"error": str(e),
|
|
267
|
+
"correlation_id": str(correlation_id),
|
|
268
|
+
},
|
|
269
|
+
)
|
|
270
|
+
return {
|
|
271
|
+
"success": False,
|
|
272
|
+
"error": f"Unexpected error: {e}",
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
def _build_envelope(
|
|
276
|
+
self,
|
|
277
|
+
tool: ModelMCPToolDefinition,
|
|
278
|
+
arguments: dict[str, object],
|
|
279
|
+
correlation_id: UUID,
|
|
280
|
+
) -> dict[str, object]:
|
|
281
|
+
"""Build an ONEX envelope for the orchestrator.
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
tool: Tool definition.
|
|
285
|
+
arguments: Input arguments from MCP.
|
|
286
|
+
correlation_id: Correlation ID.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Envelope dict for the orchestrator.
|
|
290
|
+
"""
|
|
291
|
+
return {
|
|
292
|
+
"envelope_id": str(uuid4()),
|
|
293
|
+
"correlation_id": str(correlation_id),
|
|
294
|
+
"source": "mcp-adapter",
|
|
295
|
+
"payload": arguments,
|
|
296
|
+
"metadata": {
|
|
297
|
+
"tool_name": tool.name,
|
|
298
|
+
"tool_version": tool.version,
|
|
299
|
+
},
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
async def _http_dispatch(
|
|
303
|
+
self,
|
|
304
|
+
endpoint: str,
|
|
305
|
+
envelope: dict[str, object],
|
|
306
|
+
timeout: float,
|
|
307
|
+
correlation_id: UUID,
|
|
308
|
+
) -> dict[str, object]:
|
|
309
|
+
"""Dispatch envelope to orchestrator endpoint via HTTP.
|
|
310
|
+
|
|
311
|
+
Args:
|
|
312
|
+
endpoint: Target endpoint URL.
|
|
313
|
+
envelope: Request envelope.
|
|
314
|
+
timeout: Request timeout in seconds.
|
|
315
|
+
correlation_id: Correlation ID.
|
|
316
|
+
|
|
317
|
+
Returns:
|
|
318
|
+
Response from orchestrator.
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
InfraTimeoutError: If request times out.
|
|
322
|
+
InfraConnectionError: If connection fails.
|
|
323
|
+
"""
|
|
324
|
+
# Use provided client or create one
|
|
325
|
+
if self._http_client is not None:
|
|
326
|
+
return await self._dispatch_with_client(
|
|
327
|
+
self._http_client,
|
|
328
|
+
endpoint,
|
|
329
|
+
envelope,
|
|
330
|
+
timeout,
|
|
331
|
+
correlation_id,
|
|
332
|
+
)
|
|
333
|
+
else:
|
|
334
|
+
# Create temporary client
|
|
335
|
+
async with httpx.AsyncClient() as client:
|
|
336
|
+
return await self._dispatch_with_client(
|
|
337
|
+
client,
|
|
338
|
+
endpoint,
|
|
339
|
+
envelope,
|
|
340
|
+
timeout,
|
|
341
|
+
correlation_id,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
async def _dispatch_with_client(
|
|
345
|
+
self,
|
|
346
|
+
client: httpx.AsyncClient,
|
|
347
|
+
endpoint: str,
|
|
348
|
+
envelope: dict[str, object],
|
|
349
|
+
timeout: float,
|
|
350
|
+
correlation_id: UUID,
|
|
351
|
+
) -> dict[str, object]:
|
|
352
|
+
"""Dispatch using the provided HTTP client.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
client: HTTP client.
|
|
356
|
+
endpoint: Target endpoint URL.
|
|
357
|
+
envelope: Request envelope.
|
|
358
|
+
timeout: Request timeout in seconds.
|
|
359
|
+
correlation_id: Correlation ID.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
Response from orchestrator.
|
|
363
|
+
|
|
364
|
+
Raises:
|
|
365
|
+
InfraTimeoutError: If request times out.
|
|
366
|
+
InfraConnectionError: If connection fails.
|
|
367
|
+
"""
|
|
368
|
+
try:
|
|
369
|
+
response = await asyncio.wait_for(
|
|
370
|
+
client.post(
|
|
371
|
+
endpoint,
|
|
372
|
+
json=envelope,
|
|
373
|
+
headers={
|
|
374
|
+
"X-Correlation-ID": str(correlation_id),
|
|
375
|
+
"Content-Type": "application/json",
|
|
376
|
+
},
|
|
377
|
+
timeout=timeout,
|
|
378
|
+
),
|
|
379
|
+
timeout=timeout,
|
|
380
|
+
)
|
|
381
|
+
|
|
382
|
+
response.raise_for_status()
|
|
383
|
+
result: dict[str, object] = response.json()
|
|
384
|
+
return result
|
|
385
|
+
|
|
386
|
+
except TimeoutError as e:
|
|
387
|
+
timeout_ctx = ModelTimeoutErrorContext(
|
|
388
|
+
transport_type=EnumInfraTransportType.HTTP,
|
|
389
|
+
operation="http_dispatch",
|
|
390
|
+
target_name=endpoint,
|
|
391
|
+
correlation_id=correlation_id,
|
|
392
|
+
timeout_seconds=timeout,
|
|
393
|
+
)
|
|
394
|
+
raise InfraTimeoutError(
|
|
395
|
+
f"Timeout dispatching to {endpoint} after {timeout}s",
|
|
396
|
+
context=timeout_ctx,
|
|
397
|
+
) from e
|
|
398
|
+
|
|
399
|
+
except httpx.ConnectError as e:
|
|
400
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
401
|
+
correlation_id=correlation_id,
|
|
402
|
+
transport_type=EnumInfraTransportType.HTTP,
|
|
403
|
+
operation="http_dispatch",
|
|
404
|
+
target_name=endpoint,
|
|
405
|
+
)
|
|
406
|
+
raise InfraConnectionError(
|
|
407
|
+
f"Connection failed to {endpoint}: {e}",
|
|
408
|
+
context=ctx,
|
|
409
|
+
) from e
|
|
410
|
+
|
|
411
|
+
except httpx.HTTPStatusError as e:
|
|
412
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
413
|
+
correlation_id=correlation_id,
|
|
414
|
+
transport_type=EnumInfraTransportType.HTTP,
|
|
415
|
+
operation="http_dispatch",
|
|
416
|
+
target_name=endpoint,
|
|
417
|
+
)
|
|
418
|
+
raise InfraConnectionError(
|
|
419
|
+
f"HTTP error from {endpoint}: {e.response.status_code}",
|
|
420
|
+
context=ctx,
|
|
421
|
+
) from e
|
|
422
|
+
|
|
423
|
+
except Exception as e:
|
|
424
|
+
ctx = ModelInfraErrorContext.with_correlation(
|
|
425
|
+
correlation_id=correlation_id,
|
|
426
|
+
transport_type=EnumInfraTransportType.HTTP,
|
|
427
|
+
operation="http_dispatch",
|
|
428
|
+
target_name=endpoint,
|
|
429
|
+
)
|
|
430
|
+
raise InfraConnectionError(
|
|
431
|
+
f"Request to {endpoint} failed: {e}",
|
|
432
|
+
context=ctx,
|
|
433
|
+
) from e
|
|
434
|
+
|
|
435
|
+
async def close(self) -> None:
|
|
436
|
+
"""Close the HTTP client if owned by this adapter."""
|
|
437
|
+
if self._owns_client and self._http_client is not None:
|
|
438
|
+
await self._http_client.aclose()
|
|
439
|
+
self._http_client = None
|
|
440
|
+
|
|
441
|
+
def describe(self) -> dict[str, object]:
|
|
442
|
+
"""Return adapter metadata for observability."""
|
|
443
|
+
return {
|
|
444
|
+
"adapter_name": "AdapterONEXToolExecution",
|
|
445
|
+
"default_timeout": self._default_timeout,
|
|
446
|
+
"owns_client": self._owns_client,
|
|
447
|
+
"circuit_breaker": self._get_circuit_breaker_state(),
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
__all__ = ["AdapterONEXToolExecution"]
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Capability extraction and inference for ONEX contracts."""
|
|
2
|
+
|
|
3
|
+
from omnibase_infra.capabilities.capability_inference_rules import (
|
|
4
|
+
CapabilityInferenceRules,
|
|
5
|
+
)
|
|
6
|
+
from omnibase_infra.capabilities.contract_capability_extractor import (
|
|
7
|
+
ContractCapabilityExtractor,
|
|
8
|
+
)
|
|
9
|
+
from omnibase_infra.capabilities.intent_type_extractor import IntentTypeExtractor
|
|
10
|
+
|
|
11
|
+
__all__ = [
|
|
12
|
+
"CapabilityInferenceRules",
|
|
13
|
+
"ContractCapabilityExtractor",
|
|
14
|
+
"IntentTypeExtractor",
|
|
15
|
+
]
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Capability inference rules for deriving tags from contract structure.
|
|
2
|
+
|
|
3
|
+
This module provides deterministic pattern matching to infer capability tags
|
|
4
|
+
from contract fields like intent_types and protocols. Rules are injectable
|
|
5
|
+
via constructor for extensibility while providing sensible defaults.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CapabilityInferenceRules:
|
|
12
|
+
"""Code-driven capability inference rules with injectable patterns.
|
|
13
|
+
|
|
14
|
+
Deterministic pattern matching for inferring capability_tags from
|
|
15
|
+
contract structure. All rule mappings are injectable via constructor
|
|
16
|
+
while providing sensible defaults.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
intent_patterns: Custom/additional intent patterns (merged with defaults).
|
|
20
|
+
protocol_tags: Custom/additional protocol tags (merged with defaults).
|
|
21
|
+
node_type_tags: Custom/additional node type tags (merged with defaults).
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
# Use defaults
|
|
25
|
+
rules = CapabilityInferenceRules()
|
|
26
|
+
|
|
27
|
+
# Override specific pattern
|
|
28
|
+
rules = CapabilityInferenceRules(
|
|
29
|
+
intent_patterns={"redis.": "redis.caching"}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
# Override existing pattern
|
|
33
|
+
rules = CapabilityInferenceRules(
|
|
34
|
+
intent_patterns={"postgres.": "custom.database"}
|
|
35
|
+
)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
# Default intent pattern -> capability tag mappings
|
|
39
|
+
DEFAULT_INTENT_PATTERNS: dict[str, str] = {
|
|
40
|
+
"postgres.": "postgres.storage",
|
|
41
|
+
"consul.": "consul.registration",
|
|
42
|
+
"kafka.": "kafka.messaging",
|
|
43
|
+
"vault.": "vault.secrets",
|
|
44
|
+
"valkey.": "valkey.caching",
|
|
45
|
+
"http.": "http.transport",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Default protocol -> capability tag mappings
|
|
49
|
+
DEFAULT_PROTOCOL_TAGS: dict[str, str] = {
|
|
50
|
+
"ProtocolReducer": "state.reducer",
|
|
51
|
+
"ProtocolDatabaseAdapter": "database.adapter",
|
|
52
|
+
"ProtocolEventBus": "event.bus",
|
|
53
|
+
"ProtocolCacheAdapter": "cache.adapter",
|
|
54
|
+
"ProtocolServiceDiscovery": "service.discovery",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Default node type -> base capability tag
|
|
58
|
+
DEFAULT_NODE_TYPE_TAGS: dict[str, str] = {
|
|
59
|
+
"effect": "node.effect",
|
|
60
|
+
"compute": "node.compute",
|
|
61
|
+
"reducer": "node.reducer",
|
|
62
|
+
"orchestrator": "node.orchestrator",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
intent_patterns: dict[str, str] | None = None,
|
|
68
|
+
protocol_tags: dict[str, str] | None = None,
|
|
69
|
+
node_type_tags: dict[str, str] | None = None,
|
|
70
|
+
) -> None:
|
|
71
|
+
"""Initialize with optional custom rules.
|
|
72
|
+
|
|
73
|
+
Custom rules are merged with defaults. If a custom rule has the same
|
|
74
|
+
key as a default rule, the custom rule takes precedence (override).
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
intent_patterns: Custom/additional intent patterns (merged with defaults).
|
|
78
|
+
protocol_tags: Custom/additional protocol tags (merged with defaults).
|
|
79
|
+
node_type_tags: Custom/additional node type tags (merged with defaults).
|
|
80
|
+
"""
|
|
81
|
+
self._intent_patterns = {
|
|
82
|
+
**self.DEFAULT_INTENT_PATTERNS,
|
|
83
|
+
**(intent_patterns or {}),
|
|
84
|
+
}
|
|
85
|
+
self._protocol_tags = {
|
|
86
|
+
**self.DEFAULT_PROTOCOL_TAGS,
|
|
87
|
+
**(protocol_tags or {}),
|
|
88
|
+
}
|
|
89
|
+
self._node_type_tags = {
|
|
90
|
+
**self.DEFAULT_NODE_TYPE_TAGS,
|
|
91
|
+
**(node_type_tags or {}),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
def infer_from_intent_types(self, intent_types: list[str]) -> list[str]:
|
|
95
|
+
"""Infer capability tags from intent type patterns.
|
|
96
|
+
|
|
97
|
+
Pattern matching uses first-match-wins semantics: each intent is matched
|
|
98
|
+
against patterns in iteration order, and only the FIRST matching pattern
|
|
99
|
+
is used (early exit via break). This is intentional because intents should
|
|
100
|
+
belong to a single capability category.
|
|
101
|
+
|
|
102
|
+
Example:
|
|
103
|
+
- "postgres.upsert" matches "postgres." -> "postgres.storage"
|
|
104
|
+
- "postgres.kafka.hybrid" matches "postgres." only (NOT both postgres and kafka)
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
intent_types: List of intent type strings (e.g., ["postgres.upsert", "consul.register"])
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Sorted list of inferred capability tags (deduplicated)
|
|
111
|
+
"""
|
|
112
|
+
tags: set[str] = set()
|
|
113
|
+
for intent in intent_types:
|
|
114
|
+
if intent is None: # Skip None values
|
|
115
|
+
continue
|
|
116
|
+
for pattern, tag in self._intent_patterns.items():
|
|
117
|
+
if intent.startswith(pattern):
|
|
118
|
+
tags.add(tag)
|
|
119
|
+
break
|
|
120
|
+
return sorted(tags)
|
|
121
|
+
|
|
122
|
+
def infer_from_protocols(self, protocols: list[str]) -> list[str]:
|
|
123
|
+
"""Infer capability tags from protocol names.
|
|
124
|
+
|
|
125
|
+
Matching behavior:
|
|
126
|
+
- Exact match: "ProtocolReducer" matches DEFAULT_PROTOCOL_TAGS["ProtocolReducer"]
|
|
127
|
+
- Suffix match: "MyCustomProtocolReducer" also matches because it ends with "ProtocolReducer"
|
|
128
|
+
- No match: "ProtocolReducerExtended" does NOT match (doesn't end with known protocol)
|
|
129
|
+
|
|
130
|
+
This allows custom-prefixed protocol implementations to inherit base capability tags.
|
|
131
|
+
|
|
132
|
+
Warning:
|
|
133
|
+
Suffix matching can cause unexpected over-matching if your protocol name
|
|
134
|
+
accidentally ends with a known protocol name. The matching is strict:
|
|
135
|
+
the protocol name must END with the exact known protocol string.
|
|
136
|
+
|
|
137
|
+
Examples:
|
|
138
|
+
Protocols that MATCH (suffix ends with known protocol)::
|
|
139
|
+
|
|
140
|
+
"ProtocolReducer" -> matches "ProtocolReducer" (exact match)
|
|
141
|
+
"MyCustomProtocolReducer" -> matches "ProtocolReducer" (suffix match)
|
|
142
|
+
"InfraProtocolDatabaseAdapter" -> matches "ProtocolDatabaseAdapter"
|
|
143
|
+
"V2ProtocolEventBus" -> matches "ProtocolEventBus"
|
|
144
|
+
|
|
145
|
+
Protocols that DO NOT MATCH (suffix has additional characters)::
|
|
146
|
+
|
|
147
|
+
"ProtocolReducerV2" -> NO match (ends with "V2", not "ProtocolReducer")
|
|
148
|
+
"ProtocolReducerExtended" -> NO match (ends with "Extended")
|
|
149
|
+
"ProtocolReducerExtendedVersion" -> NO match (ends with "Version")
|
|
150
|
+
"MyReducer" -> NO match (must end with full "ProtocolReducer")
|
|
151
|
+
"ProtocolReducerImpl" -> NO match (ends with "Impl")
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
protocols: List of protocol class names
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
Sorted list of inferred capability tags (deduplicated)
|
|
158
|
+
"""
|
|
159
|
+
tags: set[str] = set()
|
|
160
|
+
for protocol in protocols:
|
|
161
|
+
if protocol is None: # Skip None values
|
|
162
|
+
continue
|
|
163
|
+
# Check exact match
|
|
164
|
+
if protocol in self._protocol_tags:
|
|
165
|
+
tags.add(self._protocol_tags[protocol])
|
|
166
|
+
# Also check if protocol name ends with known suffix
|
|
167
|
+
for known_protocol, tag in self._protocol_tags.items():
|
|
168
|
+
if protocol.endswith(known_protocol):
|
|
169
|
+
tags.add(tag)
|
|
170
|
+
return sorted(tags)
|
|
171
|
+
|
|
172
|
+
def infer_from_node_type(self, node_type: str) -> list[str]:
|
|
173
|
+
"""Infer base capability tag from node type.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
node_type: Node type string (effect, compute, reducer, orchestrator)
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
List with single node type capability tag, or empty if unknown
|
|
180
|
+
"""
|
|
181
|
+
normalized = node_type.lower().replace("_generic", "")
|
|
182
|
+
if normalized in self._node_type_tags:
|
|
183
|
+
return [self._node_type_tags[normalized]]
|
|
184
|
+
return []
|
|
185
|
+
|
|
186
|
+
def infer_all(
|
|
187
|
+
self,
|
|
188
|
+
intent_types: list[str] | None = None,
|
|
189
|
+
protocols: list[str] | None = None,
|
|
190
|
+
node_type: str | None = None,
|
|
191
|
+
) -> list[str]:
|
|
192
|
+
"""Infer all capability tags from available contract data.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
intent_types: Optional list of intent types
|
|
196
|
+
protocols: Optional list of protocol names
|
|
197
|
+
node_type: Optional node type string
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
Sorted, deduplicated list of all inferred capability tags
|
|
201
|
+
"""
|
|
202
|
+
tags: set[str] = set()
|
|
203
|
+
|
|
204
|
+
if intent_types:
|
|
205
|
+
tags.update(self.infer_from_intent_types(intent_types))
|
|
206
|
+
if protocols:
|
|
207
|
+
tags.update(self.infer_from_protocols(protocols))
|
|
208
|
+
if node_type:
|
|
209
|
+
tags.update(self.infer_from_node_type(node_type))
|
|
210
|
+
|
|
211
|
+
return sorted(tags)
|