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
@@ -27,6 +27,7 @@ from uuid import uuid4
27
27
 
28
28
  import hvac
29
29
 
30
+ from omnibase_core.container import ModelONEXContainer
30
31
  from omnibase_core.models.dispatch import ModelHandlerOutput
31
32
  from omnibase_infra.enums import (
32
33
  EnumHandlerType,
@@ -122,13 +123,18 @@ class HandlerVault(
122
123
  - MixinVaultToken: Token management and renewal
123
124
  """
124
125
 
125
- def __init__(self) -> None:
126
- """Initialize HandlerVault in uninitialized state.
126
+ def __init__(self, container: ModelONEXContainer) -> None:
127
+ """Initialize HandlerVault with ONEX container for dependency injection.
127
128
 
128
- Note: Circuit breaker is initialized during initialize() call when
129
- configuration is available. The mixin's _init_circuit_breaker() method
130
- is called there with the actual config values.
129
+ Args:
130
+ container: ONEX container for dependency injection.
131
+
132
+ Note:
133
+ Circuit breaker is initialized during initialize() call when
134
+ configuration is available. The mixin's _init_circuit_breaker() method
135
+ is called there with the actual config values.
131
136
  """
137
+ self._container = container
132
138
  self._client: hvac.Client | None = None
133
139
  self._config: ModelVaultHandlerConfig | None = None
134
140
  self._initialized: bool = False
@@ -94,7 +94,7 @@ class MixinConsulKV:
94
94
  correlation_id: UUID,
95
95
  ) -> T:
96
96
  """Execute operation with retry logic - provided by host class."""
97
- raise NotImplementedError("Must be provided by implementing class")
97
+ raise NotImplementedError("Must be provided by implementing class") # type: ignore[return-value]
98
98
 
99
99
  def _build_response(
100
100
  self,
@@ -103,6 +103,7 @@ class MixinConsulKV:
103
103
  input_envelope_id: UUID,
104
104
  ) -> ModelHandlerOutput[ModelConsulHandlerResponse]:
105
105
  """Build standardized response - provided by host class."""
106
+ raise NotImplementedError("Must be provided by implementing class") # type: ignore[return-value]
106
107
 
107
108
  async def _kv_get(
108
109
  self,
@@ -176,7 +177,7 @@ class MixinConsulKV:
176
177
  get_func,
177
178
  correlation_id,
178
179
  )
179
- index, data = cast(KVGetResult, result)
180
+ index, data = cast("KVGetResult", result)
180
181
 
181
182
  # Handle response - data can be None if key doesn't exist
182
183
  if data is None:
@@ -325,7 +326,7 @@ class MixinConsulKV:
325
326
  put_func,
326
327
  correlation_id,
327
328
  )
328
- success = cast(bool, result)
329
+ success = cast("bool", result)
329
330
 
330
331
  typed_payload = ModelConsulKVPutPayload(
331
332
  success=success,
@@ -91,7 +91,7 @@ class MixinConsulService:
91
91
  correlation_id: UUID,
92
92
  ) -> T:
93
93
  """Execute operation with retry logic - provided by host class."""
94
- raise NotImplementedError("Must be provided by implementing class")
94
+ raise NotImplementedError("Must be provided by implementing class") # type: ignore[return-value]
95
95
 
96
96
  def _build_response(
97
97
  self,
@@ -100,6 +100,7 @@ class MixinConsulService:
100
100
  input_envelope_id: UUID,
101
101
  ) -> ModelHandlerOutput[ModelConsulHandlerResponse]:
102
102
  """Build standardized response - provided by host class."""
103
+ raise NotImplementedError("Must be provided by implementing class") # type: ignore[return-value]
103
104
 
