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.
Files changed (116) hide show
  1. omnibase_infra/__init__.py +1 -1
  2. omnibase_infra/adapters/adapter_onex_tool_execution.py +446 -0
  3. omnibase_infra/cli/commands.py +1 -1
  4. omnibase_infra/configs/widget_mapping.yaml +176 -0
  5. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +4 -1
  6. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +4 -1
  7. omnibase_infra/errors/error_compute_registry.py +4 -1
  8. omnibase_infra/errors/error_event_bus_registry.py +4 -1
  9. omnibase_infra/errors/error_infra.py +3 -1
  10. omnibase_infra/errors/error_policy_registry.py +4 -1
  11. omnibase_infra/handlers/handler_db.py +2 -1
  12. omnibase_infra/handlers/handler_graph.py +10 -5
  13. omnibase_infra/handlers/handler_mcp.py +736 -63
  14. omnibase_infra/handlers/mixins/mixin_consul_kv.py +4 -3
  15. omnibase_infra/handlers/mixins/mixin_consul_service.py +2 -1
  16. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +301 -4
  17. omnibase_infra/handlers/service_discovery/models/model_service_info.py +10 -0
  18. omnibase_infra/mixins/mixin_async_circuit_breaker.py +3 -2
  19. omnibase_infra/mixins/mixin_node_introspection.py +24 -7
  20. omnibase_infra/mixins/mixin_retry_execution.py +1 -1
  21. omnibase_infra/models/handlers/__init__.py +10 -0
  22. omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
  23. omnibase_infra/models/handlers/model_handler_descriptor.py +15 -0
  24. omnibase_infra/models/mcp/__init__.py +15 -0
  25. omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
  26. omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
  27. omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
  28. omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
  29. omnibase_infra/models/registration/model_node_capabilities.py +11 -0
  30. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +0 -5
  31. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +17 -10
  32. omnibase_infra/nodes/effects/contract.yaml +0 -5
  33. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +7 -0
  34. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +86 -1
  35. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +3 -3
  36. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +9 -8
  37. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +14 -13
  38. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +0 -5
  39. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +46 -25
  40. omnibase_infra/nodes/node_registry_effect/contract.yaml +0 -5
  41. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +2 -1
  42. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +24 -19
  43. omnibase_infra/plugins/examples/plugin_json_normalizer.py +2 -2
  44. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +2 -2
  45. omnibase_infra/plugins/plugin_compute_base.py +16 -2
  46. omnibase_infra/protocols/protocol_event_projector.py +1 -1
  47. omnibase_infra/runtime/__init__.py +51 -1
  48. omnibase_infra/runtime/binding_config_resolver.py +102 -37
  49. omnibase_infra/runtime/constants_notification.py +75 -0
  50. omnibase_infra/runtime/contract_handler_discovery.py +6 -1
  51. omnibase_infra/runtime/handler_bootstrap_source.py +514 -0
  52. omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
  53. omnibase_infra/runtime/handler_contract_source.py +289 -167
  54. omnibase_infra/runtime/handler_plugin_loader.py +4 -2
  55. omnibase_infra/runtime/mixin_semver_cache.py +25 -1
  56. omnibase_infra/runtime/mixins/__init__.py +7 -0
  57. omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
  58. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +31 -10
  59. omnibase_infra/runtime/models/__init__.py +24 -0
  60. omnibase_infra/runtime/models/model_health_check_result.py +2 -1
  61. omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
  62. omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
  63. omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
  64. omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
  65. omnibase_infra/runtime/projector_plugin_loader.py +1 -1
  66. omnibase_infra/runtime/projector_shell.py +229 -1
  67. omnibase_infra/runtime/protocols/__init__.py +10 -0
  68. omnibase_infra/runtime/registry/registry_protocol_binding.py +3 -2
  69. omnibase_infra/runtime/registry_policy.py +9 -326
  70. omnibase_infra/runtime/secret_resolver.py +4 -2
  71. omnibase_infra/runtime/service_kernel.py +10 -2
  72. omnibase_infra/runtime/service_message_dispatch_engine.py +4 -2
  73. omnibase_infra/runtime/service_runtime_host_process.py +225 -15
  74. omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
  75. omnibase_infra/runtime/transition_notification_publisher.py +764 -0
  76. omnibase_infra/runtime/util_container_wiring.py +6 -5
  77. omnibase_infra/runtime/util_wiring.py +5 -1
  78. omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
  79. omnibase_infra/services/mcp/__init__.py +31 -0
  80. omnibase_infra/services/mcp/mcp_server_lifecycle.py +443 -0
  81. omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
  82. omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
  83. omnibase_infra/services/mcp/service_mcp_tool_sync.py +547 -0
  84. omnibase_infra/services/registry_api/__init__.py +40 -0
  85. omnibase_infra/services/registry_api/main.py +243 -0
  86. omnibase_infra/services/registry_api/models/__init__.py +66 -0
  87. omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
  88. omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
  89. omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
  90. omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
  91. omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
  92. omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
  93. omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
  94. omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
  95. omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
  96. omnibase_infra/services/registry_api/models/model_warning.py +49 -0
  97. omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
  98. omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
  99. omnibase_infra/services/registry_api/routes.py +371 -0
  100. omnibase_infra/services/registry_api/service.py +846 -0
  101. omnibase_infra/services/service_capability_query.py +4 -4
  102. omnibase_infra/services/service_health.py +3 -2
  103. omnibase_infra/services/service_timeout_emitter.py +13 -2
  104. omnibase_infra/utils/util_dsn_validation.py +1 -1
  105. omnibase_infra/validation/__init__.py +3 -19
  106. omnibase_infra/validation/contracts/security.validation.yaml +114 -0
  107. omnibase_infra/validation/infra_validators.py +35 -24
  108. omnibase_infra/validation/validation_exemptions.yaml +113 -9
  109. omnibase_infra/validation/validator_chain_propagation.py +2 -2
  110. omnibase_infra/validation/validator_runtime_shape.py +1 -1
  111. omnibase_infra/validation/validator_security.py +473 -370
  112. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/METADATA +2 -2
  113. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/RECORD +116 -74
  114. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/WHEEL +0 -0
  115. {omnibase_infra-0.2.1.dist-info → omnibase_infra-0.2.2.dist-info}/entry_points.txt +0 -0
  116. {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, 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,
@@ -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 omnibase_infra.event_bus.models import ModelEventMessage
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=publish_event,
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=heartbeat,
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(self, message: ModelEventMessage) -> None:
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(self, message: ModelEventMessage) -> None:
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",