omnibase_infra 0.2.1__py3-none-any.whl → 0.2.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnibase_infra/__init__.py +1 -1
- omnibase_infra/adapters/adapter_onex_tool_execution.py +446 -0
- omnibase_infra/cli/commands.py +1 -1
- omnibase_infra/configs/widget_mapping.yaml +176 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +4 -1
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +4 -1
- omnibase_infra/errors/error_compute_registry.py +4 -1
- omnibase_infra/errors/error_event_bus_registry.py +4 -1
- omnibase_infra/errors/error_infra.py +3 -1
- omnibase_infra/errors/error_policy_registry.py +4 -1
- omnibase_infra/handlers/handler_db.py +2 -1
- omnibase_infra/handlers/handler_graph.py +10 -5
- omnibase_infra/handlers/handler_mcp.py +736 -63
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
- omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
- omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +301 -4
- omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
- omnibase_infra/mixins/mixin_node_introspection.py +24 -7
- omnibase_infra/mixins/mixin_retry_execution.py +1 -1
- omnibase_infra/models/handlers/__init__.py +10 -0
- omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
- omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
- omnibase_infra/models/mcp/__init__.py +15 -0
- omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
- omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
- omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
- omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
- omnibase_infra/models/registration/model_node_capabilities.py +11 -0
- omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
- omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
- omnibase_infra/nodes/effects/contract.yaml +0 -5
- omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
- omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
- omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
- omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +46 -25
- omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
- omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
- omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +24 -19
- omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
- omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
- omnibase_infra/plugins/plugin_compute_base.py +16 -2
- omnibase_infra/protocols/protocol_event_projector.py +1 -1
- omnibase_infra/runtime/__init__.py +51 -1
- omnibase_infra/runtime/binding_config_resolver.py +102 -37
- omnibase_infra/runtime/constants_notification.py +75 -0
- omnibase_infra/runtime/contract_handler_discovery.py +6 -1
- omnibase_infra/runtime/handler_bootstrap_source.py +514 -0
- omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
- omnibase_infra/runtime/handler_contract_source.py +289 -167
- omnibase_infra/runtime/handler_plugin_loader.py +4 -2
- omnibase_infra/runtime/mixin_semver_cache.py +25 -1
- omnibase_infra/runtime/mixins/__init__.py +7 -0
- omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
- omnibase_infra/runtime/models/__init__.py +24 -0
- omnibase_infra/runtime/models/model_health_check_result.py +2 -1
- omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
- omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
- omnibase_infra/runtime/projector_plugin_loader.py +1 -1
- omnibase_infra/runtime/projector_shell.py +229 -1
- omnibase_infra/runtime/protocols/__init__.py +10 -0
- omnibase_infra/runtime/registry/registry_protocol_binding.py +3 -2
- omnibase_infra/runtime/registry_policy.py +9 -326
- omnibase_infra/runtime/secret_resolver.py +4 -2
- omnibase_infra/runtime/service_kernel.py +10 -2
- omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
- omnibase_infra/runtime/service_runtime_host_process.py +225 -15
- omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
- omnibase_infra/runtime/transition_notification_publisher.py +764 -0
- omnibase_infra/runtime/util_container_wiring.py +6 -5
- omnibase_infra/runtime/util_wiring.py +5 -1
- omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
- omnibase_infra/services/mcp/__init__.py +31 -0
- omnibase_infra/services/mcp/mcp_server_lifecycle.py +443 -0
- omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
- omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
- omnibase_infra/services/registry_api/__init__.py +40 -0
- omnibase_infra/services/registry_api/main.py +243 -0
- omnibase_infra/services/registry_api/models/__init__.py +66 -0
- omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
- omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
- omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
- omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
- omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
- omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
- omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
- omnibase_infra/services/registry_api/models/model_warning.py +49 -0
- omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
- omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
- omnibase_infra/services/registry_api/routes.py +371 -0
- omnibase_infra/services/registry_api/service.py +846 -0
- omnibase_infra/services/service_capability_query.py +4 -4
- omnibase_infra/services/service_health.py +3 -2
- omnibase_infra/services/service_timeout_emitter.py +13 -2
- omnibase_infra/utils/util_dsn_validation.py +1 -1
- omnibase_infra/validation/__init__.py +3 -19
- omnibase_infra/validation/contracts/security.validation.yaml +114 -0
- omnibase_infra/validation/infra_validators.py +35 -24
- omnibase_infra/validation/validation_exemptions.yaml +113 -9
- omnibase_infra/validation/validator_chain_propagation.py +2 -2
- omnibase_infra/validation/validator_runtime_shape.py +1 -1
- omnibase_infra/validation/validator_security.py +473 -370
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/METADATA +2 -2
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/RECORD +116 -74
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/WHEEL +0 -0
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/entry_points.txt +0 -0
- {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -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,
|
|
@@ -567,15 +567,15 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
|
|
|
567
567
|
service_infos: list[ModelServiceInfo] = []
|
|
568
568
|
for svc in services:
|
|
569
569
|
# 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", {}))
|
|
570
|
+
svc_data = cast("dict[str, object]", svc.get("Service", {}))
|
|
571
|
+
node_data = cast("dict[str, object]", svc.get("Node", {}))
|
|
572
572
|
svc_id_str = str(svc_data.get("ID", ""))
|
|
573
573
|
svc_name = svc_data.get("Service", "")
|
|
574
574
|
address = svc_data.get("Address", "") or node_data.get("Address", "")
|
|
575
575
|
port_raw = svc_data.get("Port", 0)
|
|
576
576
|
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", {}))
|
|
577
|
+
svc_tags = cast("list[str]", svc_data.get("Tags", []))
|
|
578
|
+
svc_meta = cast("dict[str, str]", svc_data.get("Meta", {}))
|
|
579
579
|
|
|
580
580
|
if svc_id_str and svc_name and address and port:
|
|
581
581
|
# Convert Consul service ID string to UUID
|
|
@@ -664,6 +664,303 @@ class HandlerServiceDiscoveryConsul(MixinAsyncCircuitBreaker):
|
|
|
664
664
|
context=context,
|
|
665
665
|
) from e
|
|
666
666
|
|
|
667
|
+
async def list_all_services(
|
|
668
|
+
self,
|
|
669
|
+
tag_filter: str | None = None,
|
|
670
|
+
correlation_id: UUID | None = None,
|
|
671
|
+
) -> dict[str, list[str]]:
|
|
672
|
+
"""List all registered service names with their tags.
|
|
673
|
+
|
|
674
|
+
Uses Consul catalog API (/v1/catalog/services) to retrieve all
|
|
675
|
+
service names registered in the Consul catalog, along with their tags.
|
|
676
|
+
|
|
677
|
+
Args:
|
|
678
|
+
tag_filter: Optional tag to filter services by. If provided,
|
|
679
|
+
only services with this tag will be returned.
|
|
680
|
+
correlation_id: Optional correlation ID for tracing.
|
|
681
|
+
|
|
682
|
+
Returns:
|
|
683
|
+
Dictionary mapping service names to their list of tags.
|
|
684
|
+
Example: {"my-service": ["web", "api"], "other-service": ["db"]}
|
|
685
|
+
|
|
686
|
+
Raises:
|
|
687
|
+
InfraConnectionError: If connection to Consul fails.
|
|
688
|
+
InfraTimeoutError: If operation times out.
|
|
689
|
+
InfraUnavailableError: If circuit breaker is open.
|
|
690
|
+
"""
|
|
691
|
+
correlation_id = correlation_id or uuid4()
|
|
692
|
+
start_time = time.monotonic()
|
|
693
|
+
|
|
694
|
+
# Guard against use-after-shutdown
|
|
695
|
+
self._check_not_shutdown("list_all_services", correlation_id)
|
|
696
|
+
|
|
697
|
+
# Check circuit breaker
|
|
698
|
+
async with self._circuit_breaker_lock:
|
|
699
|
+
await self._check_circuit_breaker(
|
|
700
|
+
operation="list_all_services",
|
|
701
|
+
correlation_id=correlation_id,
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
try:
|
|
705
|
+
# Query Consul catalog for all services
|
|
706
|
+
client = self._consul_client
|
|
707
|
+
executor = await self._ensure_executor()
|
|
708
|
+
loop = asyncio.get_running_loop()
|
|
709
|
+
|
|
710
|
+
def _query_catalog() -> tuple[int, dict[str, list[str]]]:
|
|
711
|
+
# NOTE: client is duck-typed ProtocolConsulClient; mypy cannot verify
|
|
712
|
+
# catalog.services exists on Optional[Consul] union type.
|
|
713
|
+
result: tuple[int, dict[str, list[str]]] = client.catalog.services() # type: ignore[union-attr] # NOTE: duck-typed client
|
|
714
|
+
return result
|
|
715
|
+
|
|
716
|
+
_, services = await asyncio.wait_for(
|
|
717
|
+
loop.run_in_executor(executor, _query_catalog),
|
|
718
|
+
timeout=self._timeout_seconds,
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
# Reset circuit breaker on success
|
|
722
|
+
async with self._circuit_breaker_lock:
|
|
723
|
+
await self._reset_circuit_breaker()
|
|
724
|
+
|
|
725
|
+
# Apply tag filter if provided
|
|
726
|
+
if tag_filter is not None:
|
|
727
|
+
services = {
|
|
728
|
+
name: tags for name, tags in services.items() if tag_filter in tags
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
duration_ms = (time.monotonic() - start_time) * 1000
|
|
732
|
+
|
|
733
|
+
logger.info(
|
|
734
|
+
"Listed all services from Consul catalog",
|
|
735
|
+
extra={
|
|
736
|
+
"service_count": len(services),
|
|
737
|
+
"tag_filter": tag_filter,
|
|
738
|
+
"duration_ms": duration_ms,
|
|
739
|
+
"correlation_id": str(correlation_id),
|
|
740
|
+
},
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
return services
|
|
744
|
+
|
|
745
|
+
except TimeoutError as e:
|
|
746
|
+
async with self._circuit_breaker_lock:
|
|
747
|
+
await self._record_circuit_failure(
|
|
748
|
+
operation="list_all_services",
|
|
749
|
+
correlation_id=correlation_id,
|
|
750
|
+
)
|
|
751
|
+
raise InfraTimeoutError(
|
|
752
|
+
f"Consul catalog query timed out after {self._timeout_seconds}s",
|
|
753
|
+
context=ModelTimeoutErrorContext(
|
|
754
|
+
transport_type=EnumInfraTransportType.CONSUL,
|
|
755
|
+
operation="list_all_services",
|
|
756
|
+
target_name="consul.discovery",
|
|
757
|
+
correlation_id=correlation_id,
|
|
758
|
+
timeout_seconds=self._timeout_seconds,
|
|
759
|
+
),
|
|
760
|
+
) from e
|
|
761
|
+
|
|
762
|
+
except consul.ConsulException as e:
|
|
763
|
+
async with self._circuit_breaker_lock:
|
|
764
|
+
await self._record_circuit_failure(
|
|
765
|
+
operation="list_all_services",
|
|
766
|
+
correlation_id=correlation_id,
|
|
767
|
+
)
|
|
768
|
+
context = ModelInfraErrorContext(
|
|
769
|
+
transport_type=EnumInfraTransportType.CONSUL,
|
|
770
|
+
operation="list_all_services",
|
|
771
|
+
target_name="consul.discovery",
|
|
772
|
+
correlation_id=correlation_id,
|
|
773
|
+
)
|
|
774
|
+
raise InfraConnectionError(
|
|
775
|
+
"Consul catalog query failed",
|
|
776
|
+
context=context,
|
|
777
|
+
) from e
|
|
778
|
+
|
|
779
|
+
async def get_all_service_instances(
|
|
780
|
+
self,
|
|
781
|
+
service_name: str,
|
|
782
|
+
include_unhealthy: bool = True,
|
|
783
|
+
correlation_id: UUID | None = None,
|
|
784
|
+
) -> list[ModelServiceInfo]:
|
|
785
|
+
"""Get all instances of a service, optionally including unhealthy ones.
|
|
786
|
+
|
|
787
|
+
Uses Consul health API (/v1/health/service/<name>) to retrieve all
|
|
788
|
+
instances of a service with their health status information.
|
|
789
|
+
|
|
790
|
+
Args:
|
|
791
|
+
service_name: Name of the service to query.
|
|
792
|
+
include_unhealthy: If True, return all instances regardless of health.
|
|
793
|
+
If False, only return healthy (passing) instances.
|
|
794
|
+
correlation_id: Optional correlation ID for tracing.
|
|
795
|
+
|
|
796
|
+
Returns:
|
|
797
|
+
List of ModelServiceInfo objects representing service instances,
|
|
798
|
+
including health status, health output, and last check timestamp.
|
|
799
|
+
|
|
800
|
+
Raises:
|
|
801
|
+
InfraConnectionError: If connection to Consul fails.
|
|
802
|
+
InfraTimeoutError: If operation times out.
|
|
803
|
+
InfraUnavailableError: If circuit breaker is open.
|
|
804
|
+
"""
|
|
805
|
+
correlation_id = correlation_id or uuid4()
|
|
806
|
+
start_time = time.monotonic()
|
|
807
|
+
|
|
808
|
+
# Guard against use-after-shutdown
|
|
809
|
+
self._check_not_shutdown("get_all_service_instances", correlation_id)
|
|
810
|
+
|
|
811
|
+
# Check circuit breaker
|
|
812
|
+
async with self._circuit_breaker_lock:
|
|
813
|
+
await self._check_circuit_breaker(
|
|
814
|
+
operation="get_all_service_instances",
|
|
815
|
+
correlation_id=correlation_id,
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
try:
|
|
819
|
+
# Query Consul health API for service instances
|
|
820
|
+
client = self._consul_client
|
|
821
|
+
executor = await self._ensure_executor()
|
|
822
|
+
loop = asyncio.get_running_loop()
|
|
823
|
+
|
|
824
|
+
def _query_health() -> tuple[int, list[dict[str, object]]]:
|
|
825
|
+
# NOTE: client is duck-typed ProtocolConsulClient; mypy cannot verify
|
|
826
|
+
# health.service exists on Optional[Consul] union type.
|
|
827
|
+
# When passing=False, we get ALL instances
|
|
828
|
+
# When passing=True, we only get healthy instances
|
|
829
|
+
result: tuple[int, list[dict[str, object]]] = client.health.service( # type: ignore[union-attr] # NOTE: duck-typed client
|
|
830
|
+
service_name,
|
|
831
|
+
passing=not include_unhealthy,
|
|
832
|
+
)
|
|
833
|
+
return result
|
|
834
|
+
|
|
835
|
+
_, services = await asyncio.wait_for(
|
|
836
|
+
loop.run_in_executor(executor, _query_health),
|
|
837
|
+
timeout=self._timeout_seconds,
|
|
838
|
+
)
|
|
839
|
+
|
|
840
|
+
# Reset circuit breaker on success
|
|
841
|
+
async with self._circuit_breaker_lock:
|
|
842
|
+
await self._reset_circuit_breaker()
|
|
843
|
+
|
|
844
|
+
# Convert to ModelServiceInfo list with full health details
|
|
845
|
+
service_infos: list[ModelServiceInfo] = []
|
|
846
|
+
for svc in services:
|
|
847
|
+
# Cast nested dicts for type safety
|
|
848
|
+
svc_data = cast("dict[str, object]", svc.get("Service", {}))
|
|
849
|
+
node_data = cast("dict[str, object]", svc.get("Node", {}))
|
|
850
|
+
checks = cast("list[dict[str, object]]", svc.get("Checks", []))
|
|
851
|
+
|
|
852
|
+
svc_id_str = str(svc_data.get("ID", ""))
|
|
853
|
+
svc_name = svc_data.get("Service", "")
|
|
854
|
+
address = svc_data.get("Address", "") or node_data.get("Address", "")
|
|
855
|
+
port_raw = svc_data.get("Port", 0)
|
|
856
|
+
port = int(port_raw) if isinstance(port_raw, int | float | str) else 0
|
|
857
|
+
svc_tags = cast("list[str]", svc_data.get("Tags", []))
|
|
858
|
+
svc_meta = cast("dict[str, str]", svc_data.get("Meta", {}))
|
|
859
|
+
|
|
860
|
+
if svc_id_str and svc_name and address and port:
|
|
861
|
+
# Convert Consul service ID string to UUID
|
|
862
|
+
try:
|
|
863
|
+
svc_id = UUID(svc_id_str)
|
|
864
|
+
except ValueError:
|
|
865
|
+
# Non-UUID service ID from Consul - generate deterministic UUID5
|
|
866
|
+
logger.warning(
|
|
867
|
+
"Non-UUID service ID from Consul, using deterministic UUID5 conversion",
|
|
868
|
+
extra={
|
|
869
|
+
"original_id": svc_id_str,
|
|
870
|
+
"correlation_id": str(correlation_id),
|
|
871
|
+
},
|
|
872
|
+
)
|
|
873
|
+
svc_id = uuid5(NAMESPACE_ONEX_SERVICE, svc_id_str)
|
|
874
|
+
|
|
875
|
+
# Determine health status from checks
|
|
876
|
+
health_status = EnumHealthStatus.UNKNOWN
|
|
877
|
+
health_output: str | None = None
|
|
878
|
+
last_check_at: datetime | None = None
|
|
879
|
+
|
|
880
|
+
# Find the service-specific health check
|
|
881
|
+
for check in checks:
|
|
882
|
+
check_service_id = check.get("ServiceID", "")
|
|
883
|
+
if check_service_id == svc_id_str or not check_service_id:
|
|
884
|
+
status_str = str(check.get("Status", "")).lower()
|
|
885
|
+
if status_str == "passing":
|
|
886
|
+
health_status = EnumHealthStatus.HEALTHY
|
|
887
|
+
elif status_str in ("critical", "warning"):
|
|
888
|
+
health_status = EnumHealthStatus.UNHEALTHY
|
|
889
|
+
else:
|
|
890
|
+
health_status = EnumHealthStatus.UNKNOWN
|
|
891
|
+
|
|
892
|
+
health_output = str(check.get("Output", "")) or None
|
|
893
|
+
|
|
894
|
+
# Parse CreateIndex as a proxy for last check time
|
|
895
|
+
# (Consul doesn't directly expose last check time in this API)
|
|
896
|
+
# In production, you might use the check's CreateIndex or ModifyIndex
|
|
897
|
+
break
|
|
898
|
+
|
|
899
|
+
service_infos.append(
|
|
900
|
+
ModelServiceInfo(
|
|
901
|
+
service_id=svc_id,
|
|
902
|
+
service_name=str(svc_name),
|
|
903
|
+
address=str(address),
|
|
904
|
+
port=port,
|
|
905
|
+
tags=tuple(svc_tags or []),
|
|
906
|
+
health_status=health_status,
|
|
907
|
+
health_output=health_output,
|
|
908
|
+
last_check_at=last_check_at,
|
|
909
|
+
metadata=svc_meta or {},
|
|
910
|
+
registered_at=datetime.now(UTC),
|
|
911
|
+
correlation_id=correlation_id,
|
|
912
|
+
)
|
|
913
|
+
)
|
|
914
|
+
|
|
915
|
+
duration_ms = (time.monotonic() - start_time) * 1000
|
|
916
|
+
|
|
917
|
+
logger.info(
|
|
918
|
+
"Retrieved service instances from Consul",
|
|
919
|
+
extra={
|
|
920
|
+
"service_name": service_name,
|
|
921
|
+
"instance_count": len(service_infos),
|
|
922
|
+
"include_unhealthy": include_unhealthy,
|
|
923
|
+
"duration_ms": duration_ms,
|
|
924
|
+
"correlation_id": str(correlation_id),
|
|
925
|
+
},
|
|
926
|
+
)
|
|
927
|
+
|
|
928
|
+
return service_infos
|
|
929
|
+
|
|
930
|
+
except TimeoutError as e:
|
|
931
|
+
async with self._circuit_breaker_lock:
|
|
932
|
+
await self._record_circuit_failure(
|
|
933
|
+
operation="get_all_service_instances",
|
|
934
|
+
correlation_id=correlation_id,
|
|
935
|
+
)
|
|
936
|
+
raise InfraTimeoutError(
|
|
937
|
+
f"Consul health query timed out after {self._timeout_seconds}s",
|
|
938
|
+
context=ModelTimeoutErrorContext(
|
|
939
|
+
transport_type=EnumInfraTransportType.CONSUL,
|
|
940
|
+
operation="get_all_service_instances",
|
|
941
|
+
target_name="consul.discovery",
|
|
942
|
+
correlation_id=correlation_id,
|
|
943
|
+
timeout_seconds=self._timeout_seconds,
|
|
944
|
+
),
|
|
945
|
+
) from e
|
|
946
|
+
|
|
947
|
+
except consul.ConsulException as e:
|
|
948
|
+
async with self._circuit_breaker_lock:
|
|
949
|
+
await self._record_circuit_failure(
|
|
950
|
+
operation="get_all_service_instances",
|
|
951
|
+
correlation_id=correlation_id,
|
|
952
|
+
)
|
|
953
|
+
context = ModelInfraErrorContext(
|
|
954
|
+
transport_type=EnumInfraTransportType.CONSUL,
|
|
955
|
+
operation="get_all_service_instances",
|
|
956
|
+
target_name="consul.discovery",
|
|
957
|
+
correlation_id=correlation_id,
|
|
958
|
+
)
|
|
959
|
+
raise InfraConnectionError(
|
|
960
|
+
"Consul health query failed",
|
|
961
|
+
context=context,
|
|
962
|
+
) from e
|
|
963
|
+
|
|
667
964
|
async def health_check(
|
|
668
965
|
self,
|
|
669
966
|
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,
|
|
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,
|
|
642
|
+
result: dict[str, JsonType] = {
|
|
642
643
|
"initialized": cb_initialized,
|
|
643
644
|
"state": cb_state,
|
|
644
645
|
"failures": cb_failures,
|
|
@@ -207,6 +207,7 @@ from typing import TYPE_CHECKING, ClassVar, TypedDict, cast
|
|
|
207
207
|
from uuid import UUID, uuid4
|
|
208
208
|
|
|
209
209
|
from omnibase_core.enums import EnumNodeKind
|
|
210
|
+
from omnibase_core.models.events.model_event_envelope import ModelEventEnvelope
|
|
210
211
|
from omnibase_core.models.primitives.model_semver import ModelSemVer
|
|
211
212
|
from omnibase_infra.enums import EnumInfraTransportType, EnumIntrospectionReason
|
|
212
213
|
from omnibase_infra.errors import ModelInfraErrorContext, ProtocolConfigurationError
|
|
@@ -228,7 +229,9 @@ from omnibase_infra.models.registration.model_node_introspection_event import (
|
|
|
228
229
|
|
|
229
230
|
if TYPE_CHECKING:
|
|
230
231
|
from omnibase_core.protocols.event_bus.protocol_event_bus import ProtocolEventBus
|
|
231
|
-
from
|
|
232
|
+
from omnibase_core.protocols.event_bus.protocol_event_message import (
|
|
233
|
+
ProtocolEventMessage,
|
|
234
|
+
)
|
|
232
235
|
|
|
233
236
|
logger = logging.getLogger(__name__)
|
|
234
237
|
|
|
@@ -1353,7 +1356,7 @@ class MixinNodeIntrospection:
|
|
|
1353
1356
|
# Update cache - cast the model_dump output to our typed dict since we know
|
|
1354
1357
|
# the structure matches (model_dump returns dict[str, Any] by default)
|
|
1355
1358
|
self._introspection_cache = cast(
|
|
1356
|
-
IntrospectionCacheDict, event.model_dump(mode="json")
|
|
1359
|
+
"IntrospectionCacheDict", event.model_dump(mode="json")
|
|
1357
1360
|
)
|
|
1358
1361
|
self._introspection_cached_at = current_time
|
|
1359
1362
|
|
|
@@ -1489,8 +1492,13 @@ class MixinNodeIntrospection:
|
|
|
1489
1492
|
assert event_bus is not None # Redundant but helps mypy
|
|
1490
1493
|
topic = self._introspection_topic
|
|
1491
1494
|
if hasattr(event_bus, "publish_envelope"):
|
|
1495
|
+
# Wrap event in ModelEventEnvelope for protocol compliance
|
|
1496
|
+
envelope: ModelEventEnvelope[object] = ModelEventEnvelope(
|
|
1497
|
+
payload=publish_event,
|
|
1498
|
+
correlation_id=final_correlation_id,
|
|
1499
|
+
)
|
|
1492
1500
|
await event_bus.publish_envelope(
|
|
1493
|
-
envelope=
|
|
1501
|
+
envelope=envelope, # type: ignore[arg-type]
|
|
1494
1502
|
topic=topic,
|
|
1495
1503
|
)
|
|
1496
1504
|
else:
|
|
@@ -1604,8 +1612,13 @@ class MixinNodeIntrospection:
|
|
|
1604
1612
|
assert event_bus is not None # Redundant but helps mypy
|
|
1605
1613
|
topic = self._heartbeat_topic
|
|
1606
1614
|
if hasattr(event_bus, "publish_envelope"):
|
|
1615
|
+
# Wrap event in ModelEventEnvelope for protocol compliance
|
|
1616
|
+
envelope: ModelEventEnvelope[object] = ModelEventEnvelope(
|
|
1617
|
+
payload=heartbeat,
|
|
1618
|
+
correlation_id=heartbeat.correlation_id,
|
|
1619
|
+
)
|
|
1607
1620
|
await event_bus.publish_envelope(
|
|
1608
|
-
envelope=
|
|
1621
|
+
envelope=envelope, # type: ignore[arg-type]
|
|
1609
1622
|
topic=topic,
|
|
1610
1623
|
)
|
|
1611
1624
|
else:
|
|
@@ -1771,7 +1784,9 @@ class MixinNodeIntrospection:
|
|
|
1771
1784
|
)
|
|
1772
1785
|
self._registry_unsubscribe = None
|
|
1773
1786
|
|
|
1774
|
-
async def _handle_introspection_request(
|
|
1787
|
+
async def _handle_introspection_request(
|
|
1788
|
+
self, message: ProtocolEventMessage
|
|
1789
|
+
) -> None:
|
|
1775
1790
|
"""Handle incoming introspection request.
|
|
1776
1791
|
|
|
1777
1792
|
Includes error recovery with rate-limited logging to prevent
|
|
@@ -1779,7 +1794,7 @@ class MixinNodeIntrospection:
|
|
|
1779
1794
|
non-fatal errors to maintain graceful degradation.
|
|
1780
1795
|
|
|
1781
1796
|
Args:
|
|
1782
|
-
message: The incoming event message
|
|
1797
|
+
message: The incoming event message (implements ProtocolEventMessage protocol)
|
|
1783
1798
|
"""
|
|
1784
1799
|
try:
|
|
1785
1800
|
await self._process_introspection_request(message)
|
|
@@ -1788,7 +1803,9 @@ class MixinNodeIntrospection:
|
|
|
1788
1803
|
except Exception as e:
|
|
1789
1804
|
self._handle_request_error(e)
|
|
1790
1805
|
|
|
1791
|
-
async def _process_introspection_request(
|
|
1806
|
+
async def _process_introspection_request(
|
|
1807
|
+
self, message: ProtocolEventMessage
|
|
1808
|
+
) -> None:
|
|
1792
1809
|
"""Process the introspection request message.
|
|
1793
1810
|
|
|
1794
1811
|
Args:
|
|
@@ -196,7 +196,7 @@ class MixinRetryExecution(ABC):
|
|
|
196
196
|
This should only be called when _circuit_breaker_initialized is True,
|
|
197
197
|
which guarantees the circuit breaker methods are available.
|
|
198
198
|
"""
|
|
199
|
-
return cast(ProtocolCircuitBreakerAware, self)
|
|
199
|
+
return cast("ProtocolCircuitBreakerAware", self)
|
|
200
200
|
|
|
201
201
|
async def _record_circuit_failure_if_enabled(
|
|
202
202
|
self, operation: str, correlation_id: UUID
|
|
@@ -12,6 +12,10 @@ and error reporting in ONEX handlers.
|
|
|
12
12
|
Added ModelHandlerDescriptor and ModelContractDiscoveryResult for
|
|
13
13
|
OMN-1097 filesystem handler discovery.
|
|
14
14
|
|
|
15
|
+
.. versionchanged:: 0.6.4
|
|
16
|
+
Added ModelBootstrapHandlerDescriptor for OMN-1087 bootstrap handler
|
|
17
|
+
validation with required handler_class field.
|
|
18
|
+
|
|
15
19
|
Note:
|
|
16
20
|
ModelContractDiscoveryResult uses a forward reference to
|
|
17
21
|
ModelHandlerValidationError to avoid circular imports. The forward
|
|
@@ -20,10 +24,14 @@ Note:
|
|
|
20
24
|
tests/unit/runtime/test_handler_contract_source.py.
|
|
21
25
|
"""
|
|
22
26
|
|
|
27
|
+
from omnibase_infra.models.handlers.model_bootstrap_handler_descriptor import (
|
|
28
|
+
ModelBootstrapHandlerDescriptor,
|
|
29
|
+
)
|
|
23
30
|
from omnibase_infra.models.handlers.model_contract_discovery_result import (
|
|
24
31
|
ModelContractDiscoveryResult,
|
|
25
32
|
)
|
|
26
33
|
from omnibase_infra.models.handlers.model_handler_descriptor import (
|
|
34
|
+
LiteralHandlerKind,
|
|
27
35
|
ModelHandlerDescriptor,
|
|
28
36
|
)
|
|
29
37
|
from omnibase_infra.models.handlers.model_handler_identifier import (
|
|
@@ -31,6 +39,8 @@ from omnibase_infra.models.handlers.model_handler_identifier import (
|
|
|
31
39
|
)
|
|
32
40
|
|
|
33
41
|
__all__ = [
|
|
42
|
+
"LiteralHandlerKind",
|
|
43
|
+
"ModelBootstrapHandlerDescriptor",
|
|
34
44
|
"ModelContractDiscoveryResult",
|
|
35
45
|
"ModelHandlerDescriptor",
|
|
36
46
|
"ModelHandlerIdentifier",
|