104
105
  async def _register_service(
105
106
  self,
@@ -48,6 +48,7 @@ from uuid import UUID, uuid4
48
48
  # Import asyncpg at module level to avoid redundant imports inside methods
49
49
  import asyncpg
50
50
 
51
+ from omnibase_core.container import ModelONEXContainer
51
52
  from omnibase_core.enums.enum_node_kind import EnumNodeKind
52
53
  from omnibase_infra.enums import EnumInfraTransportType
53
54
  from omnibase_infra.errors import (
@@ -152,7 +153,10 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
152
153
  handler_type: Returns "postgresql" identifier.
153
154
 
154
155
  Example:
156
+ >>> from omnibase_core.container import ModelONEXContainer
157
+ >>> container = ModelONEXContainer(...)
155
158
  >>> handler = HandlerRegistrationStoragePostgres(
159
+ ... container=container,
156
160
  ... postgres_adapter=postgres_adapter,
157
161
  ... circuit_breaker_config={"threshold": 5, "reset_timeout": 30.0},
158
162
  ... )
@@ -161,6 +165,7 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
161
165
 
162
166
  def __init__(
163
167
  self,
168
+ container: ModelONEXContainer,
164
169
  postgres_adapter: ProtocolPostgresAdapter | None = None,
165
170
  dsn: str | None = None,
166
171
  host: str = "localhost",
@@ -178,6 +183,7 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
178
183
  """Initialize HandlerRegistrationStoragePostgres.
179
184
 
180
185
  Args:
186
+ container: ONEX dependency injection container (required).
181
187
  postgres_adapter: Optional existing PostgreSQL adapter (ProtocolPostgresAdapter).
182
188
  If not provided, a new asyncpg connection pool will be created.
183
189
  dsn: Optional PostgreSQL connection DSN (overrides host/port/etc).
@@ -197,6 +203,7 @@ class HandlerRegistrationStoragePostgres(MixinAsyncCircuitBreaker):
197
203
  table on first connection. Default is False. Production deployments
198
204
  should use database migrations instead of auto-creation.
199
205
  """
206
+ self._container = container
200
207
  # Normalize circuit breaker config to ModelCircuitBreakerConfig
201
208
  if isinstance(circuit_breaker_config, ModelCircuitBreakerConfig):
202
209
  cb_config = circuit_breaker_config
@@ -30,6 +30,7 @@ from uuid import NAMESPACE_DNS, UUID, uuid4, uuid5
30
30
 
31
31
  import consul
32
32
 
33
+ from omnibase_core.container import ModelONEXContainer
33
34
  from omnibase_infra.enums import EnumInfraTransportType
34
35
  from omnibase_infra.errors import (
35
36
  InfraConnectionError,
@@ -91,7 +92,10 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
91
92
  handler_type: Returns "consul" identifier.
92
93
 
93
94
  Example:
95
+ >>> from unittest.mock import MagicMock
96
+ >>> container = MagicMock(spec=ModelONEXContainer)
94
97
  >>> handler = HandlerServiceDiscoveryConsul(
98
+ ... container=container,
95
99
  ... consul_client=consul_client,
96
100
  ... circuit_breaker_config=ModelCircuitBreakerConfig(threshold=5),
97
101
  ... )
@@ -100,6 +104,7 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
100
104
 
101
105
  def __init__(
102
106
  self,
107
+ container: ModelONEXContainer,
103
108
  consul_client: ProtocolConsulClient | None = None,
104
109
  consul_host: str = "localhost",
105
110
  consul_port: int = 8500,
@@ -114,6 +119,7 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
114
119
  """Initialize HandlerServiceDiscoveryConsul.
115
120
 
116
121
  Args:
122
+ container: ONEX container for dependency injection and service resolution.
117
123
  consul_client: Optional existing Consul client (ProtocolConsulClient).
118
124
  If not provided, a new python-consul client will be created.
119
125
  consul_host: Consul server hostname (default: "localhost").
@@ -129,6 +135,7 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
129
135
  max_workers: Thread pool max workers (default: 10).
130
136
  timeout_seconds: Operation timeout in seconds (default: 30.0).
131
137
  """
138
+ self._container = container
132
139
  # Parse circuit breaker configuration using ModelCircuitBreakerConfig
133
140
  if isinstance(circuit_breaker_config, ModelCircuitBreakerConfig):
134
141
  cb_config = circuit_breaker_config
@@ -567,15 +574,15 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
567
574
  service_infos: list[ModelServiceInfo] = []
568
575
  for svc in services:
569
576
  # Cast nested dicts for type safety
570
- svc_data = cast(dict[str, object], svc.get("Service", {}))
571
- node_data = cast(dict[str, object], svc.get("Node", {}))
577
+ svc_data = cast("dict[str, object]", svc.get("Service", {}))
578
+ node_data = cast("dict[str, object]", svc.get("Node", {}))
572
579
  svc_id_str = str(svc_data.get("ID", ""))
573
580
  svc_name = svc_data.get("Service", "")
574
581
  address = svc_data.get("Address", "") or node_data.get("Address", "")
575
582
  port_raw = svc_data.get("Port", 0)
576
583
  port = int(port_raw) if isinstance(port_raw, int | float | str) else 0
577
- svc_tags = cast(list[str], svc_data.get("Tags", []))
578
- svc_meta = cast(dict[str, str], svc_data.get("Meta", {}))
584
+ svc_tags = cast("list[str]", svc_data.get("Tags", []))
585
+ svc_meta = cast("dict[str, str]", svc_data.get("Meta", {}))
579
586
 
580
587
  if svc_id_str and svc_name and address and port:
581
588
  # Convert Consul service ID string to UUID
@@ -664,6 +671,303 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
664
671
  context=context,
665
672
  ) from e
666
673
 
674
+ async def list_all_services(
675
+ self,
676
+ tag_filter: str | None = None,
677
+ correlation_id: UUID | None = None,
678
+ ) -> dict[str, list[str]]:
679
+ """List all registered service names with their tags.
680
+
681
+ Uses Consul catalog API (/v1/catalog/services) to retrieve all
682
+ service names registered in the Consul catalog, along with their tags.
683
+
684
+ Args:
685
+ tag_filter: Optional tag to filter services by. If provided,
686
+ only services with this tag will be returned.
687
+ correlation_id: Optional correlation ID for tracing.
688
+
689
+ Returns:
690
+ Dictionary mapping service names to their list of tags.
691
+ Example: {"my-service": ["web", "api"], "other-service": ["db"]}
692
+
693
+ Raises:
694
+ InfraConnectionError: If connection to Consul fails.
695
+ InfraTimeoutError: If operation times out.
696
+ InfraUnavailableError: If circuit breaker is open.
697
+ """
698
+ correlation_id = correlation_id or uuid4()
699
+ start_time = time.monotonic()
700
+
701
+ # Guard against use-after-shutdown
702
+ self._check_not_shutdown("list_all_services", correlation_id)
703
+
704
+ # Check circuit breaker
705
+ async with self._circuit_breaker_lock:
706
+ await self._check_circuit_breaker(
707
+ operation="list_all_services",
708
+ correlation_id=correlation_id,
709
+ )
710
+
711
+ try:
712
+ # Query Consul catalog for all services
713
+ client = self._consul_client
714
+ executor = await self._ensure_executor()
715
+ loop = asyncio.get_running_loop()
716
+
717
+ def _query_catalog() -> tuple[int, dict[str, list[str]]]:
718
+ # NOTE: client is duck-typed ProtocolConsulClient; mypy cannot verify
719
+ # catalog.services exists on Optional[Consul] union type.
720
+ result: tuple[int, dict[str, list[str]]] = client.catalog.services() # type: ignore[union-attr] # NOTE: duck-typed client
721
+ return result
722
+
723
+ _, services = await asyncio.wait_for(
724
+ loop.run_in_executor(executor, _query_catalog),
725
+ timeout=self._timeout_seconds,
726
+ )
727
+
728
+ # Reset circuit breaker on success
729
+ async with self._circuit_breaker_lock:
730
+ await self._reset_circuit_breaker()
731
+
732
+ # Apply tag filter if provided
733
+ if tag_filter is not None:
734
+ services = {
735
+ name: tags for name, tags in services.items() if tag_filter in tags
736
+ }
737
+
738
+ duration_ms = (time.monotonic() - start_time) * 1000
739
+
740
+ logger.info(
741
+ "Listed all services from Consul catalog",
742
+ extra={
743
+ "service_count": len(services),
744
+ "tag_filter": tag_filter,
745
+ "duration_ms": duration_ms,
746
+ "correlation_id": str(correlation_id),
747
+ },
748
+ )
749
+
750
+ return services
751
+
752
+ except TimeoutError as e:
753
+ async with self._circuit_breaker_lock:
754
+ await self._record_circuit_failure(
755
+ operation="list_all_services",
756
+ correlation_id=correlation_id,
757
+ )
758
+ raise InfraTimeoutError(
759
+ f"Consul catalog query timed out after {self._timeout_seconds}s",
760
+ context=ModelTimeoutErrorContext(
761
+ transport_type=EnumInfraTransportType.CONSUL,
762
+ operation="list_all_services",
763
+ target_name="consul.discovery",
764
+ correlation_id=correlation_id,
765
+ timeout_seconds=self._timeout_seconds,
766
+ ),
767
+ ) from e
768
+
769
+ except consul.ConsulException as e:
770
+ async with self._circuit_breaker_lock:
771
+ await self._record_circuit_failure(
772
+ operation="list_all_services",
773
+ correlation_id=correlation_id,
774
+ )
775
+ context = ModelInfraErrorContext(
776
+ transport_type=EnumInfraTransportType.CONSUL,
777
+ operation="list_all_services",
778
+ target_name="consul.discovery",
779
+ correlation_id=correlation_id,
780
+ )
781
+ raise InfraConnectionError(
782
+ "Consul catalog query failed",
783
+ context=context,
784
+ ) from e
785
+
786
+ async def get_all_service_instances(
787
+ self,
788
+ service_name: str,
789
+ include_unhealthy: bool = True,
790
+ correlation_id: UUID | None = None,
791
+ ) -> list[ModelServiceInfo]:
792
+ """Get all instances of a service, optionally including unhealthy ones.
793
+
794
+ Uses Consul health API (/v1/health/service/<name>) to retrieve all
795
+ instances of a service with their health status information.
796
+
797
+ Args:
798
+ service_name: Name of the service to query.
799
+ include_unhealthy: If True, return all instances regardless of health.
800
+ If False, only return healthy (passing) instances.
801
+ correlation_id: Optional correlation ID for tracing.
802
+
803
+ Returns:
804
+ List of ModelServiceInfo objects representing service instances,
805
+ including health status, health output, and last check timestamp.
806
+
807
+ Raises:
808
+ InfraConnectionError: If connection to Consul fails.
809
+ InfraTimeoutError: If operation times out.
810
+ InfraUnavailableError: If circuit breaker is open.
811
+ """
812
+ correlation_id = correlation_id or uuid4()
813
+ start_time = time.monotonic()
814
+
815
+ # Guard against use-after-shutdown
816
+ self._check_not_shutdown("get_all_service_instances", correlation_id)
817
+
818
+ # Check circuit breaker
819
+ async with self._circuit_breaker_lock:
820
+ await self._check_circuit_breaker(
821
+ operation="get_all_service_instances",
822
+ correlation_id=correlation_id,
823
+ )
824
+
825
+ try:
826
+ # Query Consul health API for service instances
827
+ client = self._consul_client
828
+ executor = await self._ensure_executor()
829
+ loop = asyncio.get_running_loop()
830
+
831
+ def _query_health() -> tuple[int, list[dict[str, object]]]:
832
+ # NOTE: client is duck-typed ProtocolConsulClient; mypy cannot verify
833
+ # health.service exists on Optional[Consul] union type.
834
+ # When passing=False, we get ALL instances
835
+ # When passing=True, we only get healthy instances
836
+ result: tuple[int, list[dict[str, object]]] = client.health.service( # type: ignore[union-attr] # NOTE: duck-typed client
837
+ service_name,
838
+ passing=not include_unhealthy,
839
+ )
840
+ return result
841
+
842
+ _, services = await asyncio.wait_for(
843
+ loop.run_in_executor(executor, _query_health),
844
+ timeout=self._timeout_seconds,
845
+ )
846
+
847
+ # Reset circuit breaker on success
848
+ async with self._circuit_breaker_lock:
849
+ await self._reset_circuit_breaker()
850
+
851
+ # Convert to ModelServiceInfo list with full health details
852
+ service_infos: list[ModelServiceInfo] = []
853
+ for svc in services:
854
+ # Cast nested dicts for type safety
855
+ svc_data = cast("dict[str, object]", svc.get("Service", {}))
856
+ node_data = cast("dict[str, object]", svc.get("Node", {}))
857
+ checks = cast("list[dict[str, object]]", svc.get("Checks", []))
858
+
859
+ svc_id_str = str(svc_data.get("ID", ""))
860
+ svc_name = svc_data.get("Service", "")
861
+ address = svc_data.get("Address", "") or node_data.get("Address", "")
862
+ port_raw = svc_data.get("Port", 0)
863
+ port = int(port_raw) if isinstance(port_raw, int | float | str) else 0
864
+ svc_tags = cast("list[str]", svc_data.get("Tags", []))
865
+ svc_meta = cast("dict[str, str]", svc_data.get("Meta", {}))
866
+
867
+ if svc_id_str and svc_name and address and port:
868
+ # Convert Consul service ID string to UUID
869
+ try:
870
+ svc_id = UUID(svc_id_str)
871
+ except ValueError:
872
+ # Non-UUID service ID from Consul - generate deterministic UUID5
873
+ logger.warning(
874
+ "Non-UUID service ID from Consul, using deterministic UUID5 conversion",
875
+ extra={
876
+ "original_id": svc_id_str,
877
+ "correlation_id": str(correlation_id),
878
+ },
879
+ )
880
+ svc_id = uuid5(NAMESPACE_ONEX_SERVICE, svc_id_str)
881
+
882
+ # Determine health status from checks
883
+ health_status = EnumHealthStatus.UNKNOWN
884
+ health_output: str | None = None
885
+ last_check_at: datetime | None = None
886
+
887
+ # Find the service-specific health check
888
+ for check in checks:
889
+ check_service_id = check.get("ServiceID", "")
890
+ if check_service_id == svc_id_str or not check_service_id:
891
+ status_str = str(check.get("Status", "")).lower()
892
+ if status_str == "passing":
893
+ health_status = EnumHealthStatus.HEALTHY
894
+ elif status_str in ("critical", "warning"):
895
+ health_status = EnumHealthStatus.UNHEALTHY
896
+ else:
897
+ health_status = EnumHealthStatus.UNKNOWN
898
+
899
+ health_output = str(check.get("Output", "")) or None
900
+
901
+ # Parse CreateIndex as a proxy for last check time
902
+ # (Consul doesn't directly expose last check time in this API)
903
+ # In production, you might use the check's CreateIndex or ModifyIndex
904
+ break
905
+
906
+ service_infos.append(
907
+ ModelServiceInfo(
908
+ service_id=svc_id,
909
+ service_name=str(svc_name),
910
+ address=str(address),
911
+ port=port,
912
+ tags=tuple(svc_tags or []),
913
+ health_status=health_status,
914
+ health_output=health_output,
915
+ last_check_at=last_check_at,
916
+ metadata=svc_meta or {},
917
+ registered_at=datetime.now(UTC),
918
+ correlation_id=correlation_id,
919
+ )
920
+ )
921
+
922
+ duration_ms = (time.monotonic() - start_time) * 1000
923
+
924
+ logger.info(
925
+ "Retrieved service instances from Consul",
926
+ extra={
927
+ "service_name": service_name,
928
+ "instance_count": len(service_infos),
929
+ "include_unhealthy": include_unhealthy,
930
+ "duration_ms": duration_ms,
931
+ "correlation_id": str(correlation_id),
932
+ },
933
+ )
934
+
935
+ return service_infos
936
+
937
+ except TimeoutError as e:
938
+ async with self._circuit_breaker_lock:
939
+ await self._record_circuit_failure(
940
+ operation="get_all_service_instances",
941
+ correlation_id=correlation_id,
942
+ )
943
+ raise InfraTimeoutError(
944
+ f"Consul health query timed out after {self._timeout_seconds}s",
945
+ context=ModelTimeoutErrorContext(
946
+ transport_type=EnumInfraTransportType.CONSUL,
947
+ operation="get_all_service_instances",
948
+ target_name="consul.discovery",
949
+ correlation_id=correlation_id,
950
+ timeout_seconds=self._timeout_seconds,
951
+ ),
952
+ ) from e
953
+
954
+ except consul.ConsulException as e:
955
+ async with self._circuit_breaker_lock:
956
+ await self._record_circuit_failure(
957
+ operation="get_all_service_instances",
958
+ correlation_id=correlation_id,
959
+ )
960
+ context = ModelInfraErrorContext(
961
+ transport_type=EnumInfraTransportType.CONSUL,
962
+ operation="get_all_service_instances",
963
+ target_name="consul.discovery",
964
+ correlation_id=correlation_id,
965
+ )
966
+ raise InfraConnectionError(
967
+ "Consul health query failed",
968
+ context=context,
969
+ ) from e
970
+
667
971
  async def health_check(
668
972
  self,
669
973
  correlation_id: UUID | None = None,
@@ -45,6 +45,8 @@ class ModelServiceInfo(BaseModel):
45
45
  health_status: Current health status of the service.
46
46
  metadata: Additional key-value metadata for the service.
47
47
  health_check_url: Optional URL for health checks (handler extension).
48
+ health_output: Output message from the last health check (handler extension).
49
+ last_check_at: Timestamp of the last health check (handler extension).
48
50
  registered_at: Timestamp when the service was registered (handler extension).
49
51
  correlation_id: Correlation ID for tracing (handler extension).
50
52
  """
@@ -86,6 +88,14 @@ class ModelServiceInfo(BaseModel):
86
88
  default=None,
87
89
  description="Optional URL for health checks",
88
90
  )
91
+ health_output: str | None = Field(
92
+ default=None,
93
+ description="Output message from the last health check",
94
+ )
95
+ last_check_at: datetime | None = Field(
96
+ default=None,
97
+ description="Timestamp of the last health check",
98
+ )
89
99
  registered_at: datetime | None = Field(
90
100
  default=None,
91
101
  description="Timestamp when the service was registered",
@@ -101,6 +101,7 @@ import logging
101
101
  import time
102
102
  from uuid import UUID, uuid4
103
103
 
104
+ from omnibase_core.types import JsonType
104
105
  from omnibase_infra.enums import EnumCircuitState, EnumInfraTransportType
105
106
  from omnibase_infra.errors import (
106
107
  InfraUnavailableError,
@@ -580,7 +581,7 @@ class MixinAsyncCircuitBreaker:
580
581
  self._circuit_breaker_failures = 0
581
582
  self._circuit_breaker_open_until = 0.0
582
583
 
583
- def _get_circuit_breaker_state(self) -> dict[str, object]:
584
+ def _get_circuit_breaker_state(self) -> dict[str, JsonType]:
584
585
  """Return current circuit breaker state for introspection.
585
586
 
586
587
  This method encapsulates circuit breaker internals for safe access
@@ -638,7 +639,7 @@ class MixinAsyncCircuitBreaker:
638
639
  cb_state = "closed"
639
640
  seconds_until_half_open = None
640
641
 
641
- result: dict[str, object] = {
642
+ result: dict[str, JsonType] = {
642
643
  "initialized": cb_initialized,
643
644
  "state": cb_state,
644
645
  "failures": cb_failures,