omnibase_infra 0.2.1__py3-none-any.whl → 0.2.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (161) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/adapters/adapter_onex_tool_execution.py +451 -0
  3. omnibase_infra/capabilities/__init__.py +15 -0
  4. omnibase_infra/capabilities/capability_inference_rules.py +211 -0
  5. omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
  6. omnibase_infra/capabilities/intent_type_extractor.py +160 -0
  7. omnibase_infra/cli/commands.py +1 -1
  8. omnibase_infra/configs/widget_mapping.yaml +176 -0
  9. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +5 -2
  10. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +5 -2
  11. omnibase_infra/enums/__init__.py +6 -0
  12. omnibase_infra/enums/enum_handler_error_type.py +10 -0
  13. omnibase_infra/enums/enum_handler_source_mode.py +72 -0
  14. omnibase_infra/enums/enum_kafka_acks.py +99 -0
  15. omnibase_infra/errors/error_compute_registry.py +4 -1
  16. omnibase_infra/errors/error_event_bus_registry.py +4 -1
  17. omnibase_infra/errors/error_infra.py +3 -1
  18. omnibase_infra/errors/error_policy_registry.py +4 -1
  19. omnibase_infra/event_bus/event_bus_kafka.py +1 -1
  20. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +59 -10
  21. omnibase_infra/handlers/__init__.py +8 -1
  22. omnibase_infra/handlers/handler_consul.py +7 -1
  23. omnibase_infra/handlers/handler_db.py +10 -3
  24. omnibase_infra/handlers/handler_graph.py +10 -5
  25. omnibase_infra/handlers/handler_http.py +8 -2
  26. omnibase_infra/handlers/handler_intent.py +387 -0
  27. omnibase_infra/handlers/handler_mcp.py +745 -63
  28. omnibase_infra/handlers/handler_vault.py +11 -5
  29. omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
  30. omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
  31. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +7 -0
  32. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +308 -4
  33. omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
  34. omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
  35. omnibase_infra/mixins/mixin_node_introspection.py +42 -7
  36. omnibase_infra/mixins/mixin_retry_execution.py +1 -1
  37. omnibase_infra/models/discovery/model_introspection_config.py +11 -0
  38. omnibase_infra/models/handlers/__init__.py +48 -5
  39. omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
  40. omnibase_infra/models/handlers/model_contract_discovery_result.py +6 -4
  41. omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
  42. omnibase_infra/models/handlers/model_handler_source_config.py +220 -0
  43. omnibase_infra/models/mcp/__init__.py +15 -0
  44. omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
  45. omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
  46. omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
  47. omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
  48. omnibase_infra/models/registration/model_node_capabilities.py +11 -0
  49. omnibase_infra/models/registration/model_node_introspection_event.py +9 -0
  50. omnibase_infra/models/runtime/model_handler_contract.py +25 -9
  51. omnibase_infra/models/runtime/model_loaded_handler.py +9 -0
  52. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
  53. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
  54. omnibase_infra/nodes/effects/contract.yaml +0 -5
  55. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
  56. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
  57. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
  58. omnibase_infra/nodes/node_registration_orchestrator/plugin.py +1 -1
  59. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
  60. omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +4 -3
  61. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
  62. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
  63. omnibase_infra/nodes/node_registration_storage_effect/node.py +4 -1
  64. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +47 -26
  65. omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
  66. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
  67. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +28 -20
  68. omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
  69. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
  70. omnibase_infra/plugins/plugin_compute_base.py +16 -2
  71. omnibase_infra/protocols/__init__.py +2 -0
  72. omnibase_infra/protocols/protocol_container_aware.py +200 -0
  73. omnibase_infra/protocols/protocol_event_projector.py +1 -1
  74. omnibase_infra/runtime/__init__.py +90 -1
  75. omnibase_infra/runtime/binding_config_resolver.py +102 -37
  76. omnibase_infra/runtime/constants_notification.py +75 -0
  77. omnibase_infra/runtime/contract_handler_discovery.py +6 -1
  78. omnibase_infra/runtime/handler_bootstrap_source.py +507 -0
  79. omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
  80. omnibase_infra/runtime/handler_contract_source.py +267 -186
  81. omnibase_infra/runtime/handler_identity.py +81 -0
  82. omnibase_infra/runtime/handler_plugin_loader.py +19 -2
  83. omnibase_infra/runtime/handler_registry.py +11 -3
  84. omnibase_infra/runtime/handler_source_resolver.py +326 -0
  85. omnibase_infra/runtime/mixin_semver_cache.py +25 -1
  86. omnibase_infra/runtime/mixins/__init__.py +7 -0
  87. omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
  88. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
  89. omnibase_infra/runtime/models/__init__.py +24 -0
  90. omnibase_infra/runtime/models/model_health_check_result.py +2 -1
  91. omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
  92. omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
  93. omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
  94. omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
  95. omnibase_infra/runtime/projector_plugin_loader.py +1 -1
  96. omnibase_infra/runtime/projector_shell.py +229 -1
  97. omnibase_infra/runtime/protocol_lifecycle_executor.py +6 -6
  98. omnibase_infra/runtime/protocols/__init__.py +10 -0
  99. omnibase_infra/runtime/registry/registry_protocol_binding.py +16 -15
  100. omnibase_infra/runtime/registry_contract_source.py +693 -0
  101. omnibase_infra/runtime/registry_policy.py +9 -326
  102. omnibase_infra/runtime/secret_resolver.py +4 -2
  103. omnibase_infra/runtime/service_kernel.py +11 -3
  104. omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
  105. omnibase_infra/runtime/service_runtime_host_process.py +589 -106
  106. omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
  107. omnibase_infra/runtime/transition_notification_publisher.py +764 -0
  108. omnibase_infra/runtime/util_container_wiring.py +6 -5
  109. omnibase_infra/runtime/util_wiring.py +17 -4
  110. omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
  111. omnibase_infra/services/__init__.py +21 -0
  112. omnibase_infra/services/corpus_capture.py +7 -1
  113. omnibase_infra/services/mcp/__init__.py +31 -0
  114. omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -0
  115. omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
  116. omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
  117. omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
  118. omnibase_infra/services/registry_api/__init__.py +40 -0
  119. omnibase_infra/services/registry_api/main.py +261 -0
  120. omnibase_infra/services/registry_api/models/__init__.py +66 -0
  121. omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
  122. omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
  123. omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
  124. omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
  125. omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
  126. omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
  127. omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
  128. omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
  129. omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
  130. omnibase_infra/services/registry_api/models/model_warning.py +49 -0
  131. omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
  132. omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
  133. omnibase_infra/services/registry_api/routes.py +371 -0
  134. omnibase_infra/services/registry_api/service.py +837 -0
  135. omnibase_infra/services/service_capability_query.py +4 -4
  136. omnibase_infra/services/service_health.py +3 -2
  137. omnibase_infra/services/service_timeout_emitter.py +20 -3
  138. omnibase_infra/services/service_timeout_scanner.py +7 -3
  139. omnibase_infra/services/session/__init__.py +56 -0
  140. omnibase_infra/services/session/config_consumer.py +120 -0
  141. omnibase_infra/services/session/config_store.py +139 -0
  142. omnibase_infra/services/session/consumer.py +1007 -0
  143. omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
  144. omnibase_infra/services/session/store.py +997 -0
  145. omnibase_infra/utils/__init__.py +19 -0
  146. omnibase_infra/utils/util_atomic_file.py +261 -0
  147. omnibase_infra/utils/util_db_transaction.py +239 -0
  148. omnibase_infra/utils/util_dsn_validation.py +1 -1
  149. omnibase_infra/utils/util_retry_optimistic.py +281 -0
  150. omnibase_infra/validation/__init__.py +3 -19
  151. omnibase_infra/validation/contracts/security.validation.yaml +114 -0
  152. omnibase_infra/validation/infra_validators.py +35 -24
  153. omnibase_infra/validation/validation_exemptions.yaml +140 -9
  154. omnibase_infra/validation/validator_chain_propagation.py +2 -2
  155. omnibase_infra/validation/validator_runtime_shape.py +1 -1
  156. omnibase_infra/validation/validator_security.py +473 -370
  157. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/METADATA +3 -3
  158. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/RECORD +161 -98
  159. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/WHEEL +0 -0
  160. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/entry_points.txt +0 -0
  161. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.3.dist-info}/licenses/LICENSE +0 -0
@@ -76,7 +76,7 @@ See Also
76
76
  - Runtime kernel: omnibase_infra.runtime.service_kernel
77
77
  """
78
78
 
79
- __version__ = "0.2.1"
79
+ __version__ = "0.2.3"
80
80
 
81
81
  from . import (
82
82
  enums,
@@ -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)