omnibase_infra 0.2.6__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 (833) hide show
  1. omnibase_infra/__init__.py +101 -0
  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/__init__.py +1 -0
  8. omnibase_infra/cli/commands.py +216 -0
  9. omnibase_infra/clients/__init__.py +0 -0
  10. omnibase_infra/configs/widget_mapping.yaml +176 -0
  11. omnibase_infra/constants_topic_patterns.py +26 -0
  12. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +264 -0
  13. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +141 -0
  14. omnibase_infra/decorators/__init__.py +29 -0
  15. omnibase_infra/decorators/allow_any.py +109 -0
  16. omnibase_infra/dlq/__init__.py +90 -0
  17. omnibase_infra/dlq/constants_dlq.py +57 -0
  18. omnibase_infra/dlq/models/__init__.py +26 -0
  19. omnibase_infra/dlq/models/enum_replay_status.py +37 -0
  20. omnibase_infra/dlq/models/model_dlq_replay_record.py +135 -0
  21. omnibase_infra/dlq/models/model_dlq_tracking_config.py +184 -0
  22. omnibase_infra/dlq/service_dlq_tracking.py +611 -0
  23. omnibase_infra/enums/__init__.py +132 -0
  24. omnibase_infra/enums/enum_any_type_violation.py +104 -0
  25. omnibase_infra/enums/enum_backend_type.py +27 -0
  26. omnibase_infra/enums/enum_capture_outcome.py +42 -0
  27. omnibase_infra/enums/enum_capture_state.py +88 -0
  28. omnibase_infra/enums/enum_chain_violation_type.py +119 -0
  29. omnibase_infra/enums/enum_circuit_state.py +51 -0
  30. omnibase_infra/enums/enum_confirmation_event_type.py +27 -0
  31. omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
  32. omnibase_infra/enums/enum_contract_type.py +84 -0
  33. omnibase_infra/enums/enum_dedupe_strategy.py +46 -0
  34. omnibase_infra/enums/enum_dispatch_status.py +191 -0
  35. omnibase_infra/enums/enum_environment.py +46 -0
  36. omnibase_infra/enums/enum_execution_shape_violation.py +103 -0
  37. omnibase_infra/enums/enum_handler_error_type.py +111 -0
  38. omnibase_infra/enums/enum_handler_loader_error.py +178 -0
  39. omnibase_infra/enums/enum_handler_source_mode.py +86 -0
  40. omnibase_infra/enums/enum_handler_source_type.py +87 -0
  41. omnibase_infra/enums/enum_handler_type.py +77 -0
  42. omnibase_infra/enums/enum_handler_type_category.py +61 -0
  43. omnibase_infra/enums/enum_infra_transport_type.py +73 -0
  44. omnibase_infra/enums/enum_introspection_reason.py +154 -0
  45. omnibase_infra/enums/enum_kafka_acks.py +99 -0
  46. omnibase_infra/enums/enum_message_category.py +213 -0
  47. omnibase_infra/enums/enum_node_archetype.py +74 -0
  48. omnibase_infra/enums/enum_node_output_type.py +185 -0
  49. omnibase_infra/enums/enum_non_retryable_error_category.py +224 -0
  50. omnibase_infra/enums/enum_policy_type.py +32 -0
  51. omnibase_infra/enums/enum_registration_state.py +261 -0
  52. omnibase_infra/enums/enum_registration_status.py +33 -0
  53. omnibase_infra/enums/enum_registry_response_status.py +28 -0
  54. omnibase_infra/enums/enum_response_status.py +26 -0
  55. omnibase_infra/enums/enum_retry_error_category.py +98 -0
  56. omnibase_infra/enums/enum_security_rule_id.py +103 -0
  57. omnibase_infra/enums/enum_selection_strategy.py +91 -0
  58. omnibase_infra/enums/enum_topic_standard.py +42 -0
  59. omnibase_infra/enums/enum_validation_severity.py +78 -0
  60. omnibase_infra/errors/__init__.py +160 -0
  61. omnibase_infra/errors/error_architecture_violation.py +152 -0
  62. omnibase_infra/errors/error_binding_resolution.py +128 -0
  63. omnibase_infra/errors/error_chain_propagation.py +188 -0
  64. omnibase_infra/errors/error_compute_registry.py +95 -0
  65. omnibase_infra/errors/error_consul.py +132 -0
  66. omnibase_infra/errors/error_container_wiring.py +243 -0
  67. omnibase_infra/errors/error_event_bus_registry.py +105 -0
  68. omnibase_infra/errors/error_infra.py +610 -0
  69. omnibase_infra/errors/error_message_type_registry.py +101 -0
  70. omnibase_infra/errors/error_policy_registry.py +115 -0
  71. omnibase_infra/errors/error_vault.py +123 -0
  72. omnibase_infra/event_bus/__init__.py +72 -0
  73. omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +84 -0
  74. omnibase_infra/event_bus/event_bus_inmemory.py +797 -0
  75. omnibase_infra/event_bus/event_bus_kafka.py +1716 -0
  76. omnibase_infra/event_bus/mixin_kafka_broadcast.py +180 -0
  77. omnibase_infra/event_bus/mixin_kafka_dlq.py +771 -0
  78. omnibase_infra/event_bus/models/__init__.py +29 -0
  79. omnibase_infra/event_bus/models/config/__init__.py +20 -0
  80. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +693 -0
  81. omnibase_infra/event_bus/models/model_dlq_event.py +206 -0
  82. omnibase_infra/event_bus/models/model_dlq_metrics.py +304 -0
  83. omnibase_infra/event_bus/models/model_event_headers.py +115 -0
  84. omnibase_infra/event_bus/models/model_event_message.py +60 -0
  85. omnibase_infra/event_bus/testing/__init__.py +26 -0
  86. omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
  87. omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
  88. omnibase_infra/event_bus/topic_constants.py +376 -0
  89. omnibase_infra/handlers/__init__.py +82 -0
  90. omnibase_infra/handlers/filesystem/__init__.py +48 -0
  91. omnibase_infra/handlers/filesystem/enum_file_system_operation.py +35 -0
  92. omnibase_infra/handlers/filesystem/model_file_system_request.py +298 -0
  93. omnibase_infra/handlers/filesystem/model_file_system_result.py +166 -0
  94. omnibase_infra/handlers/handler_consul.py +795 -0
  95. omnibase_infra/handlers/handler_db.py +1046 -0
  96. omnibase_infra/handlers/handler_filesystem.py +1478 -0
  97. omnibase_infra/handlers/handler_graph.py +2015 -0
  98. omnibase_infra/handlers/handler_http.py +926 -0
  99. omnibase_infra/handlers/handler_intent.py +387 -0
  100. omnibase_infra/handlers/handler_manifest_persistence.contract.yaml +184 -0
  101. omnibase_infra/handlers/handler_manifest_persistence.py +1539 -0
  102. omnibase_infra/handlers/handler_mcp.py +1430 -0
  103. omnibase_infra/handlers/handler_qdrant.py +1076 -0
  104. omnibase_infra/handlers/handler_vault.py +428 -0
  105. omnibase_infra/handlers/mcp/__init__.py +19 -0
  106. omnibase_infra/handlers/mcp/adapter_onex_to_mcp.py +446 -0
  107. omnibase_infra/handlers/mcp/protocols.py +178 -0
  108. omnibase_infra/handlers/mcp/transport_streamable_http.py +352 -0
  109. omnibase_infra/handlers/mixins/__init__.py +47 -0
  110. omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
  111. omnibase_infra/handlers/mixins/mixin_consul_kv.py +338 -0
  112. omnibase_infra/handlers/mixins/mixin_consul_service.py +542 -0
  113. omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
  114. omnibase_infra/handlers/mixins/mixin_vault_initialization.py +338 -0
  115. omnibase_infra/handlers/mixins/mixin_vault_retry.py +412 -0
  116. omnibase_infra/handlers/mixins/mixin_vault_secrets.py +450 -0
  117. omnibase_infra/handlers/mixins/mixin_vault_token.py +365 -0
  118. omnibase_infra/handlers/models/__init__.py +286 -0
  119. omnibase_infra/handlers/models/consul/__init__.py +81 -0
  120. omnibase_infra/handlers/models/consul/enum_consul_operation_type.py +57 -0
  121. omnibase_infra/handlers/models/consul/model_consul_deregister_payload.py +51 -0
  122. omnibase_infra/handlers/models/consul/model_consul_handler_config.py +153 -0
  123. omnibase_infra/handlers/models/consul/model_consul_handler_payload.py +89 -0
  124. omnibase_infra/handlers/models/consul/model_consul_kv_get_found_payload.py +55 -0
  125. omnibase_infra/handlers/models/consul/model_consul_kv_get_not_found_payload.py +49 -0
  126. omnibase_infra/handlers/models/consul/model_consul_kv_get_recurse_payload.py +50 -0
  127. omnibase_infra/handlers/models/consul/model_consul_kv_item.py +33 -0
  128. omnibase_infra/handlers/models/consul/model_consul_kv_put_payload.py +41 -0
  129. omnibase_infra/handlers/models/consul/model_consul_register_payload.py +53 -0
  130. omnibase_infra/handlers/models/consul/model_consul_retry_config.py +66 -0
  131. omnibase_infra/handlers/models/consul/model_payload_consul.py +66 -0
  132. omnibase_infra/handlers/models/consul/registry_payload_consul.py +214 -0
  133. omnibase_infra/handlers/models/graph/__init__.py +35 -0
  134. omnibase_infra/handlers/models/graph/enum_graph_operation_type.py +20 -0
  135. omnibase_infra/handlers/models/graph/model_graph_execute_payload.py +38 -0
  136. omnibase_infra/handlers/models/graph/model_graph_handler_config.py +54 -0
  137. omnibase_infra/handlers/models/graph/model_graph_handler_payload.py +44 -0
  138. omnibase_infra/handlers/models/graph/model_graph_query_payload.py +40 -0
  139. omnibase_infra/handlers/models/graph/model_graph_record.py +22 -0
  140. omnibase_infra/handlers/models/http/__init__.py +50 -0
  141. omnibase_infra/handlers/models/http/enum_http_operation_type.py +29 -0
  142. omnibase_infra/handlers/models/http/model_http_body_content.py +45 -0
  143. omnibase_infra/handlers/models/http/model_http_get_payload.py +88 -0
  144. omnibase_infra/handlers/models/http/model_http_handler_payload.py +90 -0
  145. omnibase_infra/handlers/models/http/model_http_post_payload.py +88 -0
  146. omnibase_infra/handlers/models/http/model_payload_http.py +66 -0
  147. omnibase_infra/handlers/models/http/registry_payload_http.py +212 -0
  148. omnibase_infra/handlers/models/mcp/__init__.py +23 -0
  149. omnibase_infra/handlers/models/mcp/enum_mcp_operation_type.py +24 -0
  150. omnibase_infra/handlers/models/mcp/model_mcp_handler_config.py +40 -0
  151. omnibase_infra/handlers/models/mcp/model_mcp_tool_call.py +32 -0
  152. omnibase_infra/handlers/models/mcp/model_mcp_tool_result.py +45 -0
  153. omnibase_infra/handlers/models/model_consul_handler_response.py +96 -0
  154. omnibase_infra/handlers/models/model_db_describe_response.py +83 -0
  155. omnibase_infra/handlers/models/model_db_query_payload.py +95 -0
  156. omnibase_infra/handlers/models/model_db_query_response.py +60 -0
  157. omnibase_infra/handlers/models/model_filesystem_config.py +98 -0
  158. omnibase_infra/handlers/models/model_filesystem_delete_payload.py +54 -0
  159. omnibase_infra/handlers/models/model_filesystem_delete_result.py +77 -0
  160. omnibase_infra/handlers/models/model_filesystem_directory_entry.py +75 -0
  161. omnibase_infra/handlers/models/model_filesystem_ensure_directory_payload.py +54 -0
  162. omnibase_infra/handlers/models/model_filesystem_ensure_directory_result.py +60 -0
  163. omnibase_infra/handlers/models/model_filesystem_list_directory_payload.py +60 -0
  164. omnibase_infra/handlers/models/model_filesystem_list_directory_result.py +68 -0
  165. omnibase_infra/handlers/models/model_filesystem_read_payload.py +62 -0
  166. omnibase_infra/handlers/models/model_filesystem_read_result.py +61 -0
  167. omnibase_infra/handlers/models/model_filesystem_write_payload.py +70 -0
  168. omnibase_infra/handlers/models/model_filesystem_write_result.py +55 -0
  169. omnibase_infra/handlers/models/model_graph_handler_response.py +98 -0
  170. omnibase_infra/handlers/models/model_handler_response.py +103 -0
  171. omnibase_infra/handlers/models/model_http_handler_response.py +101 -0
  172. omnibase_infra/handlers/models/model_manifest_metadata.py +75 -0
  173. omnibase_infra/handlers/models/model_manifest_persistence_config.py +62 -0
  174. omnibase_infra/handlers/models/model_manifest_query_payload.py +90 -0
  175. omnibase_infra/handlers/models/model_manifest_query_result.py +97 -0
  176. omnibase_infra/handlers/models/model_manifest_retrieve_payload.py +44 -0
  177. omnibase_infra/handlers/models/model_manifest_retrieve_result.py +98 -0
  178. omnibase_infra/handlers/models/model_manifest_store_payload.py +47 -0
  179. omnibase_infra/handlers/models/model_manifest_store_result.py +67 -0
  180. omnibase_infra/handlers/models/model_operation_context.py +187 -0
  181. omnibase_infra/handlers/models/model_qdrant_handler_response.py +98 -0
  182. omnibase_infra/handlers/models/model_retry_state.py +162 -0
  183. omnibase_infra/handlers/models/model_vault_handler_response.py +98 -0
  184. omnibase_infra/handlers/models/qdrant/__init__.py +44 -0
  185. omnibase_infra/handlers/models/qdrant/enum_qdrant_operation_type.py +26 -0
  186. omnibase_infra/handlers/models/qdrant/model_qdrant_collection_payload.py +42 -0
  187. omnibase_infra/handlers/models/qdrant/model_qdrant_delete_payload.py +36 -0
  188. omnibase_infra/handlers/models/qdrant/model_qdrant_handler_config.py +42 -0
  189. omnibase_infra/handlers/models/qdrant/model_qdrant_handler_payload.py +54 -0
  190. omnibase_infra/handlers/models/qdrant/model_qdrant_search_payload.py +42 -0
  191. omnibase_infra/handlers/models/qdrant/model_qdrant_search_result.py +30 -0
  192. omnibase_infra/handlers/models/qdrant/model_qdrant_upsert_payload.py +36 -0
  193. omnibase_infra/handlers/models/vault/__init__.py +69 -0
  194. omnibase_infra/handlers/models/vault/enum_vault_operation_type.py +35 -0
  195. omnibase_infra/handlers/models/vault/model_payload_vault.py +66 -0
  196. omnibase_infra/handlers/models/vault/model_vault_delete_payload.py +57 -0
  197. omnibase_infra/handlers/models/vault/model_vault_handler_config.py +148 -0
  198. omnibase_infra/handlers/models/vault/model_vault_handler_payload.py +101 -0
  199. omnibase_infra/handlers/models/vault/model_vault_list_payload.py +58 -0
  200. omnibase_infra/handlers/models/vault/model_vault_renew_token_payload.py +67 -0
  201. omnibase_infra/handlers/models/vault/model_vault_retry_config.py +66 -0
  202. omnibase_infra/handlers/models/vault/model_vault_secret_payload.py +106 -0
  203. omnibase_infra/handlers/models/vault/model_vault_write_payload.py +66 -0
  204. omnibase_infra/handlers/models/vault/registry_payload_vault.py +213 -0
  205. omnibase_infra/handlers/registration_storage/__init__.py +43 -0
  206. omnibase_infra/handlers/registration_storage/handler_registration_storage_mock.py +392 -0
  207. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +922 -0
  208. omnibase_infra/handlers/registration_storage/models/__init__.py +23 -0
  209. omnibase_infra/handlers/registration_storage/models/model_delete_registration_request.py +58 -0
  210. omnibase_infra/handlers/registration_storage/models/model_update_registration_request.py +73 -0
  211. omnibase_infra/handlers/registration_storage/protocol_registration_persistence.py +191 -0
  212. omnibase_infra/handlers/service_discovery/__init__.py +43 -0
  213. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +1051 -0
  214. omnibase_infra/handlers/service_discovery/handler_service_discovery_mock.py +258 -0
  215. omnibase_infra/handlers/service_discovery/models/__init__.py +22 -0
  216. omnibase_infra/handlers/service_discovery/models/model_discovery_result.py +64 -0
  217. omnibase_infra/handlers/service_discovery/models/model_registration_result.py +138 -0
  218. omnibase_infra/handlers/service_discovery/models/model_service_info.py +109 -0
  219. omnibase_infra/handlers/service_discovery/protocol_discovery_operations.py +170 -0
  220. omnibase_infra/idempotency/__init__.py +94 -0
  221. omnibase_infra/idempotency/models/__init__.py +43 -0
  222. omnibase_infra/idempotency/models/model_idempotency_check_result.py +85 -0
  223. omnibase_infra/idempotency/models/model_idempotency_guard_config.py +130 -0
  224. omnibase_infra/idempotency/models/model_idempotency_record.py +86 -0
  225. omnibase_infra/idempotency/models/model_idempotency_store_health_check_result.py +81 -0
  226. omnibase_infra/idempotency/models/model_idempotency_store_metrics.py +140 -0
  227. omnibase_infra/idempotency/models/model_postgres_idempotency_store_config.py +299 -0
  228. omnibase_infra/idempotency/protocol_idempotency_store.py +184 -0
  229. omnibase_infra/idempotency/store_inmemory.py +265 -0
  230. omnibase_infra/idempotency/store_postgres.py +923 -0
  231. omnibase_infra/infrastructure/__init__.py +0 -0
  232. omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
  233. omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
  234. omnibase_infra/mixins/__init__.py +71 -0
  235. omnibase_infra/mixins/mixin_async_circuit_breaker.py +656 -0
  236. omnibase_infra/mixins/mixin_dict_like_accessors.py +146 -0
  237. omnibase_infra/mixins/mixin_envelope_extraction.py +119 -0
  238. omnibase_infra/mixins/mixin_node_introspection.py +2670 -0
  239. omnibase_infra/mixins/mixin_retry_execution.py +386 -0
  240. omnibase_infra/mixins/protocol_circuit_breaker_aware.py +133 -0
  241. omnibase_infra/models/__init__.py +144 -0
  242. omnibase_infra/models/bindings/__init__.py +59 -0
  243. omnibase_infra/models/bindings/constants.py +144 -0
  244. omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
  245. omnibase_infra/models/bindings/model_operation_binding.py +44 -0
  246. omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
  247. omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
  248. omnibase_infra/models/corpus/__init__.py +17 -0
  249. omnibase_infra/models/corpus/model_capture_config.py +133 -0
  250. omnibase_infra/models/corpus/model_capture_result.py +86 -0
  251. omnibase_infra/models/discovery/__init__.py +42 -0
  252. omnibase_infra/models/discovery/model_dependency_spec.py +319 -0
  253. omnibase_infra/models/discovery/model_discovered_capabilities.py +50 -0
  254. omnibase_infra/models/discovery/model_introspection_config.py +330 -0
  255. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +169 -0
  256. omnibase_infra/models/discovery/model_introspection_task_config.py +116 -0
  257. omnibase_infra/models/dispatch/__init__.py +155 -0
  258. omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
  259. omnibase_infra/models/dispatch/model_dispatch_context.py +439 -0
  260. omnibase_infra/models/dispatch/model_dispatch_error.py +336 -0
  261. omnibase_infra/models/dispatch/model_dispatch_log_context.py +400 -0
  262. omnibase_infra/models/dispatch/model_dispatch_metadata.py +228 -0
  263. omnibase_infra/models/dispatch/model_dispatch_metrics.py +496 -0
  264. omnibase_infra/models/dispatch/model_dispatch_outcome.py +317 -0
  265. omnibase_infra/models/dispatch/model_dispatch_outputs.py +231 -0
  266. omnibase_infra/models/dispatch/model_dispatch_result.py +436 -0
  267. omnibase_infra/models/dispatch/model_dispatch_route.py +279 -0
  268. omnibase_infra/models/dispatch/model_dispatcher_metrics.py +275 -0
  269. omnibase_infra/models/dispatch/model_dispatcher_registration.py +352 -0
  270. omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
  271. omnibase_infra/models/dispatch/model_parsed_topic.py +135 -0
  272. omnibase_infra/models/dispatch/model_topic_parser.py +725 -0
  273. omnibase_infra/models/dispatch/model_tracing_context.py +285 -0
  274. omnibase_infra/models/errors/__init__.py +45 -0
  275. omnibase_infra/models/errors/model_handler_validation_error.py +594 -0
  276. omnibase_infra/models/errors/model_infra_error_context.py +99 -0
  277. omnibase_infra/models/errors/model_message_type_registry_error_context.py +71 -0
  278. omnibase_infra/models/errors/model_timeout_error_context.py +110 -0
  279. omnibase_infra/models/handlers/__init__.py +80 -0
  280. omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
  281. omnibase_infra/models/handlers/model_contract_discovery_result.py +82 -0
  282. omnibase_infra/models/handlers/model_handler_descriptor.py +200 -0
  283. omnibase_infra/models/handlers/model_handler_identifier.py +215 -0
  284. omnibase_infra/models/handlers/model_handler_source_config.py +220 -0
  285. omnibase_infra/models/health/__init__.py +9 -0
  286. omnibase_infra/models/health/model_health_check_result.py +40 -0
  287. omnibase_infra/models/lifecycle/__init__.py +39 -0
  288. omnibase_infra/models/logging/__init__.py +51 -0
  289. omnibase_infra/models/logging/model_log_context.py +756 -0
  290. omnibase_infra/models/mcp/__init__.py +15 -0
  291. omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
  292. omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
  293. omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
  294. omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
  295. omnibase_infra/models/model_node_identity.py +126 -0
  296. omnibase_infra/models/model_retry_error_classification.py +78 -0
  297. omnibase_infra/models/projection/__init__.py +43 -0
  298. omnibase_infra/models/projection/model_capability_fields.py +112 -0
  299. omnibase_infra/models/projection/model_registration_projection.py +434 -0
  300. omnibase_infra/models/projection/model_registration_snapshot.py +322 -0
  301. omnibase_infra/models/projection/model_sequence_info.py +182 -0
  302. omnibase_infra/models/projection/model_snapshot_topic_config.py +591 -0
  303. omnibase_infra/models/projectors/__init__.py +41 -0
  304. omnibase_infra/models/projectors/model_projector_column.py +289 -0
  305. omnibase_infra/models/projectors/model_projector_discovery_result.py +65 -0
  306. omnibase_infra/models/projectors/model_projector_index.py +270 -0
  307. omnibase_infra/models/projectors/model_projector_schema.py +415 -0
  308. omnibase_infra/models/projectors/model_projector_validation_error.py +63 -0
  309. omnibase_infra/models/projectors/util_sql_identifiers.py +115 -0
  310. omnibase_infra/models/registration/__init__.py +68 -0
  311. omnibase_infra/models/registration/commands/__init__.py +15 -0
  312. omnibase_infra/models/registration/commands/model_node_registration_acked.py +108 -0
  313. omnibase_infra/models/registration/events/__init__.py +56 -0
  314. omnibase_infra/models/registration/events/model_node_became_active.py +103 -0
  315. omnibase_infra/models/registration/events/model_node_liveness_expired.py +103 -0
  316. omnibase_infra/models/registration/events/model_node_registration_accepted.py +98 -0
  317. omnibase_infra/models/registration/events/model_node_registration_ack_received.py +98 -0
  318. omnibase_infra/models/registration/events/model_node_registration_ack_timed_out.py +112 -0
  319. omnibase_infra/models/registration/events/model_node_registration_initiated.py +107 -0
  320. omnibase_infra/models/registration/events/model_node_registration_rejected.py +104 -0
  321. omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
  322. omnibase_infra/models/registration/model_introspection_metrics.py +253 -0
  323. omnibase_infra/models/registration/model_node_capabilities.py +190 -0
  324. omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
  325. omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
  326. omnibase_infra/models/registration/model_node_introspection_event.py +195 -0
  327. omnibase_infra/models/registration/model_node_metadata.py +79 -0
  328. omnibase_infra/models/registration/model_node_registration.py +162 -0
  329. omnibase_infra/models/registration/model_node_registration_record.py +162 -0
  330. omnibase_infra/models/registry/__init__.py +29 -0
  331. omnibase_infra/models/registry/model_domain_constraint.py +202 -0
  332. omnibase_infra/models/registry/model_message_type_entry.py +271 -0
  333. omnibase_infra/models/resilience/__init__.py +9 -0
  334. omnibase_infra/models/resilience/model_circuit_breaker_config.py +227 -0
  335. omnibase_infra/models/routing/__init__.py +25 -0
  336. omnibase_infra/models/routing/model_routing_entry.py +52 -0
  337. omnibase_infra/models/routing/model_routing_subcontract.py +70 -0
  338. omnibase_infra/models/runtime/__init__.py +49 -0
  339. omnibase_infra/models/runtime/model_contract_security_config.py +41 -0
  340. omnibase_infra/models/runtime/model_discovery_error.py +81 -0
  341. omnibase_infra/models/runtime/model_discovery_result.py +162 -0
  342. omnibase_infra/models/runtime/model_discovery_warning.py +74 -0
  343. omnibase_infra/models/runtime/model_failed_plugin_load.py +63 -0
  344. omnibase_infra/models/runtime/model_handler_contract.py +296 -0
  345. omnibase_infra/models/runtime/model_loaded_handler.py +129 -0
  346. omnibase_infra/models/runtime/model_plugin_load_context.py +93 -0
  347. omnibase_infra/models/runtime/model_plugin_load_summary.py +124 -0
  348. omnibase_infra/models/security/__init__.py +50 -0
  349. omnibase_infra/models/security/classification_levels.py +99 -0
  350. omnibase_infra/models/security/model_environment_policy.py +145 -0
  351. omnibase_infra/models/security/model_handler_security_policy.py +107 -0
  352. omnibase_infra/models/security/model_security_error.py +81 -0
  353. omnibase_infra/models/security/model_security_validation_result.py +328 -0
  354. omnibase_infra/models/security/model_security_warning.py +67 -0
  355. omnibase_infra/models/snapshot/__init__.py +27 -0
  356. omnibase_infra/models/snapshot/model_field_change.py +65 -0
  357. omnibase_infra/models/snapshot/model_snapshot.py +270 -0
  358. omnibase_infra/models/snapshot/model_snapshot_diff.py +203 -0
  359. omnibase_infra/models/snapshot/model_subject_ref.py +81 -0
  360. omnibase_infra/models/types/__init__.py +71 -0
  361. omnibase_infra/models/validation/__init__.py +89 -0
  362. omnibase_infra/models/validation/model_any_type_validation_result.py +118 -0
  363. omnibase_infra/models/validation/model_any_type_violation.py +141 -0
  364. omnibase_infra/models/validation/model_category_match_result.py +345 -0
  365. omnibase_infra/models/validation/model_chain_violation.py +166 -0
  366. omnibase_infra/models/validation/model_coverage_metrics.py +316 -0
  367. omnibase_infra/models/validation/model_execution_shape_rule.py +159 -0
  368. omnibase_infra/models/validation/model_execution_shape_validation.py +208 -0
  369. omnibase_infra/models/validation/model_execution_shape_validation_result.py +294 -0
  370. omnibase_infra/models/validation/model_execution_shape_violation.py +122 -0
  371. omnibase_infra/models/validation/model_localhandler_validation_result.py +139 -0
  372. omnibase_infra/models/validation/model_localhandler_violation.py +100 -0
  373. omnibase_infra/models/validation/model_output_validation_params.py +74 -0
  374. omnibase_infra/models/validation/model_validate_and_raise_params.py +84 -0
  375. omnibase_infra/models/validation/model_validation_error_params.py +84 -0
  376. omnibase_infra/models/validation/model_validation_outcome.py +287 -0
  377. omnibase_infra/nodes/__init__.py +57 -0
  378. omnibase_infra/nodes/architecture_validator/__init__.py +79 -0
  379. omnibase_infra/nodes/architecture_validator/contract.yaml +252 -0
  380. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +203 -0
  381. omnibase_infra/nodes/architecture_validator/mixins/__init__.py +16 -0
  382. omnibase_infra/nodes/architecture_validator/mixins/mixin_file_path_rule.py +92 -0
  383. omnibase_infra/nodes/architecture_validator/models/__init__.py +36 -0
  384. omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_request.py +56 -0
  385. omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_result.py +311 -0
  386. omnibase_infra/nodes/architecture_validator/models/model_architecture_violation.py +163 -0
  387. omnibase_infra/nodes/architecture_validator/models/model_rule_check_result.py +265 -0
  388. omnibase_infra/nodes/architecture_validator/models/model_validation_request.py +105 -0
  389. omnibase_infra/nodes/architecture_validator/models/model_validation_result.py +314 -0
  390. omnibase_infra/nodes/architecture_validator/node.py +262 -0
  391. omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +383 -0
  392. omnibase_infra/nodes/architecture_validator/protocols/__init__.py +9 -0
  393. omnibase_infra/nodes/architecture_validator/protocols/protocol_architecture_rule.py +225 -0
  394. omnibase_infra/nodes/architecture_validator/registry/__init__.py +28 -0
  395. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +106 -0
  396. omnibase_infra/nodes/architecture_validator/validators/__init__.py +104 -0
  397. omnibase_infra/nodes/architecture_validator/validators/validator_no_direct_dispatch.py +422 -0
  398. omnibase_infra/nodes/architecture_validator/validators/validator_no_handler_publishing.py +481 -0
  399. omnibase_infra/nodes/architecture_validator/validators/validator_no_orchestrator_fsm.py +491 -0
  400. omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
  401. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
  402. omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
  403. omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
  404. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
  405. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
  406. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
  407. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
  408. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
  409. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
  410. omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
  411. omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
  412. omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
  413. omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
  414. omnibase_infra/nodes/effects/README.md +358 -0
  415. omnibase_infra/nodes/effects/__init__.py +26 -0
  416. omnibase_infra/nodes/effects/contract.yaml +167 -0
  417. omnibase_infra/nodes/effects/models/__init__.py +32 -0
  418. omnibase_infra/nodes/effects/models/model_backend_result.py +190 -0
  419. omnibase_infra/nodes/effects/models/model_effect_idempotency_config.py +92 -0
  420. omnibase_infra/nodes/effects/models/model_registry_request.py +132 -0
  421. omnibase_infra/nodes/effects/models/model_registry_response.py +263 -0
  422. omnibase_infra/nodes/effects/protocol_consul_client.py +89 -0
  423. omnibase_infra/nodes/effects/protocol_effect_idempotency_store.py +143 -0
  424. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +96 -0
  425. omnibase_infra/nodes/effects/registry_effect.py +525 -0
  426. omnibase_infra/nodes/effects/store_effect_idempotency_inmemory.py +425 -0
  427. omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
  428. omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
  429. omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
  430. omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
  431. omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
  432. omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
  433. omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
  434. omnibase_infra/nodes/node_intent_storage_effect/__init__.py +50 -0
  435. omnibase_infra/nodes/node_intent_storage_effect/contract.yaml +194 -0
  436. omnibase_infra/nodes/node_intent_storage_effect/models/__init__.py +24 -0
  437. omnibase_infra/nodes/node_intent_storage_effect/models/model_intent_storage_input.py +141 -0
  438. omnibase_infra/nodes/node_intent_storage_effect/models/model_intent_storage_output.py +130 -0
  439. omnibase_infra/nodes/node_intent_storage_effect/node.py +94 -0
  440. omnibase_infra/nodes/node_intent_storage_effect/registry/__init__.py +35 -0
  441. omnibase_infra/nodes/node_intent_storage_effect/registry/registry_infra_intent_storage.py +294 -0
  442. omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
  443. omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
  444. omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
  445. omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
  446. omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
  447. omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
  448. omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
  449. omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
  450. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
  451. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
  452. omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
  453. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
  454. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
  455. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
  456. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
  457. omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
  458. omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
  459. omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
  460. omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
  461. omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
  462. omnibase_infra/nodes/node_registration_orchestrator/README.md +542 -0
  463. omnibase_infra/nodes/node_registration_orchestrator/__init__.py +120 -0
  464. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +482 -0
  465. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/__init__.py +53 -0
  466. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_introspected.py +376 -0
  467. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_registration_acked.py +376 -0
  468. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_runtime_tick.py +373 -0
  469. omnibase_infra/nodes/node_registration_orchestrator/handlers/__init__.py +62 -0
  470. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_heartbeat.py +376 -0
  471. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +694 -0
  472. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_registration_acked.py +458 -0
  473. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_runtime_tick.py +364 -0
  474. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +544 -0
  475. omnibase_infra/nodes/node_registration_orchestrator/models/__init__.py +75 -0
  476. omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_intent_payload.py +194 -0
  477. omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_registration_intent.py +67 -0
  478. omnibase_infra/nodes/node_registration_orchestrator/models/model_intent_execution_result.py +50 -0
  479. omnibase_infra/nodes/node_registration_orchestrator/models/model_node_liveness_expired.py +107 -0
  480. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_config.py +67 -0
  481. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_input.py +41 -0
  482. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_output.py +166 -0
  483. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +235 -0
  484. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_upsert_intent.py +68 -0
  485. omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_execution_result.py +384 -0
  486. omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_state.py +60 -0
  487. omnibase_infra/nodes/node_registration_orchestrator/models/model_registration_intent.py +177 -0
  488. omnibase_infra/nodes/node_registration_orchestrator/models/model_registry_intent.py +247 -0
  489. omnibase_infra/nodes/node_registration_orchestrator/node.py +195 -0
  490. omnibase_infra/nodes/node_registration_orchestrator/plugin.py +909 -0
  491. omnibase_infra/nodes/node_registration_orchestrator/protocols.py +439 -0
  492. omnibase_infra/nodes/node_registration_orchestrator/registry/__init__.py +41 -0
  493. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +528 -0
  494. omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +393 -0
  495. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +743 -0
  496. omnibase_infra/nodes/node_registration_reducer/__init__.py +15 -0
  497. omnibase_infra/nodes/node_registration_reducer/contract.yaml +301 -0
  498. omnibase_infra/nodes/node_registration_reducer/models/__init__.py +38 -0
  499. omnibase_infra/nodes/node_registration_reducer/models/model_validation_result.py +113 -0
  500. omnibase_infra/nodes/node_registration_reducer/node.py +139 -0
  501. omnibase_infra/nodes/node_registration_reducer/registry/__init__.py +9 -0
  502. omnibase_infra/nodes/node_registration_reducer/registry/registry_infra_node_registration_reducer.py +79 -0
  503. omnibase_infra/nodes/node_registration_storage_effect/__init__.py +41 -0
  504. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +220 -0
  505. omnibase_infra/nodes/node_registration_storage_effect/models/__init__.py +44 -0
  506. omnibase_infra/nodes/node_registration_storage_effect/models/model_delete_result.py +132 -0
  507. omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_record.py +199 -0
  508. omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_update.py +155 -0
  509. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_details.py +123 -0
  510. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_result.py +117 -0
  511. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_query.py +100 -0
  512. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_result.py +136 -0
  513. omnibase_infra/nodes/node_registration_storage_effect/models/model_upsert_result.py +127 -0
  514. omnibase_infra/nodes/node_registration_storage_effect/node.py +112 -0
  515. omnibase_infra/nodes/node_registration_storage_effect/protocols/__init__.py +22 -0
  516. omnibase_infra/nodes/node_registration_storage_effect/protocols/protocol_registration_persistence.py +333 -0
  517. omnibase_infra/nodes/node_registration_storage_effect/registry/__init__.py +23 -0
  518. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +215 -0
  519. omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
  520. omnibase_infra/nodes/node_registry_effect/contract.yaml +677 -0
  521. omnibase_infra/nodes/node_registry_effect/handlers/__init__.py +70 -0
  522. omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_deregister.py +211 -0
  523. omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_register.py +212 -0
  524. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +417 -0
  525. omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_deactivate.py +215 -0
  526. omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_upsert.py +208 -0
  527. omnibase_infra/nodes/node_registry_effect/models/__init__.py +43 -0
  528. omnibase_infra/nodes/node_registry_effect/models/model_partial_retry_request.py +92 -0
  529. omnibase_infra/nodes/node_registry_effect/node.py +165 -0
  530. omnibase_infra/nodes/node_registry_effect/registry/__init__.py +27 -0
  531. omnibase_infra/nodes/node_registry_effect/registry/registry_infra_registry_effect.py +196 -0
  532. omnibase_infra/nodes/node_service_discovery_effect/__init__.py +111 -0
  533. omnibase_infra/nodes/node_service_discovery_effect/contract.yaml +246 -0
  534. omnibase_infra/nodes/node_service_discovery_effect/models/__init__.py +67 -0
  535. omnibase_infra/nodes/node_service_discovery_effect/models/enum_health_status.py +72 -0
  536. omnibase_infra/nodes/node_service_discovery_effect/models/enum_service_discovery_operation.py +58 -0
  537. omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_query.py +99 -0
  538. omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_result.py +98 -0
  539. omnibase_infra/nodes/node_service_discovery_effect/models/model_health_check_config.py +121 -0
  540. omnibase_infra/nodes/node_service_discovery_effect/models/model_query_metadata.py +63 -0
  541. omnibase_infra/nodes/node_service_discovery_effect/models/model_registration_result.py +130 -0
  542. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_details.py +111 -0
  543. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_result.py +119 -0
  544. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_info.py +106 -0
  545. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_registration.py +121 -0
  546. omnibase_infra/nodes/node_service_discovery_effect/node.py +111 -0
  547. omnibase_infra/nodes/node_service_discovery_effect/protocols/__init__.py +14 -0
  548. omnibase_infra/nodes/node_service_discovery_effect/protocols/protocol_discovery_operations.py +279 -0
  549. omnibase_infra/nodes/node_service_discovery_effect/registry/__init__.py +13 -0
  550. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +222 -0
  551. omnibase_infra/nodes/reducers/__init__.py +30 -0
  552. omnibase_infra/nodes/reducers/models/__init__.py +37 -0
  553. omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +87 -0
  554. omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
  555. omnibase_infra/nodes/reducers/models/model_payload_postgres_upsert_registration.py +60 -0
  556. omnibase_infra/nodes/reducers/models/model_registration_confirmation.py +166 -0
  557. omnibase_infra/nodes/reducers/models/model_registration_state.py +433 -0
  558. omnibase_infra/nodes/reducers/registration_reducer.py +1138 -0
  559. omnibase_infra/observability/__init__.py +143 -0
  560. omnibase_infra/observability/constants_metrics.py +91 -0
  561. omnibase_infra/observability/factory_observability_sink.py +525 -0
  562. omnibase_infra/observability/handlers/__init__.py +118 -0
  563. omnibase_infra/observability/handlers/handler_logging_structured.py +967 -0
  564. omnibase_infra/observability/handlers/handler_metrics_prometheus.py +1120 -0
  565. omnibase_infra/observability/handlers/model_logging_handler_config.py +71 -0
  566. omnibase_infra/observability/handlers/model_logging_handler_response.py +77 -0
  567. omnibase_infra/observability/handlers/model_metrics_handler_config.py +172 -0
  568. omnibase_infra/observability/handlers/model_metrics_handler_payload.py +135 -0
  569. omnibase_infra/observability/handlers/model_metrics_handler_response.py +101 -0
  570. omnibase_infra/observability/hooks/__init__.py +74 -0
  571. omnibase_infra/observability/hooks/hook_observability.py +1223 -0
  572. omnibase_infra/observability/models/__init__.py +30 -0
  573. omnibase_infra/observability/models/enum_required_log_context_key.py +77 -0
  574. omnibase_infra/observability/models/model_buffered_log_entry.py +117 -0
  575. omnibase_infra/observability/models/model_logging_sink_config.py +73 -0
  576. omnibase_infra/observability/models/model_metrics_sink_config.py +156 -0
  577. omnibase_infra/observability/sinks/__init__.py +69 -0
  578. omnibase_infra/observability/sinks/sink_logging_structured.py +809 -0
  579. omnibase_infra/observability/sinks/sink_metrics_prometheus.py +710 -0
  580. omnibase_infra/plugins/__init__.py +27 -0
  581. omnibase_infra/plugins/examples/__init__.py +28 -0
  582. omnibase_infra/plugins/examples/plugin_json_normalizer.py +271 -0
  583. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +210 -0
  584. omnibase_infra/plugins/models/__init__.py +21 -0
  585. omnibase_infra/plugins/models/model_plugin_context.py +76 -0
  586. omnibase_infra/plugins/models/model_plugin_input_data.py +58 -0
  587. omnibase_infra/plugins/models/model_plugin_output_data.py +62 -0
  588. omnibase_infra/plugins/plugin_compute_base.py +449 -0
  589. omnibase_infra/projectors/__init__.py +30 -0
  590. omnibase_infra/projectors/contracts/__init__.py +63 -0
  591. omnibase_infra/projectors/contracts/registration_projector.yaml +370 -0
  592. omnibase_infra/projectors/projection_reader_registration.py +1559 -0
  593. omnibase_infra/projectors/snapshot_publisher_registration.py +1329 -0
  594. omnibase_infra/protocols/__init__.py +104 -0
  595. omnibase_infra/protocols/protocol_capability_projection.py +253 -0
  596. omnibase_infra/protocols/protocol_capability_query.py +251 -0
  597. omnibase_infra/protocols/protocol_container_aware.py +200 -0
  598. omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
  599. omnibase_infra/protocols/protocol_event_bus_like.py +127 -0
  600. omnibase_infra/protocols/protocol_event_projector.py +96 -0
  601. omnibase_infra/protocols/protocol_idempotency_store.py +142 -0
  602. omnibase_infra/protocols/protocol_message_dispatcher.py +247 -0
  603. omnibase_infra/protocols/protocol_message_type_registry.py +306 -0
  604. omnibase_infra/protocols/protocol_plugin_compute.py +368 -0
  605. omnibase_infra/protocols/protocol_projector_schema_validator.py +82 -0
  606. omnibase_infra/protocols/protocol_registry_metrics.py +215 -0
  607. omnibase_infra/protocols/protocol_snapshot_publisher.py +396 -0
  608. omnibase_infra/protocols/protocol_snapshot_store.py +567 -0
  609. omnibase_infra/runtime/__init__.py +445 -0
  610. omnibase_infra/runtime/binding_config_resolver.py +2771 -0
  611. omnibase_infra/runtime/binding_resolver.py +753 -0
  612. omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
  613. omnibase_infra/runtime/constants_notification.py +75 -0
  614. omnibase_infra/runtime/constants_security.py +70 -0
  615. omnibase_infra/runtime/contract_handler_discovery.py +587 -0
  616. omnibase_infra/runtime/contract_loaders/__init__.py +51 -0
  617. omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
  618. omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
  619. omnibase_infra/runtime/dispatch_context_enforcer.py +427 -0
  620. omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
  621. omnibase_infra/runtime/emit_daemon/cli.py +844 -0
  622. omnibase_infra/runtime/emit_daemon/client.py +811 -0
  623. omnibase_infra/runtime/emit_daemon/config.py +535 -0
  624. omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
  625. omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
  626. omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
  627. omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
  628. omnibase_infra/runtime/emit_daemon/queue.py +618 -0
  629. omnibase_infra/runtime/enums/__init__.py +18 -0
  630. omnibase_infra/runtime/enums/enum_config_ref_scheme.py +33 -0
  631. omnibase_infra/runtime/enums/enum_scheduler_status.py +170 -0
  632. omnibase_infra/runtime/envelope_validator.py +179 -0
  633. omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
  634. omnibase_infra/runtime/handler_bootstrap_source.py +507 -0
  635. omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
  636. omnibase_infra/runtime/handler_contract_source.py +750 -0
  637. omnibase_infra/runtime/handler_identity.py +81 -0
  638. omnibase_infra/runtime/handler_plugin_loader.py +2046 -0
  639. omnibase_infra/runtime/handler_registry.py +329 -0
  640. omnibase_infra/runtime/handler_source_resolver.py +367 -0
  641. omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
  642. omnibase_infra/runtime/kafka_contract_source.py +984 -0
  643. omnibase_infra/runtime/kernel.py +40 -0
  644. omnibase_infra/runtime/mixin_policy_validation.py +522 -0
  645. omnibase_infra/runtime/mixin_semver_cache.py +402 -0
  646. omnibase_infra/runtime/mixins/__init__.py +24 -0
  647. omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
  648. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +778 -0
  649. omnibase_infra/runtime/models/__init__.py +229 -0
  650. omnibase_infra/runtime/models/model_batch_lifecycle_result.py +217 -0
  651. omnibase_infra/runtime/models/model_binding_config.py +168 -0
  652. omnibase_infra/runtime/models/model_binding_config_cache_stats.py +135 -0
  653. omnibase_infra/runtime/models/model_binding_config_resolver_config.py +329 -0
  654. omnibase_infra/runtime/models/model_cached_secret.py +138 -0
  655. omnibase_infra/runtime/models/model_compute_key.py +138 -0
  656. omnibase_infra/runtime/models/model_compute_registration.py +97 -0
  657. omnibase_infra/runtime/models/model_config_cache_entry.py +61 -0
  658. omnibase_infra/runtime/models/model_config_ref.py +331 -0
  659. omnibase_infra/runtime/models/model_config_ref_parse_result.py +125 -0
  660. omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
  661. omnibase_infra/runtime/models/model_domain_plugin_config.py +92 -0
  662. omnibase_infra/runtime/models/model_domain_plugin_result.py +270 -0
  663. omnibase_infra/runtime/models/model_duplicate_response.py +54 -0
  664. omnibase_infra/runtime/models/model_enabled_protocols_config.py +61 -0
  665. omnibase_infra/runtime/models/model_event_bus_config.py +54 -0
  666. omnibase_infra/runtime/models/model_failed_component.py +55 -0
  667. omnibase_infra/runtime/models/model_health_check_response.py +168 -0
  668. omnibase_infra/runtime/models/model_health_check_result.py +229 -0
  669. omnibase_infra/runtime/models/model_lifecycle_result.py +245 -0
  670. omnibase_infra/runtime/models/model_logging_config.py +42 -0
  671. omnibase_infra/runtime/models/model_optional_correlation_id.py +167 -0
  672. omnibase_infra/runtime/models/model_optional_string.py +94 -0
  673. omnibase_infra/runtime/models/model_optional_uuid.py +110 -0
  674. omnibase_infra/runtime/models/model_policy_context.py +100 -0
  675. omnibase_infra/runtime/models/model_policy_key.py +138 -0
  676. omnibase_infra/runtime/models/model_policy_registration.py +139 -0
  677. omnibase_infra/runtime/models/model_policy_result.py +103 -0
  678. omnibase_infra/runtime/models/model_policy_type_filter.py +157 -0
  679. omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
  680. omnibase_infra/runtime/models/model_projector_plugin_loader_config.py +47 -0
  681. omnibase_infra/runtime/models/model_protocol_registration_config.py +65 -0
  682. omnibase_infra/runtime/models/model_retry_policy.py +105 -0
  683. omnibase_infra/runtime/models/model_runtime_config.py +150 -0
  684. omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
  685. omnibase_infra/runtime/models/model_runtime_scheduler_config.py +625 -0
  686. omnibase_infra/runtime/models/model_runtime_scheduler_metrics.py +233 -0
  687. omnibase_infra/runtime/models/model_runtime_tick.py +193 -0
  688. omnibase_infra/runtime/models/model_secret_cache_stats.py +82 -0
  689. omnibase_infra/runtime/models/model_secret_mapping.py +63 -0
  690. omnibase_infra/runtime/models/model_secret_resolver_config.py +107 -0
  691. omnibase_infra/runtime/models/model_secret_resolver_metrics.py +111 -0
  692. omnibase_infra/runtime/models/model_secret_source_info.py +72 -0
  693. omnibase_infra/runtime/models/model_secret_source_spec.py +66 -0
  694. omnibase_infra/runtime/models/model_security_config.py +109 -0
  695. omnibase_infra/runtime/models/model_shutdown_batch_result.py +75 -0
  696. omnibase_infra/runtime/models/model_shutdown_config.py +94 -0
  697. omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
  698. omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
  699. omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
  700. omnibase_infra/runtime/projector_plugin_loader.py +1462 -0
  701. omnibase_infra/runtime/projector_schema_manager.py +565 -0
  702. omnibase_infra/runtime/projector_shell.py +1330 -0
  703. omnibase_infra/runtime/protocol_contract_descriptor.py +92 -0
  704. omnibase_infra/runtime/protocol_contract_source.py +92 -0
  705. omnibase_infra/runtime/protocol_domain_plugin.py +474 -0
  706. omnibase_infra/runtime/protocol_handler_discovery.py +221 -0
  707. omnibase_infra/runtime/protocol_handler_plugin_loader.py +327 -0
  708. omnibase_infra/runtime/protocol_lifecycle_executor.py +435 -0
  709. omnibase_infra/runtime/protocol_policy.py +366 -0
  710. omnibase_infra/runtime/protocols/__init__.py +37 -0
  711. omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -0
  712. omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
  713. omnibase_infra/runtime/registry/__init__.py +93 -0
  714. omnibase_infra/runtime/registry/mixin_message_type_query.py +326 -0
  715. omnibase_infra/runtime/registry/mixin_message_type_registration.py +354 -0
  716. omnibase_infra/runtime/registry/registry_event_bus_binding.py +268 -0
  717. omnibase_infra/runtime/registry/registry_message_type.py +542 -0
  718. omnibase_infra/runtime/registry/registry_protocol_binding.py +445 -0
  719. omnibase_infra/runtime/registry_compute.py +1143 -0
  720. omnibase_infra/runtime/registry_contract_source.py +693 -0
  721. omnibase_infra/runtime/registry_dispatcher.py +678 -0
  722. omnibase_infra/runtime/registry_policy.py +1185 -0
  723. omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
  724. omnibase_infra/runtime/runtime_scheduler.py +1070 -0
  725. omnibase_infra/runtime/secret_resolver.py +2112 -0
  726. omnibase_infra/runtime/security_metadata_validator.py +776 -0
  727. omnibase_infra/runtime/service_kernel.py +1651 -0
  728. omnibase_infra/runtime/service_message_dispatch_engine.py +2350 -0
  729. omnibase_infra/runtime/service_runtime_host_process.py +3493 -0
  730. omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
  731. omnibase_infra/runtime/transition_notification_publisher.py +765 -0
  732. omnibase_infra/runtime/util_container_wiring.py +1124 -0
  733. omnibase_infra/runtime/util_validation.py +314 -0
  734. omnibase_infra/runtime/util_version.py +98 -0
  735. omnibase_infra/runtime/util_wiring.py +723 -0
  736. omnibase_infra/schemas/schema_registration_projection.sql +320 -0
  737. omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
  738. omnibase_infra/services/__init__.py +89 -0
  739. omnibase_infra/services/corpus_capture.py +684 -0
  740. omnibase_infra/services/mcp/__init__.py +31 -0
  741. omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -0
  742. omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
  743. omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
  744. omnibase_infra/services/mcp/service_mcp_tool_sync.py +565 -0
  745. omnibase_infra/services/registry_api/__init__.py +40 -0
  746. omnibase_infra/services/registry_api/main.py +261 -0
  747. omnibase_infra/services/registry_api/models/__init__.py +66 -0
  748. omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
  749. omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
  750. omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
  751. omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
  752. omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
  753. omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
  754. omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
  755. omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
  756. omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
  757. omnibase_infra/services/registry_api/models/model_warning.py +49 -0
  758. omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
  759. omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
  760. omnibase_infra/services/registry_api/routes.py +371 -0
  761. omnibase_infra/services/registry_api/service.py +837 -0
  762. omnibase_infra/services/service_capability_query.py +945 -0
  763. omnibase_infra/services/service_health.py +898 -0
  764. omnibase_infra/services/service_node_selector.py +530 -0
  765. omnibase_infra/services/service_timeout_emitter.py +699 -0
  766. omnibase_infra/services/service_timeout_scanner.py +394 -0
  767. omnibase_infra/services/session/__init__.py +56 -0
  768. omnibase_infra/services/session/config_consumer.py +137 -0
  769. omnibase_infra/services/session/config_store.py +139 -0
  770. omnibase_infra/services/session/consumer.py +1007 -0
  771. omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
  772. omnibase_infra/services/session/store.py +997 -0
  773. omnibase_infra/services/snapshot/__init__.py +31 -0
  774. omnibase_infra/services/snapshot/service_snapshot.py +647 -0
  775. omnibase_infra/services/snapshot/store_inmemory.py +637 -0
  776. omnibase_infra/services/snapshot/store_postgres.py +1279 -0
  777. omnibase_infra/shared/__init__.py +8 -0
  778. omnibase_infra/testing/__init__.py +10 -0
  779. omnibase_infra/testing/utils.py +23 -0
  780. omnibase_infra/topics/__init__.py +45 -0
  781. omnibase_infra/topics/platform_topic_suffixes.py +140 -0
  782. omnibase_infra/topics/util_topic_composition.py +95 -0
  783. omnibase_infra/types/__init__.py +48 -0
  784. omnibase_infra/types/type_cache_info.py +49 -0
  785. omnibase_infra/types/type_dsn.py +173 -0
  786. omnibase_infra/types/type_infra_aliases.py +60 -0
  787. omnibase_infra/types/typed_dict/__init__.py +29 -0
  788. omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
  789. omnibase_infra/types/typed_dict/typed_dict_introspection_cache.py +128 -0
  790. omnibase_infra/types/typed_dict/typed_dict_performance_metrics_cache.py +140 -0
  791. omnibase_infra/types/typed_dict_capabilities.py +64 -0
  792. omnibase_infra/utils/__init__.py +117 -0
  793. omnibase_infra/utils/correlation.py +208 -0
  794. omnibase_infra/utils/util_atomic_file.py +261 -0
  795. omnibase_infra/utils/util_consumer_group.py +232 -0
  796. omnibase_infra/utils/util_datetime.py +372 -0
  797. omnibase_infra/utils/util_db_transaction.py +239 -0
  798. omnibase_infra/utils/util_dsn_validation.py +333 -0
  799. omnibase_infra/utils/util_env_parsing.py +264 -0
  800. omnibase_infra/utils/util_error_sanitization.py +457 -0
  801. omnibase_infra/utils/util_pydantic_validators.py +477 -0
  802. omnibase_infra/utils/util_retry_optimistic.py +281 -0
  803. omnibase_infra/utils/util_semver.py +233 -0
  804. omnibase_infra/validation/__init__.py +307 -0
  805. omnibase_infra/validation/contracts/security.validation.yaml +114 -0
  806. omnibase_infra/validation/enums/__init__.py +11 -0
  807. omnibase_infra/validation/enums/enum_contract_violation_severity.py +13 -0
  808. omnibase_infra/validation/infra_validators.py +1514 -0
  809. omnibase_infra/validation/linter_contract.py +907 -0
  810. omnibase_infra/validation/mixin_any_type_classification.py +120 -0
  811. omnibase_infra/validation/mixin_any_type_exemption.py +580 -0
  812. omnibase_infra/validation/mixin_any_type_reporting.py +106 -0
  813. omnibase_infra/validation/mixin_execution_shape_violation_checks.py +596 -0
  814. omnibase_infra/validation/mixin_node_archetype_detection.py +254 -0
  815. omnibase_infra/validation/models/__init__.py +15 -0
  816. omnibase_infra/validation/models/model_contract_lint_result.py +101 -0
  817. omnibase_infra/validation/models/model_contract_violation.py +41 -0
  818. omnibase_infra/validation/service_validation_aggregator.py +395 -0
  819. omnibase_infra/validation/validation_exemptions.yaml +2033 -0
  820. omnibase_infra/validation/validator_any_type.py +715 -0
  821. omnibase_infra/validation/validator_chain_propagation.py +839 -0
  822. omnibase_infra/validation/validator_execution_shape.py +465 -0
  823. omnibase_infra/validation/validator_localhandler.py +261 -0
  824. omnibase_infra/validation/validator_registration_security.py +410 -0
  825. omnibase_infra/validation/validator_routing_coverage.py +1020 -0
  826. omnibase_infra/validation/validator_runtime_shape.py +915 -0
  827. omnibase_infra/validation/validator_security.py +513 -0
  828. omnibase_infra/validation/validator_topic_category.py +1152 -0
  829. omnibase_infra-0.2.6.dist-info/METADATA +197 -0
  830. omnibase_infra-0.2.6.dist-info/RECORD +833 -0
  831. omnibase_infra-0.2.6.dist-info/WHEEL +4 -0
  832. omnibase_infra-0.2.6.dist-info/entry_points.txt +5 -0
  833. omnibase_infra-0.2.6.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,3493 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Runtime Host Process implementation for ONEX Infrastructure.
4
+
5
+ This module implements the RuntimeHostProcess class, which is responsible for:
6
+ - Owning and managing an event bus instance (EventBusInmemory or EventBusKafka)
7
+ - Registering handlers via the wiring module
8
+ - Subscribing to event bus topics and routing envelopes to handlers
9
+ - Handling errors by producing success=False response envelopes
10
+ - Processing envelopes sequentially (no parallelism in MVP)
11
+ - Basic shutdown (no graceful drain in MVP)
12
+
13
+ The RuntimeHostProcess is the central coordinator for infrastructure runtime,
14
+ bridging event-driven message routing with protocol handlers.
15
+
16
+ Event Bus Support:
17
+ The RuntimeHostProcess supports two event bus implementations:
18
+ - EventBusInmemory: For local development and testing
19
+ - EventBusKafka: For production use with Kafka/Redpanda
20
+
21
+ The event bus can be injected via constructor or auto-created based on config.
22
+
23
+ Example Usage:
24
+ ```python
25
+ from omnibase_infra.runtime import RuntimeHostProcess
26
+
27
+ async def main() -> None:
28
+ process = RuntimeHostProcess()
29
+ await process.start()
30
+ try:
31
+ # Process handles messages via event bus subscription
32
+ await asyncio.sleep(60)
33
+ finally:
34
+ await process.stop()
35
+ ```
36
+
37
+ Integration with Handlers:
38
+ Handlers are registered during start() via the wiring module. Each handler
39
+ processes envelopes for a specific protocol type (e.g., "http", "db").
40
+ The handler_type field in envelopes determines routing.
41
+ """
42
+
43
+ from __future__ import annotations
44
+
45
+ import asyncio
46
+ import importlib
47
+ import json
48
+ import logging
49
+ import os
50
+ from collections.abc import Awaitable, Callable
51
+ from pathlib import Path
52
+ from typing import TYPE_CHECKING, cast
53
+ from uuid import UUID, uuid4
54
+
55
+ from pydantic import BaseModel
56
+
57
+ from omnibase_infra.enums import (
58
+ EnumConsumerGroupPurpose,
59
+ EnumHandlerSourceMode,
60
+ EnumHandlerTypeCategory,
61
+ EnumInfraTransportType,
62
+ )
63
+ from omnibase_infra.errors import (
64
+ EnvelopeValidationError,
65
+ InfraConsulError,
66
+ InfraTimeoutError,
67
+ InfraUnavailableError,
68
+ ModelInfraErrorContext,
69
+ ProtocolConfigurationError,
70
+ RuntimeHostError,
71
+ UnknownHandlerTypeError,
72
+ )
73
+ from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
74
+ from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
75
+ from omnibase_infra.models import ModelNodeIdentity
76
+ from omnibase_infra.runtime.envelope_validator import (
77
+ normalize_correlation_id,
78
+ validate_envelope,
79
+ )
80
+ from omnibase_infra.runtime.handler_registry import RegistryProtocolBinding
81
+ from omnibase_infra.runtime.models import (
82
+ ModelDuplicateResponse,
83
+ ModelRuntimeContractConfig,
84
+ )
85
+ from omnibase_infra.runtime.protocol_lifecycle_executor import ProtocolLifecycleExecutor
86
+ from omnibase_infra.runtime.runtime_contract_config_loader import (
87
+ RuntimeContractConfigLoader,
88
+ )
89
+ from omnibase_infra.runtime.util_wiring import wire_default_handlers
90
+ from omnibase_infra.utils.util_consumer_group import compute_consumer_group_id
91
+ from omnibase_infra.utils.util_env_parsing import parse_env_float
92
+
93
+ if TYPE_CHECKING:
94
+ from omnibase_core.container import ModelONEXContainer
95
+ from omnibase_infra.event_bus.models import ModelEventMessage
96
+ from omnibase_infra.idempotency import ModelIdempotencyGuardConfig
97
+ from omnibase_infra.idempotency.protocol_idempotency_store import (
98
+ ProtocolIdempotencyStore,
99
+ )
100
+ from omnibase_infra.models.handlers import ModelHandlerSourceConfig
101
+ from omnibase_infra.nodes.architecture_validator import ProtocolArchitectureRule
102
+ from omnibase_infra.protocols import ProtocolContainerAware
103
+ from omnibase_infra.runtime.contract_handler_discovery import (
104
+ ContractHandlerDiscovery,
105
+ )
106
+ from omnibase_infra.runtime.service_message_dispatch_engine import (
107
+ MessageDispatchEngine,
108
+ )
109
+
110
+ # Imports for PluginLoaderContractSource adapter class
111
+ from omnibase_core.protocols.event_bus.protocol_event_bus_subscriber import (
112
+ ProtocolEventBusSubscriber,
113
+ )
114
+ from omnibase_infra.models.errors import ModelHandlerValidationError
115
+ from omnibase_infra.models.handlers import (
116
+ LiteralHandlerKind,
117
+ ModelContractDiscoveryResult,
118
+ ModelHandlerDescriptor,
119
+ )
120
+ from omnibase_infra.models.types import JsonDict
121
+ from omnibase_infra.runtime.event_bus_subcontract_wiring import (
122
+ EventBusSubcontractWiring,
123
+ load_event_bus_subcontract,
124
+ )
125
+ from omnibase_infra.runtime.handler_identity import (
126
+ HANDLER_IDENTITY_PREFIX,
127
+ handler_identity,
128
+ )
129
+ from omnibase_infra.runtime.handler_plugin_loader import HandlerPluginLoader
130
+ from omnibase_infra.runtime.kafka_contract_source import (
131
+ TOPIC_SUFFIX_CONTRACT_DEREGISTERED,
132
+ TOPIC_SUFFIX_CONTRACT_REGISTERED,
133
+ KafkaContractSource,
134
+ )
135
+ from omnibase_infra.runtime.protocol_contract_source import ProtocolContractSource
136
+
137
+ # Expose wire_default_handlers as wire_handlers for test patching compatibility
138
+ # Tests patch "omnibase_infra.runtime.service_runtime_host_process.wire_handlers"
139
+ wire_handlers = wire_default_handlers
140
+
141
+ logger = logging.getLogger(__name__)
142
+
143
+ # Mapping from EnumHandlerTypeCategory to LiteralHandlerKind for descriptor creation.
144
+ # COMPUTE and EFFECT map directly to their string values.
145
+ # NONDETERMINISTIC_COMPUTE maps to "compute" because it is architecturally pure
146
+ # (no I/O) even though it may produce different results between runs.
147
+ # "effect" is used as the fallback for any unknown types as the safer option
148
+ # (effect handlers have stricter policy envelopes for I/O operations).
149
+ _HANDLER_TYPE_TO_KIND: dict[EnumHandlerTypeCategory, LiteralHandlerKind] = {
150
+ EnumHandlerTypeCategory.COMPUTE: "compute",
151
+ EnumHandlerTypeCategory.EFFECT: "effect",
152
+ EnumHandlerTypeCategory.NONDETERMINISTIC_COMPUTE: "compute",
153
+ }
154
+
155
+ # Default handler kind for unknown handler types. "effect" is the safe default
156
+ # because effect handlers have stricter policy envelopes for I/O operations.
157
+ _DEFAULT_HANDLER_KIND: LiteralHandlerKind = "effect"
158
+
159
+ # Default configuration values
160
+ DEFAULT_INPUT_TOPIC = "requests"
161
+ DEFAULT_OUTPUT_TOPIC = "responses"
162
+ DEFAULT_GROUP_ID = "runtime-host"
163
+
164
+ # Health check timeout bounds (per ModelLifecycleSubcontract)
165
+ MIN_HEALTH_CHECK_TIMEOUT = 1.0
166
+ MAX_HEALTH_CHECK_TIMEOUT = 60.0
167
+ DEFAULT_HEALTH_CHECK_TIMEOUT: float = parse_env_float(
168
+ "ONEX_HEALTH_CHECK_TIMEOUT",
169
+ 5.0,
170
+ min_value=MIN_HEALTH_CHECK_TIMEOUT,
171
+ max_value=MAX_HEALTH_CHECK_TIMEOUT,
172
+ transport_type=EnumInfraTransportType.RUNTIME,
173
+ service_name="runtime_host_process",
174
+ )
175
+
176
+ # Drain timeout bounds for graceful shutdown (OMN-756)
177
+ # Controls how long to wait for in-flight messages to complete before shutdown
178
+ MIN_DRAIN_TIMEOUT_SECONDS = 1.0
179
+ MAX_DRAIN_TIMEOUT_SECONDS = 300.0
180
+ DEFAULT_DRAIN_TIMEOUT_SECONDS: float = parse_env_float(
181
+ "ONEX_DRAIN_TIMEOUT",
182
+ 30.0,
183
+ min_value=MIN_DRAIN_TIMEOUT_SECONDS,
184
+ max_value=MAX_DRAIN_TIMEOUT_SECONDS,
185
+ transport_type=EnumInfraTransportType.RUNTIME,
186
+ service_name="runtime_host_process",
187
+ )
188
+
189
+
190
+ def _parse_contract_event_payload(
191
+ msg: ModelEventMessage,
192
+ ) -> tuple[dict[str, object], UUID] | None:
193
+ """Parse contract event message payload and extract correlation ID.
194
+
195
+ This helper extracts common JSON parsing and correlation ID extraction logic
196
+ used by contract registration and deregistration handlers.
197
+
198
+ Args:
199
+ msg: The event message to parse.
200
+
201
+ Returns:
202
+ A tuple of (payload_dict, correlation_id) if message has a value,
203
+ None if message value is empty.
204
+
205
+ Raises:
206
+ json.JSONDecodeError: If the message value is not valid JSON.
207
+ UnicodeDecodeError: If the message value cannot be decoded as UTF-8.
208
+
209
+ Note:
210
+ This function is intentionally a module-level utility rather than a
211
+ class method because it performs pure data transformation without
212
+ requiring any class state.
213
+
214
+ .. versionadded:: 0.8.0
215
+ Created for OMN-1654 to reduce duplication in contract event handlers.
216
+ """
217
+ if not msg.value:
218
+ return None
219
+
220
+ payload: dict[str, object] = json.loads(msg.value.decode("utf-8"))
221
+
222
+ # Extract correlation ID from headers if available, or generate new
223
+ correlation_id: UUID
224
+ if msg.headers and msg.headers.correlation_id:
225
+ try:
226
+ correlation_id = UUID(str(msg.headers.correlation_id))
227
+ except (ValueError, TypeError):
228
+ correlation_id = uuid4()
229
+ else:
230
+ correlation_id = uuid4()
231
+
232
+ return (payload, correlation_id)
233
+
234
+
235
+ class PluginLoaderContractSource(ProtocolContractSource):
236
+ """Adapter that uses HandlerPluginLoader for contract discovery.
237
+
238
+ This adapter implements ProtocolContractSource using HandlerPluginLoader,
239
+ which uses the simpler contract schema (handler_name, handler_class,
240
+ handler_type, capability_tags) rather than the full ONEX contract schema.
241
+
242
+ This class wraps the HandlerPluginLoader to conform to the ProtocolContractSource
243
+ interface expected by HandlerSourceResolver, enabling plugin-based handler
244
+ discovery within the unified handler source resolution framework.
245
+
246
+ Attributes:
247
+ _contract_paths: List of filesystem paths to scan for handler contracts.
248
+ _plugin_loader: The underlying HandlerPluginLoader instance.
249
+
250
+ Example:
251
+ ```python
252
+ from pathlib import Path
253
+ source = PluginLoaderContractSource(
254
+ contract_paths=[Path("/etc/onex/handlers")]
255
+ )
256
+ result = await source.discover_handlers()
257
+ for descriptor in result.descriptors:
258
+ print(f"Found handler: {descriptor.name}")
259
+ ```
260
+
261
+ .. versionadded:: 0.7.0
262
+ Extracted from _resolve_handler_descriptors() method for better
263
+ testability and code organization.
264
+ """
265
+
266
+ def __init__(
267
+ self,
268
+ contract_paths: list[Path],
269
+ allowed_namespaces: tuple[str, ...] | None = None,
270
+ ) -> None:
271
+ """Initialize the contract source with paths to scan.
272
+
273
+ Args:
274
+ contract_paths: List of filesystem paths containing handler contracts.
275
+ allowed_namespaces: Optional tuple of allowed module namespaces for
276
+ handler class imports. If None, all namespaces are allowed.
277
+ """
278
+ self._contract_paths = contract_paths
279
+ self._allowed_namespaces = allowed_namespaces
280
+ self._plugin_loader = HandlerPluginLoader(
281
+ allowed_namespaces=list(allowed_namespaces) if allowed_namespaces else None
282
+ )
283
+
284
+ @property
285
+ def source_type(self) -> str:
286
+ """Return the source type identifier.
287
+
288
+ Returns:
289
+ str: Always "CONTRACT" for this filesystem-based source.
290
+ """
291
+ return "CONTRACT"
292
+
293
+ async def discover_handlers(self) -> ModelContractDiscoveryResult:
294
+ """Discover handlers using HandlerPluginLoader.
295
+
296
+ Scans all configured contract paths and loads handler contracts using
297
+ the HandlerPluginLoader. Each discovered handler is converted to a
298
+ ModelHandlerDescriptor for use by the handler resolution framework.
299
+
300
+ Returns:
301
+ ModelContractDiscoveryResult: Container with discovered descriptors
302
+ and any validation errors encountered during discovery.
303
+
304
+ Note:
305
+ This method uses graceful degradation - if a single contract path
306
+ fails to load, discovery continues with remaining paths and the
307
+ error is logged but not raised.
308
+ """
309
+ # NOTE: ModelContractDiscoveryResult.model_rebuild() is called at module-level
310
+ # in handler_source_resolver.py and handler_contract_source.py to resolve
311
+ # forward references. No need to call it here - see those modules for rationale.
312
+
313
+ descriptors: list[ModelHandlerDescriptor] = []
314
+ validation_errors: list[ModelHandlerValidationError] = []
315
+
316
+ for path in self._contract_paths:
317
+ path_obj = Path(path) if isinstance(path, str) else path
318
+ if not path_obj.exists():
319
+ logger.warning(
320
+ "Contract path does not exist, skipping: %s",
321
+ path_obj,
322
+ )
323
+ continue
324
+
325
+ try:
326
+ # Use plugin loader to discover handlers with simpler schema
327
+ loaded_handlers = self._plugin_loader.load_from_directory(
328
+ directory=path_obj,
329
+ )
330
+
331
+ # Convert ModelLoadedHandler to ModelHandlerDescriptor
332
+ for loaded in loaded_handlers:
333
+ # Map EnumHandlerTypeCategory to LiteralHandlerKind.
334
+ # handler_type is required on ModelLoadedHandler, so this always
335
+ # provides a valid value. The mapping handles COMPUTE, EFFECT,
336
+ # and NONDETERMINISTIC_COMPUTE. Falls back to "effect" for any
337
+ # unknown types as the safer option (stricter policy envelope).
338
+ handler_kind = _HANDLER_TYPE_TO_KIND.get(
339
+ loaded.handler_type, _DEFAULT_HANDLER_KIND
340
+ )
341
+
342
+ descriptor = ModelHandlerDescriptor(
343
+ # NOTE: Uses handler_identity() for consistent ID generation.
344
+ # In HYBRID mode, HandlerSourceResolver compares handler_id values to
345
+ # determine which handler wins when both sources provide the same handler.
346
+ # Contract handlers need matching IDs to override their bootstrap equivalents.
347
+ #
348
+ # The "proto." prefix is a **protocol identity namespace**, NOT a source
349
+ # indicator. Both bootstrap and contract sources use this prefix via the
350
+ # shared handler_identity() helper. This enables per-handler identity
351
+ # matching regardless of which source discovered the handler.
352
+ #
353
+ # See: HandlerSourceResolver._resolve_hybrid() for resolution logic.
354
+ # See: handler_identity.py for the shared helper function.
355
+ handler_id=handler_identity(loaded.protocol_type),
356
+ name=loaded.handler_name,
357
+ version=loaded.handler_version,
358
+ handler_kind=handler_kind,
359
+ input_model="omnibase_infra.models.types.JsonDict",
360
+ output_model="omnibase_core.models.dispatch.ModelHandlerOutput",
361
+ description=f"Handler: {loaded.handler_name}",
362
+ handler_class=loaded.handler_class,
363
+ contract_path=str(loaded.contract_path),
364
+ )
365
+ descriptors.append(descriptor)
366
+
367
+ except Exception as e:
368
+ logger.warning(
369
+ "Failed to load handlers from path %s: %s",
370
+ path_obj,
371
+ e,
372
+ )
373
+ # Continue with other paths (graceful degradation)
374
+
375
+ return ModelContractDiscoveryResult(
376
+ descriptors=descriptors,
377
+ validation_errors=validation_errors,
378
+ )
379
+
380
+
381
+ class RuntimeHostProcess:
382
+ """Runtime host process that owns event bus and coordinates handlers.
383
+
384
+ The RuntimeHostProcess is the central coordinator for ONEX infrastructure
385
+ runtime. It owns an event bus instance (EventBusInmemory or EventBusKafka),
386
+ registers handlers via the wiring module, and routes incoming envelopes to
387
+ appropriate handlers.
388
+
389
+ Container Integration:
390
+ RuntimeHostProcess now accepts a ModelONEXContainer parameter for
391
+ dependency injection. The container provides access to:
392
+ - RegistryProtocolBinding: Handler registry for protocol routing
393
+
394
+ This follows ONEX container-based DI patterns for better testability
395
+ and lifecycle management. The legacy singleton pattern is deprecated
396
+ in favor of container resolution.
397
+
398
+ Attributes:
399
+ event_bus: The owned event bus instance (EventBusInmemory or EventBusKafka)
400
+ is_running: Whether the process is currently running
401
+ input_topic: Topic to subscribe to for incoming envelopes
402
+ output_topic: Topic to publish responses to
403
+ group_id: Consumer group identifier
404
+
405
+ Example:
406
+ ```python
407
+ from omnibase_core.container import ModelONEXContainer
408
+ from omnibase_infra.runtime.util_container_wiring import wire_infrastructure_services
409
+
410
+ # Container-based initialization (preferred)
411
+ container = ModelONEXContainer()
412
+ wire_infrastructure_services(container)
413
+ process = RuntimeHostProcess(container=container)
414
+ await process.start()
415
+ health = await process.health_check()
416
+ await process.stop()
417
+
418
+ # Direct initialization (without container)
419
+ process = RuntimeHostProcess() # Uses singleton registries
420
+ ```
421
+
422
+ Graceful Shutdown:
423
+ The stop() method implements graceful shutdown with a configurable drain
424
+ period. After unsubscribing from topics, it waits for in-flight messages
425
+ to complete before shutting down handlers and closing the event bus.
426
+ See stop() docstring for configuration details.
427
+ """
428
+
429
+ def __init__(
430
+ self,
431
+ container: ModelONEXContainer | None = None,
432
+ event_bus: EventBusInmemory | EventBusKafka | None = None,
433
+ input_topic: str = DEFAULT_INPUT_TOPIC,
434
+ output_topic: str = DEFAULT_OUTPUT_TOPIC,
435
+ config: dict[str, object] | None = None,
436
+ handler_registry: RegistryProtocolBinding | None = None,
437
+ architecture_rules: tuple[ProtocolArchitectureRule, ...] | None = None,
438
+ contract_paths: list[str] | None = None,
439
+ ) -> None:
440
+ """Initialize the runtime host process.
441
+
442
+ Args:
443
+ container: Optional ONEX dependency injection container. When provided,
444
+ the runtime host can resolve dependencies from the container if they
445
+ are not explicitly provided. This follows the ONEX container-based
446
+ DI pattern for better testability and explicit dependency management.
447
+
448
+ Container Resolution (during async start()):
449
+ - If handler_registry is None and container is provided, resolves
450
+ RegistryProtocolBinding from container.service_registry
451
+ - Event bus must be provided explicitly or defaults to EventBusInmemory
452
+ (required immediately during __init__)
453
+
454
+ Usage:
455
+ ```python
456
+ from omnibase_core.container import ModelONEXContainer
457
+ from omnibase_infra.runtime.util_container_wiring import wire_infrastructure_services
458
+
459
+ container = ModelONEXContainer()
460
+ await wire_infrastructure_services(container)
461
+ process = RuntimeHostProcess(container=container)
462
+ await process.start()
463
+ ```
464
+
465
+ event_bus: Optional event bus instance (EventBusInmemory or EventBusKafka).
466
+ If None, creates EventBusInmemory.
467
+ input_topic: Topic to subscribe to for incoming envelopes.
468
+ output_topic: Topic to publish responses to.
469
+ config: Optional configuration dict that can override topics and group_id.
470
+ Supported keys:
471
+ - input_topic: Override input topic
472
+ - output_topic: Override output topic
473
+ - group_id: Override consumer group identifier
474
+ - health_check_timeout_seconds: Timeout for individual handler
475
+ health checks (default: 5.0 seconds, valid range: 1-60 per
476
+ ModelLifecycleSubcontract). Values outside this range are
477
+ clamped to the nearest bound with a warning logged.
478
+ Invalid string values fall back to the default with a warning.
479
+ - drain_timeout_seconds: Maximum time to wait for in-flight
480
+ messages to complete during graceful shutdown (default: 30.0
481
+ seconds, valid range: 1-300). Values outside this range are
482
+ clamped to the nearest bound with a warning logged.
483
+ handler_registry: Optional RegistryProtocolBinding instance for handler lookup.
484
+ Type: RegistryProtocolBinding | None
485
+
486
+ Purpose:
487
+ Provides the registry that maps handler_type strings (e.g., "http", "db")
488
+ to their corresponding ProtocolContainerAware classes. The registry is queried
489
+ during start() to instantiate and initialize all registered handlers.
490
+
491
+ Resolution Order:
492
+ 1. If handler_registry is provided, uses this pre-resolved registry
493
+ 2. If container is provided, resolves from container.service_registry
494
+ 3. If None, falls back to singleton via get_handler_registry()
495
+
496
+ Container Integration:
497
+ When using container-based DI (recommended), resolve the registry from
498
+ the container and pass it to RuntimeHostProcess:
499
+
500
+ ```python
501
+ async def create_runtime() -> RuntimeHostProcess:
502
+ container = ModelONEXContainer()
503
+ await wire_infrastructure_services(container)
504
+ registry = await container.service_registry.resolve_service(
505
+ RegistryProtocolBinding
506
+ )
507
+ return RuntimeHostProcess(handler_registry=registry)
508
+ ```
509
+
510
+ This follows ONEX container-based DI patterns for better testability
511
+ and explicit dependency management.
512
+
513
+ container: Optional ONEX container for dependency injection. Required for
514
+ architecture validation. If None and architecture validation is requested,
515
+ a minimal container will be created.
516
+
517
+ architecture_rules: Optional tuple of architecture rules to validate at startup.
518
+ Type: tuple[ProtocolArchitectureRule, ...] | None
519
+
520
+ Purpose:
521
+ Architecture rules are validated BEFORE the runtime starts. Violations
522
+ with ERROR severity will prevent startup. Violations with WARNING
523
+ severity are logged but don't block startup.
524
+
525
+ Rules implementing ProtocolArchitectureRule can be:
526
+ - Custom rules specific to your application
527
+ - Standard rules from OMN-1099 validators
528
+
529
+ Example:
530
+ ```python
531
+ from my_rules import NoHandlerPublishingRule, NoAnyTypesRule
532
+
533
+ process = RuntimeHostProcess(
534
+ container=container,
535
+ architecture_rules=(
536
+ NoHandlerPublishingRule(),
537
+ NoAnyTypesRule(),
538
+ ),
539
+ )
540
+ await process.start() # Validates architecture first
541
+ ```
542
+
543
+ contract_paths: Optional list of paths to scan for handler contracts.
544
+ Type: list[str] | None
545
+
546
+ Purpose:
547
+ Enables contract-based handler discovery. When provided, the runtime
548
+ will auto-discover and register handlers from these paths during
549
+ start() instead of using wire_default_handlers().
550
+
551
+ Paths can be:
552
+ - Directories: Recursively scanned for handler contracts
553
+ - Files: Directly loaded as contract files
554
+
555
+ Behavior:
556
+ - If contract_paths is provided: Uses ContractHandlerDiscovery
557
+ to auto-discover and register handlers from the specified paths.
558
+ - If contract_paths is None or empty: Falls back to the existing
559
+ wire_default_handlers() behavior.
560
+
561
+ Error Handling:
562
+ Discovery errors are logged but do not block startup. This enables
563
+ graceful degradation where some handlers can be registered even
564
+ if others fail to load.
565
+
566
+ Example:
567
+ ```python
568
+ # Contract-based handler discovery
569
+ process = RuntimeHostProcess(
570
+ contract_paths=["src/nodes/handlers", "plugins/"]
571
+ )
572
+ await process.start()
573
+
574
+ # Or with explicit file paths
575
+ process = RuntimeHostProcess(
576
+ contract_paths=[
577
+ "handlers/auth/handler_contract.yaml",
578
+ "handlers/db/handler_contract.yaml",
579
+ ]
580
+ )
581
+ ```
582
+ """
583
+ # Store container reference for dependency resolution
584
+ self._container: ModelONEXContainer | None = container
585
+ # Handler registry (container-based DI or singleton fallback)
586
+ self._handler_registry: RegistryProtocolBinding | None = handler_registry
587
+
588
+ # Architecture rules for startup validation
589
+ self._architecture_rules: tuple[ProtocolArchitectureRule, ...] = (
590
+ architecture_rules or ()
591
+ )
592
+
593
+ # Contract paths for handler discovery (OMN-1133)
594
+ # Convert strings to Path objects for consistent filesystem operations
595
+ self._contract_paths: list[Path] = (
596
+ [Path(p) for p in contract_paths] if contract_paths else []
597
+ )
598
+
599
+ # Handler discovery service (lazy-created if contract_paths provided)
600
+ self._handler_discovery: ContractHandlerDiscovery | None = None
601
+
602
+ # Kafka contract source (created if KAFKA_EVENTS mode, wired separately)
603
+ self._kafka_contract_source: KafkaContractSource | None = None
604
+
605
+ # Create or use provided event bus
606
+ self._event_bus: EventBusInmemory | EventBusKafka = (
607
+ event_bus or EventBusInmemory()
608
+ )
609
+
610
+ # Extract configuration with defaults
611
+ config = config or {}
612
+
613
+ # Topic configuration (config overrides constructor args)
614
+ self._input_topic: str = str(config.get("input_topic", input_topic))
615
+ self._output_topic: str = str(config.get("output_topic", output_topic))
616
+
617
+ # Node identity configuration (required for consumer group derivation)
618
+ # Extract components from config - fail-fast if required fields are missing
619
+ _env = config.get("env")
620
+ env: str = str(_env).strip() if _env else "local"
621
+
622
+ _service_name = config.get("service_name")
623
+ if not _service_name or not str(_service_name).strip():
624
+ raise ValueError(
625
+ "RuntimeHostProcess requires 'service_name' in config. "
626
+ "This is the service name from your node's contract (e.g., 'omniintelligence'). "
627
+ "Cannot infer service_name - please provide it explicitly."
628
+ )
629
+ service_name: str = str(_service_name).strip()
630
+
631
+ _node_name = config.get("node_name")
632
+ if not _node_name or not str(_node_name).strip():
633
+ raise ValueError(
634
+ "RuntimeHostProcess requires 'node_name' in config. "
635
+ "This is the node name from your contract (e.g., 'claude_hook_event_effect'). "
636
+ "Cannot infer node_name - please provide it explicitly."
637
+ )
638
+ node_name: str = str(_node_name).strip()
639
+
640
+ _version = config.get("version")
641
+ version: str = (
642
+ str(_version).strip() if _version and str(_version).strip() else "v1"
643
+ )
644
+
645
+ self._node_identity: ModelNodeIdentity = ModelNodeIdentity(
646
+ env=env,
647
+ service=service_name,
648
+ node_name=node_name,
649
+ version=version,
650
+ )
651
+
652
+ # Health check configuration (from lifecycle subcontract pattern)
653
+ # Default: 5.0 seconds, valid range: 1-60 seconds per ModelLifecycleSubcontract
654
+ # Values outside bounds are clamped with a warning
655
+ _timeout_raw = config.get("health_check_timeout_seconds")
656
+ timeout_value: float = DEFAULT_HEALTH_CHECK_TIMEOUT
657
+ if isinstance(_timeout_raw, int | float):
658
+ timeout_value = float(_timeout_raw)
659
+ elif isinstance(_timeout_raw, str):
660
+ try:
661
+ timeout_value = float(_timeout_raw)
662
+ except ValueError:
663
+ logger.warning(
664
+ "Invalid health_check_timeout_seconds string value, using default",
665
+ extra={
666
+ "invalid_value": _timeout_raw,
667
+ "default_value": DEFAULT_HEALTH_CHECK_TIMEOUT,
668
+ },
669
+ )
670
+ timeout_value = DEFAULT_HEALTH_CHECK_TIMEOUT
671
+
672
+ # Validate bounds and clamp if necessary
673
+ if (
674
+ timeout_value < MIN_HEALTH_CHECK_TIMEOUT
675
+ or timeout_value > MAX_HEALTH_CHECK_TIMEOUT
676
+ ):
677
+ logger.warning(
678
+ "health_check_timeout_seconds out of valid range, clamping",
679
+ extra={
680
+ "original_value": timeout_value,
681
+ "min_value": MIN_HEALTH_CHECK_TIMEOUT,
682
+ "max_value": MAX_HEALTH_CHECK_TIMEOUT,
683
+ "clamped_value": max(
684
+ MIN_HEALTH_CHECK_TIMEOUT,
685
+ min(timeout_value, MAX_HEALTH_CHECK_TIMEOUT),
686
+ ),
687
+ },
688
+ )
689
+ timeout_value = max(
690
+ MIN_HEALTH_CHECK_TIMEOUT,
691
+ min(timeout_value, MAX_HEALTH_CHECK_TIMEOUT),
692
+ )
693
+
694
+ self._health_check_timeout_seconds: float = timeout_value
695
+
696
+ # Drain timeout configuration for graceful shutdown (OMN-756)
697
+ # Default: 30.0 seconds, valid range: 1-300 seconds
698
+ # Values outside bounds are clamped with a warning
699
+ _drain_timeout_raw = config.get("drain_timeout_seconds")
700
+ drain_timeout_value: float = DEFAULT_DRAIN_TIMEOUT_SECONDS
701
+ if isinstance(_drain_timeout_raw, int | float):
702
+ drain_timeout_value = float(_drain_timeout_raw)
703
+ elif isinstance(_drain_timeout_raw, str):
704
+ try:
705
+ drain_timeout_value = float(_drain_timeout_raw)
706
+ except ValueError:
707
+ logger.warning(
708
+ "Invalid drain_timeout_seconds string value, using default",
709
+ extra={
710
+ "invalid_value": _drain_timeout_raw,
711
+ "default_value": DEFAULT_DRAIN_TIMEOUT_SECONDS,
712
+ },
713
+ )
714
+ drain_timeout_value = DEFAULT_DRAIN_TIMEOUT_SECONDS
715
+
716
+ # Validate drain timeout bounds and clamp if necessary
717
+ if (
718
+ drain_timeout_value < MIN_DRAIN_TIMEOUT_SECONDS
719
+ or drain_timeout_value > MAX_DRAIN_TIMEOUT_SECONDS
720
+ ):
721
+ logger.warning(
722
+ "drain_timeout_seconds out of valid range, clamping",
723
+ extra={
724
+ "original_value": drain_timeout_value,
725
+ "min_value": MIN_DRAIN_TIMEOUT_SECONDS,
726
+ "max_value": MAX_DRAIN_TIMEOUT_SECONDS,
727
+ "clamped_value": max(
728
+ MIN_DRAIN_TIMEOUT_SECONDS,
729
+ min(drain_timeout_value, MAX_DRAIN_TIMEOUT_SECONDS),
730
+ ),
731
+ },
732
+ )
733
+ drain_timeout_value = max(
734
+ MIN_DRAIN_TIMEOUT_SECONDS,
735
+ min(drain_timeout_value, MAX_DRAIN_TIMEOUT_SECONDS),
736
+ )
737
+
738
+ self._drain_timeout_seconds: float = drain_timeout_value
739
+
740
+ # Handler executor for lifecycle operations (shutdown, health check)
741
+ self._lifecycle_executor = ProtocolLifecycleExecutor(
742
+ health_check_timeout_seconds=self._health_check_timeout_seconds
743
+ )
744
+
745
+ # Store full config for handler initialization
746
+ self._config: dict[str, object] | None = config
747
+
748
+ # Runtime state
749
+ self._is_running: bool = False
750
+
751
+ # Subscription handle (callable to unsubscribe)
752
+ self._subscription: Callable[[], Awaitable[None]] | None = None
753
+
754
+ # Handler registry (handler_type -> handler instance)
755
+ # This will be populated from the singleton registry during start()
756
+ self._handlers: dict[str, ProtocolContainerAware] = {}
757
+
758
+ # Track failed handler instantiations (handler_type -> error message)
759
+ # Used by health_check() to report degraded state
760
+ self._failed_handlers: dict[str, str] = {}
761
+
762
+ # Handler descriptors (handler_type -> descriptor with contract_config)
763
+ # Stored during registration for use during handler initialization
764
+ # Enables contract config to be passed to handlers via initialize()
765
+ self._handler_descriptors: dict[str, ModelHandlerDescriptor] = {}
766
+
767
+ # Pending message tracking for graceful shutdown (OMN-756)
768
+ # Tracks count of in-flight messages currently being processed
769
+ self._pending_message_count: int = 0
770
+ self._pending_lock: asyncio.Lock = asyncio.Lock()
771
+
772
+ # Drain state tracking for graceful shutdown (OMN-756)
773
+ # True when stop() has been called and we're waiting for messages to drain
774
+ self._is_draining: bool = False
775
+
776
+ # Idempotency guard for duplicate message detection (OMN-945)
777
+ # None = disabled, otherwise points to configured store
778
+ self._idempotency_store: ProtocolIdempotencyStore | None = None
779
+ self._idempotency_config: ModelIdempotencyGuardConfig | None = None
780
+
781
+ # Event bus subcontract wiring for contract-driven subscriptions (OMN-1621)
782
+ # Bridges contract-declared topics to Kafka subscriptions.
783
+ # None until wired during start() when dispatch_engine is available.
784
+ self._event_bus_wiring: EventBusSubcontractWiring | None = None
785
+
786
+ # Message dispatch engine for routing received messages (OMN-1621)
787
+ # Used by event_bus_wiring to dispatch messages to handlers.
788
+ # None = not configured, wiring will be skipped
789
+ self._dispatch_engine: MessageDispatchEngine | None = None
790
+
791
+ # Baseline subscriptions for platform-reserved topics (OMN-1654)
792
+ # Stores unsubscribe callbacks for contract registration/deregistration topics.
793
+ # Wired when KAFKA_EVENTS mode is active with a KafkaContractSource.
794
+ self._baseline_subscriptions: list[Callable[[], Awaitable[None]]] = []
795
+
796
+ # Contract configuration loaded at startup (OMN-1519)
797
+ # Contains consolidated handler_routing and operation_bindings from all contracts.
798
+ # None until loaded during start() via _load_contract_configs()
799
+ self._contract_config: ModelRuntimeContractConfig | None = None
800
+
801
+ logger.debug(
802
+ "RuntimeHostProcess initialized",
803
+ extra={
804
+ "input_topic": self._input_topic,
805
+ "output_topic": self._output_topic,
806
+ "group_id": self.group_id,
807
+ "health_check_timeout_seconds": self._health_check_timeout_seconds,
808
+ "drain_timeout_seconds": self._drain_timeout_seconds,
809
+ "has_container": self._container is not None,
810
+ "has_handler_registry": self._handler_registry is not None,
811
+ "has_contract_paths": len(self._contract_paths) > 0,
812
+ "contract_path_count": len(self._contract_paths),
813
+ },
814
+ )
815
+
816
+ @property
817
+ def container(self) -> ModelONEXContainer | None:
818
+ """Return the optional ONEX dependency injection container.
819
+
820
+ Returns:
821
+ The container if provided during initialization, None otherwise.
822
+ """
823
+ return self._container
824
+
825
+ @property
826
+ def contract_config(self) -> ModelRuntimeContractConfig | None:
827
+ """Return the loaded contract configuration.
828
+
829
+ Contains consolidated handler_routing and operation_bindings from all
830
+ contracts discovered during startup. Returns None if contracts have
831
+ not been loaded yet (before start() is called).
832
+
833
+ The contract config provides access to:
834
+ - handler_routing_configs: All loaded handler routing configurations
835
+ - operation_bindings_configs: All loaded operation bindings
836
+ - success_rate: Ratio of successfully loaded contracts
837
+ - error_messages: Any errors encountered during loading
838
+
839
+ Returns:
840
+ ModelRuntimeContractConfig if loaded, None if not yet loaded.
841
+
842
+ Example:
843
+ >>> process = RuntimeHostProcess(...)
844
+ >>> await process.start()
845
+ >>> if process.contract_config:
846
+ ... print(f"Loaded {process.contract_config.total_contracts_loaded} contracts")
847
+ """
848
+ return self._contract_config
849
+
850
+ @property
851
+ def event_bus(self) -> EventBusInmemory | EventBusKafka:
852
+ """Return the owned event bus instance.
853
+
854
+ Returns:
855
+ The event bus instance managed by this process.
856
+ """
857
+ return self._event_bus
858
+
859
+ @property
860
+ def is_running(self) -> bool:
861
+ """Return True if runtime is started.
862
+
863
+ Returns:
864
+ Boolean indicating whether the process is running.
865
+ """
866
+ return self._is_running
867
+
868
+ @property
869
+ def input_topic(self) -> str:
870
+ """Return the input topic for envelope subscription.
871
+
872
+ Returns:
873
+ The topic name to subscribe to for incoming envelopes.
874
+ """
875
+ return self._input_topic
876
+
877
+ @property
878
+ def output_topic(self) -> str:
879
+ """Return the output topic for response publishing.
880
+
881
+ Returns:
882
+ The topic name to publish responses to.
883
+ """
884
+ return self._output_topic
885
+
886
+ @property
887
+ def group_id(self) -> str:
888
+ """Return the consumer group identifier.
889
+
890
+ Computes the consumer group ID from the node identity using the canonical
891
+ format: ``{env}.{service}.{node_name}.{purpose}.{version}``
892
+
893
+ Returns:
894
+ The computed consumer group ID for this process.
895
+ """
896
+ return compute_consumer_group_id(
897
+ self._node_identity, EnumConsumerGroupPurpose.CONSUME
898
+ )
899
+
900
+ @property
901
+ def node_identity(self) -> ModelNodeIdentity:
902
+ """Return the node identity used for consumer group derivation.
903
+
904
+ The node identity contains the environment, service name, node name,
905
+ and version that uniquely identify this runtime host process within
906
+ the ONEX infrastructure.
907
+
908
+ Returns:
909
+ The immutable node identity for this process.
910
+ """
911
+ return self._node_identity
912
+
913
+ @property
914
+ def is_draining(self) -> bool:
915
+ """Return True if the process is draining pending messages during shutdown.
916
+
917
+ This property indicates whether the runtime host is in the graceful shutdown
918
+ drain period - the phase where stop() has been called, new messages are no
919
+ longer being accepted, and the process is waiting for in-flight messages to
920
+ complete before shutting down handlers and the event bus.
921
+
922
+ Drain State Transitions:
923
+ - False: Normal operation (accepting and processing messages)
924
+ - True: Drain period active (stop() called, waiting for pending messages)
925
+ - False: After drain completes and shutdown finishes
926
+
927
+ Use Cases:
928
+ - Health check reporting (indicate service is shutting down)
929
+ - Load balancer integration (remove from rotation during drain)
930
+ - Monitoring dashboards (show lifecycle state)
931
+ - Debugging shutdown behavior
932
+
933
+ Returns:
934
+ True if currently in drain period during graceful shutdown, False otherwise.
935
+ """
936
+ return self._is_draining
937
+
938
+ @property
939
+ def pending_message_count(self) -> int:
940
+ """Return the current count of in-flight messages being processed.
941
+
942
+ This property provides visibility into how many messages are currently
943
+ being processed by the runtime host. Used for graceful shutdown to
944
+ determine when it's safe to complete the shutdown process.
945
+
946
+ Atomicity Guarantees:
947
+ This property returns the raw counter value WITHOUT acquiring the
948
+ async lock (_pending_lock). This is safe because:
949
+
950
+ 1. Single int read is atomic under CPython's GIL - reading a single
951
+ integer value cannot be interrupted mid-operation
952
+ 2. The value is only used for observability/monitoring purposes
953
+ where exact precision is not required
954
+ 3. The slight possibility of reading a stale value during concurrent
955
+ increment/decrement is acceptable for monitoring use cases
956
+
957
+ Thread Safety Considerations:
958
+ While the read itself is atomic, the value may be approximate if
959
+ read occurs during concurrent message processing:
960
+ - Another coroutine may be in the middle of incrementing/decrementing
961
+ - The value represents a point-in-time snapshot, not a synchronized view
962
+ - For observability, this approximation is acceptable and avoids
963
+ lock contention that would impact performance
964
+
965
+ Use Cases (appropriate for this property):
966
+ - Logging current message count for debugging
967
+ - Metrics/observability dashboards
968
+ - Approximate health status reporting
969
+ - Monitoring drain progress during shutdown
970
+
971
+ When to use shutdown_ready() instead:
972
+ For shutdown decisions requiring precise count, use the async
973
+ shutdown_ready() method which acquires the lock to ensure no
974
+ race condition with in-flight message processing. The stop()
975
+ method uses shutdown_ready() internally for this reason.
976
+
977
+ Returns:
978
+ Current count of messages being processed. May be approximate
979
+ if reads occur during concurrent increment/decrement operations.
980
+ """
981
+ return self._pending_message_count
982
+
983
+ async def shutdown_ready(self) -> bool:
984
+ """Check if process is ready for shutdown (no pending messages).
985
+
986
+ This method acquires the pending message lock to ensure an accurate
987
+ count of in-flight messages. Use this method during graceful shutdown
988
+ to determine when all pending messages have been processed.
989
+
990
+ Returns:
991
+ True if no messages are currently being processed, False otherwise.
992
+ """
993
+ async with self._pending_lock:
994
+ return self._pending_message_count == 0
995
+
996
+ async def start(self) -> None:
997
+ """Start the runtime host.
998
+
999
+ Performs the following steps:
1000
+ 1. Validate architecture compliance (if rules configured) - OMN-1138
1001
+ 2. Start event bus (if not already started)
1002
+ 3. Discover/wire handlers:
1003
+ - If contract_paths provided: Auto-discover handlers from contracts (OMN-1133)
1004
+ - Otherwise: Wire default handlers via wiring module
1005
+ 4. Populate self._handlers from singleton registry (instantiate and initialize)
1006
+ 5. Subscribe to input topic
1007
+
1008
+ Architecture Validation (OMN-1138):
1009
+ If architecture_rules were provided at init, validation runs FIRST
1010
+ before any other startup logic. This ensures:
1011
+ - Violations are caught before resources are allocated
1012
+ - Fast feedback for CI/CD pipelines
1013
+ - Clean startup/failure without partial state
1014
+
1015
+ ERROR severity violations block startup by raising
1016
+ ArchitectureViolationError. WARNING/INFO violations are logged
1017
+ but don't block startup.
1018
+
1019
+ Contract-Based Handler Discovery (OMN-1133):
1020
+ If contract_paths were provided at init, the runtime will auto-discover
1021
+ handlers from these paths instead of using wire_default_handlers().
1022
+
1023
+ Discovery errors are logged but do not block startup, enabling
1024
+ graceful degradation where some handlers can be registered even
1025
+ if others fail to load.
1026
+
1027
+ This method is idempotent - calling start() on an already started
1028
+ process is safe and has no effect.
1029
+
1030
+ Raises:
1031
+ ArchitectureViolationError: If architecture validation fails with
1032
+ blocking violations (ERROR severity).
1033
+ """
1034
+ if self._is_running:
1035
+ logger.debug("RuntimeHostProcess already started, skipping")
1036
+ return
1037
+
1038
+ logger.info(
1039
+ "Starting RuntimeHostProcess",
1040
+ extra={
1041
+ "input_topic": self._input_topic,
1042
+ "output_topic": self._output_topic,
1043
+ "group_id": self.group_id,
1044
+ "has_contract_paths": len(self._contract_paths) > 0,
1045
+ },
1046
+ )
1047
+
1048
+ # Step 1: Validate architecture compliance FIRST (OMN-1138)
1049
+ # This runs before event bus starts or handlers are wired to ensure
1050
+ # clean failure without partial state if validation fails
1051
+ await self._validate_architecture()
1052
+
1053
+ # Step 2: Start event bus
1054
+ await self._event_bus.start()
1055
+
1056
+ # Step 3: Discover/wire handlers (OMN-1133)
1057
+ # If contract_paths provided, use ContractHandlerDiscovery to auto-discover
1058
+ # handlers from contract files. Otherwise, fall back to wire_default_handlers().
1059
+ await self._discover_or_wire_handlers()
1060
+
1061
+ # Step 4: Populate self._handlers from singleton registry
1062
+ # The wiring/discovery step registers handler classes, so we need to:
1063
+ # - Get each registered handler class from the singleton registry
1064
+ # - Instantiate the handler class
1065
+ # - Call initialize() on each handler instance with config
1066
+ # - Store the handler instance in self._handlers for routing
1067
+ await self._populate_handlers_from_registry()
1068
+
1069
+ # Step 4.1: FAIL-FAST validation - runtime MUST have at least one handler
1070
+ # A runtime with no handlers cannot process any events and is misconfigured.
1071
+ # This catches configuration issues early rather than silently starting a
1072
+ # runtime that cannot do anything useful.
1073
+ if not self._handlers:
1074
+ correlation_id = uuid4()
1075
+ context = ModelInfraErrorContext(
1076
+ transport_type=EnumInfraTransportType.RUNTIME,
1077
+ operation="validate_handlers",
1078
+ target_name="runtime_host_process",
1079
+ correlation_id=correlation_id,
1080
+ )
1081
+
1082
+ # Build informative error message with context about what was attempted
1083
+ contract_paths_info = (
1084
+ f" * contract_paths provided: {[str(p) for p in self._contract_paths]}\n"
1085
+ if self._contract_paths
1086
+ else " * contract_paths: NOT PROVIDED (using ONEX_CONTRACTS_DIR env var)\n"
1087
+ )
1088
+
1089
+ # Get registry count for additional context
1090
+ handler_registry = await self._get_handler_registry()
1091
+ registry_protocol_count = len(handler_registry.list_protocols())
1092
+
1093
+ # Build additional diagnostic info
1094
+ failed_handlers_detail = ""
1095
+ if self._failed_handlers:
1096
+ failed_handlers_detail = "FAILED HANDLERS (check these first):\n"
1097
+ for handler_type, error_msg in self._failed_handlers.items():
1098
+ failed_handlers_detail += f" * {handler_type}: {error_msg}\n"
1099
+ failed_handlers_detail += "\n"
1100
+
1101
+ raise ProtocolConfigurationError(
1102
+ "No handlers registered. The runtime cannot start without at least one handler.\n\n"
1103
+ "CURRENT CONFIGURATION:\n"
1104
+ f"{contract_paths_info}"
1105
+ f" * Registry protocol count: {registry_protocol_count}\n"
1106
+ f" * Failed handlers: {len(self._failed_handlers)}\n"
1107
+ f" * Correlation ID: {correlation_id}\n\n"
1108
+ f"{failed_handlers_detail}"
1109
+ "TROUBLESHOOTING STEPS:\n"
1110
+ " 1. Verify ONEX_CONTRACTS_DIR points to a valid contracts directory:\n"
1111
+ " - Run: echo $ONEX_CONTRACTS_DIR && ls -la $ONEX_CONTRACTS_DIR\n"
1112
+ " - Expected: Directory containing handler_contract.yaml or contract.yaml files\n\n"
1113
+ " 2. Check for handler contract files:\n"
1114
+ " - Run: find $ONEX_CONTRACTS_DIR -name 'handler_contract.yaml' -o -name 'contract.yaml'\n"
1115
+ " - If empty: No contracts found - create handler contracts or set correct path\n\n"
1116
+ " 3. Verify handler contracts have required fields:\n"
1117
+ " - Required: handler_name, handler_class, handler_type\n"
1118
+ " - Example:\n"
1119
+ " handler_name: my_handler\n"
1120
+ " handler_class: mymodule.handlers.MyHandler\n"
1121
+ " handler_type: http\n\n"
1122
+ " 4. Verify handler modules are importable:\n"
1123
+ " - Run: python -c 'from mymodule.handlers import MyHandler; print(MyHandler)'\n"
1124
+ " - Check PYTHONPATH includes your handler module paths\n\n"
1125
+ " 5. Check application logs for loader errors:\n"
1126
+ " - Look for: MODULE_NOT_FOUND (HANDLER_LOADER_010)\n"
1127
+ " - Look for: CLASS_NOT_FOUND (HANDLER_LOADER_011)\n"
1128
+ " - Look for: IMPORT_ERROR (HANDLER_LOADER_012)\n"
1129
+ " - Look for: AMBIGUOUS_CONTRACT (HANDLER_LOADER_040)\n\n"
1130
+ " 6. If using wire_handlers() manually:\n"
1131
+ " - Ensure wire_handlers() is called before RuntimeHostProcess.start()\n"
1132
+ " - Check that handlers implement ProtocolContainerAware interface\n\n"
1133
+ " 7. Docker/container environment:\n"
1134
+ " - Verify volume mounts include handler contract directories\n"
1135
+ " - Check ONEX_CONTRACTS_DIR is set in docker-compose.yml/Dockerfile\n"
1136
+ " - Run: docker exec <container> ls $ONEX_CONTRACTS_DIR\n\n"
1137
+ "For verbose handler discovery logging, set LOG_LEVEL=DEBUG.",
1138
+ context=context,
1139
+ registered_handler_count=0,
1140
+ failed_handler_count=len(self._failed_handlers),
1141
+ failed_handlers=list(self._failed_handlers.keys()),
1142
+ contract_paths=[str(p) for p in self._contract_paths],
1143
+ registry_protocol_count=registry_protocol_count,
1144
+ )
1145
+
1146
+ # Step 4.15: Load contract configurations (OMN-1519)
1147
+ # Loads handler_routing and operation_bindings from all discovered contracts.
1148
+ # Uses the same contract_paths configured for handler discovery.
1149
+ # The loaded config is accessible via self.contract_config property.
1150
+ startup_correlation_id = uuid4()
1151
+ await self._load_contract_configs(correlation_id=startup_correlation_id)
1152
+
1153
+ # Step 4.2: Wire event bus subscriptions from contracts (OMN-1621)
1154
+ # This bridges contract-declared topics to Kafka subscriptions.
1155
+ # Requires dispatch_engine to be available for message routing.
1156
+ await self._wire_event_bus_subscriptions()
1157
+
1158
+ # Step 4.3: Wire baseline subscriptions for contract discovery (OMN-1654)
1159
+ # When KAFKA_EVENTS mode is active, subscribe to platform-reserved
1160
+ # contract topics to receive registration/deregistration events.
1161
+ await self._wire_baseline_subscriptions()
1162
+
1163
+ # Step 4.5: Initialize idempotency store if configured (OMN-945)
1164
+ await self._initialize_idempotency_store()
1165
+
1166
+ # Step 5: Subscribe to input topic
1167
+ self._subscription = await self._event_bus.subscribe(
1168
+ topic=self._input_topic,
1169
+ node_identity=self._node_identity,
1170
+ on_message=self._on_message,
1171
+ purpose=EnumConsumerGroupPurpose.CONSUME,
1172
+ )
1173
+
1174
+ self._is_running = True
1175
+
1176
+ logger.info(
1177
+ "RuntimeHostProcess started successfully",
1178
+ extra={
1179
+ "input_topic": self._input_topic,
1180
+ "output_topic": self._output_topic,
1181
+ "group_id": self.group_id,
1182
+ "registered_handlers": list(self._handlers.keys()),
1183
+ },
1184
+ )
1185
+
1186
+ async def stop(self) -> None:
1187
+ """Stop the runtime host with graceful drain period.
1188
+
1189
+ Performs the following steps:
1190
+ 1. Unsubscribe from topics (stop receiving new messages)
1191
+ 2. Wait for in-flight messages to drain (up to drain_timeout_seconds)
1192
+ 3. Shutdown all registered handlers by priority (release resources)
1193
+ 4. Close event bus
1194
+
1195
+ This method is idempotent - calling stop() on an already stopped
1196
+ process is safe and has no effect.
1197
+
1198
+ Drain Period:
1199
+ After unsubscribing from topics, the process waits for in-flight
1200
+ messages to complete processing. The drain period is controlled by
1201
+ the drain_timeout_seconds configuration parameter (default: 30.0
1202
+ seconds, valid range: 1-300).
1203
+
1204
+ During the drain period:
1205
+ - No new messages are received (unsubscribed from topics)
1206
+ - Messages currently being processed are allowed to complete
1207
+ - shutdown_ready() is polled every 100ms to check completion
1208
+ - If timeout is exceeded, shutdown proceeds with a warning
1209
+
1210
+ Handler Shutdown Order:
1211
+ Handlers are shutdown in priority order, with higher priority handlers
1212
+ shutting down first. Within the same priority level, handlers are
1213
+ shutdown in parallel for performance.
1214
+
1215
+ Priority is determined by the handler's shutdown_priority() method:
1216
+ - Higher values = shutdown first
1217
+ - Handlers without shutdown_priority() get default priority of 0
1218
+
1219
+ Recommended Priority Scheme:
1220
+ - 100: Consumers (stop receiving before stopping producers)
1221
+ - 80: Active connections (close before closing pools)
1222
+ - 50: Producers (stop producing before closing pools)
1223
+ - 40: Connection pools (close last)
1224
+ - 0: Default for handlers without explicit priority
1225
+
1226
+ This ensures dependency-based ordering:
1227
+ - Consumers shutdown before producers
1228
+ - Connections shutdown before connection pools
1229
+ - Downstream resources shutdown before upstream resources
1230
+ """
1231
+ if not self._is_running:
1232
+ logger.debug("RuntimeHostProcess already stopped, skipping")
1233
+ return
1234
+
1235
+ logger.info("Stopping RuntimeHostProcess")
1236
+
1237
+ # Step 1: Unsubscribe from topics (stop receiving new messages)
1238
+ if self._subscription is not None:
1239
+ await self._subscription()
1240
+ self._subscription = None
1241
+
1242
+ # Step 1.5: Wait for in-flight messages to drain (OMN-756)
1243
+ # This allows messages currently being processed to complete
1244
+ loop = asyncio.get_running_loop()
1245
+ drain_start = loop.time()
1246
+ drain_deadline = drain_start + self._drain_timeout_seconds
1247
+ last_progress_log = drain_start
1248
+
1249
+ # Mark drain state for health check visibility (OMN-756)
1250
+ self._is_draining = True
1251
+
1252
+ # Log drain start for observability
1253
+ logger.info(
1254
+ "Starting drain period",
1255
+ extra={
1256
+ "pending_messages": self._pending_message_count,
1257
+ "drain_timeout_seconds": self._drain_timeout_seconds,
1258
+ },
1259
+ )
1260
+
1261
+ while not await self.shutdown_ready():
1262
+ remaining = drain_deadline - loop.time()
1263
+ if remaining <= 0:
1264
+ logger.warning(
1265
+ "Drain timeout exceeded, forcing shutdown",
1266
+ extra={
1267
+ "pending_messages": self._pending_message_count,
1268
+ "drain_timeout_seconds": self._drain_timeout_seconds,
1269
+ "metric.drain_timeout_exceeded": True,
1270
+ "metric.pending_at_timeout": self._pending_message_count,
1271
+ },
1272
+ )
1273
+ break
1274
+
1275
+ # Wait a short interval before checking again
1276
+ await asyncio.sleep(min(0.1, remaining))
1277
+
1278
+ # Log progress every 5 seconds during long drains for observability
1279
+ elapsed = loop.time() - drain_start
1280
+ if elapsed - (last_progress_log - drain_start) >= 5.0:
1281
+ logger.info(
1282
+ "Drain in progress",
1283
+ extra={
1284
+ "pending_messages": self._pending_message_count,
1285
+ "elapsed_seconds": round(elapsed, 2),
1286
+ "remaining_seconds": round(remaining, 2),
1287
+ },
1288
+ )
1289
+ last_progress_log = loop.time()
1290
+
1291
+ # Clear drain state after drain period completes
1292
+ self._is_draining = False
1293
+
1294
+ logger.info(
1295
+ "Drain period completed",
1296
+ extra={
1297
+ "drain_duration_seconds": loop.time() - drain_start,
1298
+ "pending_messages": self._pending_message_count,
1299
+ "metric.drain_duration": loop.time() - drain_start,
1300
+ "metric.forced_shutdown": self._pending_message_count > 0,
1301
+ },
1302
+ )
1303
+
1304
+ # Step 2: Shutdown all handlers by priority (release resources like DB/Kafka connections)
1305
+ # Delegates to ProtocolLifecycleExecutor which handles:
1306
+ # - Grouping handlers by priority (higher priority first)
1307
+ # - Parallel shutdown within priority groups for performance
1308
+ if self._handlers:
1309
+ shutdown_result = (
1310
+ await self._lifecycle_executor.shutdown_handlers_by_priority(
1311
+ self._handlers
1312
+ )
1313
+ )
1314
+
1315
+ # Log summary (ProtocolLifecycleExecutor already logs detailed info)
1316
+ logger.info(
1317
+ "Handler shutdown completed",
1318
+ extra={
1319
+ "succeeded_handlers": shutdown_result.succeeded_handlers,
1320
+ "failed_handlers": [
1321
+ f.handler_type for f in shutdown_result.failed_handlers
1322
+ ],
1323
+ "total_handlers": shutdown_result.total_count,
1324
+ "success_count": shutdown_result.success_count,
1325
+ "failure_count": shutdown_result.failure_count,
1326
+ },
1327
+ )
1328
+
1329
+ # Step 2.5: Cleanup idempotency store if initialized (OMN-945)
1330
+ await self._cleanup_idempotency_store()
1331
+
1332
+ # Step 2.6: Cleanup event bus subcontract wiring (OMN-1621)
1333
+ if self._event_bus_wiring:
1334
+ await self._event_bus_wiring.cleanup()
1335
+
1336
+ # Step 2.7: Cleanup baseline subscriptions for contract discovery (OMN-1654)
1337
+ if self._baseline_subscriptions:
1338
+ for unsubscribe in self._baseline_subscriptions:
1339
+ try:
1340
+ await unsubscribe()
1341
+ except Exception as e:
1342
+ logger.warning(
1343
+ "Failed to unsubscribe baseline subscription",
1344
+ extra={"error": str(e)},
1345
+ )
1346
+ self._baseline_subscriptions.clear()
1347
+ logger.debug("Baseline contract subscriptions cleaned up")
1348
+
1349
+ # Step 2.8: Nullify KafkaContractSource reference for proper cleanup (OMN-1654)
1350
+ self._kafka_contract_source = None
1351
+
1352
+ # Step 3: Close event bus
1353
+ await self._event_bus.close()
1354
+
1355
+ self._is_running = False
1356
+
1357
+ logger.info("RuntimeHostProcess stopped successfully")
1358
+
1359
+ def _load_handler_source_config(self) -> ModelHandlerSourceConfig:
1360
+ """Load handler source configuration from runtime config.
1361
+
1362
+ Loads the handler source mode configuration that controls how handlers
1363
+ are discovered (BOOTSTRAP, CONTRACT, or HYBRID mode).
1364
+
1365
+ Config Keys:
1366
+ handler_source_mode: "bootstrap" | "contract" | "hybrid" (default: "hybrid")
1367
+ bootstrap_expires_at: ISO-8601 datetime string (optional, UTC required)
1368
+
1369
+ Returns:
1370
+ ModelHandlerSourceConfig with validated settings.
1371
+
1372
+ Note:
1373
+ If no configuration is provided, defaults to HYBRID mode with no
1374
+ bootstrap expiry (bootstrap handlers always available as fallback).
1375
+
1376
+ .. versionadded:: 0.7.0
1377
+ Part of OMN-1095 handler source mode integration.
1378
+ """
1379
+ # Deferred imports: avoid circular dependencies at module load time
1380
+ # and reduce import overhead when this method is not called.
1381
+ from datetime import datetime
1382
+
1383
+ from pydantic import ValidationError
1384
+
1385
+ from omnibase_infra.models.handlers import ModelHandlerSourceConfig
1386
+
1387
+ config = self._config or {}
1388
+ handler_source_config = config.get("handler_source", {})
1389
+
1390
+ if isinstance(handler_source_config, dict):
1391
+ mode_str = handler_source_config.get(
1392
+ "mode", EnumHandlerSourceMode.HYBRID.value
1393
+ )
1394
+ expires_at_str = handler_source_config.get("bootstrap_expires_at")
1395
+ allow_override_raw = handler_source_config.get(
1396
+ "allow_bootstrap_override", False
1397
+ )
1398
+
1399
+ # Parse mode
1400
+ try:
1401
+ mode = EnumHandlerSourceMode(mode_str)
1402
+ except ValueError:
1403
+ logger.warning(
1404
+ "Invalid handler_source_mode, defaulting to HYBRID",
1405
+ extra={"invalid_value": mode_str},
1406
+ )
1407
+ mode = EnumHandlerSourceMode.HYBRID
1408
+
1409
+ # Parse expiry datetime
1410
+ expires_at = None
1411
+ if expires_at_str:
1412
+ try:
1413
+ expires_at = datetime.fromisoformat(str(expires_at_str))
1414
+ except ValueError:
1415
+ logger.warning(
1416
+ "Invalid bootstrap_expires_at format, ignoring",
1417
+ extra={"invalid_value": expires_at_str},
1418
+ )
1419
+
1420
+ # Construct config with validation - catch naive datetime errors
1421
+ # Note: allow_bootstrap_override coercion handled by Pydantic field validator
1422
+ try:
1423
+ return ModelHandlerSourceConfig(
1424
+ handler_source_mode=mode,
1425
+ bootstrap_expires_at=expires_at,
1426
+ allow_bootstrap_override=allow_override_raw,
1427
+ )
1428
+ except ValidationError as e:
1429
+ # Check if error is due to naive datetime (no timezone info)
1430
+ error_messages = [err.get("msg", "") for err in e.errors()]
1431
+ if any("timezone-aware" in msg for msg in error_messages):
1432
+ logger.warning(
1433
+ "bootstrap_expires_at must be timezone-aware (UTC recommended). "
1434
+ "Naive datetime provided - falling back to no expiry. "
1435
+ "Use ISO format with timezone: '2026-02-01T00:00:00+00:00' "
1436
+ "or '2026-02-01T00:00:00Z'",
1437
+ extra={
1438
+ "invalid_value": expires_at_str,
1439
+ "parsed_datetime": str(expires_at) if expires_at else None,
1440
+ },
1441
+ )
1442
+ # Fall back to config without expiry
1443
+ return ModelHandlerSourceConfig(
1444
+ handler_source_mode=mode,
1445
+ bootstrap_expires_at=None,
1446
+ allow_bootstrap_override=allow_override_raw,
1447
+ )
1448
+ # Re-raise other validation errors
1449
+ raise
1450
+
1451
+ # Default: HYBRID mode with no expiry
1452
+ return ModelHandlerSourceConfig(
1453
+ handler_source_mode=EnumHandlerSourceMode.HYBRID
1454
+ )
1455
+
1456
+ async def _resolve_handler_descriptors(self) -> list[ModelHandlerDescriptor]:
1457
+ """Resolve handler descriptors using the configured source mode.
1458
+
1459
+ Uses HandlerSourceResolver to discover handlers based on the configured
1460
+ mode (BOOTSTRAP, CONTRACT, or HYBRID). This replaces the previous
1461
+ sequential discovery logic with a unified, mode-driven approach.
1462
+
1463
+ Resolution Modes:
1464
+ - BOOTSTRAP: Only hardcoded bootstrap handlers
1465
+ - CONTRACT: Only filesystem contract-discovered handlers
1466
+ - HYBRID: Contract handlers win per-identity, bootstrap as fallback
1467
+
1468
+ Returns:
1469
+ List of resolved handler descriptors.
1470
+
1471
+ Raises:
1472
+ RuntimeHostError: If validation errors occur and fail-fast is enabled.
1473
+
1474
+ .. versionadded:: 0.7.0
1475
+ Part of OMN-1095 handler source mode integration.
1476
+ """
1477
+ from omnibase_infra.runtime.handler_bootstrap_source import (
1478
+ HandlerBootstrapSource,
1479
+ )
1480
+ from omnibase_infra.runtime.handler_source_resolver import HandlerSourceResolver
1481
+
1482
+ source_config = self._load_handler_source_config()
1483
+
1484
+ logger.info(
1485
+ "Resolving handlers with source mode",
1486
+ extra={
1487
+ "mode": source_config.handler_source_mode.value,
1488
+ "effective_mode": source_config.effective_mode.value,
1489
+ "bootstrap_expires_at": str(source_config.bootstrap_expires_at)
1490
+ if source_config.bootstrap_expires_at
1491
+ else None,
1492
+ "is_bootstrap_expired": source_config.is_bootstrap_expired,
1493
+ },
1494
+ )
1495
+
1496
+ # Create bootstrap source
1497
+ bootstrap_source = HandlerBootstrapSource()
1498
+
1499
+ # Check for KAFKA_EVENTS mode first
1500
+ if source_config.effective_mode == EnumHandlerSourceMode.KAFKA_EVENTS:
1501
+ # Create Kafka-based contract source (cache-only beta)
1502
+ # Note: Kafka subscriptions are wired separately in _wire_baseline_subscriptions()
1503
+ environment = self._get_environment_from_config()
1504
+ kafka_source = KafkaContractSource(
1505
+ environment=environment,
1506
+ graceful_mode=True,
1507
+ )
1508
+ contract_source: ProtocolContractSource = kafka_source
1509
+
1510
+ # Store reference for subscription wiring
1511
+ self._kafka_contract_source = kafka_source
1512
+
1513
+ logger.info(
1514
+ "Using KafkaContractSource for contract discovery",
1515
+ extra={
1516
+ "environment": environment,
1517
+ "mode": "KAFKA_EVENTS",
1518
+ "correlation_id": str(kafka_source.correlation_id),
1519
+ },
1520
+ )
1521
+ # Contract source needs paths - use configured paths or default
1522
+ # If no contract_paths provided, reuse bootstrap_source as placeholder
1523
+ elif self._contract_paths:
1524
+ # Use PluginLoaderContractSource which uses the simpler contract schema
1525
+ # compatible with test contracts (handler_name, handler_class, handler_type)
1526
+ contract_source = PluginLoaderContractSource(
1527
+ contract_paths=self._contract_paths,
1528
+ )
1529
+ else:
1530
+ # No contract paths provided
1531
+ if source_config.effective_mode == EnumHandlerSourceMode.CONTRACT:
1532
+ # CONTRACT mode REQUIRES contract_paths - fail fast
1533
+ raise ProtocolConfigurationError(
1534
+ "CONTRACT mode requires contract_paths to be provided. "
1535
+ "Either provide contract_paths or use HYBRID/BOOTSTRAP mode.",
1536
+ context=ModelInfraErrorContext.with_correlation(
1537
+ transport_type=EnumInfraTransportType.RUNTIME,
1538
+ operation="resolve_handler_descriptors",
1539
+ ),
1540
+ )
1541
+ # BOOTSTRAP or HYBRID mode without contract_paths - use bootstrap as fallback
1542
+ #
1543
+ # HYBRID MODE NOTE: When HYBRID mode is configured but no contract_paths
1544
+ # are provided, we reuse bootstrap_source for both the bootstrap_source
1545
+ # and contract_source parameters of HandlerSourceResolver. This means
1546
+ # discover_handlers() will be called twice on the same instance:
1547
+ # 1. Once as the "contract source" (returns bootstrap handlers)
1548
+ # 2. Once as the "bootstrap source" (returns same bootstrap handlers)
1549
+ #
1550
+ # This is intentional: HYBRID semantics require consulting both sources,
1551
+ # and with no contracts available, bootstrap provides all handlers.
1552
+ # The HandlerSourceResolver's HYBRID merge logic (contract wins per-identity,
1553
+ # bootstrap as fallback) produces the correct result since both sources
1554
+ # return identical handlers. The outcome is functionally equivalent to
1555
+ # BOOTSTRAP mode but maintains HYBRID logging/metrics for observability.
1556
+ #
1557
+ # DO NOT "optimize" this to skip the second call - it would break
1558
+ # metrics expectations (contract_handler_count would not be logged)
1559
+ # and change HYBRID mode semantics. See test_bootstrap_source_integration.py
1560
+ # test_bootstrap_source_called_during_start() for the verification test.
1561
+ logger.debug(
1562
+ "HYBRID mode: No contract_paths provided, using bootstrap source "
1563
+ "as fallback for contract source",
1564
+ extra={
1565
+ "mode": source_config.effective_mode.value,
1566
+ "behavior": "bootstrap_source_reused",
1567
+ },
1568
+ )
1569
+ contract_source = bootstrap_source
1570
+
1571
+ # Create resolver with the effective mode (handles expiry enforcement)
1572
+ resolver = HandlerSourceResolver(
1573
+ bootstrap_source=bootstrap_source,
1574
+ contract_source=contract_source,
1575
+ mode=source_config.effective_mode,
1576
+ allow_bootstrap_override=source_config.allow_bootstrap_override,
1577
+ )
1578
+
1579
+ # Resolve handlers
1580
+ result = await resolver.resolve_handlers()
1581
+
1582
+ # Log resolution results
1583
+ logger.info(
1584
+ "Handler resolution completed",
1585
+ extra={
1586
+ "descriptor_count": len(result.descriptors),
1587
+ "validation_error_count": len(result.validation_errors),
1588
+ "mode": source_config.effective_mode.value,
1589
+ },
1590
+ )
1591
+
1592
+ # Log validation errors but continue with valid descriptors (graceful degradation)
1593
+ # This allows the runtime to start with bootstrap handlers even if some contracts fail
1594
+ if result.validation_errors:
1595
+ error_summary = "; ".join(
1596
+ f"{e.handler_identity.handler_id or 'unknown'}: {e.message}"
1597
+ for e in result.validation_errors[:5] # Show first 5
1598
+ )
1599
+ if len(result.validation_errors) > 5:
1600
+ error_summary += f" ... and {len(result.validation_errors) - 5} more"
1601
+
1602
+ logger.warning(
1603
+ "Handler resolution completed with validation errors (continuing with valid handlers)",
1604
+ extra={
1605
+ "error_count": len(result.validation_errors),
1606
+ "valid_descriptor_count": len(result.descriptors),
1607
+ "error_summary": error_summary,
1608
+ },
1609
+ )
1610
+
1611
+ return list(result.descriptors)
1612
+
1613
+ async def _discover_or_wire_handlers(self) -> None:
1614
+ """Discover and register handlers for the runtime.
1615
+
1616
+ This method implements the handler discovery/wiring step (Step 3) of the
1617
+ start() sequence. It uses HandlerSourceResolver to discover handlers
1618
+ based on the configured source mode.
1619
+
1620
+ Handler Source Modes (OMN-1095):
1621
+ - BOOTSTRAP: Only hardcoded bootstrap handlers (fast, no filesystem I/O)
1622
+ - CONTRACT: Only filesystem contract-discovered handlers
1623
+ - HYBRID: Contract handlers win per-identity, bootstrap as fallback
1624
+
1625
+ The mode is configured via runtime config:
1626
+ handler_source:
1627
+ mode: "hybrid" # bootstrap|contract|hybrid
1628
+ bootstrap_expires_at: "2026-02-01T00:00:00Z" # Optional, UTC
1629
+
1630
+ The discovery/wiring step registers handler CLASSES with the handler registry.
1631
+ The subsequent _populate_handlers_from_registry() step instantiates and
1632
+ initializes these handler classes.
1633
+
1634
+ .. versionchanged:: 0.7.0
1635
+ Replaced sequential bootstrap+contract discovery with unified
1636
+ HandlerSourceResolver-based resolution (OMN-1095).
1637
+ """
1638
+ # Resolve handlers using configured source mode
1639
+ descriptors = await self._resolve_handler_descriptors()
1640
+
1641
+ # Get handler registry for registration
1642
+ handler_registry = await self._get_handler_registry()
1643
+
1644
+ registered_count = 0
1645
+ error_count = 0
1646
+
1647
+ for descriptor in descriptors:
1648
+ try:
1649
+ # Extract protocol type from handler_id
1650
+ # Handler IDs use "proto." prefix for identity matching (e.g., "proto.consul" -> "consul")
1651
+ # Contract handlers also use this prefix for HYBRID mode resolution
1652
+ # removeprefix() is a no-op if prefix doesn't exist, so handlers without prefix keep their name as-is
1653
+ protocol_type = descriptor.handler_id.removeprefix(
1654
+ f"{HANDLER_IDENTITY_PREFIX}."
1655
+ )
1656
+
1657
+ # Import the handler class from fully qualified path
1658
+ handler_class_path = descriptor.handler_class
1659
+ if handler_class_path is None:
1660
+ logger.warning(
1661
+ "Handler descriptor missing handler_class, skipping",
1662
+ extra={
1663
+ "handler_id": descriptor.handler_id,
1664
+ "handler_name": descriptor.name,
1665
+ },
1666
+ )
1667
+ error_count += 1
1668
+ continue
1669
+
1670
+ # Import class using rsplit pattern
1671
+ if "." not in handler_class_path:
1672
+ logger.error(
1673
+ "Invalid handler class path (must be fully qualified): %s",
1674
+ handler_class_path,
1675
+ extra={"handler_id": descriptor.handler_id},
1676
+ )
1677
+ error_count += 1
1678
+ continue
1679
+
1680
+ module_path, class_name = handler_class_path.rsplit(".", 1)
1681
+ module = importlib.import_module(module_path)
1682
+ handler_cls = getattr(module, class_name)
1683
+
1684
+ # Verify handler_cls is actually a class before registration
1685
+ if not isinstance(handler_cls, type):
1686
+ logger.error(
1687
+ "Handler class path does not resolve to a class type",
1688
+ extra={
1689
+ "handler_id": descriptor.handler_id,
1690
+ "handler_class_path": handler_class_path,
1691
+ "resolved_type": type(handler_cls).__name__,
1692
+ },
1693
+ )
1694
+ error_count += 1
1695
+ continue
1696
+
1697
+ # Register with handler registry
1698
+ handler_registry.register(protocol_type, handler_cls)
1699
+
1700
+ # Store descriptor for later use during initialization
1701
+ self._handler_descriptors[protocol_type] = descriptor
1702
+
1703
+ registered_count += 1
1704
+ logger.debug(
1705
+ "Registered handler from descriptor",
1706
+ extra={
1707
+ "handler_id": descriptor.handler_id,
1708
+ "protocol_type": protocol_type,
1709
+ "handler_class": handler_class_path,
1710
+ },
1711
+ )
1712
+
1713
+ except (ImportError, AttributeError):
1714
+ logger.exception(
1715
+ "Failed to import handler",
1716
+ extra={
1717
+ "handler_id": descriptor.handler_id,
1718
+ "handler_class": descriptor.handler_class,
1719
+ },
1720
+ )
1721
+ error_count += 1
1722
+ except Exception:
1723
+ logger.exception(
1724
+ "Unexpected error registering handler",
1725
+ extra={
1726
+ "handler_id": descriptor.handler_id,
1727
+ "handler_class": descriptor.handler_class,
1728
+ },
1729
+ )
1730
+ error_count += 1
1731
+
1732
+ logger.info(
1733
+ "Handler discovery completed",
1734
+ extra={
1735
+ "registered_count": registered_count,
1736
+ "error_count": error_count,
1737
+ "total_descriptors": len(descriptors),
1738
+ },
1739
+ )
1740
+
1741
+ async def _populate_handlers_from_registry(self) -> None:
1742
+ """Populate self._handlers from handler registry (container or singleton).
1743
+
1744
+ This method bridges the gap between the wiring module (which registers
1745
+ handler CLASSES to the registry) and the RuntimeHostProcess
1746
+ (which needs handler INSTANCES in self._handlers for routing).
1747
+
1748
+ Registry Resolution:
1749
+ - If handler_registry provided: Uses pre-resolved registry
1750
+ - If no handler_registry: Falls back to singleton get_handler_registry()
1751
+
1752
+ For each registered handler type in the registry:
1753
+ 1. Skip if handler type is already registered (e.g., by tests)
1754
+ 2. Get the handler class from the registry
1755
+ 3. Instantiate the handler class
1756
+ 4. Call initialize() on the handler instance with self._config
1757
+ 5. Store the handler instance in self._handlers
1758
+
1759
+ This ensures that after start() is called, self._handlers contains
1760
+ fully initialized handler instances ready for envelope routing.
1761
+
1762
+ Note: Handlers already in self._handlers (e.g., injected by tests via
1763
+ register_handler() or patch.object()) are preserved and not overwritten.
1764
+ """
1765
+ # Get handler registry (pre-resolved, container, or singleton)
1766
+ handler_registry = await self._get_handler_registry()
1767
+ registered_types = handler_registry.list_protocols()
1768
+
1769
+ logger.debug(
1770
+ "Populating handlers from registry",
1771
+ extra={
1772
+ "registered_types": registered_types,
1773
+ "existing_handlers": list(self._handlers.keys()),
1774
+ },
1775
+ )
1776
+
1777
+ # Get or create container once for all handlers to share
1778
+ # This ensures all handlers have access to the same DI container
1779
+ container = self._get_or_create_container()
1780
+
1781
+ for handler_type in registered_types:
1782
+ # Skip if handler is already registered (e.g., by tests or explicit registration)
1783
+ if handler_type in self._handlers:
1784
+ logger.debug(
1785
+ "Handler already registered, skipping",
1786
+ extra={
1787
+ "handler_type": handler_type,
1788
+ "existing_handler_class": type(
1789
+ self._handlers[handler_type]
1790
+ ).__name__,
1791
+ },
1792
+ )
1793
+ continue
1794
+
1795
+ try:
1796
+ # Get handler class from singleton registry
1797
+ handler_cls: type[ProtocolContainerAware] = handler_registry.get(
1798
+ handler_type
1799
+ )
1800
+
1801
+ # Instantiate the handler with container for dependency injection
1802
+ # ProtocolContainerAware defines __init__(container: ModelONEXContainer)
1803
+ handler_instance: ProtocolContainerAware = handler_cls(
1804
+ container=container
1805
+ )
1806
+
1807
+ # Call initialize() if the handler has this method
1808
+ # Handlers may require async initialization with config
1809
+ if hasattr(handler_instance, "initialize"):
1810
+ # Build effective config: contract config as base, runtime overrides on top
1811
+ # This enables contracts to provide handler-specific defaults while
1812
+ # allowing runtime/deploy-time customization without touching contracts
1813
+ effective_config: dict[str, object] = {}
1814
+ config_source = "runtime_only"
1815
+
1816
+ # Layer 1: Contract config as baseline (if descriptor exists with config)
1817
+ descriptor = self._handler_descriptors.get(handler_type)
1818
+ if descriptor and descriptor.contract_config:
1819
+ effective_config.update(descriptor.contract_config)
1820
+ config_source = "contract_only"
1821
+
1822
+ # Layer 2: Runtime config overrides
1823
+ # Runtime config takes precedence, enabling deploy-time customization
1824
+ if self._config:
1825
+ effective_config.update(self._config)
1826
+ if descriptor and descriptor.contract_config:
1827
+ config_source = "contract+runtime_override"
1828
+
1829
+ # Pass empty dict if no config, not None
1830
+ # Handlers expect dict interface (e.g., config.get("key"))
1831
+ await handler_instance.initialize(effective_config)
1832
+
1833
+ logger.debug(
1834
+ "Handler initialized with effective config",
1835
+ extra={
1836
+ "handler_type": handler_type,
1837
+ "config_source": config_source,
1838
+ "effective_config_keys": list(effective_config.keys()),
1839
+ "has_contract_config": bool(
1840
+ descriptor and descriptor.contract_config
1841
+ ),
1842
+ "has_runtime_config": bool(self._config),
1843
+ },
1844
+ )
1845
+
1846
+ # Store the handler instance for routing
1847
+ self._handlers[handler_type] = handler_instance
1848
+
1849
+ logger.debug(
1850
+ "Handler instantiated and initialized",
1851
+ extra={
1852
+ "handler_type": handler_type,
1853
+ "handler_class": handler_cls.__name__,
1854
+ },
1855
+ )
1856
+
1857
+ except Exception as e:
1858
+ # Track the failure for health_check() reporting
1859
+ self._failed_handlers[handler_type] = str(e)
1860
+
1861
+ # Log error but continue with other handlers
1862
+ # This allows partial handler availability
1863
+ correlation_id = uuid4()
1864
+ context = ModelInfraErrorContext(
1865
+ transport_type=EnumInfraTransportType.RUNTIME,
1866
+ operation="populate_handlers",
1867
+ target_name=handler_type,
1868
+ correlation_id=correlation_id,
1869
+ )
1870
+ infra_error = RuntimeHostError(
1871
+ f"Failed to instantiate handler for type {handler_type}: {e}",
1872
+ context=context,
1873
+ )
1874
+ infra_error.__cause__ = e
1875
+
1876
+ logger.warning(
1877
+ "Failed to instantiate handler, skipping",
1878
+ extra={
1879
+ "handler_type": handler_type,
1880
+ "error": str(e),
1881
+ "correlation_id": str(correlation_id),
1882
+ },
1883
+ )
1884
+
1885
+ logger.info(
1886
+ "Handlers populated from registry",
1887
+ extra={
1888
+ "populated_handlers": list(self._handlers.keys()),
1889
+ "total_count": len(self._handlers),
1890
+ },
1891
+ )
1892
+
1893
+ async def _load_contract_configs(self, correlation_id: UUID) -> None:
1894
+ """Load contract configurations from all discovered contracts.
1895
+
1896
+ Uses RuntimeContractConfigLoader to scan for contract.yaml files and
1897
+ load handler_routing and operation_bindings subcontracts into a
1898
+ consolidated configuration.
1899
+
1900
+ This method is called during start() after handler discovery but before
1901
+ event bus subscriptions are wired. The loaded config is stored in
1902
+ self._contract_config and accessible via the contract_config property.
1903
+
1904
+ Error Handling:
1905
+ Individual contract load failures are logged but do not stop the
1906
+ overall loading process. This enables graceful degradation where
1907
+ some contracts can be loaded even if others fail. Errors are
1908
+ collected in the ModelRuntimeContractConfig for introspection.
1909
+
1910
+ Args:
1911
+ correlation_id: Correlation ID for tracing this load operation.
1912
+
1913
+ Part of OMN-1519: Runtime contract config loader integration.
1914
+ """
1915
+ # Skip if no contract paths configured
1916
+ if not self._contract_paths:
1917
+ logger.debug(
1918
+ "No contract paths configured, skipping contract config loading",
1919
+ extra={"correlation_id": str(correlation_id)},
1920
+ )
1921
+ return
1922
+
1923
+ # Create loader - no namespace restrictions by default
1924
+ # (namespace allowlisting can be added via constructor parameter if needed)
1925
+ loader = RuntimeContractConfigLoader()
1926
+
1927
+ # Load all contracts from configured paths
1928
+ self._contract_config = loader.load_all_contracts(
1929
+ search_paths=self._contract_paths,
1930
+ correlation_id=correlation_id,
1931
+ )
1932
+
1933
+ # Log summary at INFO level
1934
+ if self._contract_config.total_errors > 0:
1935
+ logger.warning(
1936
+ "Contract config loading completed with errors",
1937
+ extra={
1938
+ "total_contracts_found": self._contract_config.total_contracts_found,
1939
+ "total_contracts_loaded": self._contract_config.total_contracts_loaded,
1940
+ "total_errors": self._contract_config.total_errors,
1941
+ "success_rate": f"{self._contract_config.success_rate:.1%}",
1942
+ "correlation_id": str(correlation_id),
1943
+ "error_paths": [
1944
+ str(p) for p in self._contract_config.error_messages
1945
+ ],
1946
+ },
1947
+ )
1948
+ else:
1949
+ logger.info(
1950
+ "Contract config loading completed successfully",
1951
+ extra={
1952
+ "total_contracts_found": self._contract_config.total_contracts_found,
1953
+ "total_contracts_loaded": self._contract_config.total_contracts_loaded,
1954
+ "handler_routing_count": len(
1955
+ self._contract_config.handler_routing_configs
1956
+ ),
1957
+ "operation_bindings_count": len(
1958
+ self._contract_config.operation_bindings_configs
1959
+ ),
1960
+ "correlation_id": str(correlation_id),
1961
+ },
1962
+ )
1963
+
1964
+ async def _get_handler_registry(self) -> RegistryProtocolBinding:
1965
+ """Get handler registry (pre-resolved, container, or singleton).
1966
+
1967
+ Resolution order:
1968
+ 1. If handler_registry was provided to __init__, uses it (cached)
1969
+ 2. If container was provided and has RegistryProtocolBinding, resolves from container
1970
+ 3. Falls back to singleton via get_handler_registry()
1971
+
1972
+ Caching Behavior:
1973
+ The resolved registry is cached after the first successful resolution.
1974
+ Subsequent calls return the cached instance without re-resolving from
1975
+ the container or re-fetching the singleton. This ensures consistent
1976
+ registry usage throughout the runtime's lifecycle and avoids redundant
1977
+ resolution operations.
1978
+
1979
+ Returns:
1980
+ RegistryProtocolBinding instance.
1981
+ """
1982
+ if self._handler_registry is not None:
1983
+ # Use pre-resolved registry from constructor
1984
+ return self._handler_registry
1985
+
1986
+ # Try to resolve from container if provided
1987
+ if self._container is not None and self._container.service_registry is not None:
1988
+ try:
1989
+ resolved_registry: RegistryProtocolBinding = (
1990
+ await self._container.service_registry.resolve_service(
1991
+ RegistryProtocolBinding
1992
+ )
1993
+ )
1994
+ # Cache the resolved registry for subsequent calls
1995
+ self._handler_registry = resolved_registry
1996
+ logger.debug(
1997
+ "Handler registry resolved from container",
1998
+ extra={"registry_type": type(resolved_registry).__name__},
1999
+ )
2000
+ return resolved_registry
2001
+ except (
2002
+ RuntimeError,
2003
+ ValueError,
2004
+ KeyError,
2005
+ AttributeError,
2006
+ LookupError,
2007
+ ) as e:
2008
+ # Container resolution failed, fall through to singleton
2009
+ logger.debug(
2010
+ "Container registry resolution failed, falling back to singleton",
2011
+ extra={"error": str(e)},
2012
+ )
2013
+
2014
+ # Graceful degradation: fall back to singleton pattern when container unavailable
2015
+ from omnibase_infra.runtime.handler_registry import get_handler_registry
2016
+
2017
+ singleton_registry = get_handler_registry()
2018
+ # Cache for consistency with container resolution path
2019
+ self._handler_registry = singleton_registry
2020
+ logger.debug(
2021
+ "Handler registry resolved from singleton",
2022
+ extra={"registry_type": type(singleton_registry).__name__},
2023
+ )
2024
+ return singleton_registry
2025
+
2026
+ async def _on_message(self, message: ModelEventMessage) -> None:
2027
+ """Handle incoming message from event bus subscription.
2028
+
2029
+ This is the callback invoked by the event bus when a message arrives
2030
+ on the input topic. It deserializes the envelope and routes it.
2031
+
2032
+ The method tracks pending messages for graceful shutdown support (OMN-756).
2033
+ The pending message count is incremented at the start of processing and
2034
+ decremented when processing completes (success or failure).
2035
+
2036
+ Args:
2037
+ message: The event message containing the envelope payload.
2038
+ """
2039
+ # Increment pending message count (OMN-756: graceful shutdown tracking)
2040
+ async with self._pending_lock:
2041
+ self._pending_message_count += 1
2042
+
2043
+ try:
2044
+ # Deserialize envelope from message value
2045
+ envelope = json.loads(message.value.decode("utf-8"))
2046
+ await self._handle_envelope(envelope)
2047
+ except json.JSONDecodeError as e:
2048
+ # Create infrastructure error context for tracing
2049
+ correlation_id = uuid4()
2050
+ context = ModelInfraErrorContext(
2051
+ transport_type=EnumInfraTransportType.RUNTIME,
2052
+ operation="decode_envelope",
2053
+ target_name=message.topic,
2054
+ correlation_id=correlation_id,
2055
+ )
2056
+ # Chain the error with infrastructure context
2057
+ infra_error = RuntimeHostError(
2058
+ f"Failed to decode JSON envelope from message: {e}",
2059
+ context=context,
2060
+ )
2061
+ infra_error.__cause__ = e # Proper error chaining
2062
+
2063
+ logger.exception(
2064
+ "Failed to decode envelope from message",
2065
+ extra={
2066
+ "error": str(e),
2067
+ "topic": message.topic,
2068
+ "offset": message.offset,
2069
+ "correlation_id": str(correlation_id),
2070
+ },
2071
+ )
2072
+ # Publish error response for malformed messages
2073
+ error_response = self._create_error_response(
2074
+ error=f"Invalid JSON in message: {e}",
2075
+ correlation_id=correlation_id,
2076
+ )
2077
+ await self._publish_envelope_safe(error_response, self._output_topic)
2078
+ finally:
2079
+ # Decrement pending message count (OMN-756: graceful shutdown tracking)
2080
+ async with self._pending_lock:
2081
+ self._pending_message_count -= 1
2082
+
2083
+ async def _handle_envelope(self, envelope: dict[str, object]) -> None:
2084
+ """Route envelope to appropriate handler.
2085
+
2086
+ Validates envelope before dispatch and routes it to the appropriate
2087
+ registered handler. Publishes the response to the output topic.
2088
+
2089
+ Validation (performed before dispatch):
2090
+ 1. Operation presence and type validation
2091
+ 2. Handler prefix validation against registry
2092
+ 3. Payload requirement validation for specific operations
2093
+ 4. Correlation ID normalization to UUID
2094
+
2095
+ Args:
2096
+ envelope: Dict with 'operation', 'payload', optional 'correlation_id',
2097
+ and 'handler_type'.
2098
+ """
2099
+ # Pre-validation: Get correlation_id for error responses if validation fails
2100
+ # This handles the case where validation itself throws before normalizing
2101
+ pre_validation_correlation_id = normalize_correlation_id(
2102
+ envelope.get("correlation_id")
2103
+ )
2104
+
2105
+ # Step 1: Validate envelope BEFORE dispatch
2106
+ # This validates operation, prefix, payload requirements, and normalizes correlation_id
2107
+ try:
2108
+ validate_envelope(envelope, await self._get_handler_registry())
2109
+ except EnvelopeValidationError as e:
2110
+ # Validation failed - missing operation or payload
2111
+ error_response = self._create_error_response(
2112
+ error=str(e),
2113
+ correlation_id=pre_validation_correlation_id,
2114
+ )
2115
+ await self._publish_envelope_safe(error_response, self._output_topic)
2116
+ logger.warning(
2117
+ "Envelope validation failed",
2118
+ extra={
2119
+ "error": str(e),
2120
+ "correlation_id": str(pre_validation_correlation_id),
2121
+ "error_type": "EnvelopeValidationError",
2122
+ },
2123
+ )
2124
+ return
2125
+ except UnknownHandlerTypeError as e:
2126
+ # Unknown handler prefix - hard failure
2127
+ error_response = self._create_error_response(
2128
+ error=str(e),
2129
+ correlation_id=pre_validation_correlation_id,
2130
+ )
2131
+ await self._publish_envelope_safe(error_response, self._output_topic)
2132
+ logger.warning(
2133
+ "Unknown handler type in envelope",
2134
+ extra={
2135
+ "error": str(e),
2136
+ "correlation_id": str(pre_validation_correlation_id),
2137
+ "error_type": "UnknownHandlerTypeError",
2138
+ },
2139
+ )
2140
+ return
2141
+
2142
+ # After validation, correlation_id is guaranteed to be a UUID
2143
+ correlation_id = envelope.get("correlation_id")
2144
+ if not isinstance(correlation_id, UUID):
2145
+ correlation_id = pre_validation_correlation_id
2146
+
2147
+ # Step 2: Check idempotency before handler dispatch (OMN-945)
2148
+ # This prevents duplicate processing under at-least-once delivery
2149
+ if not await self._check_idempotency(envelope, correlation_id):
2150
+ # Duplicate detected - response already published, return early
2151
+ return
2152
+
2153
+ # Extract operation (validated to exist and be a string)
2154
+ operation = str(envelope.get("operation"))
2155
+
2156
+ # Determine handler_type from envelope
2157
+ # If handler_type not explicit, extract from operation (e.g., "http.get" -> "http")
2158
+ handler_type = envelope.get("handler_type")
2159
+ if handler_type is None:
2160
+ handler_type = operation.split(".")[0]
2161
+
2162
+ # Get handler from registry
2163
+ handler = self._handlers.get(str(handler_type))
2164
+
2165
+ if handler is None:
2166
+ # Handler not instantiated (different from unknown prefix - validation already passed)
2167
+ # This can happen if handler registration failed during start()
2168
+ context = ModelInfraErrorContext(
2169
+ transport_type=EnumInfraTransportType.RUNTIME,
2170
+ operation=str(operation),
2171
+ target_name=str(handler_type),
2172
+ correlation_id=correlation_id,
2173
+ )
2174
+
2175
+ # Create structured error for logging and tracking
2176
+ routing_error = RuntimeHostError(
2177
+ f"Handler type {handler_type!r} is registered but not instantiated",
2178
+ context=context,
2179
+ )
2180
+
2181
+ # Publish error response for envelope-based error handling
2182
+ error_response = self._create_error_response(
2183
+ error=str(routing_error),
2184
+ correlation_id=correlation_id,
2185
+ )
2186
+ await self._publish_envelope_safe(error_response, self._output_topic)
2187
+
2188
+ # Log with structured error
2189
+ logger.warning(
2190
+ "Handler registered but not instantiated",
2191
+ extra={
2192
+ "handler_type": handler_type,
2193
+ "correlation_id": str(correlation_id),
2194
+ "operation": operation,
2195
+ "registered_handlers": list(self._handlers.keys()),
2196
+ "error": str(routing_error),
2197
+ },
2198
+ )
2199
+ return
2200
+
2201
+ # Execute handler
2202
+ try:
2203
+ # Handler expected to have async execute(envelope) method
2204
+ # NOTE: MVP adapters use legacy execute(envelope: dict) signature.
2205
+ # TODO(OMN-40): Migrate handlers to new protocol signature execute(request, operation_config)
2206
+ response = await handler.execute(envelope) # type: ignore[call-arg] # NOTE: legacy signature
2207
+
2208
+ # Ensure response has correlation_id
2209
+ # Make a copy to avoid mutating handler's internal state
2210
+ if isinstance(response, dict):
2211
+ response = dict(response)
2212
+ if "correlation_id" not in response:
2213
+ response["correlation_id"] = correlation_id
2214
+
2215
+ await self._publish_envelope_safe(response, self._output_topic)
2216
+
2217
+ logger.debug(
2218
+ "Handler executed successfully",
2219
+ extra={
2220
+ "handler_type": handler_type,
2221
+ "correlation_id": str(correlation_id),
2222
+ "operation": operation,
2223
+ },
2224
+ )
2225
+
2226
+ except Exception as e:
2227
+ # Create infrastructure error context for handler execution failure
2228
+ context = ModelInfraErrorContext(
2229
+ transport_type=EnumInfraTransportType.RUNTIME,
2230
+ operation="handler_execution",
2231
+ target_name=str(handler_type),
2232
+ correlation_id=correlation_id,
2233
+ )
2234
+ # Chain the error with infrastructure context
2235
+ infra_error = RuntimeHostError(
2236
+ f"Handler execution failed for {handler_type}: {e}",
2237
+ context=context,
2238
+ )
2239
+ infra_error.__cause__ = e # Proper error chaining
2240
+
2241
+ # Handler execution failed - produce failure envelope
2242
+ error_response = self._create_error_response(
2243
+ error=str(e),
2244
+ correlation_id=correlation_id,
2245
+ )
2246
+ await self._publish_envelope_safe(error_response, self._output_topic)
2247
+
2248
+ logger.exception(
2249
+ "Handler execution failed",
2250
+ extra={
2251
+ "handler_type": handler_type,
2252
+ "correlation_id": str(correlation_id),
2253
+ "operation": operation,
2254
+ "error": str(e),
2255
+ "infra_error": str(infra_error),
2256
+ },
2257
+ )
2258
+
2259
+ def _create_error_response(
2260
+ self,
2261
+ error: str,
2262
+ correlation_id: UUID | None,
2263
+ ) -> dict[str, object]:
2264
+ """Create a standardized error response envelope.
2265
+
2266
+ Args:
2267
+ error: Error message to include.
2268
+ correlation_id: Correlation ID to preserve for tracking.
2269
+
2270
+ Returns:
2271
+ Error response dict with success=False and error details.
2272
+ """
2273
+ # Use correlation_id or generate a new one, keeping as UUID for internal use
2274
+ final_correlation_id = correlation_id or uuid4()
2275
+ return {
2276
+ "success": False,
2277
+ "status": "error",
2278
+ "error": error,
2279
+ "correlation_id": final_correlation_id,
2280
+ }
2281
+
2282
+ def _serialize_envelope(
2283
+ self, envelope: dict[str, object] | BaseModel
2284
+ ) -> dict[str, object]:
2285
+ """Recursively convert UUID objects to strings for JSON serialization.
2286
+
2287
+ Handles both dict envelopes and Pydantic models (e.g., ModelDuplicateResponse).
2288
+
2289
+ Args:
2290
+ envelope: Envelope dict or Pydantic model that may contain UUID objects.
2291
+
2292
+ Returns:
2293
+ New dict with all UUIDs converted to strings.
2294
+ """
2295
+ # Convert Pydantic models to dict first, ensuring type safety
2296
+ envelope_dict: JsonDict = (
2297
+ envelope.model_dump() if isinstance(envelope, BaseModel) else envelope
2298
+ )
2299
+
2300
+ def convert_value(value: object) -> object:
2301
+ if isinstance(value, UUID):
2302
+ return str(value)
2303
+ elif isinstance(value, dict):
2304
+ return {k: convert_value(v) for k, v in value.items()}
2305
+ elif isinstance(value, list):
2306
+ return [convert_value(item) for item in value]
2307
+ return value
2308
+
2309
+ return {k: convert_value(v) for k, v in envelope_dict.items()}
2310
+
2311
+ async def _publish_envelope_safe(
2312
+ self, envelope: dict[str, object] | BaseModel, topic: str
2313
+ ) -> None:
2314
+ """Publish envelope with UUID serialization support.
2315
+
2316
+ Converts any UUID objects to strings before publishing to ensure
2317
+ JSON serialization works correctly.
2318
+
2319
+ Args:
2320
+ envelope: Envelope dict or Pydantic model (may contain UUID objects).
2321
+ topic: Target topic to publish to.
2322
+ """
2323
+ # Always serialize UUIDs upfront - single code path
2324
+ json_safe_envelope = self._serialize_envelope(envelope)
2325
+ await self._event_bus.publish_envelope(json_safe_envelope, topic)
2326
+
2327
+ async def health_check(self) -> dict[str, object]:
2328
+ """Return health check status.
2329
+
2330
+ Returns:
2331
+ Dictionary with health status information:
2332
+ - healthy: Overall health status (True only if running,
2333
+ event bus healthy, no handlers failed to instantiate,
2334
+ all registered handlers are healthy, AND at least one
2335
+ handler is registered - a runtime without handlers is useless)
2336
+ - degraded: True when process is running but some handlers
2337
+ failed to instantiate. Indicates partial functionality -
2338
+ the system is operational but not at full capacity.
2339
+ - is_running: Whether the process is running
2340
+ - is_draining: Whether the process is in graceful shutdown drain
2341
+ period, waiting for in-flight messages to complete (OMN-756).
2342
+ Load balancers can use this to remove the service from rotation
2343
+ before the container becomes unhealthy.
2344
+ - pending_message_count: Number of messages currently being
2345
+ processed. Useful for monitoring drain progress and determining
2346
+ when the service is ready for shutdown.
2347
+ - event_bus: Event bus health status (if running)
2348
+ - event_bus_healthy: Boolean indicating event bus health
2349
+ - failed_handlers: Dict of handler_type -> error message for
2350
+ handlers that failed to instantiate during start()
2351
+ - registered_handlers: List of successfully registered handler types
2352
+ - handlers: Dict of handler_type -> health status for each
2353
+ registered handler
2354
+ - no_handlers_registered: True if no handlers are registered.
2355
+ This indicates a critical configuration issue - the runtime
2356
+ cannot process any events without handlers (OMN-1317).
2357
+
2358
+ Health State Matrix:
2359
+ - healthy=True, degraded=False: Fully operational
2360
+ - healthy=False, degraded=True: Running with reduced functionality
2361
+ - healthy=False, degraded=False: Not running, event bus unhealthy,
2362
+ or no handlers registered (critical configuration issue)
2363
+ - healthy=False, no_handlers_registered=True: Configuration error,
2364
+ runtime cannot process events
2365
+
2366
+ Drain State:
2367
+ When is_draining=True, the service is shutting down gracefully:
2368
+ - New messages are no longer being accepted
2369
+ - In-flight messages are being allowed to complete
2370
+ - Health status may still show healthy during drain
2371
+ - Load balancers should remove the service from rotation
2372
+
2373
+ Note:
2374
+ Handler health checks are performed concurrently using asyncio.gather()
2375
+ with individual timeouts (configurable via health_check_timeout_seconds
2376
+ config, default: 5.0 seconds) to prevent slow handlers from blocking.
2377
+ """
2378
+ # Get event bus health if available
2379
+ event_bus_health: dict[str, object] = {}
2380
+ event_bus_healthy = False
2381
+
2382
+ try:
2383
+ event_bus_health = await self._event_bus.health_check()
2384
+ # Explicit type guard (not assert) for production safety
2385
+ # health_check() returns dict per contract
2386
+ if not isinstance(event_bus_health, dict):
2387
+ context = ModelInfraErrorContext(
2388
+ transport_type=EnumInfraTransportType.RUNTIME,
2389
+ operation="health_check",
2390
+ )
2391
+ raise ProtocolConfigurationError(
2392
+ f"health_check() must return dict, got {type(event_bus_health).__name__}",
2393
+ context=context,
2394
+ )
2395
+ event_bus_healthy = bool(event_bus_health.get("healthy", False))
2396
+ except Exception as e:
2397
+ # Create infrastructure error context for health check failure
2398
+ correlation_id = uuid4()
2399
+ context = ModelInfraErrorContext(
2400
+ transport_type=EnumInfraTransportType.RUNTIME,
2401
+ operation="health_check",
2402
+ target_name="event_bus",
2403
+ correlation_id=correlation_id,
2404
+ )
2405
+ # Chain the error with infrastructure context
2406
+ infra_error = RuntimeHostError(
2407
+ f"Event bus health check failed: {e}",
2408
+ context=context,
2409
+ )
2410
+ infra_error.__cause__ = e # Proper error chaining
2411
+
2412
+ logger.warning(
2413
+ "Event bus health check failed",
2414
+ extra={
2415
+ "error": str(e),
2416
+ "correlation_id": str(correlation_id),
2417
+ "infra_error": str(infra_error),
2418
+ },
2419
+ exc_info=True,
2420
+ )
2421
+ event_bus_health = {"error": str(e), "correlation_id": str(correlation_id)}
2422
+ event_bus_healthy = False
2423
+
2424
+ # Check handler health for all registered handlers concurrently
2425
+ # Delegates to ProtocolLifecycleExecutor with configured timeout to prevent blocking
2426
+ handler_health_results: dict[str, object] = {}
2427
+ handlers_all_healthy = True
2428
+
2429
+ if self._handlers:
2430
+ # Run all handler health checks concurrently using asyncio.gather()
2431
+ health_check_tasks = [
2432
+ self._lifecycle_executor.check_handler_health(handler_type, handler)
2433
+ for handler_type, handler in self._handlers.items()
2434
+ ]
2435
+ results = await asyncio.gather(*health_check_tasks)
2436
+
2437
+ # Process results and build the results dict
2438
+ for health_result in results:
2439
+ handler_health_results[health_result.handler_type] = (
2440
+ health_result.details
2441
+ )
2442
+ if not health_result.healthy:
2443
+ handlers_all_healthy = False
2444
+
2445
+ # Check for failed handlers - any failures indicate degraded state
2446
+ has_failed_handlers = len(self._failed_handlers) > 0
2447
+
2448
+ # Check for no handlers registered - critical configuration issue
2449
+ # A runtime with no handlers cannot process any events and should be unhealthy
2450
+ no_handlers_registered = len(self._handlers) == 0
2451
+
2452
+ # Degraded state: process is running but some handlers failed to instantiate
2453
+ # This means the system is operational but with reduced functionality
2454
+ degraded = self._is_running and has_failed_handlers
2455
+
2456
+ # Overall health is True only if running, event bus is healthy,
2457
+ # no handlers failed to instantiate, all registered handlers are healthy,
2458
+ # AND at least one handler is registered (runtime without handlers is useless)
2459
+ healthy = (
2460
+ self._is_running
2461
+ and event_bus_healthy
2462
+ and not has_failed_handlers
2463
+ and handlers_all_healthy
2464
+ and not no_handlers_registered
2465
+ )
2466
+
2467
+ return {
2468
+ "healthy": healthy,
2469
+ "degraded": degraded,
2470
+ "is_running": self._is_running,
2471
+ "is_draining": self._is_draining,
2472
+ "pending_message_count": self._pending_message_count,
2473
+ "event_bus": event_bus_health,
2474
+ "event_bus_healthy": event_bus_healthy,
2475
+ "failed_handlers": self._failed_handlers,
2476
+ "registered_handlers": list(self._handlers.keys()),
2477
+ "handlers": handler_health_results,
2478
+ "no_handlers_registered": no_handlers_registered,
2479
+ }
2480
+
2481
+ def register_handler(
2482
+ self, handler_type: str, handler: ProtocolContainerAware
2483
+ ) -> None:
2484
+ """Register a handler for a specific type.
2485
+
2486
+ Args:
2487
+ handler_type: Protocol type identifier (e.g., "http", "db").
2488
+ handler: Handler instance implementing the ProtocolContainerAware protocol.
2489
+ """
2490
+ self._handlers[handler_type] = handler
2491
+ logger.debug(
2492
+ "Handler registered",
2493
+ extra={
2494
+ "handler_type": handler_type,
2495
+ "handler_class": type(handler).__name__,
2496
+ },
2497
+ )
2498
+
2499
+ def get_handler(self, handler_type: str) -> ProtocolContainerAware | None:
2500
+ """Get handler for type, returns None if not registered.
2501
+
2502
+ Args:
2503
+ handler_type: Protocol type identifier.
2504
+
2505
+ Returns:
2506
+ Handler instance if registered, None otherwise.
2507
+ """
2508
+ return self._handlers.get(handler_type)
2509
+
2510
+ async def get_subscribers_for_topic(self, topic: str) -> list[UUID]:
2511
+ """Query Consul for node IDs that subscribe to a topic.
2512
+
2513
+ This method provides dynamic topic-to-subscriber lookup via Consul KV store.
2514
+ Topics are stored at `onex/topics/{topic}/subscribers` and contain a JSON
2515
+ array of node UUID strings.
2516
+
2517
+ Args:
2518
+ topic: Environment-qualified topic string
2519
+ (e.g., "dev.onex.evt.intent-classified.v1")
2520
+
2521
+ Returns:
2522
+ List of node UUIDs that subscribe to this topic.
2523
+ Empty list if no subscribers registered or Consul unavailable.
2524
+
2525
+ Note:
2526
+ Returns node IDs, not handler names. Node ID is the stable registry key.
2527
+ Handler selection is a separate concern that can change independently.
2528
+
2529
+ Example:
2530
+ >>> runtime = RuntimeHostProcess()
2531
+ >>> await runtime.start()
2532
+ >>> subscribers = await runtime.get_subscribers_for_topic(
2533
+ ... "dev.onex.evt.intent-classified.v1"
2534
+ ... )
2535
+ >>> print(subscribers) # [UUID('abc123...'), UUID('def456...')]
2536
+
2537
+ Related:
2538
+ - OMN-1613: Add event bus topic storage to registry for dynamic topic discovery
2539
+ - MixinConsulTopicIndex: Consul mixin that manages topic index storage
2540
+ """
2541
+ consul_handler = self.get_handler("consul")
2542
+ if consul_handler is None:
2543
+ logger.debug(
2544
+ "Consul handler not available for topic subscriber lookup",
2545
+ extra={"topic": topic},
2546
+ )
2547
+ return []
2548
+
2549
+ try:
2550
+ correlation_id = uuid4()
2551
+ envelope: dict[str, object] = {
2552
+ "operation": "consul.kv_get",
2553
+ "payload": {"key": f"onex/topics/{topic}/subscribers"},
2554
+ "correlation_id": str(correlation_id),
2555
+ }
2556
+
2557
+ # Execute the Consul KV get operation
2558
+ # NOTE: MVP adapters use legacy execute(envelope: dict) signature.
2559
+ result = await consul_handler.execute(envelope) # type: ignore[call-arg]
2560
+
2561
+ # Navigate to the value in the response structure:
2562
+ # ModelHandlerOutput -> result (ModelConsulHandlerResponse)
2563
+ # -> payload (ModelConsulHandlerPayload) -> data (ConsulPayload)
2564
+ if result is None:
2565
+ return []
2566
+
2567
+ # Check if result has the expected structure
2568
+ if not hasattr(result, "result") or result.result is None:
2569
+ return []
2570
+
2571
+ response = result.result
2572
+ if not hasattr(response, "payload") or response.payload is None:
2573
+ return []
2574
+
2575
+ payload_data = response.payload.data
2576
+ if payload_data is None:
2577
+ return []
2578
+
2579
+ # Check for "not found" response - key doesn't exist
2580
+ if hasattr(payload_data, "found") and payload_data.found is False:
2581
+ return []
2582
+
2583
+ # Get the value field from the payload
2584
+ value = getattr(payload_data, "value", None)
2585
+ if not value:
2586
+ return []
2587
+
2588
+ # Parse JSON array of node ID strings
2589
+ node_ids_raw = json.loads(value)
2590
+ if not isinstance(node_ids_raw, list):
2591
+ logger.warning(
2592
+ "Topic subscriber value is not a list",
2593
+ extra={
2594
+ "topic": topic,
2595
+ "correlation_id": str(correlation_id),
2596
+ "value_type": type(node_ids_raw).__name__,
2597
+ },
2598
+ )
2599
+ return []
2600
+
2601
+ # Convert string UUIDs to UUID objects (skip invalid entries)
2602
+ subscribers: list[UUID] = []
2603
+ invalid_ids: list[str] = []
2604
+ for nid in node_ids_raw:
2605
+ if not isinstance(nid, str):
2606
+ continue
2607
+ try:
2608
+ subscribers.append(UUID(nid))
2609
+ except ValueError:
2610
+ invalid_ids.append(nid)
2611
+
2612
+ if invalid_ids:
2613
+ logger.warning(
2614
+ "Invalid UUIDs in topic subscriber list",
2615
+ extra={
2616
+ "topic": topic,
2617
+ "correlation_id": str(correlation_id),
2618
+ "invalid_count": len(invalid_ids),
2619
+ },
2620
+ )
2621
+ return subscribers
2622
+
2623
+ except json.JSONDecodeError as e:
2624
+ logger.warning(
2625
+ "Failed to parse topic subscriber JSON",
2626
+ extra={
2627
+ "topic": topic,
2628
+ "error": str(e),
2629
+ },
2630
+ )
2631
+ return []
2632
+ except InfraConsulError as e:
2633
+ logger.warning(
2634
+ "Consul error querying topic subscribers",
2635
+ extra={
2636
+ "topic": topic,
2637
+ "error": str(e),
2638
+ "error_type": "InfraConsulError",
2639
+ "consul_key": getattr(e, "consul_key", None),
2640
+ },
2641
+ )
2642
+ return []
2643
+ except InfraTimeoutError as e:
2644
+ logger.warning(
2645
+ "Timeout querying topic subscribers",
2646
+ extra={
2647
+ "topic": topic,
2648
+ "error": str(e),
2649
+ "error_type": "InfraTimeoutError",
2650
+ },
2651
+ )
2652
+ return []
2653
+ except InfraUnavailableError as e:
2654
+ logger.warning(
2655
+ "Service unavailable for topic subscriber query",
2656
+ extra={
2657
+ "topic": topic,
2658
+ "error": str(e),
2659
+ "error_type": "InfraUnavailableError",
2660
+ },
2661
+ )
2662
+ return []
2663
+ except Exception as e:
2664
+ # Graceful degradation - Consul unavailable is not fatal
2665
+ logger.warning(
2666
+ "Failed to query topic subscribers from Consul",
2667
+ extra={
2668
+ "topic": topic,
2669
+ "error": str(e),
2670
+ "error_type": type(e).__name__,
2671
+ },
2672
+ )
2673
+ return []
2674
+
2675
+ # =========================================================================
2676
+ # Architecture Validation Methods (OMN-1138)
2677
+ # =========================================================================
2678
+
2679
+ async def _validate_architecture(self) -> None:
2680
+ """Validate architecture compliance before starting runtime.
2681
+
2682
+ This method is called at the beginning of start() to validate nodes
2683
+ and handlers against registered architecture rules. If any violations
2684
+ with ERROR severity are detected, startup is blocked.
2685
+
2686
+ Validation occurs BEFORE:
2687
+ - Event bus starts
2688
+ - Handlers are wired
2689
+ - Subscription begins
2690
+
2691
+ Validation Behavior:
2692
+ - ERROR severity violations: Block startup, raise ArchitectureViolationError
2693
+ - WARNING severity violations: Log warning, continue startup
2694
+ - INFO severity violations: Log info, continue startup
2695
+
2696
+ Raises:
2697
+ ArchitectureViolationError: If blocking violations (ERROR severity)
2698
+ are detected. Contains all blocking violations for inspection.
2699
+
2700
+ Example:
2701
+ >>> # Validation is automatic in start()
2702
+ >>> try:
2703
+ ... await runtime.start()
2704
+ ... except ArchitectureViolationError as e:
2705
+ ... print(f"Startup blocked: {len(e.violations)} violations")
2706
+ ... for v in e.violations:
2707
+ ... print(v.format_for_logging())
2708
+
2709
+ Note:
2710
+ Validation only runs if architecture_rules were provided at init.
2711
+ If no rules are configured, this method returns immediately.
2712
+
2713
+ Related:
2714
+ - OMN-1138: Architecture Validator for omnibase_infra
2715
+ - OMN-1099: Validators implementing ProtocolArchitectureRule
2716
+ """
2717
+ # Skip validation if no rules configured
2718
+ if not self._architecture_rules:
2719
+ logger.debug("No architecture rules configured, skipping validation")
2720
+ return
2721
+
2722
+ logger.info(
2723
+ "Validating architecture compliance",
2724
+ extra={
2725
+ "rule_count": len(self._architecture_rules),
2726
+ "rule_ids": tuple(r.rule_id for r in self._architecture_rules),
2727
+ },
2728
+ )
2729
+
2730
+ # Import architecture validator components
2731
+ from omnibase_infra.errors import ArchitectureViolationError
2732
+ from omnibase_infra.nodes.architecture_validator import (
2733
+ ModelArchitectureValidationRequest,
2734
+ NodeArchitectureValidatorCompute,
2735
+ )
2736
+
2737
+ # Create or get container
2738
+ container = self._get_or_create_container()
2739
+
2740
+ # Instantiate validator with rules
2741
+ validator = NodeArchitectureValidatorCompute(
2742
+ container=container,
2743
+ rules=self._architecture_rules,
2744
+ )
2745
+
2746
+ # Build validation request
2747
+ # Note: At this point, handlers haven't been instantiated yet (that happens
2748
+ # after validation in _populate_handlers_from_registry). We validate the
2749
+ # handler CLASSES from the registry, not handler instances.
2750
+ handler_registry = await self._get_handler_registry()
2751
+ handler_classes: list[type[ProtocolContainerAware]] = []
2752
+ for handler_type in handler_registry.list_protocols():
2753
+ try:
2754
+ handler_cls = handler_registry.get(handler_type)
2755
+ handler_classes.append(handler_cls)
2756
+ except Exception as e:
2757
+ # If a handler class can't be retrieved, skip it for validation
2758
+ # (it will fail later during instantiation anyway)
2759
+ logger.debug(
2760
+ "Skipping handler class for architecture validation",
2761
+ extra={
2762
+ "handler_type": handler_type,
2763
+ "error": str(e),
2764
+ },
2765
+ )
2766
+
2767
+ request = ModelArchitectureValidationRequest(
2768
+ nodes=(), # Nodes not yet available at this point
2769
+ handlers=tuple(handler_classes),
2770
+ )
2771
+
2772
+ # Execute validation
2773
+ result = validator.compute(request)
2774
+
2775
+ # Separate blocking and non-blocking violations
2776
+ blocking_violations = tuple(v for v in result.violations if v.blocks_startup())
2777
+ warning_violations = tuple(
2778
+ v for v in result.violations if not v.blocks_startup()
2779
+ )
2780
+
2781
+ # Log warnings but don't block
2782
+ for violation in warning_violations:
2783
+ # Note: We can't use to_structured_dict() directly because 'message'
2784
+ # is a reserved key in Python logging's extra parameter.
2785
+ # We use format_for_logging() instead for the log message.
2786
+ logger.warning(
2787
+ "Architecture warning: %s",
2788
+ violation.format_for_logging(),
2789
+ extra={
2790
+ "rule_id": violation.rule_id,
2791
+ "severity": violation.severity.value,
2792
+ "target_type": violation.target_type,
2793
+ "target_name": violation.target_name,
2794
+ },
2795
+ )
2796
+
2797
+ # Block startup on ERROR violations
2798
+ if blocking_violations:
2799
+ logger.error(
2800
+ "Architecture validation failed",
2801
+ extra={
2802
+ "blocking_violation_count": len(blocking_violations),
2803
+ "warning_violation_count": len(warning_violations),
2804
+ "blocking_rule_ids": tuple(v.rule_id for v in blocking_violations),
2805
+ },
2806
+ )
2807
+ raise ArchitectureViolationError(
2808
+ message=f"Architecture validation failed with {len(blocking_violations)} blocking violations",
2809
+ violations=blocking_violations,
2810
+ )
2811
+
2812
+ logger.info(
2813
+ "Architecture validation passed",
2814
+ extra={
2815
+ "rules_checked": result.rules_checked,
2816
+ "handlers_checked": result.handlers_checked,
2817
+ "warning_count": len(warning_violations),
2818
+ },
2819
+ )
2820
+
2821
+ def _get_or_create_container(self) -> ModelONEXContainer:
2822
+ """Get the injected container or create and cache a new one.
2823
+
2824
+ Returns:
2825
+ ModelONEXContainer instance for dependency injection.
2826
+
2827
+ Note:
2828
+ If no container was provided at init, a new container is created
2829
+ and cached in self._container. This ensures all handlers share
2830
+ the same container instance. The container provides basic
2831
+ infrastructure for node execution but may not have all services wired.
2832
+ """
2833
+ if self._container is not None:
2834
+ return self._container
2835
+
2836
+ # Create container and cache it for reuse
2837
+ from omnibase_core.models.container.model_onex_container import (
2838
+ ModelONEXContainer,
2839
+ )
2840
+
2841
+ logger.debug("Creating and caching container (no container provided at init)")
2842
+ self._container = ModelONEXContainer()
2843
+ return self._container
2844
+
2845
+ def _get_environment_from_config(self) -> str:
2846
+ """Extract environment setting from config with consistent fallback.
2847
+
2848
+ Handles both dict-based config and object-based config (e.g., Pydantic models)
2849
+ with a unified access pattern.
2850
+
2851
+ Resolution order:
2852
+ 1. config["event_bus"]["environment"] (if config is dict-like)
2853
+ 2. config.event_bus.environment (if config is object-like)
2854
+ 3. ONEX_ENVIRONMENT environment variable
2855
+ 4. "dev" (hardcoded default)
2856
+
2857
+ Returns:
2858
+ Environment string (e.g., "dev", "staging", "prod").
2859
+ """
2860
+ default_env = os.getenv("ONEX_ENVIRONMENT", "dev")
2861
+ config = self._config or {}
2862
+
2863
+ event_bus_config = config.get("event_bus", {})
2864
+ if isinstance(event_bus_config, dict):
2865
+ return str(event_bus_config.get("environment", default_env))
2866
+
2867
+ # Object-based config (e.g., ModelEventBusConfig)
2868
+ return str(getattr(event_bus_config, "environment", default_env))
2869
+
2870
+ # =========================================================================
2871
+ # Event Bus Subcontract Wiring Methods (OMN-1621)
2872
+ # =========================================================================
2873
+
2874
+ async def _wire_event_bus_subscriptions(self) -> None:
2875
+ """Wire Kafka subscriptions from handler contract event_bus sections.
2876
+
2877
+ This method bridges contract-declared topics to actual Kafka subscriptions
2878
+ using the EventBusSubcontractWiring class. It reads the event_bus subcontract
2879
+ from each handler's contract YAML and creates subscriptions for declared
2880
+ subscribe_topics.
2881
+
2882
+ Preconditions:
2883
+ - self._event_bus must be available and started
2884
+ - self._dispatch_engine must be set (otherwise wiring is skipped)
2885
+ - self._handler_descriptors must be populated
2886
+
2887
+ The wiring creates subscriptions that route messages to the dispatch engine,
2888
+ which then routes to appropriate handlers based on topic/category matching.
2889
+
2890
+ Per ARCH-002: "Runtime owns all Kafka plumbing" - nodes and handlers declare
2891
+ their topic requirements in contracts but never directly interact with Kafka.
2892
+
2893
+ Note:
2894
+ If dispatch_engine is not configured, this method logs a debug message
2895
+ and returns without creating any subscriptions. This allows the runtime
2896
+ to operate in legacy mode without contract-driven subscriptions.
2897
+
2898
+ .. versionadded:: 0.2.5
2899
+ Part of OMN-1621 contract-driven event bus wiring.
2900
+ """
2901
+ # Guard: require both event_bus and dispatch_engine
2902
+ if not self._event_bus:
2903
+ logger.debug("Event bus not available, skipping subcontract wiring")
2904
+ return
2905
+
2906
+ if not self._dispatch_engine:
2907
+ logger.debug(
2908
+ "Dispatch engine not configured, skipping event bus subcontract wiring"
2909
+ )
2910
+ return
2911
+
2912
+ if not self._handler_descriptors:
2913
+ logger.debug(
2914
+ "No handler descriptors available, skipping subcontract wiring"
2915
+ )
2916
+ return
2917
+
2918
+ environment = self._get_environment_from_config()
2919
+
2920
+ # Create wiring instance
2921
+ # Cast to protocol type - both EventBusKafka and EventBusInmemory implement
2922
+ # the ProtocolEventBusSubscriber interface (subscribe method)
2923
+ self._event_bus_wiring = EventBusSubcontractWiring(
2924
+ event_bus=cast("ProtocolEventBusSubscriber", self._event_bus),
2925
+ dispatch_engine=self._dispatch_engine,
2926
+ environment=environment,
2927
+ )
2928
+
2929
+ # Wire subscriptions for each handler with a contract
2930
+ wired_count = 0
2931
+ for handler_type, descriptor in self._handler_descriptors.items():
2932
+ contract_path_str = descriptor.contract_path
2933
+ if not contract_path_str:
2934
+ continue
2935
+
2936
+ contract_path = Path(contract_path_str)
2937
+
2938
+ # Load event_bus subcontract from contract YAML
2939
+ subcontract = load_event_bus_subcontract(contract_path, logger)
2940
+ if subcontract and subcontract.subscribe_topics:
2941
+ await self._event_bus_wiring.wire_subscriptions(
2942
+ subcontract=subcontract,
2943
+ node_name=descriptor.name or handler_type,
2944
+ )
2945
+ wired_count += 1
2946
+ logger.info(
2947
+ "Wired subscription(s) for handler '%s': topics=%s",
2948
+ descriptor.name or handler_type,
2949
+ subcontract.subscribe_topics,
2950
+ )
2951
+
2952
+ if wired_count > 0:
2953
+ logger.info(
2954
+ "Event bus subcontract wiring complete",
2955
+ extra={
2956
+ "wired_handler_count": wired_count,
2957
+ "total_handler_count": len(self._handler_descriptors),
2958
+ "environment": environment,
2959
+ },
2960
+ )
2961
+ else:
2962
+ logger.debug(
2963
+ "No handlers with event_bus subscriptions found",
2964
+ extra={"handler_count": len(self._handler_descriptors)},
2965
+ )
2966
+
2967
+ async def _wire_baseline_subscriptions(self) -> None:
2968
+ """Wire platform-baseline topic subscriptions for contract discovery.
2969
+
2970
+ These subscriptions are wired at runtime startup to receive contract
2971
+ registration and deregistration events from Kafka. This enables
2972
+ dynamic contract discovery without polling.
2973
+
2974
+ The subscriptions route events to KafkaContractSource callbacks:
2975
+ - on_contract_registered(): Parses contract YAML and caches descriptor
2976
+ - on_contract_deregistered(): Removes descriptor from cache
2977
+
2978
+ Preconditions:
2979
+ - KAFKA_EVENTS mode must be active (self._kafka_contract_source set)
2980
+ - Event bus must be available and started
2981
+
2982
+ Topic Format:
2983
+ - Registration: {env}.{TOPIC_SUFFIX_CONTRACT_REGISTERED}
2984
+ - Deregistration: {env}.{TOPIC_SUFFIX_CONTRACT_DEREGISTERED}
2985
+
2986
+ Note:
2987
+ Unsubscribe callbacks are stored in self._baseline_subscriptions
2988
+ for cleanup during stop().
2989
+
2990
+ Part of OMN-1654: KafkaContractSource cache discovery.
2991
+
2992
+ .. versionadded:: 0.8.0
2993
+ Created for event-driven contract discovery.
2994
+ """
2995
+ # Guard: only wire if KafkaContractSource is active
2996
+ if self._kafka_contract_source is None:
2997
+ logger.debug(
2998
+ "KafkaContractSource not active, skipping baseline subscriptions"
2999
+ )
3000
+ return
3001
+
3002
+ # Guard: event bus must be available
3003
+ if self._event_bus is None:
3004
+ logger.warning(
3005
+ "Event bus not available, cannot wire baseline contract subscriptions",
3006
+ extra={"mode": "KAFKA_EVENTS"},
3007
+ )
3008
+ return
3009
+
3010
+ source = self._kafka_contract_source
3011
+ environment = source.environment
3012
+
3013
+ # Compose topic names using platform-reserved suffixes
3014
+ registration_topic = f"{environment}.{TOPIC_SUFFIX_CONTRACT_REGISTERED}"
3015
+ deregistration_topic = f"{environment}.{TOPIC_SUFFIX_CONTRACT_DEREGISTERED}"
3016
+
3017
+ # Import ModelEventMessage type for handler signature
3018
+ from omnibase_infra.event_bus.models.model_event_message import (
3019
+ ModelEventMessage,
3020
+ )
3021
+
3022
+ async def handle_registration(msg: ModelEventMessage) -> None:
3023
+ """Handle contract registration event from Kafka."""
3024
+ try:
3025
+ parsed = _parse_contract_event_payload(msg)
3026
+ if parsed is None:
3027
+ return
3028
+
3029
+ payload, correlation_id = parsed
3030
+
3031
+ source.on_contract_registered(
3032
+ node_name=str(payload.get("node_name", "")),
3033
+ contract_yaml=str(payload.get("contract_yaml", "")),
3034
+ correlation_id=correlation_id,
3035
+ )
3036
+
3037
+ logger.debug(
3038
+ "Processed contract registration event",
3039
+ extra={
3040
+ "node_name": payload.get("node_name"),
3041
+ "topic": registration_topic,
3042
+ "correlation_id": str(correlation_id),
3043
+ },
3044
+ )
3045
+
3046
+ except Exception as e:
3047
+ logger.warning(
3048
+ "Failed to process contract registration event",
3049
+ extra={
3050
+ "error": str(e),
3051
+ "error_type": type(e).__name__,
3052
+ "topic": registration_topic,
3053
+ },
3054
+ )
3055
+
3056
+ async def handle_deregistration(msg: ModelEventMessage) -> None:
3057
+ """Handle contract deregistration event from Kafka."""
3058
+ try:
3059
+ parsed = _parse_contract_event_payload(msg)
3060
+ if parsed is None:
3061
+ return
3062
+
3063
+ payload, correlation_id = parsed
3064
+
3065
+ source.on_contract_deregistered(
3066
+ node_name=str(payload.get("node_name", "")),
3067
+ correlation_id=correlation_id,
3068
+ )
3069
+
3070
+ logger.debug(
3071
+ "Processed contract deregistration event",
3072
+ extra={
3073
+ "node_name": payload.get("node_name"),
3074
+ "topic": deregistration_topic,
3075
+ "correlation_id": str(correlation_id),
3076
+ },
3077
+ )
3078
+
3079
+ except Exception as e:
3080
+ logger.warning(
3081
+ "Failed to process contract deregistration event",
3082
+ extra={
3083
+ "error": str(e),
3084
+ "error_type": type(e).__name__,
3085
+ "topic": deregistration_topic,
3086
+ },
3087
+ )
3088
+
3089
+ # Subscribe to topics
3090
+ try:
3091
+ # Create node identity for baseline subscriptions
3092
+ baseline_identity = ModelNodeIdentity(
3093
+ env=environment,
3094
+ service=self._node_identity.service,
3095
+ node_name=f"{self._node_identity.node_name}-contract-discovery",
3096
+ version=self._node_identity.version,
3097
+ )
3098
+
3099
+ # Subscribe to registration topic
3100
+ reg_unsub = await self._event_bus.subscribe(
3101
+ topic=registration_topic,
3102
+ node_identity=baseline_identity,
3103
+ on_message=handle_registration,
3104
+ purpose=EnumConsumerGroupPurpose.CONSUME,
3105
+ )
3106
+ self._baseline_subscriptions.append(reg_unsub)
3107
+
3108
+ # Subscribe to deregistration topic
3109
+ dereg_unsub = await self._event_bus.subscribe(
3110
+ topic=deregistration_topic,
3111
+ node_identity=baseline_identity,
3112
+ on_message=handle_deregistration,
3113
+ purpose=EnumConsumerGroupPurpose.CONSUME,
3114
+ )
3115
+ self._baseline_subscriptions.append(dereg_unsub)
3116
+
3117
+ logger.info(
3118
+ "Wired baseline contract subscriptions",
3119
+ extra={
3120
+ "registration_topic": registration_topic,
3121
+ "deregistration_topic": deregistration_topic,
3122
+ "environment": environment,
3123
+ "subscription_count": len(self._baseline_subscriptions),
3124
+ },
3125
+ )
3126
+
3127
+ except Exception:
3128
+ logger.exception(
3129
+ "Failed to wire baseline subscriptions",
3130
+ extra={
3131
+ "registration_topic": registration_topic,
3132
+ "deregistration_topic": deregistration_topic,
3133
+ },
3134
+ )
3135
+
3136
+ # =========================================================================
3137
+ # Idempotency Guard Methods (OMN-945)
3138
+ # =========================================================================
3139
+
3140
+ async def _initialize_idempotency_store(self) -> None:
3141
+ """Initialize idempotency store from configuration.
3142
+
3143
+ Reads idempotency configuration from the runtime config and wires
3144
+ the appropriate store implementation. If not configured or disabled,
3145
+ idempotency checking is skipped.
3146
+
3147
+ Supported store types:
3148
+ - "postgres": PostgreSQL-backed durable store (production)
3149
+ - "memory": In-memory store (testing only)
3150
+
3151
+ Configuration keys:
3152
+ - idempotency.enabled: bool (default: False)
3153
+ - idempotency.store_type: "postgres" | "memory" (default: "postgres")
3154
+ - idempotency.domain_from_operation: bool (default: True)
3155
+ - idempotency.skip_operations: list[str] (default: [])
3156
+ - idempotency_database: dict (PostgreSQL connection config)
3157
+ """
3158
+ # Check if config exists
3159
+ if self._config is None:
3160
+ logger.debug("No runtime config provided, skipping idempotency setup")
3161
+ return
3162
+
3163
+ # Check if config has idempotency section
3164
+ idempotency_raw = self._config.get("idempotency")
3165
+ if idempotency_raw is None:
3166
+ logger.debug("Idempotency guard not configured, skipping")
3167
+ return
3168
+
3169
+ try:
3170
+ from omnibase_infra.idempotency import ModelIdempotencyGuardConfig
3171
+
3172
+ if isinstance(idempotency_raw, dict):
3173
+ self._idempotency_config = ModelIdempotencyGuardConfig.model_validate(
3174
+ idempotency_raw
3175
+ )
3176
+ elif isinstance(idempotency_raw, ModelIdempotencyGuardConfig):
3177
+ self._idempotency_config = idempotency_raw
3178
+ else:
3179
+ logger.warning(
3180
+ "Invalid idempotency config type",
3181
+ extra={"type": type(idempotency_raw).__name__},
3182
+ )
3183
+ return
3184
+
3185
+ if not self._idempotency_config.enabled:
3186
+ logger.debug("Idempotency guard disabled in config")
3187
+ return
3188
+
3189
+ # Create store based on store_type
3190
+ if self._idempotency_config.store_type == "postgres":
3191
+ from omnibase_infra.idempotency import (
3192
+ ModelPostgresIdempotencyStoreConfig,
3193
+ StoreIdempotencyPostgres,
3194
+ )
3195
+
3196
+ # Get database config from container or config
3197
+ db_config_raw = self._config.get("idempotency_database", {})
3198
+ if isinstance(db_config_raw, dict):
3199
+ db_config = ModelPostgresIdempotencyStoreConfig.model_validate(
3200
+ db_config_raw
3201
+ )
3202
+ elif isinstance(db_config_raw, ModelPostgresIdempotencyStoreConfig):
3203
+ db_config = db_config_raw
3204
+ else:
3205
+ logger.warning(
3206
+ "Invalid idempotency_database config type",
3207
+ extra={"type": type(db_config_raw).__name__},
3208
+ )
3209
+ return
3210
+
3211
+ self._idempotency_store = StoreIdempotencyPostgres(config=db_config)
3212
+ await self._idempotency_store.initialize()
3213
+
3214
+ elif self._idempotency_config.store_type == "memory":
3215
+ from omnibase_infra.idempotency import StoreIdempotencyInmemory
3216
+
3217
+ self._idempotency_store = StoreIdempotencyInmemory()
3218
+
3219
+ else:
3220
+ logger.warning(
3221
+ "Unknown idempotency store type",
3222
+ extra={"store_type": self._idempotency_config.store_type},
3223
+ )
3224
+ return
3225
+
3226
+ logger.info(
3227
+ "Idempotency guard initialized",
3228
+ extra={
3229
+ "store_type": self._idempotency_config.store_type,
3230
+ "domain_from_operation": self._idempotency_config.domain_from_operation,
3231
+ "skip_operations": self._idempotency_config.skip_operations,
3232
+ },
3233
+ )
3234
+
3235
+ except Exception as e:
3236
+ logger.warning(
3237
+ "Failed to initialize idempotency store, proceeding without",
3238
+ extra={"error": str(e)},
3239
+ )
3240
+ self._idempotency_store = None
3241
+ self._idempotency_config = None
3242
+
3243
+ # =========================================================================
3244
+ # WARNING: FAIL-OPEN BEHAVIOR
3245
+ # =========================================================================
3246
+ # This method implements FAIL-OPEN semantics: if the idempotency store
3247
+ # is unavailable or errors, messages are ALLOWED THROUGH for processing.
3248
+ #
3249
+ # This is an intentional design decision prioritizing availability over
3250
+ # exactly-once guarantees. See docstring below for full trade-off analysis.
3251
+ #
3252
+ # IMPORTANT: Downstream handlers MUST be designed for at-least-once delivery
3253
+ # and implement their own idempotency for critical operations.
3254
+ # =========================================================================
3255
+ async def _check_idempotency(
3256
+ self,
3257
+ envelope: dict[str, object],
3258
+ correlation_id: UUID,
3259
+ ) -> bool:
3260
+ """Check if envelope should be processed (idempotency guard).
3261
+
3262
+ Extracts message_id from envelope headers and checks against the
3263
+ idempotency store. If duplicate detected, publishes a duplicate
3264
+ response and returns False.
3265
+
3266
+ Fail-Open Semantics:
3267
+ This method implements **fail-open** error handling: if the
3268
+ idempotency store is unavailable or throws an error, the message
3269
+ is allowed through for processing (with a warning log).
3270
+
3271
+ **Design Rationale**: In distributed event-driven systems, the
3272
+ idempotency store (e.g., Redis/Valkey) is a supporting service,
3273
+ not a critical path dependency. A temporary store outage should
3274
+ not halt message processing entirely, as this would cascade into
3275
+ broader system unavailability.
3276
+
3277
+ **Trade-offs**:
3278
+ - Pro: High availability - processing continues during store outages
3279
+ - Pro: Graceful degradation - system remains functional
3280
+ - Con: May result in duplicate message processing during outages
3281
+ - Con: Downstream handlers must be designed for at-least-once delivery
3282
+
3283
+ **Mitigation**: Handlers consuming messages should implement their
3284
+ own idempotency logic for critical operations (e.g., using database
3285
+ constraints or transaction guards) to ensure correctness even when
3286
+ duplicates slip through.
3287
+
3288
+ Args:
3289
+ envelope: Validated envelope dict.
3290
+ correlation_id: Normalized correlation ID (UUID).
3291
+
3292
+ Returns:
3293
+ True if message should be processed (new message).
3294
+ False if message is duplicate (skip processing).
3295
+ """
3296
+ # Skip check if idempotency not configured
3297
+ if self._idempotency_store is None or self._idempotency_config is None:
3298
+ return True
3299
+
3300
+ if not self._idempotency_config.enabled:
3301
+ return True
3302
+
3303
+ # Check if operation is in skip list
3304
+ operation = envelope.get("operation")
3305
+ if isinstance(operation, str):
3306
+ if not self._idempotency_config.should_check_idempotency(operation):
3307
+ logger.debug(
3308
+ "Skipping idempotency check for operation",
3309
+ extra={
3310
+ "operation": operation,
3311
+ "correlation_id": str(correlation_id),
3312
+ },
3313
+ )
3314
+ return True
3315
+
3316
+ # Extract message_id from envelope
3317
+ message_id = self._extract_message_id(envelope, correlation_id)
3318
+
3319
+ # Extract domain from operation if configured
3320
+ domain = self._extract_idempotency_domain(envelope)
3321
+
3322
+ # Check and record in store
3323
+ try:
3324
+ is_new = await self._idempotency_store.check_and_record(
3325
+ message_id=message_id,
3326
+ domain=domain,
3327
+ correlation_id=correlation_id,
3328
+ )
3329
+
3330
+ if not is_new:
3331
+ # Duplicate detected - publish duplicate response (NOT an error)
3332
+ logger.info(
3333
+ "Duplicate message detected, skipping processing",
3334
+ extra={
3335
+ "message_id": str(message_id),
3336
+ "domain": domain,
3337
+ "correlation_id": str(correlation_id),
3338
+ },
3339
+ )
3340
+
3341
+ duplicate_response = self._create_duplicate_response(
3342
+ message_id=message_id,
3343
+ correlation_id=correlation_id,
3344
+ )
3345
+ # duplicate_response is already a dict from _create_duplicate_response
3346
+ await self._publish_envelope_safe(
3347
+ duplicate_response, self._output_topic
3348
+ )
3349
+ return False
3350
+
3351
+ return True
3352
+
3353
+ except Exception as e:
3354
+ # FAIL-OPEN: Allow message through on idempotency store errors.
3355
+ # Rationale: Availability over exactly-once. Store outages should not
3356
+ # halt processing. Downstream handlers must tolerate duplicates.
3357
+ # See docstring for full trade-off analysis.
3358
+ logger.warning(
3359
+ "Idempotency check failed, allowing message through (fail-open)",
3360
+ extra={
3361
+ "error": str(e),
3362
+ "error_type": type(e).__name__,
3363
+ "message_id": str(message_id),
3364
+ "domain": domain,
3365
+ "correlation_id": str(correlation_id),
3366
+ },
3367
+ )
3368
+ return True
3369
+
3370
+ def _extract_message_id(
3371
+ self,
3372
+ envelope: dict[str, object],
3373
+ correlation_id: UUID,
3374
+ ) -> UUID:
3375
+ """Extract message_id from envelope, falling back to correlation_id.
3376
+
3377
+ Priority:
3378
+ 1. envelope["headers"]["message_id"]
3379
+ 2. envelope["message_id"]
3380
+ 3. Use correlation_id as message_id (fallback)
3381
+
3382
+ Args:
3383
+ envelope: Envelope dict to extract message_id from.
3384
+ correlation_id: Fallback UUID if message_id not found.
3385
+
3386
+ Returns:
3387
+ UUID representing the message_id.
3388
+ """
3389
+ # Try headers first
3390
+ headers = envelope.get("headers")
3391
+ if isinstance(headers, dict):
3392
+ header_msg_id = headers.get("message_id")
3393
+ if header_msg_id is not None:
3394
+ if isinstance(header_msg_id, UUID):
3395
+ return header_msg_id
3396
+ if isinstance(header_msg_id, str):
3397
+ try:
3398
+ return UUID(header_msg_id)
3399
+ except ValueError:
3400
+ pass
3401
+
3402
+ # Try top-level message_id
3403
+ top_level_msg_id = envelope.get("message_id")
3404
+ if top_level_msg_id is not None:
3405
+ if isinstance(top_level_msg_id, UUID):
3406
+ return top_level_msg_id
3407
+ if isinstance(top_level_msg_id, str):
3408
+ try:
3409
+ return UUID(top_level_msg_id)
3410
+ except ValueError:
3411
+ pass
3412
+
3413
+ # Fallback: use correlation_id as message_id
3414
+ return correlation_id
3415
+
3416
+ def _extract_idempotency_domain(
3417
+ self,
3418
+ envelope: dict[str, object],
3419
+ ) -> str | None:
3420
+ """Extract domain for idempotency key from envelope.
3421
+
3422
+ If domain_from_operation is enabled in config, extracts domain
3423
+ from the operation prefix (e.g., "db.query" -> "db").
3424
+
3425
+ Args:
3426
+ envelope: Envelope dict to extract domain from.
3427
+
3428
+ Returns:
3429
+ Domain string if found and configured, None otherwise.
3430
+ """
3431
+ if self._idempotency_config is None:
3432
+ return None
3433
+
3434
+ if not self._idempotency_config.domain_from_operation:
3435
+ return None
3436
+
3437
+ operation = envelope.get("operation")
3438
+ if isinstance(operation, str):
3439
+ return self._idempotency_config.extract_domain(operation)
3440
+
3441
+ return None
3442
+
3443
+ def _create_duplicate_response(
3444
+ self,
3445
+ message_id: UUID,
3446
+ correlation_id: UUID,
3447
+ ) -> dict[str, object]:
3448
+ """Create response for duplicate message detection.
3449
+
3450
+ This is NOT an error response - duplicates are expected under
3451
+ at-least-once delivery. The response indicates successful
3452
+ deduplication.
3453
+
3454
+ Args:
3455
+ message_id: UUID of the duplicate message.
3456
+ correlation_id: Correlation ID for tracing.
3457
+
3458
+ Returns:
3459
+ Dict representation of ModelDuplicateResponse for envelope publishing.
3460
+ """
3461
+ return ModelDuplicateResponse(
3462
+ message_id=message_id,
3463
+ correlation_id=correlation_id,
3464
+ ).model_dump()
3465
+
3466
+ async def _cleanup_idempotency_store(self) -> None:
3467
+ """Cleanup idempotency store during shutdown.
3468
+
3469
+ Closes the idempotency store connection if initialized.
3470
+ Called during stop() to release resources.
3471
+ """
3472
+ if self._idempotency_store is None:
3473
+ return
3474
+
3475
+ try:
3476
+ if hasattr(self._idempotency_store, "shutdown"):
3477
+ await self._idempotency_store.shutdown()
3478
+ elif hasattr(self._idempotency_store, "close"):
3479
+ await self._idempotency_store.close()
3480
+ logger.debug("Idempotency store shutdown complete")
3481
+ except Exception as e:
3482
+ logger.warning(
3483
+ "Failed to shutdown idempotency store",
3484
+ extra={"error": str(e)},
3485
+ )
3486
+ finally:
3487
+ self._idempotency_store = None
3488
+
3489
+
3490
+ __all__: list[str] = [
3491
+ "RuntimeHostProcess",
3492
+ "wire_handlers",
3493
+ ]