omnibase_infra 0.2.1__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 (675) hide show
  1. omnibase_infra/__init__.py +101 -0
  2. omnibase_infra/cli/__init__.py +1 -0
  3. omnibase_infra/cli/commands.py +216 -0
  4. omnibase_infra/clients/__init__.py +0 -0
  5. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +261 -0
  6. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +138 -0
  7. omnibase_infra/decorators/__init__.py +29 -0
  8. omnibase_infra/decorators/allow_any.py +109 -0
  9. omnibase_infra/dlq/__init__.py +90 -0
  10. omnibase_infra/dlq/constants_dlq.py +57 -0
  11. omnibase_infra/dlq/models/__init__.py +26 -0
  12. omnibase_infra/dlq/models/enum_replay_status.py +37 -0
  13. omnibase_infra/dlq/models/model_dlq_replay_record.py +135 -0
  14. omnibase_infra/dlq/models/model_dlq_tracking_config.py +184 -0
  15. omnibase_infra/dlq/service_dlq_tracking.py +611 -0
  16. omnibase_infra/enums/__init__.py +123 -0
  17. omnibase_infra/enums/enum_any_type_violation.py +104 -0
  18. omnibase_infra/enums/enum_backend_type.py +27 -0
  19. omnibase_infra/enums/enum_capture_outcome.py +42 -0
  20. omnibase_infra/enums/enum_capture_state.py +88 -0
  21. omnibase_infra/enums/enum_chain_violation_type.py +119 -0
  22. omnibase_infra/enums/enum_circuit_state.py +51 -0
  23. omnibase_infra/enums/enum_confirmation_event_type.py +27 -0
  24. omnibase_infra/enums/enum_contract_type.py +84 -0
  25. omnibase_infra/enums/enum_dedupe_strategy.py +46 -0
  26. omnibase_infra/enums/enum_dispatch_status.py +191 -0
  27. omnibase_infra/enums/enum_environment.py +46 -0
  28. omnibase_infra/enums/enum_execution_shape_violation.py +103 -0
  29. omnibase_infra/enums/enum_handler_error_type.py +101 -0
  30. omnibase_infra/enums/enum_handler_loader_error.py +178 -0
  31. omnibase_infra/enums/enum_handler_source_type.py +87 -0
  32. omnibase_infra/enums/enum_handler_type.py +77 -0
  33. omnibase_infra/enums/enum_handler_type_category.py +61 -0
  34. omnibase_infra/enums/enum_infra_transport_type.py +73 -0
  35. omnibase_infra/enums/enum_introspection_reason.py +154 -0
  36. omnibase_infra/enums/enum_message_category.py +213 -0
  37. omnibase_infra/enums/enum_node_archetype.py +74 -0
  38. omnibase_infra/enums/enum_node_output_type.py +185 -0
  39. omnibase_infra/enums/enum_non_retryable_error_category.py +224 -0
  40. omnibase_infra/enums/enum_policy_type.py +32 -0
  41. omnibase_infra/enums/enum_registration_state.py +261 -0
  42. omnibase_infra/enums/enum_registration_status.py +33 -0
  43. omnibase_infra/enums/enum_registry_response_status.py +28 -0
  44. omnibase_infra/enums/enum_response_status.py +26 -0
  45. omnibase_infra/enums/enum_retry_error_category.py +98 -0
  46. omnibase_infra/enums/enum_security_rule_id.py +103 -0
  47. omnibase_infra/enums/enum_selection_strategy.py +91 -0
  48. omnibase_infra/enums/enum_topic_standard.py +42 -0
  49. omnibase_infra/enums/enum_validation_severity.py +78 -0
  50. omnibase_infra/errors/__init__.py +156 -0
  51. omnibase_infra/errors/error_architecture_violation.py +152 -0
  52. omnibase_infra/errors/error_chain_propagation.py +188 -0
  53. omnibase_infra/errors/error_compute_registry.py +92 -0
  54. omnibase_infra/errors/error_consul.py +132 -0
  55. omnibase_infra/errors/error_container_wiring.py +243 -0
  56. omnibase_infra/errors/error_event_bus_registry.py +102 -0
  57. omnibase_infra/errors/error_infra.py +608 -0
  58. omnibase_infra/errors/error_message_type_registry.py +101 -0
  59. omnibase_infra/errors/error_policy_registry.py +112 -0
  60. omnibase_infra/errors/error_vault.py +123 -0
  61. omnibase_infra/event_bus/__init__.py +72 -0
  62. omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +86 -0
  63. omnibase_infra/event_bus/event_bus_inmemory.py +743 -0
  64. omnibase_infra/event_bus/event_bus_kafka.py +1658 -0
  65. omnibase_infra/event_bus/mixin_kafka_broadcast.py +184 -0
  66. omnibase_infra/event_bus/mixin_kafka_dlq.py +765 -0
  67. omnibase_infra/event_bus/models/__init__.py +29 -0
  68. omnibase_infra/event_bus/models/config/__init__.py +20 -0
  69. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +725 -0
  70. omnibase_infra/event_bus/models/model_dlq_event.py +206 -0
  71. omnibase_infra/event_bus/models/model_dlq_metrics.py +304 -0
  72. omnibase_infra/event_bus/models/model_event_headers.py +115 -0
  73. omnibase_infra/event_bus/models/model_event_message.py +60 -0
  74. omnibase_infra/event_bus/topic_constants.py +376 -0
  75. omnibase_infra/handlers/__init__.py +75 -0
  76. omnibase_infra/handlers/filesystem/__init__.py +48 -0
  77. omnibase_infra/handlers/filesystem/enum_file_system_operation.py +35 -0
  78. omnibase_infra/handlers/filesystem/model_file_system_request.py +298 -0
  79. omnibase_infra/handlers/filesystem/model_file_system_result.py +166 -0
  80. omnibase_infra/handlers/handler_consul.py +787 -0
  81. omnibase_infra/handlers/handler_db.py +1039 -0
  82. omnibase_infra/handlers/handler_filesystem.py +1478 -0
  83. omnibase_infra/handlers/handler_graph.py +1154 -0
  84. omnibase_infra/handlers/handler_http.py +920 -0
  85. omnibase_infra/handlers/handler_manifest_persistence.contract.yaml +184 -0
  86. omnibase_infra/handlers/handler_manifest_persistence.py +1539 -0
  87. omnibase_infra/handlers/handler_mcp.py +748 -0
  88. omnibase_infra/handlers/handler_qdrant.py +1076 -0
  89. omnibase_infra/handlers/handler_vault.py +422 -0
  90. omnibase_infra/handlers/mcp/__init__.py +19 -0
  91. omnibase_infra/handlers/mcp/adapter_onex_to_mcp.py +446 -0
  92. omnibase_infra/handlers/mcp/protocols.py +178 -0
  93. omnibase_infra/handlers/mcp/transport_streamable_http.py +352 -0
  94. omnibase_infra/handlers/mixins/__init__.py +42 -0
  95. omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
  96. omnibase_infra/handlers/mixins/mixin_consul_kv.py +337 -0
  97. omnibase_infra/handlers/mixins/mixin_consul_service.py +277 -0
  98. omnibase_infra/handlers/mixins/mixin_vault_initialization.py +338 -0
  99. omnibase_infra/handlers/mixins/mixin_vault_retry.py +412 -0
  100. omnibase_infra/handlers/mixins/mixin_vault_secrets.py +450 -0
  101. omnibase_infra/handlers/mixins/mixin_vault_token.py +365 -0
  102. omnibase_infra/handlers/models/__init__.py +286 -0
  103. omnibase_infra/handlers/models/consul/__init__.py +81 -0
  104. omnibase_infra/handlers/models/consul/enum_consul_operation_type.py +57 -0
  105. omnibase_infra/handlers/models/consul/model_consul_deregister_payload.py +51 -0
  106. omnibase_infra/handlers/models/consul/model_consul_handler_config.py +153 -0
  107. omnibase_infra/handlers/models/consul/model_consul_handler_payload.py +89 -0
  108. omnibase_infra/handlers/models/consul/model_consul_kv_get_found_payload.py +55 -0
  109. omnibase_infra/handlers/models/consul/model_consul_kv_get_not_found_payload.py +49 -0
  110. omnibase_infra/handlers/models/consul/model_consul_kv_get_recurse_payload.py +50 -0
  111. omnibase_infra/handlers/models/consul/model_consul_kv_item.py +33 -0
  112. omnibase_infra/handlers/models/consul/model_consul_kv_put_payload.py +41 -0
  113. omnibase_infra/handlers/models/consul/model_consul_register_payload.py +53 -0
  114. omnibase_infra/handlers/models/consul/model_consul_retry_config.py +66 -0
  115. omnibase_infra/handlers/models/consul/model_payload_consul.py +66 -0
  116. omnibase_infra/handlers/models/consul/registry_payload_consul.py +214 -0
  117. omnibase_infra/handlers/models/graph/__init__.py +35 -0
  118. omnibase_infra/handlers/models/graph/enum_graph_operation_type.py +20 -0
  119. omnibase_infra/handlers/models/graph/model_graph_execute_payload.py +38 -0
  120. omnibase_infra/handlers/models/graph/model_graph_handler_config.py +54 -0
  121. omnibase_infra/handlers/models/graph/model_graph_handler_payload.py +44 -0
  122. omnibase_infra/handlers/models/graph/model_graph_query_payload.py +40 -0
  123. omnibase_infra/handlers/models/graph/model_graph_record.py +22 -0
  124. omnibase_infra/handlers/models/http/__init__.py +50 -0
  125. omnibase_infra/handlers/models/http/enum_http_operation_type.py +29 -0
  126. omnibase_infra/handlers/models/http/model_http_body_content.py +45 -0
  127. omnibase_infra/handlers/models/http/model_http_get_payload.py +88 -0
  128. omnibase_infra/handlers/models/http/model_http_handler_payload.py +90 -0
  129. omnibase_infra/handlers/models/http/model_http_post_payload.py +88 -0
  130. omnibase_infra/handlers/models/http/model_payload_http.py +66 -0
  131. omnibase_infra/handlers/models/http/registry_payload_http.py +212 -0
  132. omnibase_infra/handlers/models/mcp/__init__.py +23 -0
  133. omnibase_infra/handlers/models/mcp/enum_mcp_operation_type.py +24 -0
  134. omnibase_infra/handlers/models/mcp/model_mcp_handler_config.py +40 -0
  135. omnibase_infra/handlers/models/mcp/model_mcp_tool_call.py +32 -0
  136. omnibase_infra/handlers/models/mcp/model_mcp_tool_result.py +45 -0
  137. omnibase_infra/handlers/models/model_consul_handler_response.py +96 -0
  138. omnibase_infra/handlers/models/model_db_describe_response.py +83 -0
  139. omnibase_infra/handlers/models/model_db_query_payload.py +95 -0
  140. omnibase_infra/handlers/models/model_db_query_response.py +60 -0
  141. omnibase_infra/handlers/models/model_filesystem_config.py +98 -0
  142. omnibase_infra/handlers/models/model_filesystem_delete_payload.py +54 -0
  143. omnibase_infra/handlers/models/model_filesystem_delete_result.py +77 -0
  144. omnibase_infra/handlers/models/model_filesystem_directory_entry.py +75 -0
  145. omnibase_infra/handlers/models/model_filesystem_ensure_directory_payload.py +54 -0
  146. omnibase_infra/handlers/models/model_filesystem_ensure_directory_result.py +60 -0
  147. omnibase_infra/handlers/models/model_filesystem_list_directory_payload.py +60 -0
  148. omnibase_infra/handlers/models/model_filesystem_list_directory_result.py +68 -0
  149. omnibase_infra/handlers/models/model_filesystem_read_payload.py +62 -0
  150. omnibase_infra/handlers/models/model_filesystem_read_result.py +61 -0
  151. omnibase_infra/handlers/models/model_filesystem_write_payload.py +70 -0
  152. omnibase_infra/handlers/models/model_filesystem_write_result.py +55 -0
  153. omnibase_infra/handlers/models/model_graph_handler_response.py +98 -0
  154. omnibase_infra/handlers/models/model_handler_response.py +103 -0
  155. omnibase_infra/handlers/models/model_http_handler_response.py +101 -0
  156. omnibase_infra/handlers/models/model_manifest_metadata.py +75 -0
  157. omnibase_infra/handlers/models/model_manifest_persistence_config.py +62 -0
  158. omnibase_infra/handlers/models/model_manifest_query_payload.py +90 -0
  159. omnibase_infra/handlers/models/model_manifest_query_result.py +97 -0
  160. omnibase_infra/handlers/models/model_manifest_retrieve_payload.py +44 -0
  161. omnibase_infra/handlers/models/model_manifest_retrieve_result.py +98 -0
  162. omnibase_infra/handlers/models/model_manifest_store_payload.py +47 -0
  163. omnibase_infra/handlers/models/model_manifest_store_result.py +67 -0
  164. omnibase_infra/handlers/models/model_operation_context.py +187 -0
  165. omnibase_infra/handlers/models/model_qdrant_handler_response.py +98 -0
  166. omnibase_infra/handlers/models/model_retry_state.py +162 -0
  167. omnibase_infra/handlers/models/model_vault_handler_response.py +98 -0
  168. omnibase_infra/handlers/models/qdrant/__init__.py +44 -0
  169. omnibase_infra/handlers/models/qdrant/enum_qdrant_operation_type.py +26 -0
  170. omnibase_infra/handlers/models/qdrant/model_qdrant_collection_payload.py +42 -0
  171. omnibase_infra/handlers/models/qdrant/model_qdrant_delete_payload.py +36 -0
  172. omnibase_infra/handlers/models/qdrant/model_qdrant_handler_config.py +42 -0
  173. omnibase_infra/handlers/models/qdrant/model_qdrant_handler_payload.py +54 -0
  174. omnibase_infra/handlers/models/qdrant/model_qdrant_search_payload.py +42 -0
  175. omnibase_infra/handlers/models/qdrant/model_qdrant_search_result.py +30 -0
  176. omnibase_infra/handlers/models/qdrant/model_qdrant_upsert_payload.py +36 -0
  177. omnibase_infra/handlers/models/vault/__init__.py +69 -0
  178. omnibase_infra/handlers/models/vault/enum_vault_operation_type.py +35 -0
  179. omnibase_infra/handlers/models/vault/model_payload_vault.py +66 -0
  180. omnibase_infra/handlers/models/vault/model_vault_delete_payload.py +57 -0
  181. omnibase_infra/handlers/models/vault/model_vault_handler_config.py +148 -0
  182. omnibase_infra/handlers/models/vault/model_vault_handler_payload.py +101 -0
  183. omnibase_infra/handlers/models/vault/model_vault_list_payload.py +58 -0
  184. omnibase_infra/handlers/models/vault/model_vault_renew_token_payload.py +67 -0
  185. omnibase_infra/handlers/models/vault/model_vault_retry_config.py +66 -0
  186. omnibase_infra/handlers/models/vault/model_vault_secret_payload.py +106 -0
  187. omnibase_infra/handlers/models/vault/model_vault_write_payload.py +66 -0
  188. omnibase_infra/handlers/models/vault/registry_payload_vault.py +213 -0
  189. omnibase_infra/handlers/registration_storage/__init__.py +43 -0
  190. omnibase_infra/handlers/registration_storage/handler_registration_storage_mock.py +392 -0
  191. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +915 -0
  192. omnibase_infra/handlers/registration_storage/models/__init__.py +23 -0
  193. omnibase_infra/handlers/registration_storage/models/model_delete_registration_request.py +58 -0
  194. omnibase_infra/handlers/registration_storage/models/model_update_registration_request.py +73 -0
  195. omnibase_infra/handlers/registration_storage/protocol_registration_persistence.py +191 -0
  196. omnibase_infra/handlers/service_discovery/__init__.py +43 -0
  197. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +747 -0
  198. omnibase_infra/handlers/service_discovery/handler_service_discovery_mock.py +258 -0
  199. omnibase_infra/handlers/service_discovery/models/__init__.py +22 -0
  200. omnibase_infra/handlers/service_discovery/models/model_discovery_result.py +64 -0
  201. omnibase_infra/handlers/service_discovery/models/model_registration_result.py +138 -0
  202. omnibase_infra/handlers/service_discovery/models/model_service_info.py +99 -0
  203. omnibase_infra/handlers/service_discovery/protocol_discovery_operations.py +170 -0
  204. omnibase_infra/idempotency/__init__.py +94 -0
  205. omnibase_infra/idempotency/models/__init__.py +43 -0
  206. omnibase_infra/idempotency/models/model_idempotency_check_result.py +85 -0
  207. omnibase_infra/idempotency/models/model_idempotency_guard_config.py +130 -0
  208. omnibase_infra/idempotency/models/model_idempotency_record.py +86 -0
  209. omnibase_infra/idempotency/models/model_idempotency_store_health_check_result.py +81 -0
  210. omnibase_infra/idempotency/models/model_idempotency_store_metrics.py +140 -0
  211. omnibase_infra/idempotency/models/model_postgres_idempotency_store_config.py +299 -0
  212. omnibase_infra/idempotency/protocol_idempotency_store.py +184 -0
  213. omnibase_infra/idempotency/store_inmemory.py +265 -0
  214. omnibase_infra/idempotency/store_postgres.py +923 -0
  215. omnibase_infra/infrastructure/__init__.py +0 -0
  216. omnibase_infra/mixins/__init__.py +71 -0
  217. omnibase_infra/mixins/mixin_async_circuit_breaker.py +655 -0
  218. omnibase_infra/mixins/mixin_dict_like_accessors.py +146 -0
  219. omnibase_infra/mixins/mixin_envelope_extraction.py +119 -0
  220. omnibase_infra/mixins/mixin_node_introspection.py +2465 -0
  221. omnibase_infra/mixins/mixin_retry_execution.py +386 -0
  222. omnibase_infra/mixins/protocol_circuit_breaker_aware.py +133 -0
  223. omnibase_infra/models/__init__.py +136 -0
  224. omnibase_infra/models/corpus/__init__.py +17 -0
  225. omnibase_infra/models/corpus/model_capture_config.py +133 -0
  226. omnibase_infra/models/corpus/model_capture_result.py +86 -0
  227. omnibase_infra/models/discovery/__init__.py +42 -0
  228. omnibase_infra/models/discovery/model_dependency_spec.py +319 -0
  229. omnibase_infra/models/discovery/model_discovered_capabilities.py +50 -0
  230. omnibase_infra/models/discovery/model_introspection_config.py +311 -0
  231. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +169 -0
  232. omnibase_infra/models/discovery/model_introspection_task_config.py +116 -0
  233. omnibase_infra/models/dispatch/__init__.py +147 -0
  234. omnibase_infra/models/dispatch/model_dispatch_context.py +439 -0
  235. omnibase_infra/models/dispatch/model_dispatch_error.py +336 -0
  236. omnibase_infra/models/dispatch/model_dispatch_log_context.py +400 -0
  237. omnibase_infra/models/dispatch/model_dispatch_metadata.py +228 -0
  238. omnibase_infra/models/dispatch/model_dispatch_metrics.py +496 -0
  239. omnibase_infra/models/dispatch/model_dispatch_outcome.py +317 -0
  240. omnibase_infra/models/dispatch/model_dispatch_outputs.py +231 -0
  241. omnibase_infra/models/dispatch/model_dispatch_result.py +436 -0
  242. omnibase_infra/models/dispatch/model_dispatch_route.py +279 -0
  243. omnibase_infra/models/dispatch/model_dispatcher_metrics.py +275 -0
  244. omnibase_infra/models/dispatch/model_dispatcher_registration.py +352 -0
  245. omnibase_infra/models/dispatch/model_parsed_topic.py +135 -0
  246. omnibase_infra/models/dispatch/model_topic_parser.py +725 -0
  247. omnibase_infra/models/dispatch/model_tracing_context.py +285 -0
  248. omnibase_infra/models/errors/__init__.py +45 -0
  249. omnibase_infra/models/errors/model_handler_validation_error.py +594 -0
  250. omnibase_infra/models/errors/model_infra_error_context.py +99 -0
  251. omnibase_infra/models/errors/model_message_type_registry_error_context.py +71 -0
  252. omnibase_infra/models/errors/model_timeout_error_context.py +110 -0
  253. omnibase_infra/models/handlers/__init__.py +37 -0
  254. omnibase_infra/models/handlers/model_contract_discovery_result.py +80 -0
  255. omnibase_infra/models/handlers/model_handler_descriptor.py +185 -0
  256. omnibase_infra/models/handlers/model_handler_identifier.py +215 -0
  257. omnibase_infra/models/health/__init__.py +9 -0
  258. omnibase_infra/models/health/model_health_check_result.py +40 -0
  259. omnibase_infra/models/lifecycle/__init__.py +39 -0
  260. omnibase_infra/models/logging/__init__.py +51 -0
  261. omnibase_infra/models/logging/model_log_context.py +756 -0
  262. omnibase_infra/models/model_retry_error_classification.py +78 -0
  263. omnibase_infra/models/projection/__init__.py +43 -0
  264. omnibase_infra/models/projection/model_capability_fields.py +112 -0
  265. omnibase_infra/models/projection/model_registration_projection.py +434 -0
  266. omnibase_infra/models/projection/model_registration_snapshot.py +322 -0
  267. omnibase_infra/models/projection/model_sequence_info.py +182 -0
  268. omnibase_infra/models/projection/model_snapshot_topic_config.py +590 -0
  269. omnibase_infra/models/projectors/__init__.py +41 -0
  270. omnibase_infra/models/projectors/model_projector_column.py +289 -0
  271. omnibase_infra/models/projectors/model_projector_discovery_result.py +65 -0
  272. omnibase_infra/models/projectors/model_projector_index.py +270 -0
  273. omnibase_infra/models/projectors/model_projector_schema.py +415 -0
  274. omnibase_infra/models/projectors/model_projector_validation_error.py +63 -0
  275. omnibase_infra/models/projectors/util_sql_identifiers.py +115 -0
  276. omnibase_infra/models/registration/__init__.py +59 -0
  277. omnibase_infra/models/registration/commands/__init__.py +15 -0
  278. omnibase_infra/models/registration/commands/model_node_registration_acked.py +108 -0
  279. omnibase_infra/models/registration/events/__init__.py +56 -0
  280. omnibase_infra/models/registration/events/model_node_became_active.py +103 -0
  281. omnibase_infra/models/registration/events/model_node_liveness_expired.py +103 -0
  282. omnibase_infra/models/registration/events/model_node_registration_accepted.py +98 -0
  283. omnibase_infra/models/registration/events/model_node_registration_ack_received.py +98 -0
  284. omnibase_infra/models/registration/events/model_node_registration_ack_timed_out.py +112 -0
  285. omnibase_infra/models/registration/events/model_node_registration_initiated.py +107 -0
  286. omnibase_infra/models/registration/events/model_node_registration_rejected.py +104 -0
  287. omnibase_infra/models/registration/model_introspection_metrics.py +253 -0
  288. omnibase_infra/models/registration/model_node_capabilities.py +179 -0
  289. omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
  290. omnibase_infra/models/registration/model_node_introspection_event.py +175 -0
  291. omnibase_infra/models/registration/model_node_metadata.py +79 -0
  292. omnibase_infra/models/registration/model_node_registration.py +162 -0
  293. omnibase_infra/models/registration/model_node_registration_record.py +162 -0
  294. omnibase_infra/models/registry/__init__.py +29 -0
  295. omnibase_infra/models/registry/model_domain_constraint.py +202 -0
  296. omnibase_infra/models/registry/model_message_type_entry.py +271 -0
  297. omnibase_infra/models/resilience/__init__.py +9 -0
  298. omnibase_infra/models/resilience/model_circuit_breaker_config.py +227 -0
  299. omnibase_infra/models/routing/__init__.py +25 -0
  300. omnibase_infra/models/routing/model_routing_entry.py +52 -0
  301. omnibase_infra/models/routing/model_routing_subcontract.py +70 -0
  302. omnibase_infra/models/runtime/__init__.py +40 -0
  303. omnibase_infra/models/runtime/model_contract_security_config.py +41 -0
  304. omnibase_infra/models/runtime/model_discovery_error.py +81 -0
  305. omnibase_infra/models/runtime/model_discovery_result.py +162 -0
  306. omnibase_infra/models/runtime/model_discovery_warning.py +74 -0
  307. omnibase_infra/models/runtime/model_failed_plugin_load.py +63 -0
  308. omnibase_infra/models/runtime/model_handler_contract.py +280 -0
  309. omnibase_infra/models/runtime/model_loaded_handler.py +120 -0
  310. omnibase_infra/models/runtime/model_plugin_load_context.py +93 -0
  311. omnibase_infra/models/runtime/model_plugin_load_summary.py +124 -0
  312. omnibase_infra/models/security/__init__.py +50 -0
  313. omnibase_infra/models/security/classification_levels.py +99 -0
  314. omnibase_infra/models/security/model_environment_policy.py +145 -0
  315. omnibase_infra/models/security/model_handler_security_policy.py +107 -0
  316. omnibase_infra/models/security/model_security_error.py +81 -0
  317. omnibase_infra/models/security/model_security_validation_result.py +328 -0
  318. omnibase_infra/models/security/model_security_warning.py +67 -0
  319. omnibase_infra/models/snapshot/__init__.py +27 -0
  320. omnibase_infra/models/snapshot/model_field_change.py +65 -0
  321. omnibase_infra/models/snapshot/model_snapshot.py +270 -0
  322. omnibase_infra/models/snapshot/model_snapshot_diff.py +203 -0
  323. omnibase_infra/models/snapshot/model_subject_ref.py +81 -0
  324. omnibase_infra/models/types/__init__.py +71 -0
  325. omnibase_infra/models/validation/__init__.py +89 -0
  326. omnibase_infra/models/validation/model_any_type_validation_result.py +118 -0
  327. omnibase_infra/models/validation/model_any_type_violation.py +141 -0
  328. omnibase_infra/models/validation/model_category_match_result.py +345 -0
  329. omnibase_infra/models/validation/model_chain_violation.py +166 -0
  330. omnibase_infra/models/validation/model_coverage_metrics.py +316 -0
  331. omnibase_infra/models/validation/model_execution_shape_rule.py +159 -0
  332. omnibase_infra/models/validation/model_execution_shape_validation.py +208 -0
  333. omnibase_infra/models/validation/model_execution_shape_validation_result.py +294 -0
  334. omnibase_infra/models/validation/model_execution_shape_violation.py +122 -0
  335. omnibase_infra/models/validation/model_localhandler_validation_result.py +139 -0
  336. omnibase_infra/models/validation/model_localhandler_violation.py +100 -0
  337. omnibase_infra/models/validation/model_output_validation_params.py +74 -0
  338. omnibase_infra/models/validation/model_validate_and_raise_params.py +84 -0
  339. omnibase_infra/models/validation/model_validation_error_params.py +84 -0
  340. omnibase_infra/models/validation/model_validation_outcome.py +287 -0
  341. omnibase_infra/nodes/__init__.py +48 -0
  342. omnibase_infra/nodes/architecture_validator/__init__.py +79 -0
  343. omnibase_infra/nodes/architecture_validator/contract.yaml +252 -0
  344. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +208 -0
  345. omnibase_infra/nodes/architecture_validator/mixins/__init__.py +16 -0
  346. omnibase_infra/nodes/architecture_validator/mixins/mixin_file_path_rule.py +92 -0
  347. omnibase_infra/nodes/architecture_validator/models/__init__.py +36 -0
  348. omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_request.py +56 -0
  349. omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_result.py +311 -0
  350. omnibase_infra/nodes/architecture_validator/models/model_architecture_violation.py +163 -0
  351. omnibase_infra/nodes/architecture_validator/models/model_rule_check_result.py +265 -0
  352. omnibase_infra/nodes/architecture_validator/models/model_validation_request.py +105 -0
  353. omnibase_infra/nodes/architecture_validator/models/model_validation_result.py +314 -0
  354. omnibase_infra/nodes/architecture_validator/node.py +262 -0
  355. omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +383 -0
  356. omnibase_infra/nodes/architecture_validator/protocols/__init__.py +9 -0
  357. omnibase_infra/nodes/architecture_validator/protocols/protocol_architecture_rule.py +225 -0
  358. omnibase_infra/nodes/architecture_validator/registry/__init__.py +28 -0
  359. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +99 -0
  360. omnibase_infra/nodes/architecture_validator/validators/__init__.py +104 -0
  361. omnibase_infra/nodes/architecture_validator/validators/validator_no_direct_dispatch.py +422 -0
  362. omnibase_infra/nodes/architecture_validator/validators/validator_no_handler_publishing.py +481 -0
  363. omnibase_infra/nodes/architecture_validator/validators/validator_no_orchestrator_fsm.py +491 -0
  364. omnibase_infra/nodes/effects/README.md +358 -0
  365. omnibase_infra/nodes/effects/__init__.py +26 -0
  366. omnibase_infra/nodes/effects/contract.yaml +172 -0
  367. omnibase_infra/nodes/effects/models/__init__.py +32 -0
  368. omnibase_infra/nodes/effects/models/model_backend_result.py +190 -0
  369. omnibase_infra/nodes/effects/models/model_effect_idempotency_config.py +92 -0
  370. omnibase_infra/nodes/effects/models/model_registry_request.py +132 -0
  371. omnibase_infra/nodes/effects/models/model_registry_response.py +263 -0
  372. omnibase_infra/nodes/effects/protocol_consul_client.py +89 -0
  373. omnibase_infra/nodes/effects/protocol_effect_idempotency_store.py +143 -0
  374. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +96 -0
  375. omnibase_infra/nodes/effects/registry_effect.py +525 -0
  376. omnibase_infra/nodes/effects/store_effect_idempotency_inmemory.py +425 -0
  377. omnibase_infra/nodes/node_registration_orchestrator/README.md +542 -0
  378. omnibase_infra/nodes/node_registration_orchestrator/__init__.py +120 -0
  379. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +475 -0
  380. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/__init__.py +53 -0
  381. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_introspected.py +376 -0
  382. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_registration_acked.py +376 -0
  383. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_runtime_tick.py +373 -0
  384. omnibase_infra/nodes/node_registration_orchestrator/handlers/__init__.py +62 -0
  385. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_heartbeat.py +376 -0
  386. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +609 -0
  387. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_registration_acked.py +458 -0
  388. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_runtime_tick.py +364 -0
  389. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +544 -0
  390. omnibase_infra/nodes/node_registration_orchestrator/models/__init__.py +75 -0
  391. omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_intent_payload.py +194 -0
  392. omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_registration_intent.py +67 -0
  393. omnibase_infra/nodes/node_registration_orchestrator/models/model_intent_execution_result.py +50 -0
  394. omnibase_infra/nodes/node_registration_orchestrator/models/model_node_liveness_expired.py +107 -0
  395. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_config.py +67 -0
  396. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_input.py +41 -0
  397. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_output.py +166 -0
  398. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +235 -0
  399. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_upsert_intent.py +68 -0
  400. omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_execution_result.py +384 -0
  401. omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_state.py +60 -0
  402. omnibase_infra/nodes/node_registration_orchestrator/models/model_registration_intent.py +177 -0
  403. omnibase_infra/nodes/node_registration_orchestrator/models/model_registry_intent.py +247 -0
  404. omnibase_infra/nodes/node_registration_orchestrator/node.py +195 -0
  405. omnibase_infra/nodes/node_registration_orchestrator/plugin.py +909 -0
  406. omnibase_infra/nodes/node_registration_orchestrator/protocols.py +439 -0
  407. omnibase_infra/nodes/node_registration_orchestrator/registry/__init__.py +41 -0
  408. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +525 -0
  409. omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +392 -0
  410. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +742 -0
  411. omnibase_infra/nodes/node_registration_reducer/__init__.py +15 -0
  412. omnibase_infra/nodes/node_registration_reducer/contract.yaml +301 -0
  413. omnibase_infra/nodes/node_registration_reducer/models/__init__.py +38 -0
  414. omnibase_infra/nodes/node_registration_reducer/models/model_validation_result.py +113 -0
  415. omnibase_infra/nodes/node_registration_reducer/node.py +139 -0
  416. omnibase_infra/nodes/node_registration_reducer/registry/__init__.py +9 -0
  417. omnibase_infra/nodes/node_registration_reducer/registry/registry_infra_node_registration_reducer.py +79 -0
  418. omnibase_infra/nodes/node_registration_storage_effect/__init__.py +41 -0
  419. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +225 -0
  420. omnibase_infra/nodes/node_registration_storage_effect/models/__init__.py +44 -0
  421. omnibase_infra/nodes/node_registration_storage_effect/models/model_delete_result.py +132 -0
  422. omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_record.py +199 -0
  423. omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_update.py +155 -0
  424. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_details.py +123 -0
  425. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_result.py +117 -0
  426. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_query.py +100 -0
  427. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_result.py +136 -0
  428. omnibase_infra/nodes/node_registration_storage_effect/models/model_upsert_result.py +127 -0
  429. omnibase_infra/nodes/node_registration_storage_effect/node.py +109 -0
  430. omnibase_infra/nodes/node_registration_storage_effect/protocols/__init__.py +22 -0
  431. omnibase_infra/nodes/node_registration_storage_effect/protocols/protocol_registration_persistence.py +333 -0
  432. omnibase_infra/nodes/node_registration_storage_effect/registry/__init__.py +23 -0
  433. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +194 -0
  434. omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
  435. omnibase_infra/nodes/node_registry_effect/contract.yaml +682 -0
  436. omnibase_infra/nodes/node_registry_effect/handlers/__init__.py +70 -0
  437. omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_deregister.py +211 -0
  438. omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_register.py +212 -0
  439. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +416 -0
  440. omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_deactivate.py +215 -0
  441. omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_upsert.py +208 -0
  442. omnibase_infra/nodes/node_registry_effect/models/__init__.py +43 -0
  443. omnibase_infra/nodes/node_registry_effect/models/model_partial_retry_request.py +92 -0
  444. omnibase_infra/nodes/node_registry_effect/node.py +165 -0
  445. omnibase_infra/nodes/node_registry_effect/registry/__init__.py +27 -0
  446. omnibase_infra/nodes/node_registry_effect/registry/registry_infra_registry_effect.py +196 -0
  447. omnibase_infra/nodes/node_service_discovery_effect/__init__.py +111 -0
  448. omnibase_infra/nodes/node_service_discovery_effect/contract.yaml +246 -0
  449. omnibase_infra/nodes/node_service_discovery_effect/models/__init__.py +67 -0
  450. omnibase_infra/nodes/node_service_discovery_effect/models/enum_health_status.py +72 -0
  451. omnibase_infra/nodes/node_service_discovery_effect/models/enum_service_discovery_operation.py +58 -0
  452. omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_query.py +99 -0
  453. omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_result.py +98 -0
  454. omnibase_infra/nodes/node_service_discovery_effect/models/model_health_check_config.py +121 -0
  455. omnibase_infra/nodes/node_service_discovery_effect/models/model_query_metadata.py +63 -0
  456. omnibase_infra/nodes/node_service_discovery_effect/models/model_registration_result.py +130 -0
  457. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_details.py +111 -0
  458. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_result.py +119 -0
  459. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_info.py +106 -0
  460. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_registration.py +121 -0
  461. omnibase_infra/nodes/node_service_discovery_effect/node.py +111 -0
  462. omnibase_infra/nodes/node_service_discovery_effect/protocols/__init__.py +14 -0
  463. omnibase_infra/nodes/node_service_discovery_effect/protocols/protocol_discovery_operations.py +279 -0
  464. omnibase_infra/nodes/node_service_discovery_effect/registry/__init__.py +13 -0
  465. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +214 -0
  466. omnibase_infra/nodes/reducers/__init__.py +30 -0
  467. omnibase_infra/nodes/reducers/models/__init__.py +32 -0
  468. omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +76 -0
  469. omnibase_infra/nodes/reducers/models/model_payload_postgres_upsert_registration.py +60 -0
  470. omnibase_infra/nodes/reducers/models/model_registration_confirmation.py +166 -0
  471. omnibase_infra/nodes/reducers/models/model_registration_state.py +433 -0
  472. omnibase_infra/nodes/reducers/registration_reducer.py +1137 -0
  473. omnibase_infra/observability/__init__.py +143 -0
  474. omnibase_infra/observability/constants_metrics.py +91 -0
  475. omnibase_infra/observability/factory_observability_sink.py +525 -0
  476. omnibase_infra/observability/handlers/__init__.py +118 -0
  477. omnibase_infra/observability/handlers/handler_logging_structured.py +967 -0
  478. omnibase_infra/observability/handlers/handler_metrics_prometheus.py +1120 -0
  479. omnibase_infra/observability/handlers/model_logging_handler_config.py +71 -0
  480. omnibase_infra/observability/handlers/model_logging_handler_response.py +77 -0
  481. omnibase_infra/observability/handlers/model_metrics_handler_config.py +172 -0
  482. omnibase_infra/observability/handlers/model_metrics_handler_payload.py +135 -0
  483. omnibase_infra/observability/handlers/model_metrics_handler_response.py +101 -0
  484. omnibase_infra/observability/hooks/__init__.py +74 -0
  485. omnibase_infra/observability/hooks/hook_observability.py +1223 -0
  486. omnibase_infra/observability/models/__init__.py +30 -0
  487. omnibase_infra/observability/models/enum_required_log_context_key.py +77 -0
  488. omnibase_infra/observability/models/model_buffered_log_entry.py +117 -0
  489. omnibase_infra/observability/models/model_logging_sink_config.py +73 -0
  490. omnibase_infra/observability/models/model_metrics_sink_config.py +156 -0
  491. omnibase_infra/observability/sinks/__init__.py +69 -0
  492. omnibase_infra/observability/sinks/sink_logging_structured.py +809 -0
  493. omnibase_infra/observability/sinks/sink_metrics_prometheus.py +710 -0
  494. omnibase_infra/plugins/__init__.py +27 -0
  495. omnibase_infra/plugins/examples/__init__.py +28 -0
  496. omnibase_infra/plugins/examples/plugin_json_normalizer.py +271 -0
  497. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +210 -0
  498. omnibase_infra/plugins/models/__init__.py +21 -0
  499. omnibase_infra/plugins/models/model_plugin_context.py +76 -0
  500. omnibase_infra/plugins/models/model_plugin_input_data.py +58 -0
  501. omnibase_infra/plugins/models/model_plugin_output_data.py +62 -0
  502. omnibase_infra/plugins/plugin_compute_base.py +435 -0
  503. omnibase_infra/projectors/__init__.py +30 -0
  504. omnibase_infra/projectors/contracts/__init__.py +63 -0
  505. omnibase_infra/projectors/contracts/registration_projector.yaml +370 -0
  506. omnibase_infra/projectors/projection_reader_registration.py +1559 -0
  507. omnibase_infra/projectors/snapshot_publisher_registration.py +1329 -0
  508. omnibase_infra/protocols/__init__.py +99 -0
  509. omnibase_infra/protocols/protocol_capability_projection.py +253 -0
  510. omnibase_infra/protocols/protocol_capability_query.py +251 -0
  511. omnibase_infra/protocols/protocol_event_bus_like.py +127 -0
  512. omnibase_infra/protocols/protocol_event_projector.py +96 -0
  513. omnibase_infra/protocols/protocol_idempotency_store.py +142 -0
  514. omnibase_infra/protocols/protocol_message_dispatcher.py +247 -0
  515. omnibase_infra/protocols/protocol_message_type_registry.py +306 -0
  516. omnibase_infra/protocols/protocol_plugin_compute.py +368 -0
  517. omnibase_infra/protocols/protocol_projector_schema_validator.py +82 -0
  518. omnibase_infra/protocols/protocol_registry_metrics.py +215 -0
  519. omnibase_infra/protocols/protocol_snapshot_publisher.py +396 -0
  520. omnibase_infra/protocols/protocol_snapshot_store.py +567 -0
  521. omnibase_infra/runtime/__init__.py +296 -0
  522. omnibase_infra/runtime/binding_config_resolver.py +2706 -0
  523. omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
  524. omnibase_infra/runtime/contract_handler_discovery.py +582 -0
  525. omnibase_infra/runtime/contract_loaders/__init__.py +42 -0
  526. omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
  527. omnibase_infra/runtime/dispatch_context_enforcer.py +427 -0
  528. omnibase_infra/runtime/enums/__init__.py +18 -0
  529. omnibase_infra/runtime/enums/enum_config_ref_scheme.py +33 -0
  530. omnibase_infra/runtime/enums/enum_scheduler_status.py +170 -0
  531. omnibase_infra/runtime/envelope_validator.py +179 -0
  532. omnibase_infra/runtime/handler_contract_source.py +669 -0
  533. omnibase_infra/runtime/handler_plugin_loader.py +2029 -0
  534. omnibase_infra/runtime/handler_registry.py +321 -0
  535. omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
  536. omnibase_infra/runtime/kernel.py +40 -0
  537. omnibase_infra/runtime/mixin_policy_validation.py +522 -0
  538. omnibase_infra/runtime/mixin_semver_cache.py +378 -0
  539. omnibase_infra/runtime/mixins/__init__.py +17 -0
  540. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +757 -0
  541. omnibase_infra/runtime/models/__init__.py +192 -0
  542. omnibase_infra/runtime/models/model_batch_lifecycle_result.py +217 -0
  543. omnibase_infra/runtime/models/model_binding_config.py +168 -0
  544. omnibase_infra/runtime/models/model_binding_config_cache_stats.py +135 -0
  545. omnibase_infra/runtime/models/model_binding_config_resolver_config.py +329 -0
  546. omnibase_infra/runtime/models/model_cached_secret.py +138 -0
  547. omnibase_infra/runtime/models/model_compute_key.py +138 -0
  548. omnibase_infra/runtime/models/model_compute_registration.py +97 -0
  549. omnibase_infra/runtime/models/model_config_cache_entry.py +61 -0
  550. omnibase_infra/runtime/models/model_config_ref.py +331 -0
  551. omnibase_infra/runtime/models/model_config_ref_parse_result.py +125 -0
  552. omnibase_infra/runtime/models/model_domain_plugin_config.py +92 -0
  553. omnibase_infra/runtime/models/model_domain_plugin_result.py +270 -0
  554. omnibase_infra/runtime/models/model_duplicate_response.py +54 -0
  555. omnibase_infra/runtime/models/model_enabled_protocols_config.py +61 -0
  556. omnibase_infra/runtime/models/model_event_bus_config.py +54 -0
  557. omnibase_infra/runtime/models/model_failed_component.py +55 -0
  558. omnibase_infra/runtime/models/model_health_check_response.py +168 -0
  559. omnibase_infra/runtime/models/model_health_check_result.py +228 -0
  560. omnibase_infra/runtime/models/model_lifecycle_result.py +245 -0
  561. omnibase_infra/runtime/models/model_logging_config.py +42 -0
  562. omnibase_infra/runtime/models/model_optional_correlation_id.py +167 -0
  563. omnibase_infra/runtime/models/model_optional_string.py +94 -0
  564. omnibase_infra/runtime/models/model_optional_uuid.py +110 -0
  565. omnibase_infra/runtime/models/model_policy_context.py +100 -0
  566. omnibase_infra/runtime/models/model_policy_key.py +138 -0
  567. omnibase_infra/runtime/models/model_policy_registration.py +139 -0
  568. omnibase_infra/runtime/models/model_policy_result.py +103 -0
  569. omnibase_infra/runtime/models/model_policy_type_filter.py +157 -0
  570. omnibase_infra/runtime/models/model_projector_plugin_loader_config.py +47 -0
  571. omnibase_infra/runtime/models/model_protocol_registration_config.py +65 -0
  572. omnibase_infra/runtime/models/model_retry_policy.py +105 -0
  573. omnibase_infra/runtime/models/model_runtime_config.py +150 -0
  574. omnibase_infra/runtime/models/model_runtime_scheduler_config.py +624 -0
  575. omnibase_infra/runtime/models/model_runtime_scheduler_metrics.py +233 -0
  576. omnibase_infra/runtime/models/model_runtime_tick.py +193 -0
  577. omnibase_infra/runtime/models/model_secret_cache_stats.py +82 -0
  578. omnibase_infra/runtime/models/model_secret_mapping.py +63 -0
  579. omnibase_infra/runtime/models/model_secret_resolver_config.py +107 -0
  580. omnibase_infra/runtime/models/model_secret_resolver_metrics.py +111 -0
  581. omnibase_infra/runtime/models/model_secret_source_info.py +72 -0
  582. omnibase_infra/runtime/models/model_secret_source_spec.py +66 -0
  583. omnibase_infra/runtime/models/model_shutdown_batch_result.py +75 -0
  584. omnibase_infra/runtime/models/model_shutdown_config.py +94 -0
  585. omnibase_infra/runtime/projector_plugin_loader.py +1462 -0
  586. omnibase_infra/runtime/projector_schema_manager.py +565 -0
  587. omnibase_infra/runtime/projector_shell.py +1102 -0
  588. omnibase_infra/runtime/protocol_contract_descriptor.py +92 -0
  589. omnibase_infra/runtime/protocol_contract_source.py +92 -0
  590. omnibase_infra/runtime/protocol_domain_plugin.py +474 -0
  591. omnibase_infra/runtime/protocol_handler_discovery.py +221 -0
  592. omnibase_infra/runtime/protocol_handler_plugin_loader.py +327 -0
  593. omnibase_infra/runtime/protocol_lifecycle_executor.py +435 -0
  594. omnibase_infra/runtime/protocol_policy.py +366 -0
  595. omnibase_infra/runtime/protocols/__init__.py +27 -0
  596. omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -0
  597. omnibase_infra/runtime/registry/__init__.py +93 -0
  598. omnibase_infra/runtime/registry/mixin_message_type_query.py +326 -0
  599. omnibase_infra/runtime/registry/mixin_message_type_registration.py +354 -0
  600. omnibase_infra/runtime/registry/registry_event_bus_binding.py +268 -0
  601. omnibase_infra/runtime/registry/registry_message_type.py +542 -0
  602. omnibase_infra/runtime/registry/registry_protocol_binding.py +444 -0
  603. omnibase_infra/runtime/registry_compute.py +1143 -0
  604. omnibase_infra/runtime/registry_dispatcher.py +678 -0
  605. omnibase_infra/runtime/registry_policy.py +1502 -0
  606. omnibase_infra/runtime/runtime_scheduler.py +1070 -0
  607. omnibase_infra/runtime/secret_resolver.py +2110 -0
  608. omnibase_infra/runtime/security_metadata_validator.py +776 -0
  609. omnibase_infra/runtime/service_kernel.py +1573 -0
  610. omnibase_infra/runtime/service_message_dispatch_engine.py +1805 -0
  611. omnibase_infra/runtime/service_runtime_host_process.py +2260 -0
  612. omnibase_infra/runtime/util_container_wiring.py +1123 -0
  613. omnibase_infra/runtime/util_validation.py +314 -0
  614. omnibase_infra/runtime/util_version.py +98 -0
  615. omnibase_infra/runtime/util_wiring.py +566 -0
  616. omnibase_infra/schemas/schema_registration_projection.sql +320 -0
  617. omnibase_infra/services/__init__.py +68 -0
  618. omnibase_infra/services/corpus_capture.py +678 -0
  619. omnibase_infra/services/service_capability_query.py +945 -0
  620. omnibase_infra/services/service_health.py +897 -0
  621. omnibase_infra/services/service_node_selector.py +530 -0
  622. omnibase_infra/services/service_timeout_emitter.py +682 -0
  623. omnibase_infra/services/service_timeout_scanner.py +390 -0
  624. omnibase_infra/services/snapshot/__init__.py +31 -0
  625. omnibase_infra/services/snapshot/service_snapshot.py +647 -0
  626. omnibase_infra/services/snapshot/store_inmemory.py +637 -0
  627. omnibase_infra/services/snapshot/store_postgres.py +1279 -0
  628. omnibase_infra/shared/__init__.py +8 -0
  629. omnibase_infra/testing/__init__.py +10 -0
  630. omnibase_infra/testing/utils.py +23 -0
  631. omnibase_infra/types/__init__.py +48 -0
  632. omnibase_infra/types/type_cache_info.py +49 -0
  633. omnibase_infra/types/type_dsn.py +173 -0
  634. omnibase_infra/types/type_infra_aliases.py +60 -0
  635. omnibase_infra/types/typed_dict/__init__.py +21 -0
  636. omnibase_infra/types/typed_dict/typed_dict_introspection_cache.py +128 -0
  637. omnibase_infra/types/typed_dict/typed_dict_performance_metrics_cache.py +140 -0
  638. omnibase_infra/types/typed_dict_capabilities.py +64 -0
  639. omnibase_infra/utils/__init__.py +89 -0
  640. omnibase_infra/utils/correlation.py +208 -0
  641. omnibase_infra/utils/util_datetime.py +372 -0
  642. omnibase_infra/utils/util_dsn_validation.py +333 -0
  643. omnibase_infra/utils/util_env_parsing.py +264 -0
  644. omnibase_infra/utils/util_error_sanitization.py +457 -0
  645. omnibase_infra/utils/util_pydantic_validators.py +477 -0
  646. omnibase_infra/utils/util_semver.py +233 -0
  647. omnibase_infra/validation/__init__.py +307 -0
  648. omnibase_infra/validation/enums/__init__.py +11 -0
  649. omnibase_infra/validation/enums/enum_contract_violation_severity.py +13 -0
  650. omnibase_infra/validation/infra_validators.py +1486 -0
  651. omnibase_infra/validation/linter_contract.py +907 -0
  652. omnibase_infra/validation/mixin_any_type_classification.py +120 -0
  653. omnibase_infra/validation/mixin_any_type_exemption.py +580 -0
  654. omnibase_infra/validation/mixin_any_type_reporting.py +106 -0
  655. omnibase_infra/validation/mixin_execution_shape_violation_checks.py +596 -0
  656. omnibase_infra/validation/mixin_node_archetype_detection.py +254 -0
  657. omnibase_infra/validation/models/__init__.py +15 -0
  658. omnibase_infra/validation/models/model_contract_lint_result.py +101 -0
  659. omnibase_infra/validation/models/model_contract_violation.py +41 -0
  660. omnibase_infra/validation/service_validation_aggregator.py +395 -0
  661. omnibase_infra/validation/validation_exemptions.yaml +1710 -0
  662. omnibase_infra/validation/validator_any_type.py +715 -0
  663. omnibase_infra/validation/validator_chain_propagation.py +839 -0
  664. omnibase_infra/validation/validator_execution_shape.py +465 -0
  665. omnibase_infra/validation/validator_localhandler.py +261 -0
  666. omnibase_infra/validation/validator_registration_security.py +410 -0
  667. omnibase_infra/validation/validator_routing_coverage.py +1020 -0
  668. omnibase_infra/validation/validator_runtime_shape.py +915 -0
  669. omnibase_infra/validation/validator_security.py +410 -0
  670. omnibase_infra/validation/validator_topic_category.py +1152 -0
  671. omnibase_infra-0.2.1.dist-info/METADATA +197 -0
  672. omnibase_infra-0.2.1.dist-info/RECORD +675 -0
  673. omnibase_infra-0.2.1.dist-info/WHEEL +4 -0
  674. omnibase_infra-0.2.1.dist-info/entry_points.txt +4 -0
  675. omnibase_infra-0.2.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,2260 @@
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 json
47
+ import logging
48
+ from collections.abc import Awaitable, Callable
49
+ from pathlib import Path
50
+ from typing import TYPE_CHECKING
51
+ from uuid import UUID, uuid4
52
+
53
+ from pydantic import BaseModel
54
+
55
+ from omnibase_infra.enums import EnumInfraTransportType
56
+ from omnibase_infra.errors import (
57
+ EnvelopeValidationError,
58
+ ModelInfraErrorContext,
59
+ ProtocolConfigurationError,
60
+ RuntimeHostError,
61
+ UnknownHandlerTypeError,
62
+ )
63
+ from omnibase_infra.event_bus.event_bus_inmemory import EventBusInmemory
64
+ from omnibase_infra.event_bus.event_bus_kafka import EventBusKafka
65
+ from omnibase_infra.runtime.envelope_validator import (
66
+ normalize_correlation_id,
67
+ validate_envelope,
68
+ )
69
+ from omnibase_infra.runtime.handler_registry import RegistryProtocolBinding
70
+ from omnibase_infra.runtime.models import ModelDuplicateResponse
71
+ from omnibase_infra.runtime.protocol_lifecycle_executor import ProtocolLifecycleExecutor
72
+ from omnibase_infra.runtime.util_wiring import wire_default_handlers
73
+ from omnibase_infra.utils.util_env_parsing import parse_env_float
74
+
75
+ if TYPE_CHECKING:
76
+ from omnibase_core.container import ModelONEXContainer
77
+ from omnibase_infra.event_bus.models import ModelEventMessage
78
+ from omnibase_infra.idempotency import ModelIdempotencyGuardConfig
79
+ from omnibase_infra.idempotency.protocol_idempotency_store import (
80
+ ProtocolIdempotencyStore,
81
+ )
82
+ from omnibase_infra.nodes.architecture_validator import ProtocolArchitectureRule
83
+ from omnibase_infra.runtime.contract_handler_discovery import (
84
+ ContractHandlerDiscovery,
85
+ )
86
+ from omnibase_spi.protocols.handlers.protocol_handler import ProtocolHandler
87
+
88
+ from omnibase_infra.models.types import JsonDict
89
+
90
+ # Expose wire_default_handlers as wire_handlers for test patching compatibility
91
+ # Tests patch "omnibase_infra.runtime.service_runtime_host_process.wire_handlers"
92
+ wire_handlers = wire_default_handlers
93
+
94
+ logger = logging.getLogger(__name__)
95
+
96
+ # Default configuration values
97
+ DEFAULT_INPUT_TOPIC = "requests"
98
+ DEFAULT_OUTPUT_TOPIC = "responses"
99
+ DEFAULT_GROUP_ID = "runtime-host"
100
+
101
+ # Health check timeout bounds (per ModelLifecycleSubcontract)
102
+ MIN_HEALTH_CHECK_TIMEOUT = 1.0
103
+ MAX_HEALTH_CHECK_TIMEOUT = 60.0
104
+ DEFAULT_HEALTH_CHECK_TIMEOUT: float = parse_env_float(
105
+ "ONEX_HEALTH_CHECK_TIMEOUT",
106
+ 5.0,
107
+ min_value=MIN_HEALTH_CHECK_TIMEOUT,
108
+ max_value=MAX_HEALTH_CHECK_TIMEOUT,
109
+ transport_type=EnumInfraTransportType.RUNTIME,
110
+ service_name="runtime_host_process",
111
+ )
112
+
113
+ # Drain timeout bounds for graceful shutdown (OMN-756)
114
+ # Controls how long to wait for in-flight messages to complete before shutdown
115
+ MIN_DRAIN_TIMEOUT_SECONDS = 1.0
116
+ MAX_DRAIN_TIMEOUT_SECONDS = 300.0
117
+ DEFAULT_DRAIN_TIMEOUT_SECONDS: float = parse_env_float(
118
+ "ONEX_DRAIN_TIMEOUT",
119
+ 30.0,
120
+ min_value=MIN_DRAIN_TIMEOUT_SECONDS,
121
+ max_value=MAX_DRAIN_TIMEOUT_SECONDS,
122
+ transport_type=EnumInfraTransportType.RUNTIME,
123
+ service_name="runtime_host_process",
124
+ )
125
+
126
+
127
+ class RuntimeHostProcess:
128
+ """Runtime host process that owns event bus and coordinates handlers.
129
+
130
+ The RuntimeHostProcess is the central coordinator for ONEX infrastructure
131
+ runtime. It owns an event bus instance (EventBusInmemory or EventBusKafka),
132
+ registers handlers via the wiring module, and routes incoming envelopes to
133
+ appropriate handlers.
134
+
135
+ Container Integration:
136
+ RuntimeHostProcess now accepts a ModelONEXContainer parameter for
137
+ dependency injection. The container provides access to:
138
+ - RegistryProtocolBinding: Handler registry for protocol routing
139
+
140
+ This follows ONEX container-based DI patterns for better testability
141
+ and lifecycle management. The legacy singleton pattern is deprecated
142
+ in favor of container resolution.
143
+
144
+ Attributes:
145
+ event_bus: The owned event bus instance (EventBusInmemory or EventBusKafka)
146
+ is_running: Whether the process is currently running
147
+ input_topic: Topic to subscribe to for incoming envelopes
148
+ output_topic: Topic to publish responses to
149
+ group_id: Consumer group identifier
150
+
151
+ Example:
152
+ ```python
153
+ from omnibase_core.container import ModelONEXContainer
154
+ from omnibase_infra.runtime.util_container_wiring import wire_infrastructure_services
155
+
156
+ # Container-based initialization (preferred)
157
+ container = ModelONEXContainer()
158
+ wire_infrastructure_services(container)
159
+ process = RuntimeHostProcess(container=container)
160
+ await process.start()
161
+ health = await process.health_check()
162
+ await process.stop()
163
+
164
+ # Direct initialization (without container)
165
+ process = RuntimeHostProcess() # Uses singleton registries
166
+ ```
167
+
168
+ Graceful Shutdown:
169
+ The stop() method implements graceful shutdown with a configurable drain
170
+ period. After unsubscribing from topics, it waits for in-flight messages
171
+ to complete before shutting down handlers and closing the event bus.
172
+ See stop() docstring for configuration details.
173
+ """
174
+
175
+ def __init__(
176
+ self,
177
+ container: ModelONEXContainer | None = None,
178
+ event_bus: EventBusInmemory | EventBusKafka | None = None,
179
+ input_topic: str = DEFAULT_INPUT_TOPIC,
180
+ output_topic: str = DEFAULT_OUTPUT_TOPIC,
181
+ config: dict[str, object] | None = None,
182
+ handler_registry: RegistryProtocolBinding | None = None,
183
+ architecture_rules: tuple[ProtocolArchitectureRule, ...] | None = None,
184
+ contract_paths: list[str] | None = None,
185
+ ) -> None:
186
+ """Initialize the runtime host process.
187
+
188
+ Args:
189
+ container: Optional ONEX dependency injection container. When provided,
190
+ the runtime host can resolve dependencies from the container if they
191
+ are not explicitly provided. This follows the ONEX container-based
192
+ DI pattern for better testability and explicit dependency management.
193
+
194
+ Container Resolution (during async start()):
195
+ - If handler_registry is None and container is provided, resolves
196
+ RegistryProtocolBinding from container.service_registry
197
+ - Event bus must be provided explicitly or defaults to EventBusInmemory
198
+ (required immediately during __init__)
199
+
200
+ Usage:
201
+ ```python
202
+ from omnibase_core.container import ModelONEXContainer
203
+ from omnibase_infra.runtime.util_container_wiring import wire_infrastructure_services
204
+
205
+ container = ModelONEXContainer()
206
+ await wire_infrastructure_services(container)
207
+ process = RuntimeHostProcess(container=container)
208
+ await process.start()
209
+ ```
210
+
211
+ event_bus: Optional event bus instance (EventBusInmemory or EventBusKafka).
212
+ If None, creates EventBusInmemory.
213
+ input_topic: Topic to subscribe to for incoming envelopes.
214
+ output_topic: Topic to publish responses to.
215
+ config: Optional configuration dict that can override topics and group_id.
216
+ Supported keys:
217
+ - input_topic: Override input topic
218
+ - output_topic: Override output topic
219
+ - group_id: Override consumer group identifier
220
+ - health_check_timeout_seconds: Timeout for individual handler
221
+ health checks (default: 5.0 seconds, valid range: 1-60 per
222
+ ModelLifecycleSubcontract). Values outside this range are
223
+ clamped to the nearest bound with a warning logged.
224
+ Invalid string values fall back to the default with a warning.
225
+ - drain_timeout_seconds: Maximum time to wait for in-flight
226
+ messages to complete during graceful shutdown (default: 30.0
227
+ seconds, valid range: 1-300). Values outside this range are
228
+ clamped to the nearest bound with a warning logged.
229
+ handler_registry: Optional RegistryProtocolBinding instance for handler lookup.
230
+ Type: RegistryProtocolBinding | None
231
+
232
+ Purpose:
233
+ Provides the registry that maps handler_type strings (e.g., "http", "db")
234
+ to their corresponding ProtocolHandler classes. The registry is queried
235
+ during start() to instantiate and initialize all registered handlers.
236
+
237
+ Resolution Order:
238
+ 1. If handler_registry is provided, uses this pre-resolved registry
239
+ 2. If container is provided, resolves from container.service_registry
240
+ 3. If None, falls back to singleton via get_handler_registry()
241
+
242
+ Container Integration:
243
+ When using container-based DI (recommended), resolve the registry from
244
+ the container and pass it to RuntimeHostProcess:
245
+
246
+ ```python
247
+ container = ModelONEXContainer()
248
+ wire_infrastructure_services(container)
249
+ registry = container.service_registry.resolve_service(RegistryProtocolBinding)
250
+ process = RuntimeHostProcess(handler_registry=registry)
251
+ ```
252
+
253
+ This follows ONEX container-based DI patterns for better testability
254
+ and explicit dependency management.
255
+
256
+ container: Optional ONEX container for dependency injection. Required for
257
+ architecture validation. If None and architecture validation is requested,
258
+ a minimal container will be created.
259
+
260
+ architecture_rules: Optional tuple of architecture rules to validate at startup.
261
+ Type: tuple[ProtocolArchitectureRule, ...] | None
262
+
263
+ Purpose:
264
+ Architecture rules are validated BEFORE the runtime starts. Violations
265
+ with ERROR severity will prevent startup. Violations with WARNING
266
+ severity are logged but don't block startup.
267
+
268
+ Rules implementing ProtocolArchitectureRule can be:
269
+ - Custom rules specific to your application
270
+ - Standard rules from OMN-1099 validators
271
+
272
+ Example:
273
+ ```python
274
+ from my_rules import NoHandlerPublishingRule, NoAnyTypesRule
275
+
276
+ process = RuntimeHostProcess(
277
+ container=container,
278
+ architecture_rules=(
279
+ NoHandlerPublishingRule(),
280
+ NoAnyTypesRule(),
281
+ ),
282
+ )
283
+ await process.start() # Validates architecture first
284
+ ```
285
+
286
+ contract_paths: Optional list of paths to scan for handler contracts.
287
+ Type: list[str] | None
288
+
289
+ Purpose:
290
+ Enables contract-based handler discovery. When provided, the runtime
291
+ will auto-discover and register handlers from these paths during
292
+ start() instead of using wire_default_handlers().
293
+
294
+ Paths can be:
295
+ - Directories: Recursively scanned for handler contracts
296
+ - Files: Directly loaded as contract files
297
+
298
+ Behavior:
299
+ - If contract_paths is provided: Uses ContractHandlerDiscovery
300
+ to auto-discover and register handlers from the specified paths.
301
+ - If contract_paths is None or empty: Falls back to the existing
302
+ wire_default_handlers() behavior.
303
+
304
+ Error Handling:
305
+ Discovery errors are logged but do not block startup. This enables
306
+ graceful degradation where some handlers can be registered even
307
+ if others fail to load.
308
+
309
+ Example:
310
+ ```python
311
+ # Contract-based handler discovery
312
+ process = RuntimeHostProcess(
313
+ contract_paths=["src/nodes/handlers", "plugins/"]
314
+ )
315
+ await process.start()
316
+
317
+ # Or with explicit file paths
318
+ process = RuntimeHostProcess(
319
+ contract_paths=[
320
+ "handlers/auth/handler_contract.yaml",
321
+ "handlers/db/handler_contract.yaml",
322
+ ]
323
+ )
324
+ ```
325
+ """
326
+ # Store container reference for dependency resolution
327
+ self._container: ModelONEXContainer | None = container
328
+ # Handler registry (container-based DI or singleton fallback)
329
+ self._handler_registry: RegistryProtocolBinding | None = handler_registry
330
+
331
+ # Architecture rules for startup validation
332
+ self._architecture_rules: tuple[ProtocolArchitectureRule, ...] = (
333
+ architecture_rules or ()
334
+ )
335
+
336
+ # Contract paths for handler discovery (OMN-1133)
337
+ # Convert strings to Path objects for consistent filesystem operations
338
+ self._contract_paths: list[Path] = (
339
+ [Path(p) for p in contract_paths] if contract_paths else []
340
+ )
341
+
342
+ # Handler discovery service (lazy-created if contract_paths provided)
343
+ self._handler_discovery: ContractHandlerDiscovery | None = None
344
+
345
+ # Create or use provided event bus
346
+ self._event_bus: EventBusInmemory | EventBusKafka = (
347
+ event_bus or EventBusInmemory()
348
+ )
349
+
350
+ # Extract configuration with defaults
351
+ config = config or {}
352
+
353
+ # Topic configuration (config overrides constructor args)
354
+ self._input_topic: str = str(config.get("input_topic", input_topic))
355
+ self._output_topic: str = str(config.get("output_topic", output_topic))
356
+ # Note: ModelRuntimeConfig uses field name "consumer_group" with alias "group_id".
357
+ # When config.model_dump() is called, it outputs "consumer_group" by default.
358
+ # We check both keys to support either field name or alias.
359
+ # Empty strings and whitespace-only strings fall through to the next option.
360
+ consumer_group = config.get("consumer_group")
361
+ group_id = config.get("group_id")
362
+ self._group_id: str = str(
363
+ (consumer_group if consumer_group and str(consumer_group).strip() else None)
364
+ or (group_id if group_id and str(group_id).strip() else None)
365
+ or DEFAULT_GROUP_ID
366
+ )
367
+
368
+ # Health check configuration (from lifecycle subcontract pattern)
369
+ # Default: 5.0 seconds, valid range: 1-60 seconds per ModelLifecycleSubcontract
370
+ # Values outside bounds are clamped with a warning
371
+ _timeout_raw = config.get("health_check_timeout_seconds")
372
+ timeout_value: float = DEFAULT_HEALTH_CHECK_TIMEOUT
373
+ if isinstance(_timeout_raw, int | float):
374
+ timeout_value = float(_timeout_raw)
375
+ elif isinstance(_timeout_raw, str):
376
+ try:
377
+ timeout_value = float(_timeout_raw)
378
+ except ValueError:
379
+ logger.warning(
380
+ "Invalid health_check_timeout_seconds string value, using default",
381
+ extra={
382
+ "invalid_value": _timeout_raw,
383
+ "default_value": DEFAULT_HEALTH_CHECK_TIMEOUT,
384
+ },
385
+ )
386
+ timeout_value = DEFAULT_HEALTH_CHECK_TIMEOUT
387
+
388
+ # Validate bounds and clamp if necessary
389
+ if (
390
+ timeout_value < MIN_HEALTH_CHECK_TIMEOUT
391
+ or timeout_value > MAX_HEALTH_CHECK_TIMEOUT
392
+ ):
393
+ logger.warning(
394
+ "health_check_timeout_seconds out of valid range, clamping",
395
+ extra={
396
+ "original_value": timeout_value,
397
+ "min_value": MIN_HEALTH_CHECK_TIMEOUT,
398
+ "max_value": MAX_HEALTH_CHECK_TIMEOUT,
399
+ "clamped_value": max(
400
+ MIN_HEALTH_CHECK_TIMEOUT,
401
+ min(timeout_value, MAX_HEALTH_CHECK_TIMEOUT),
402
+ ),
403
+ },
404
+ )
405
+ timeout_value = max(
406
+ MIN_HEALTH_CHECK_TIMEOUT,
407
+ min(timeout_value, MAX_HEALTH_CHECK_TIMEOUT),
408
+ )
409
+
410
+ self._health_check_timeout_seconds: float = timeout_value
411
+
412
+ # Drain timeout configuration for graceful shutdown (OMN-756)
413
+ # Default: 30.0 seconds, valid range: 1-300 seconds
414
+ # Values outside bounds are clamped with a warning
415
+ _drain_timeout_raw = config.get("drain_timeout_seconds")
416
+ drain_timeout_value: float = DEFAULT_DRAIN_TIMEOUT_SECONDS
417
+ if isinstance(_drain_timeout_raw, int | float):
418
+ drain_timeout_value = float(_drain_timeout_raw)
419
+ elif isinstance(_drain_timeout_raw, str):
420
+ try:
421
+ drain_timeout_value = float(_drain_timeout_raw)
422
+ except ValueError:
423
+ logger.warning(
424
+ "Invalid drain_timeout_seconds string value, using default",
425
+ extra={
426
+ "invalid_value": _drain_timeout_raw,
427
+ "default_value": DEFAULT_DRAIN_TIMEOUT_SECONDS,
428
+ },
429
+ )
430
+ drain_timeout_value = DEFAULT_DRAIN_TIMEOUT_SECONDS
431
+
432
+ # Validate drain timeout bounds and clamp if necessary
433
+ if (
434
+ drain_timeout_value < MIN_DRAIN_TIMEOUT_SECONDS
435
+ or drain_timeout_value > MAX_DRAIN_TIMEOUT_SECONDS
436
+ ):
437
+ logger.warning(
438
+ "drain_timeout_seconds out of valid range, clamping",
439
+ extra={
440
+ "original_value": drain_timeout_value,
441
+ "min_value": MIN_DRAIN_TIMEOUT_SECONDS,
442
+ "max_value": MAX_DRAIN_TIMEOUT_SECONDS,
443
+ "clamped_value": max(
444
+ MIN_DRAIN_TIMEOUT_SECONDS,
445
+ min(drain_timeout_value, MAX_DRAIN_TIMEOUT_SECONDS),
446
+ ),
447
+ },
448
+ )
449
+ drain_timeout_value = max(
450
+ MIN_DRAIN_TIMEOUT_SECONDS,
451
+ min(drain_timeout_value, MAX_DRAIN_TIMEOUT_SECONDS),
452
+ )
453
+
454
+ self._drain_timeout_seconds: float = drain_timeout_value
455
+
456
+ # Handler executor for lifecycle operations (shutdown, health check)
457
+ self._lifecycle_executor = ProtocolLifecycleExecutor(
458
+ health_check_timeout_seconds=self._health_check_timeout_seconds
459
+ )
460
+
461
+ # Store full config for handler initialization
462
+ self._config: dict[str, object] | None = config
463
+
464
+ # Runtime state
465
+ self._is_running: bool = False
466
+
467
+ # Subscription handle (callable to unsubscribe)
468
+ self._subscription: Callable[[], Awaitable[None]] | None = None
469
+
470
+ # Handler registry (handler_type -> handler instance)
471
+ # This will be populated from the singleton registry during start()
472
+ self._handlers: dict[str, ProtocolHandler] = {}
473
+
474
+ # Track failed handler instantiations (handler_type -> error message)
475
+ # Used by health_check() to report degraded state
476
+ self._failed_handlers: dict[str, str] = {}
477
+
478
+ # Pending message tracking for graceful shutdown (OMN-756)
479
+ # Tracks count of in-flight messages currently being processed
480
+ self._pending_message_count: int = 0
481
+ self._pending_lock: asyncio.Lock = asyncio.Lock()
482
+
483
+ # Drain state tracking for graceful shutdown (OMN-756)
484
+ # True when stop() has been called and we're waiting for messages to drain
485
+ self._is_draining: bool = False
486
+
487
+ # Idempotency guard for duplicate message detection (OMN-945)
488
+ # None = disabled, otherwise points to configured store
489
+ self._idempotency_store: ProtocolIdempotencyStore | None = None
490
+ self._idempotency_config: ModelIdempotencyGuardConfig | None = None
491
+
492
+ logger.debug(
493
+ "RuntimeHostProcess initialized",
494
+ extra={
495
+ "input_topic": self._input_topic,
496
+ "output_topic": self._output_topic,
497
+ "group_id": self._group_id,
498
+ "health_check_timeout_seconds": self._health_check_timeout_seconds,
499
+ "drain_timeout_seconds": self._drain_timeout_seconds,
500
+ "has_container": self._container is not None,
501
+ "has_handler_registry": self._handler_registry is not None,
502
+ "has_contract_paths": len(self._contract_paths) > 0,
503
+ "contract_path_count": len(self._contract_paths),
504
+ },
505
+ )
506
+
507
+ @property
508
+ def container(self) -> ModelONEXContainer | None:
509
+ """Return the optional ONEX dependency injection container.
510
+
511
+ Returns:
512
+ The container if provided during initialization, None otherwise.
513
+ """
514
+ return self._container
515
+
516
+ @property
517
+ def event_bus(self) -> EventBusInmemory | EventBusKafka:
518
+ """Return the owned event bus instance.
519
+
520
+ Returns:
521
+ The event bus instance managed by this process.
522
+ """
523
+ return self._event_bus
524
+
525
+ @property
526
+ def is_running(self) -> bool:
527
+ """Return True if runtime is started.
528
+
529
+ Returns:
530
+ Boolean indicating whether the process is running.
531
+ """
532
+ return self._is_running
533
+
534
+ @property
535
+ def input_topic(self) -> str:
536
+ """Return the input topic for envelope subscription.
537
+
538
+ Returns:
539
+ The topic name to subscribe to for incoming envelopes.
540
+ """
541
+ return self._input_topic
542
+
543
+ @property
544
+ def output_topic(self) -> str:
545
+ """Return the output topic for response publishing.
546
+
547
+ Returns:
548
+ The topic name to publish responses to.
549
+ """
550
+ return self._output_topic
551
+
552
+ @property
553
+ def group_id(self) -> str:
554
+ """Return the consumer group identifier.
555
+
556
+ Returns:
557
+ The consumer group ID for this process.
558
+ """
559
+ return self._group_id
560
+
561
+ @property
562
+ def is_draining(self) -> bool:
563
+ """Return True if the process is draining pending messages during shutdown.
564
+
565
+ This property indicates whether the runtime host is in the graceful shutdown
566
+ drain period - the phase where stop() has been called, new messages are no
567
+ longer being accepted, and the process is waiting for in-flight messages to
568
+ complete before shutting down handlers and the event bus.
569
+
570
+ Drain State Transitions:
571
+ - False: Normal operation (accepting and processing messages)
572
+ - True: Drain period active (stop() called, waiting for pending messages)
573
+ - False: After drain completes and shutdown finishes
574
+
575
+ Use Cases:
576
+ - Health check reporting (indicate service is shutting down)
577
+ - Load balancer integration (remove from rotation during drain)
578
+ - Monitoring dashboards (show lifecycle state)
579
+ - Debugging shutdown behavior
580
+
581
+ Returns:
582
+ True if currently in drain period during graceful shutdown, False otherwise.
583
+ """
584
+ return self._is_draining
585
+
586
+ @property
587
+ def pending_message_count(self) -> int:
588
+ """Return the current count of in-flight messages being processed.
589
+
590
+ This property provides visibility into how many messages are currently
591
+ being processed by the runtime host. Used for graceful shutdown to
592
+ determine when it's safe to complete the shutdown process.
593
+
594
+ Atomicity Guarantees:
595
+ This property returns the raw counter value WITHOUT acquiring the
596
+ async lock (_pending_lock). This is safe because:
597
+
598
+ 1. Single int read is atomic under CPython's GIL - reading a single
599
+ integer value cannot be interrupted mid-operation
600
+ 2. The value is only used for observability/monitoring purposes
601
+ where exact precision is not required
602
+ 3. The slight possibility of reading a stale value during concurrent
603
+ increment/decrement is acceptable for monitoring use cases
604
+
605
+ Thread Safety Considerations:
606
+ While the read itself is atomic, the value may be approximate if
607
+ read occurs during concurrent message processing:
608
+ - Another coroutine may be in the middle of incrementing/decrementing
609
+ - The value represents a point-in-time snapshot, not a synchronized view
610
+ - For observability, this approximation is acceptable and avoids
611
+ lock contention that would impact performance
612
+
613
+ Use Cases (appropriate for this property):
614
+ - Logging current message count for debugging
615
+ - Metrics/observability dashboards
616
+ - Approximate health status reporting
617
+ - Monitoring drain progress during shutdown
618
+
619
+ When to use shutdown_ready() instead:
620
+ For shutdown decisions requiring precise count, use the async
621
+ shutdown_ready() method which acquires the lock to ensure no
622
+ race condition with in-flight message processing. The stop()
623
+ method uses shutdown_ready() internally for this reason.
624
+
625
+ Returns:
626
+ Current count of messages being processed. May be approximate
627
+ if reads occur during concurrent increment/decrement operations.
628
+ """
629
+ return self._pending_message_count
630
+
631
+ async def shutdown_ready(self) -> bool:
632
+ """Check if process is ready for shutdown (no pending messages).
633
+
634
+ This method acquires the pending message lock to ensure an accurate
635
+ count of in-flight messages. Use this method during graceful shutdown
636
+ to determine when all pending messages have been processed.
637
+
638
+ Returns:
639
+ True if no messages are currently being processed, False otherwise.
640
+ """
641
+ async with self._pending_lock:
642
+ return self._pending_message_count == 0
643
+
644
+ async def start(self) -> None:
645
+ """Start the runtime host.
646
+
647
+ Performs the following steps:
648
+ 1. Validate architecture compliance (if rules configured) - OMN-1138
649
+ 2. Start event bus (if not already started)
650
+ 3. Discover/wire handlers:
651
+ - If contract_paths provided: Auto-discover handlers from contracts (OMN-1133)
652
+ - Otherwise: Wire default handlers via wiring module
653
+ 4. Populate self._handlers from singleton registry (instantiate and initialize)
654
+ 5. Subscribe to input topic
655
+
656
+ Architecture Validation (OMN-1138):
657
+ If architecture_rules were provided at init, validation runs FIRST
658
+ before any other startup logic. This ensures:
659
+ - Violations are caught before resources are allocated
660
+ - Fast feedback for CI/CD pipelines
661
+ - Clean startup/failure without partial state
662
+
663
+ ERROR severity violations block startup by raising
664
+ ArchitectureViolationError. WARNING/INFO violations are logged
665
+ but don't block startup.
666
+
667
+ Contract-Based Handler Discovery (OMN-1133):
668
+ If contract_paths were provided at init, the runtime will auto-discover
669
+ handlers from these paths instead of using wire_default_handlers().
670
+
671
+ Discovery errors are logged but do not block startup, enabling
672
+ graceful degradation where some handlers can be registered even
673
+ if others fail to load.
674
+
675
+ This method is idempotent - calling start() on an already started
676
+ process is safe and has no effect.
677
+
678
+ Raises:
679
+ ArchitectureViolationError: If architecture validation fails with
680
+ blocking violations (ERROR severity).
681
+ """
682
+ if self._is_running:
683
+ logger.debug("RuntimeHostProcess already started, skipping")
684
+ return
685
+
686
+ logger.info(
687
+ "Starting RuntimeHostProcess",
688
+ extra={
689
+ "input_topic": self._input_topic,
690
+ "output_topic": self._output_topic,
691
+ "group_id": self._group_id,
692
+ "has_contract_paths": len(self._contract_paths) > 0,
693
+ },
694
+ )
695
+
696
+ # Step 1: Validate architecture compliance FIRST (OMN-1138)
697
+ # This runs before event bus starts or handlers are wired to ensure
698
+ # clean failure without partial state if validation fails
699
+ await self._validate_architecture()
700
+
701
+ # Step 2: Start event bus
702
+ await self._event_bus.start()
703
+
704
+ # Step 3: Discover/wire handlers (OMN-1133)
705
+ # If contract_paths provided, use ContractHandlerDiscovery to auto-discover
706
+ # handlers from contract files. Otherwise, fall back to wire_default_handlers().
707
+ await self._discover_or_wire_handlers()
708
+
709
+ # Step 4: Populate self._handlers from singleton registry
710
+ # The wiring/discovery step registers handler classes, so we need to:
711
+ # - Get each registered handler class from the singleton registry
712
+ # - Instantiate the handler class
713
+ # - Call initialize() on each handler instance with config
714
+ # - Store the handler instance in self._handlers for routing
715
+ await self._populate_handlers_from_registry()
716
+
717
+ # Step 4.1: FAIL-FAST validation - runtime MUST have at least one handler
718
+ # A runtime with no handlers cannot process any events and is misconfigured.
719
+ # This catches configuration issues early rather than silently starting a
720
+ # runtime that cannot do anything useful.
721
+ if not self._handlers:
722
+ correlation_id = uuid4()
723
+ context = ModelInfraErrorContext(
724
+ transport_type=EnumInfraTransportType.RUNTIME,
725
+ operation="validate_handlers",
726
+ target_name="runtime_host_process",
727
+ correlation_id=correlation_id,
728
+ )
729
+
730
+ # Build informative error message with context about what was attempted
731
+ contract_paths_info = (
732
+ f" * contract_paths provided: {[str(p) for p in self._contract_paths]}\n"
733
+ if self._contract_paths
734
+ else " * contract_paths: NOT PROVIDED (using ONEX_CONTRACTS_DIR env var)\n"
735
+ )
736
+
737
+ # Get registry count for additional context
738
+ handler_registry = await self._get_handler_registry()
739
+ registry_protocol_count = len(handler_registry.list_protocols())
740
+
741
+ # Build additional diagnostic info
742
+ failed_handlers_detail = ""
743
+ if self._failed_handlers:
744
+ failed_handlers_detail = "FAILED HANDLERS (check these first):\n"
745
+ for handler_type, error_msg in self._failed_handlers.items():
746
+ failed_handlers_detail += f" * {handler_type}: {error_msg}\n"
747
+ failed_handlers_detail += "\n"
748
+
749
+ raise ProtocolConfigurationError(
750
+ "No handlers registered. The runtime cannot start without at least one handler.\n\n"
751
+ "CURRENT CONFIGURATION:\n"
752
+ f"{contract_paths_info}"
753
+ f" * Registry protocol count: {registry_protocol_count}\n"
754
+ f" * Failed handlers: {len(self._failed_handlers)}\n"
755
+ f" * Correlation ID: {correlation_id}\n\n"
756
+ f"{failed_handlers_detail}"
757
+ "TROUBLESHOOTING STEPS:\n"
758
+ " 1. Verify ONEX_CONTRACTS_DIR points to a valid contracts directory:\n"
759
+ " - Run: echo $ONEX_CONTRACTS_DIR && ls -la $ONEX_CONTRACTS_DIR\n"
760
+ " - Expected: Directory containing handler_contract.yaml or contract.yaml files\n\n"
761
+ " 2. Check for handler contract files:\n"
762
+ " - Run: find $ONEX_CONTRACTS_DIR -name 'handler_contract.yaml' -o -name 'contract.yaml'\n"
763
+ " - If empty: No contracts found - create handler contracts or set correct path\n\n"
764
+ " 3. Verify handler contracts have required fields:\n"
765
+ " - Required: handler_name, handler_class, handler_type\n"
766
+ " - Example:\n"
767
+ " handler_name: my_handler\n"
768
+ " handler_class: mymodule.handlers.MyHandler\n"
769
+ " handler_type: http\n\n"
770
+ " 4. Verify handler modules are importable:\n"
771
+ " - Run: python -c 'from mymodule.handlers import MyHandler; print(MyHandler)'\n"
772
+ " - Check PYTHONPATH includes your handler module paths\n\n"
773
+ " 5. Check application logs for loader errors:\n"
774
+ " - Look for: MODULE_NOT_FOUND (HANDLER_LOADER_010)\n"
775
+ " - Look for: CLASS_NOT_FOUND (HANDLER_LOADER_011)\n"
776
+ " - Look for: IMPORT_ERROR (HANDLER_LOADER_012)\n"
777
+ " - Look for: AMBIGUOUS_CONTRACT (HANDLER_LOADER_040)\n\n"
778
+ " 6. If using wire_handlers() manually:\n"
779
+ " - Ensure wire_handlers() is called before RuntimeHostProcess.start()\n"
780
+ " - Check that handlers implement ProtocolHandler interface\n\n"
781
+ " 7. Docker/container environment:\n"
782
+ " - Verify volume mounts include handler contract directories\n"
783
+ " - Check ONEX_CONTRACTS_DIR is set in docker-compose.yml/Dockerfile\n"
784
+ " - Run: docker exec <container> ls $ONEX_CONTRACTS_DIR\n\n"
785
+ "For verbose handler discovery logging, set LOG_LEVEL=DEBUG.",
786
+ context=context,
787
+ registered_handler_count=0,
788
+ failed_handler_count=len(self._failed_handlers),
789
+ failed_handlers=list(self._failed_handlers.keys()),
790
+ contract_paths=[str(p) for p in self._contract_paths],
791
+ registry_protocol_count=registry_protocol_count,
792
+ )
793
+
794
+ # Step 4.5: Initialize idempotency store if configured (OMN-945)
795
+ await self._initialize_idempotency_store()
796
+
797
+ # Step 5: Subscribe to input topic
798
+ self._subscription = await self._event_bus.subscribe(
799
+ topic=self._input_topic,
800
+ group_id=self._group_id,
801
+ on_message=self._on_message,
802
+ )
803
+
804
+ self._is_running = True
805
+
806
+ logger.info(
807
+ "RuntimeHostProcess started successfully",
808
+ extra={
809
+ "input_topic": self._input_topic,
810
+ "output_topic": self._output_topic,
811
+ "group_id": self._group_id,
812
+ "registered_handlers": list(self._handlers.keys()),
813
+ },
814
+ )
815
+
816
+ async def stop(self) -> None:
817
+ """Stop the runtime host with graceful drain period.
818
+
819
+ Performs the following steps:
820
+ 1. Unsubscribe from topics (stop receiving new messages)
821
+ 2. Wait for in-flight messages to drain (up to drain_timeout_seconds)
822
+ 3. Shutdown all registered handlers by priority (release resources)
823
+ 4. Close event bus
824
+
825
+ This method is idempotent - calling stop() on an already stopped
826
+ process is safe and has no effect.
827
+
828
+ Drain Period:
829
+ After unsubscribing from topics, the process waits for in-flight
830
+ messages to complete processing. The drain period is controlled by
831
+ the drain_timeout_seconds configuration parameter (default: 30.0
832
+ seconds, valid range: 1-300).
833
+
834
+ During the drain period:
835
+ - No new messages are received (unsubscribed from topics)
836
+ - Messages currently being processed are allowed to complete
837
+ - shutdown_ready() is polled every 100ms to check completion
838
+ - If timeout is exceeded, shutdown proceeds with a warning
839
+
840
+ Handler Shutdown Order:
841
+ Handlers are shutdown in priority order, with higher priority handlers
842
+ shutting down first. Within the same priority level, handlers are
843
+ shutdown in parallel for performance.
844
+
845
+ Priority is determined by the handler's shutdown_priority() method:
846
+ - Higher values = shutdown first
847
+ - Handlers without shutdown_priority() get default priority of 0
848
+
849
+ Recommended Priority Scheme:
850
+ - 100: Consumers (stop receiving before stopping producers)
851
+ - 80: Active connections (close before closing pools)
852
+ - 50: Producers (stop producing before closing pools)
853
+ - 40: Connection pools (close last)
854
+ - 0: Default for handlers without explicit priority
855
+
856
+ This ensures dependency-based ordering:
857
+ - Consumers shutdown before producers
858
+ - Connections shutdown before connection pools
859
+ - Downstream resources shutdown before upstream resources
860
+ """
861
+ if not self._is_running:
862
+ logger.debug("RuntimeHostProcess already stopped, skipping")
863
+ return
864
+
865
+ logger.info("Stopping RuntimeHostProcess")
866
+
867
+ # Step 1: Unsubscribe from topics (stop receiving new messages)
868
+ if self._subscription is not None:
869
+ await self._subscription()
870
+ self._subscription = None
871
+
872
+ # Step 1.5: Wait for in-flight messages to drain (OMN-756)
873
+ # This allows messages currently being processed to complete
874
+ loop = asyncio.get_running_loop()
875
+ drain_start = loop.time()
876
+ drain_deadline = drain_start + self._drain_timeout_seconds
877
+ last_progress_log = drain_start
878
+
879
+ # Mark drain state for health check visibility (OMN-756)
880
+ self._is_draining = True
881
+
882
+ # Log drain start for observability
883
+ logger.info(
884
+ "Starting drain period",
885
+ extra={
886
+ "pending_messages": self._pending_message_count,
887
+ "drain_timeout_seconds": self._drain_timeout_seconds,
888
+ },
889
+ )
890
+
891
+ while not await self.shutdown_ready():
892
+ remaining = drain_deadline - loop.time()
893
+ if remaining <= 0:
894
+ logger.warning(
895
+ "Drain timeout exceeded, forcing shutdown",
896
+ extra={
897
+ "pending_messages": self._pending_message_count,
898
+ "drain_timeout_seconds": self._drain_timeout_seconds,
899
+ "metric.drain_timeout_exceeded": True,
900
+ "metric.pending_at_timeout": self._pending_message_count,
901
+ },
902
+ )
903
+ break
904
+
905
+ # Wait a short interval before checking again
906
+ await asyncio.sleep(min(0.1, remaining))
907
+
908
+ # Log progress every 5 seconds during long drains for observability
909
+ elapsed = loop.time() - drain_start
910
+ if elapsed - (last_progress_log - drain_start) >= 5.0:
911
+ logger.info(
912
+ "Drain in progress",
913
+ extra={
914
+ "pending_messages": self._pending_message_count,
915
+ "elapsed_seconds": round(elapsed, 2),
916
+ "remaining_seconds": round(remaining, 2),
917
+ },
918
+ )
919
+ last_progress_log = loop.time()
920
+
921
+ # Clear drain state after drain period completes
922
+ self._is_draining = False
923
+
924
+ logger.info(
925
+ "Drain period completed",
926
+ extra={
927
+ "drain_duration_seconds": loop.time() - drain_start,
928
+ "pending_messages": self._pending_message_count,
929
+ "metric.drain_duration": loop.time() - drain_start,
930
+ "metric.forced_shutdown": self._pending_message_count > 0,
931
+ },
932
+ )
933
+
934
+ # Step 2: Shutdown all handlers by priority (release resources like DB/Kafka connections)
935
+ # Delegates to ProtocolLifecycleExecutor which handles:
936
+ # - Grouping handlers by priority (higher priority first)
937
+ # - Parallel shutdown within priority groups for performance
938
+ if self._handlers:
939
+ shutdown_result = (
940
+ await self._lifecycle_executor.shutdown_handlers_by_priority(
941
+ self._handlers
942
+ )
943
+ )
944
+
945
+ # Log summary (ProtocolLifecycleExecutor already logs detailed info)
946
+ logger.info(
947
+ "Handler shutdown completed",
948
+ extra={
949
+ "succeeded_handlers": shutdown_result.succeeded_handlers,
950
+ "failed_handlers": [
951
+ f.handler_type for f in shutdown_result.failed_handlers
952
+ ],
953
+ "total_handlers": shutdown_result.total_count,
954
+ "success_count": shutdown_result.success_count,
955
+ "failure_count": shutdown_result.failure_count,
956
+ },
957
+ )
958
+
959
+ # Step 2.5: Cleanup idempotency store if initialized (OMN-945)
960
+ await self._cleanup_idempotency_store()
961
+
962
+ # Step 3: Close event bus
963
+ await self._event_bus.close()
964
+
965
+ self._is_running = False
966
+
967
+ logger.info("RuntimeHostProcess stopped successfully")
968
+
969
+ async def _discover_or_wire_handlers(self) -> None:
970
+ """Discover handlers from contracts or wire default handlers.
971
+
972
+ This method implements the handler discovery/wiring step (Step 3) of the
973
+ start() sequence. It supports two modes:
974
+
975
+ Contract-Based Discovery (OMN-1133):
976
+ If contract_paths were provided at init, uses ContractHandlerDiscovery
977
+ to auto-discover and register handlers from the specified paths.
978
+
979
+ Discovery errors are logged but do not block startup, enabling
980
+ graceful degradation where some handlers can be registered even
981
+ if others fail to load.
982
+
983
+ Default Handler Wiring (Fallback):
984
+ If no contract_paths were provided, falls back to wire_default_handlers()
985
+ which registers the standard set of handlers (HTTP, DB, Consul, Vault).
986
+
987
+ The discovery/wiring step registers handler CLASSES with the handler registry.
988
+ The subsequent _populate_handlers_from_registry() step instantiates and
989
+ initializes these handler classes.
990
+ """
991
+ if self._contract_paths:
992
+ # Contract-based handler discovery (OMN-1133)
993
+ await self._discover_handlers_from_contracts()
994
+ else:
995
+ # Fallback to default handler wiring (existing behavior)
996
+ wire_handlers()
997
+
998
+ async def _discover_handlers_from_contracts(self) -> None:
999
+ """Discover and register handlers from contract files.
1000
+
1001
+ This method implements contract-based handler discovery as part of OMN-1133.
1002
+ It creates a ContractHandlerDiscovery service, discovers handlers from the
1003
+ configured contract_paths, and registers them with the handler registry.
1004
+
1005
+ Error Handling:
1006
+ Discovery errors are logged but do not block startup. This enables
1007
+ graceful degradation where some handlers can be registered even if
1008
+ others fail to load.
1009
+
1010
+ The discovery service tracks:
1011
+ - handlers_discovered: Number of handlers found in contracts
1012
+ - handlers_registered: Number successfully registered
1013
+ - errors: List of individual discovery/registration failures
1014
+
1015
+ Related:
1016
+ - ContractHandlerDiscovery: Discovery service implementation
1017
+ - HandlerPluginLoader: Contract file parsing and validation
1018
+ - ModelDiscoveryResult: Result model with error tracking
1019
+ """
1020
+ from omnibase_infra.runtime.contract_handler_discovery import (
1021
+ ContractHandlerDiscovery,
1022
+ )
1023
+ from omnibase_infra.runtime.handler_plugin_loader import HandlerPluginLoader
1024
+
1025
+ logger.info(
1026
+ "Starting contract-based handler discovery",
1027
+ extra={
1028
+ "contract_paths": [str(p) for p in self._contract_paths],
1029
+ "path_count": len(self._contract_paths),
1030
+ },
1031
+ )
1032
+
1033
+ # Create handler discovery service if not already created
1034
+ # Uses the handler_registry from init or falls back to singleton
1035
+ handler_registry = await self._get_handler_registry()
1036
+
1037
+ self._handler_discovery = ContractHandlerDiscovery(
1038
+ plugin_loader=HandlerPluginLoader(),
1039
+ handler_registry=handler_registry,
1040
+ )
1041
+
1042
+ # Discover and register handlers from contract paths
1043
+ discovery_result = await self._handler_discovery.discover_and_register(
1044
+ contract_paths=self._contract_paths,
1045
+ )
1046
+
1047
+ # Log discovery results
1048
+ if discovery_result.has_errors:
1049
+ logger.warning(
1050
+ "Handler discovery completed with errors",
1051
+ extra={
1052
+ "handlers_discovered": discovery_result.handlers_discovered,
1053
+ "handlers_registered": discovery_result.handlers_registered,
1054
+ "error_count": len(discovery_result.errors),
1055
+ },
1056
+ )
1057
+ # Log individual errors for debugging
1058
+ for error in discovery_result.errors:
1059
+ logger.error(
1060
+ "Handler discovery error: %s",
1061
+ error.message,
1062
+ extra={
1063
+ "error_code": error.error_code,
1064
+ "handler_name": error.handler_name,
1065
+ "contract_path": str(error.contract_path)
1066
+ if error.contract_path
1067
+ else None,
1068
+ },
1069
+ )
1070
+ else:
1071
+ logger.info(
1072
+ "Handler discovery completed successfully",
1073
+ extra={
1074
+ "handlers_discovered": discovery_result.handlers_discovered,
1075
+ "handlers_registered": discovery_result.handlers_registered,
1076
+ },
1077
+ )
1078
+
1079
+ async def _populate_handlers_from_registry(self) -> None:
1080
+ """Populate self._handlers from handler registry (container or singleton).
1081
+
1082
+ This method bridges the gap between the wiring module (which registers
1083
+ handler CLASSES to the registry) and the RuntimeHostProcess
1084
+ (which needs handler INSTANCES in self._handlers for routing).
1085
+
1086
+ Registry Resolution:
1087
+ - If handler_registry provided: Uses pre-resolved registry
1088
+ - If no handler_registry: Falls back to singleton get_handler_registry()
1089
+
1090
+ For each registered handler type in the registry:
1091
+ 1. Skip if handler type is already registered (e.g., by tests)
1092
+ 2. Get the handler class from the registry
1093
+ 3. Instantiate the handler class
1094
+ 4. Call initialize() on the handler instance with self._config
1095
+ 5. Store the handler instance in self._handlers
1096
+
1097
+ This ensures that after start() is called, self._handlers contains
1098
+ fully initialized handler instances ready for envelope routing.
1099
+
1100
+ Note: Handlers already in self._handlers (e.g., injected by tests via
1101
+ register_handler() or patch.object()) are preserved and not overwritten.
1102
+ """
1103
+ # Get handler registry (pre-resolved, container, or singleton)
1104
+ handler_registry = await self._get_handler_registry()
1105
+ registered_types = handler_registry.list_protocols()
1106
+
1107
+ logger.debug(
1108
+ "Populating handlers from registry",
1109
+ extra={
1110
+ "registered_types": registered_types,
1111
+ "existing_handlers": list(self._handlers.keys()),
1112
+ },
1113
+ )
1114
+
1115
+ for handler_type in registered_types:
1116
+ # Skip if handler is already registered (e.g., by tests or explicit registration)
1117
+ if handler_type in self._handlers:
1118
+ logger.debug(
1119
+ "Handler already registered, skipping",
1120
+ extra={
1121
+ "handler_type": handler_type,
1122
+ "existing_handler_class": type(
1123
+ self._handlers[handler_type]
1124
+ ).__name__,
1125
+ },
1126
+ )
1127
+ continue
1128
+
1129
+ try:
1130
+ # Get handler class from singleton registry
1131
+ handler_cls: type[ProtocolHandler] = handler_registry.get(handler_type)
1132
+
1133
+ # Instantiate the handler
1134
+ handler_instance: ProtocolHandler = handler_cls()
1135
+
1136
+ # Call initialize() if the handler has this method
1137
+ # Handlers may require async initialization with config
1138
+ if hasattr(handler_instance, "initialize"):
1139
+ await handler_instance.initialize(self._config)
1140
+
1141
+ # Store the handler instance for routing
1142
+ self._handlers[handler_type] = handler_instance
1143
+
1144
+ logger.debug(
1145
+ "Handler instantiated and initialized",
1146
+ extra={
1147
+ "handler_type": handler_type,
1148
+ "handler_class": handler_cls.__name__,
1149
+ },
1150
+ )
1151
+
1152
+ except Exception as e:
1153
+ # Track the failure for health_check() reporting
1154
+ self._failed_handlers[handler_type] = str(e)
1155
+
1156
+ # Log error but continue with other handlers
1157
+ # This allows partial handler availability
1158
+ correlation_id = uuid4()
1159
+ context = ModelInfraErrorContext(
1160
+ transport_type=EnumInfraTransportType.RUNTIME,
1161
+ operation="populate_handlers",
1162
+ target_name=handler_type,
1163
+ correlation_id=correlation_id,
1164
+ )
1165
+ infra_error = RuntimeHostError(
1166
+ f"Failed to instantiate handler for type {handler_type}: {e}",
1167
+ context=context,
1168
+ )
1169
+ infra_error.__cause__ = e
1170
+
1171
+ logger.warning(
1172
+ "Failed to instantiate handler, skipping",
1173
+ extra={
1174
+ "handler_type": handler_type,
1175
+ "error": str(e),
1176
+ "correlation_id": str(correlation_id),
1177
+ },
1178
+ )
1179
+
1180
+ logger.info(
1181
+ "Handlers populated from registry",
1182
+ extra={
1183
+ "populated_handlers": list(self._handlers.keys()),
1184
+ "total_count": len(self._handlers),
1185
+ },
1186
+ )
1187
+
1188
+ async def _get_handler_registry(self) -> RegistryProtocolBinding:
1189
+ """Get handler registry (pre-resolved, container, or singleton).
1190
+
1191
+ Resolution order:
1192
+ 1. If handler_registry was provided to __init__, uses it (cached)
1193
+ 2. If container was provided and has RegistryProtocolBinding, resolves from container
1194
+ 3. Falls back to singleton via get_handler_registry()
1195
+
1196
+ Caching Behavior:
1197
+ The resolved registry is cached after the first successful resolution.
1198
+ Subsequent calls return the cached instance without re-resolving from
1199
+ the container or re-fetching the singleton. This ensures consistent
1200
+ registry usage throughout the runtime's lifecycle and avoids redundant
1201
+ resolution operations.
1202
+
1203
+ Returns:
1204
+ RegistryProtocolBinding instance.
1205
+ """
1206
+ if self._handler_registry is not None:
1207
+ # Use pre-resolved registry from constructor
1208
+ return self._handler_registry
1209
+
1210
+ # Try to resolve from container if provided
1211
+ if self._container is not None and self._container.service_registry is not None:
1212
+ try:
1213
+ resolved_registry: RegistryProtocolBinding = (
1214
+ await self._container.service_registry.resolve_service(
1215
+ RegistryProtocolBinding
1216
+ )
1217
+ )
1218
+ # Cache the resolved registry for subsequent calls
1219
+ self._handler_registry = resolved_registry
1220
+ logger.debug(
1221
+ "Handler registry resolved from container",
1222
+ extra={"registry_type": type(resolved_registry).__name__},
1223
+ )
1224
+ return resolved_registry
1225
+ except (
1226
+ RuntimeError,
1227
+ ValueError,
1228
+ KeyError,
1229
+ AttributeError,
1230
+ LookupError,
1231
+ ) as e:
1232
+ # Container resolution failed, fall through to singleton
1233
+ logger.debug(
1234
+ "Container registry resolution failed, falling back to singleton",
1235
+ extra={"error": str(e)},
1236
+ )
1237
+
1238
+ # Graceful degradation: fall back to singleton pattern when container unavailable
1239
+ from omnibase_infra.runtime.handler_registry import get_handler_registry
1240
+
1241
+ singleton_registry = get_handler_registry()
1242
+ # Cache for consistency with container resolution path
1243
+ self._handler_registry = singleton_registry
1244
+ logger.debug(
1245
+ "Handler registry resolved from singleton",
1246
+ extra={"registry_type": type(singleton_registry).__name__},
1247
+ )
1248
+ return singleton_registry
1249
+
1250
+ async def _on_message(self, message: ModelEventMessage) -> None:
1251
+ """Handle incoming message from event bus subscription.
1252
+
1253
+ This is the callback invoked by the event bus when a message arrives
1254
+ on the input topic. It deserializes the envelope and routes it.
1255
+
1256
+ The method tracks pending messages for graceful shutdown support (OMN-756).
1257
+ The pending message count is incremented at the start of processing and
1258
+ decremented when processing completes (success or failure).
1259
+
1260
+ Args:
1261
+ message: The event message containing the envelope payload.
1262
+ """
1263
+ # Increment pending message count (OMN-756: graceful shutdown tracking)
1264
+ async with self._pending_lock:
1265
+ self._pending_message_count += 1
1266
+
1267
+ try:
1268
+ # Deserialize envelope from message value
1269
+ envelope = json.loads(message.value.decode("utf-8"))
1270
+ await self._handle_envelope(envelope)
1271
+ except json.JSONDecodeError as e:
1272
+ # Create infrastructure error context for tracing
1273
+ correlation_id = uuid4()
1274
+ context = ModelInfraErrorContext(
1275
+ transport_type=EnumInfraTransportType.RUNTIME,
1276
+ operation="decode_envelope",
1277
+ target_name=message.topic,
1278
+ correlation_id=correlation_id,
1279
+ )
1280
+ # Chain the error with infrastructure context
1281
+ infra_error = RuntimeHostError(
1282
+ f"Failed to decode JSON envelope from message: {e}",
1283
+ context=context,
1284
+ )
1285
+ infra_error.__cause__ = e # Proper error chaining
1286
+
1287
+ logger.exception(
1288
+ "Failed to decode envelope from message",
1289
+ extra={
1290
+ "error": str(e),
1291
+ "topic": message.topic,
1292
+ "offset": message.offset,
1293
+ "correlation_id": str(correlation_id),
1294
+ },
1295
+ )
1296
+ # Publish error response for malformed messages
1297
+ error_response = self._create_error_response(
1298
+ error=f"Invalid JSON in message: {e}",
1299
+ correlation_id=correlation_id,
1300
+ )
1301
+ await self._publish_envelope_safe(error_response, self._output_topic)
1302
+ finally:
1303
+ # Decrement pending message count (OMN-756: graceful shutdown tracking)
1304
+ async with self._pending_lock:
1305
+ self._pending_message_count -= 1
1306
+
1307
+ async def _handle_envelope(self, envelope: dict[str, object]) -> None:
1308
+ """Route envelope to appropriate handler.
1309
+
1310
+ Validates envelope before dispatch and routes it to the appropriate
1311
+ registered handler. Publishes the response to the output topic.
1312
+
1313
+ Validation (performed before dispatch):
1314
+ 1. Operation presence and type validation
1315
+ 2. Handler prefix validation against registry
1316
+ 3. Payload requirement validation for specific operations
1317
+ 4. Correlation ID normalization to UUID
1318
+
1319
+ Args:
1320
+ envelope: Dict with 'operation', 'payload', optional 'correlation_id',
1321
+ and 'handler_type'.
1322
+ """
1323
+ # Pre-validation: Get correlation_id for error responses if validation fails
1324
+ # This handles the case where validation itself throws before normalizing
1325
+ pre_validation_correlation_id = normalize_correlation_id(
1326
+ envelope.get("correlation_id")
1327
+ )
1328
+
1329
+ # Step 1: Validate envelope BEFORE dispatch
1330
+ # This validates operation, prefix, payload requirements, and normalizes correlation_id
1331
+ try:
1332
+ validate_envelope(envelope, await self._get_handler_registry())
1333
+ except EnvelopeValidationError as e:
1334
+ # Validation failed - missing operation or payload
1335
+ error_response = self._create_error_response(
1336
+ error=str(e),
1337
+ correlation_id=pre_validation_correlation_id,
1338
+ )
1339
+ await self._publish_envelope_safe(error_response, self._output_topic)
1340
+ logger.warning(
1341
+ "Envelope validation failed",
1342
+ extra={
1343
+ "error": str(e),
1344
+ "correlation_id": str(pre_validation_correlation_id),
1345
+ "error_type": "EnvelopeValidationError",
1346
+ },
1347
+ )
1348
+ return
1349
+ except UnknownHandlerTypeError as e:
1350
+ # Unknown handler prefix - hard failure
1351
+ error_response = self._create_error_response(
1352
+ error=str(e),
1353
+ correlation_id=pre_validation_correlation_id,
1354
+ )
1355
+ await self._publish_envelope_safe(error_response, self._output_topic)
1356
+ logger.warning(
1357
+ "Unknown handler type in envelope",
1358
+ extra={
1359
+ "error": str(e),
1360
+ "correlation_id": str(pre_validation_correlation_id),
1361
+ "error_type": "UnknownHandlerTypeError",
1362
+ },
1363
+ )
1364
+ return
1365
+
1366
+ # After validation, correlation_id is guaranteed to be a UUID
1367
+ correlation_id = envelope.get("correlation_id")
1368
+ if not isinstance(correlation_id, UUID):
1369
+ correlation_id = pre_validation_correlation_id
1370
+
1371
+ # Step 2: Check idempotency before handler dispatch (OMN-945)
1372
+ # This prevents duplicate processing under at-least-once delivery
1373
+ if not await self._check_idempotency(envelope, correlation_id):
1374
+ # Duplicate detected - response already published, return early
1375
+ return
1376
+
1377
+ # Extract operation (validated to exist and be a string)
1378
+ operation = str(envelope.get("operation"))
1379
+
1380
+ # Determine handler_type from envelope
1381
+ # If handler_type not explicit, extract from operation (e.g., "http.get" -> "http")
1382
+ handler_type = envelope.get("handler_type")
1383
+ if handler_type is None:
1384
+ handler_type = operation.split(".")[0]
1385
+
1386
+ # Get handler from registry
1387
+ handler = self._handlers.get(str(handler_type))
1388
+
1389
+ if handler is None:
1390
+ # Handler not instantiated (different from unknown prefix - validation already passed)
1391
+ # This can happen if handler registration failed during start()
1392
+ context = ModelInfraErrorContext(
1393
+ transport_type=EnumInfraTransportType.RUNTIME,
1394
+ operation=str(operation),
1395
+ target_name=str(handler_type),
1396
+ correlation_id=correlation_id,
1397
+ )
1398
+
1399
+ # Create structured error for logging and tracking
1400
+ routing_error = RuntimeHostError(
1401
+ f"Handler type {handler_type!r} is registered but not instantiated",
1402
+ context=context,
1403
+ )
1404
+
1405
+ # Publish error response for envelope-based error handling
1406
+ error_response = self._create_error_response(
1407
+ error=str(routing_error),
1408
+ correlation_id=correlation_id,
1409
+ )
1410
+ await self._publish_envelope_safe(error_response, self._output_topic)
1411
+
1412
+ # Log with structured error
1413
+ logger.warning(
1414
+ "Handler registered but not instantiated",
1415
+ extra={
1416
+ "handler_type": handler_type,
1417
+ "correlation_id": str(correlation_id),
1418
+ "operation": operation,
1419
+ "registered_handlers": list(self._handlers.keys()),
1420
+ "error": str(routing_error),
1421
+ },
1422
+ )
1423
+ return
1424
+
1425
+ # Execute handler
1426
+ try:
1427
+ # Handler expected to have async execute(envelope) method
1428
+ # NOTE: MVP adapters use legacy execute(envelope: dict) signature.
1429
+ # TODO(OMN-40): Migrate handlers to new protocol signature execute(request, operation_config)
1430
+ response = await handler.execute(envelope) # type: ignore[call-arg] # NOTE: legacy signature
1431
+
1432
+ # Ensure response has correlation_id
1433
+ # Make a copy to avoid mutating handler's internal state
1434
+ if isinstance(response, dict):
1435
+ response = dict(response)
1436
+ if "correlation_id" not in response:
1437
+ response["correlation_id"] = correlation_id
1438
+
1439
+ await self._publish_envelope_safe(response, self._output_topic)
1440
+
1441
+ logger.debug(
1442
+ "Handler executed successfully",
1443
+ extra={
1444
+ "handler_type": handler_type,
1445
+ "correlation_id": str(correlation_id),
1446
+ "operation": operation,
1447
+ },
1448
+ )
1449
+
1450
+ except Exception as e:
1451
+ # Create infrastructure error context for handler execution failure
1452
+ context = ModelInfraErrorContext(
1453
+ transport_type=EnumInfraTransportType.RUNTIME,
1454
+ operation="handler_execution",
1455
+ target_name=str(handler_type),
1456
+ correlation_id=correlation_id,
1457
+ )
1458
+ # Chain the error with infrastructure context
1459
+ infra_error = RuntimeHostError(
1460
+ f"Handler execution failed for {handler_type}: {e}",
1461
+ context=context,
1462
+ )
1463
+ infra_error.__cause__ = e # Proper error chaining
1464
+
1465
+ # Handler execution failed - produce failure envelope
1466
+ error_response = self._create_error_response(
1467
+ error=str(e),
1468
+ correlation_id=correlation_id,
1469
+ )
1470
+ await self._publish_envelope_safe(error_response, self._output_topic)
1471
+
1472
+ logger.exception(
1473
+ "Handler execution failed",
1474
+ extra={
1475
+ "handler_type": handler_type,
1476
+ "correlation_id": str(correlation_id),
1477
+ "operation": operation,
1478
+ "error": str(e),
1479
+ "infra_error": str(infra_error),
1480
+ },
1481
+ )
1482
+
1483
+ def _create_error_response(
1484
+ self,
1485
+ error: str,
1486
+ correlation_id: UUID | None,
1487
+ ) -> dict[str, object]:
1488
+ """Create a standardized error response envelope.
1489
+
1490
+ Args:
1491
+ error: Error message to include.
1492
+ correlation_id: Correlation ID to preserve for tracking.
1493
+
1494
+ Returns:
1495
+ Error response dict with success=False and error details.
1496
+ """
1497
+ # Use correlation_id or generate a new one, keeping as UUID for internal use
1498
+ final_correlation_id = correlation_id or uuid4()
1499
+ return {
1500
+ "success": False,
1501
+ "status": "error",
1502
+ "error": error,
1503
+ "correlation_id": final_correlation_id,
1504
+ }
1505
+
1506
+ def _serialize_envelope(
1507
+ self, envelope: dict[str, object] | BaseModel
1508
+ ) -> dict[str, object]:
1509
+ """Recursively convert UUID objects to strings for JSON serialization.
1510
+
1511
+ Handles both dict envelopes and Pydantic models (e.g., ModelDuplicateResponse).
1512
+
1513
+ Args:
1514
+ envelope: Envelope dict or Pydantic model that may contain UUID objects.
1515
+
1516
+ Returns:
1517
+ New dict with all UUIDs converted to strings.
1518
+ """
1519
+ # Convert Pydantic models to dict first, ensuring type safety
1520
+ envelope_dict: JsonDict = (
1521
+ envelope.model_dump() if isinstance(envelope, BaseModel) else envelope
1522
+ )
1523
+
1524
+ def convert_value(value: object) -> object:
1525
+ if isinstance(value, UUID):
1526
+ return str(value)
1527
+ elif isinstance(value, dict):
1528
+ return {k: convert_value(v) for k, v in value.items()}
1529
+ elif isinstance(value, list):
1530
+ return [convert_value(item) for item in value]
1531
+ return value
1532
+
1533
+ return {k: convert_value(v) for k, v in envelope_dict.items()}
1534
+
1535
+ async def _publish_envelope_safe(
1536
+ self, envelope: dict[str, object] | BaseModel, topic: str
1537
+ ) -> None:
1538
+ """Publish envelope with UUID serialization support.
1539
+
1540
+ Converts any UUID objects to strings before publishing to ensure
1541
+ JSON serialization works correctly.
1542
+
1543
+ Args:
1544
+ envelope: Envelope dict or Pydantic model (may contain UUID objects).
1545
+ topic: Target topic to publish to.
1546
+ """
1547
+ # Always serialize UUIDs upfront - single code path
1548
+ json_safe_envelope = self._serialize_envelope(envelope)
1549
+ await self._event_bus.publish_envelope(json_safe_envelope, topic)
1550
+
1551
+ async def health_check(self) -> dict[str, object]:
1552
+ """Return health check status.
1553
+
1554
+ Returns:
1555
+ Dictionary with health status information:
1556
+ - healthy: Overall health status (True only if running,
1557
+ event bus healthy, no handlers failed to instantiate,
1558
+ all registered handlers are healthy, AND at least one
1559
+ handler is registered - a runtime without handlers is useless)
1560
+ - degraded: True when process is running but some handlers
1561
+ failed to instantiate. Indicates partial functionality -
1562
+ the system is operational but not at full capacity.
1563
+ - is_running: Whether the process is running
1564
+ - is_draining: Whether the process is in graceful shutdown drain
1565
+ period, waiting for in-flight messages to complete (OMN-756).
1566
+ Load balancers can use this to remove the service from rotation
1567
+ before the container becomes unhealthy.
1568
+ - pending_message_count: Number of messages currently being
1569
+ processed. Useful for monitoring drain progress and determining
1570
+ when the service is ready for shutdown.
1571
+ - event_bus: Event bus health status (if running)
1572
+ - event_bus_healthy: Boolean indicating event bus health
1573
+ - failed_handlers: Dict of handler_type -> error message for
1574
+ handlers that failed to instantiate during start()
1575
+ - registered_handlers: List of successfully registered handler types
1576
+ - handlers: Dict of handler_type -> health status for each
1577
+ registered handler
1578
+ - no_handlers_registered: True if no handlers are registered.
1579
+ This indicates a critical configuration issue - the runtime
1580
+ cannot process any events without handlers (OMN-1317).
1581
+
1582
+ Health State Matrix:
1583
+ - healthy=True, degraded=False: Fully operational
1584
+ - healthy=False, degraded=True: Running with reduced functionality
1585
+ - healthy=False, degraded=False: Not running, event bus unhealthy,
1586
+ or no handlers registered (critical configuration issue)
1587
+ - healthy=False, no_handlers_registered=True: Configuration error,
1588
+ runtime cannot process events
1589
+
1590
+ Drain State:
1591
+ When is_draining=True, the service is shutting down gracefully:
1592
+ - New messages are no longer being accepted
1593
+ - In-flight messages are being allowed to complete
1594
+ - Health status may still show healthy during drain
1595
+ - Load balancers should remove the service from rotation
1596
+
1597
+ Note:
1598
+ Handler health checks are performed concurrently using asyncio.gather()
1599
+ with individual timeouts (configurable via health_check_timeout_seconds
1600
+ config, default: 5.0 seconds) to prevent slow handlers from blocking.
1601
+ """
1602
+ # Get event bus health if available
1603
+ event_bus_health: dict[str, object] = {}
1604
+ event_bus_healthy = False
1605
+
1606
+ try:
1607
+ event_bus_health = await self._event_bus.health_check()
1608
+ # Explicit type guard (not assert) for production safety
1609
+ # health_check() returns dict per contract
1610
+ if not isinstance(event_bus_health, dict):
1611
+ context = ModelInfraErrorContext(
1612
+ transport_type=EnumInfraTransportType.RUNTIME,
1613
+ operation="health_check",
1614
+ )
1615
+ raise ProtocolConfigurationError(
1616
+ f"health_check() must return dict, got {type(event_bus_health).__name__}",
1617
+ context=context,
1618
+ )
1619
+ event_bus_healthy = bool(event_bus_health.get("healthy", False))
1620
+ except Exception as e:
1621
+ # Create infrastructure error context for health check failure
1622
+ correlation_id = uuid4()
1623
+ context = ModelInfraErrorContext(
1624
+ transport_type=EnumInfraTransportType.RUNTIME,
1625
+ operation="health_check",
1626
+ target_name="event_bus",
1627
+ correlation_id=correlation_id,
1628
+ )
1629
+ # Chain the error with infrastructure context
1630
+ infra_error = RuntimeHostError(
1631
+ f"Event bus health check failed: {e}",
1632
+ context=context,
1633
+ )
1634
+ infra_error.__cause__ = e # Proper error chaining
1635
+
1636
+ logger.warning(
1637
+ "Event bus health check failed",
1638
+ extra={
1639
+ "error": str(e),
1640
+ "correlation_id": str(correlation_id),
1641
+ "infra_error": str(infra_error),
1642
+ },
1643
+ exc_info=True,
1644
+ )
1645
+ event_bus_health = {"error": str(e), "correlation_id": str(correlation_id)}
1646
+ event_bus_healthy = False
1647
+
1648
+ # Check handler health for all registered handlers concurrently
1649
+ # Delegates to ProtocolLifecycleExecutor with configured timeout to prevent blocking
1650
+ handler_health_results: dict[str, object] = {}
1651
+ handlers_all_healthy = True
1652
+
1653
+ if self._handlers:
1654
+ # Run all handler health checks concurrently using asyncio.gather()
1655
+ health_check_tasks = [
1656
+ self._lifecycle_executor.check_handler_health(handler_type, handler)
1657
+ for handler_type, handler in self._handlers.items()
1658
+ ]
1659
+ results = await asyncio.gather(*health_check_tasks)
1660
+
1661
+ # Process results and build the results dict
1662
+ for health_result in results:
1663
+ handler_health_results[health_result.handler_type] = (
1664
+ health_result.details
1665
+ )
1666
+ if not health_result.healthy:
1667
+ handlers_all_healthy = False
1668
+
1669
+ # Check for failed handlers - any failures indicate degraded state
1670
+ has_failed_handlers = len(self._failed_handlers) > 0
1671
+
1672
+ # Check for no handlers registered - critical configuration issue
1673
+ # A runtime with no handlers cannot process any events and should be unhealthy
1674
+ no_handlers_registered = len(self._handlers) == 0
1675
+
1676
+ # Degraded state: process is running but some handlers failed to instantiate
1677
+ # This means the system is operational but with reduced functionality
1678
+ degraded = self._is_running and has_failed_handlers
1679
+
1680
+ # Overall health is True only if running, event bus is healthy,
1681
+ # no handlers failed to instantiate, all registered handlers are healthy,
1682
+ # AND at least one handler is registered (runtime without handlers is useless)
1683
+ healthy = (
1684
+ self._is_running
1685
+ and event_bus_healthy
1686
+ and not has_failed_handlers
1687
+ and handlers_all_healthy
1688
+ and not no_handlers_registered
1689
+ )
1690
+
1691
+ return {
1692
+ "healthy": healthy,
1693
+ "degraded": degraded,
1694
+ "is_running": self._is_running,
1695
+ "is_draining": self._is_draining,
1696
+ "pending_message_count": self._pending_message_count,
1697
+ "event_bus": event_bus_health,
1698
+ "event_bus_healthy": event_bus_healthy,
1699
+ "failed_handlers": self._failed_handlers,
1700
+ "registered_handlers": list(self._handlers.keys()),
1701
+ "handlers": handler_health_results,
1702
+ "no_handlers_registered": no_handlers_registered,
1703
+ }
1704
+
1705
+ def register_handler(self, handler_type: str, handler: ProtocolHandler) -> None:
1706
+ """Register a handler for a specific type.
1707
+
1708
+ Args:
1709
+ handler_type: Protocol type identifier (e.g., "http", "db").
1710
+ handler: Handler instance implementing the ProtocolHandler protocol.
1711
+ """
1712
+ self._handlers[handler_type] = handler
1713
+ logger.debug(
1714
+ "Handler registered",
1715
+ extra={
1716
+ "handler_type": handler_type,
1717
+ "handler_class": type(handler).__name__,
1718
+ },
1719
+ )
1720
+
1721
+ def get_handler(self, handler_type: str) -> ProtocolHandler | None:
1722
+ """Get handler for type, returns None if not registered.
1723
+
1724
+ Args:
1725
+ handler_type: Protocol type identifier.
1726
+
1727
+ Returns:
1728
+ Handler instance if registered, None otherwise.
1729
+ """
1730
+ return self._handlers.get(handler_type)
1731
+
1732
+ # =========================================================================
1733
+ # Architecture Validation Methods (OMN-1138)
1734
+ # =========================================================================
1735
+
1736
+ async def _validate_architecture(self) -> None:
1737
+ """Validate architecture compliance before starting runtime.
1738
+
1739
+ This method is called at the beginning of start() to validate nodes
1740
+ and handlers against registered architecture rules. If any violations
1741
+ with ERROR severity are detected, startup is blocked.
1742
+
1743
+ Validation occurs BEFORE:
1744
+ - Event bus starts
1745
+ - Handlers are wired
1746
+ - Subscription begins
1747
+
1748
+ Validation Behavior:
1749
+ - ERROR severity violations: Block startup, raise ArchitectureViolationError
1750
+ - WARNING severity violations: Log warning, continue startup
1751
+ - INFO severity violations: Log info, continue startup
1752
+
1753
+ Raises:
1754
+ ArchitectureViolationError: If blocking violations (ERROR severity)
1755
+ are detected. Contains all blocking violations for inspection.
1756
+
1757
+ Example:
1758
+ >>> # Validation is automatic in start()
1759
+ >>> try:
1760
+ ... await runtime.start()
1761
+ ... except ArchitectureViolationError as e:
1762
+ ... print(f"Startup blocked: {len(e.violations)} violations")
1763
+ ... for v in e.violations:
1764
+ ... print(v.format_for_logging())
1765
+
1766
+ Note:
1767
+ Validation only runs if architecture_rules were provided at init.
1768
+ If no rules are configured, this method returns immediately.
1769
+
1770
+ Related:
1771
+ - OMN-1138: Architecture Validator for omnibase_infra
1772
+ - OMN-1099: Validators implementing ProtocolArchitectureRule
1773
+ """
1774
+ # Skip validation if no rules configured
1775
+ if not self._architecture_rules:
1776
+ logger.debug("No architecture rules configured, skipping validation")
1777
+ return
1778
+
1779
+ logger.info(
1780
+ "Validating architecture compliance",
1781
+ extra={
1782
+ "rule_count": len(self._architecture_rules),
1783
+ "rule_ids": tuple(r.rule_id for r in self._architecture_rules),
1784
+ },
1785
+ )
1786
+
1787
+ # Import architecture validator components
1788
+ from omnibase_infra.errors import ArchitectureViolationError
1789
+ from omnibase_infra.nodes.architecture_validator import (
1790
+ ModelArchitectureValidationRequest,
1791
+ NodeArchitectureValidatorCompute,
1792
+ )
1793
+
1794
+ # Create or get container
1795
+ container = self._get_or_create_container()
1796
+
1797
+ # Instantiate validator with rules
1798
+ validator = NodeArchitectureValidatorCompute(
1799
+ container=container,
1800
+ rules=self._architecture_rules,
1801
+ )
1802
+
1803
+ # Build validation request
1804
+ # Note: At this point, handlers haven't been instantiated yet (that happens
1805
+ # after validation in _populate_handlers_from_registry). We validate the
1806
+ # handler CLASSES from the registry, not handler instances.
1807
+ handler_registry = await self._get_handler_registry()
1808
+ handler_classes: list[type[ProtocolHandler]] = []
1809
+ for handler_type in handler_registry.list_protocols():
1810
+ try:
1811
+ handler_cls = handler_registry.get(handler_type)
1812
+ handler_classes.append(handler_cls)
1813
+ except Exception as e:
1814
+ # If a handler class can't be retrieved, skip it for validation
1815
+ # (it will fail later during instantiation anyway)
1816
+ logger.debug(
1817
+ "Skipping handler class for architecture validation",
1818
+ extra={
1819
+ "handler_type": handler_type,
1820
+ "error": str(e),
1821
+ },
1822
+ )
1823
+
1824
+ request = ModelArchitectureValidationRequest(
1825
+ nodes=(), # Nodes not yet available at this point
1826
+ handlers=tuple(handler_classes),
1827
+ )
1828
+
1829
+ # Execute validation
1830
+ result = validator.compute(request)
1831
+
1832
+ # Separate blocking and non-blocking violations
1833
+ blocking_violations = tuple(v for v in result.violations if v.blocks_startup())
1834
+ warning_violations = tuple(
1835
+ v for v in result.violations if not v.blocks_startup()
1836
+ )
1837
+
1838
+ # Log warnings but don't block
1839
+ for violation in warning_violations:
1840
+ # Note: We can't use to_structured_dict() directly because 'message'
1841
+ # is a reserved key in Python logging's extra parameter.
1842
+ # We use format_for_logging() instead for the log message.
1843
+ logger.warning(
1844
+ "Architecture warning: %s",
1845
+ violation.format_for_logging(),
1846
+ extra={
1847
+ "rule_id": violation.rule_id,
1848
+ "severity": violation.severity.value,
1849
+ "target_type": violation.target_type,
1850
+ "target_name": violation.target_name,
1851
+ },
1852
+ )
1853
+
1854
+ # Block startup on ERROR violations
1855
+ if blocking_violations:
1856
+ logger.error(
1857
+ "Architecture validation failed",
1858
+ extra={
1859
+ "blocking_violation_count": len(blocking_violations),
1860
+ "warning_violation_count": len(warning_violations),
1861
+ "blocking_rule_ids": tuple(v.rule_id for v in blocking_violations),
1862
+ },
1863
+ )
1864
+ raise ArchitectureViolationError(
1865
+ message=f"Architecture validation failed with {len(blocking_violations)} blocking violations",
1866
+ violations=blocking_violations,
1867
+ )
1868
+
1869
+ logger.info(
1870
+ "Architecture validation passed",
1871
+ extra={
1872
+ "rules_checked": result.rules_checked,
1873
+ "handlers_checked": result.handlers_checked,
1874
+ "warning_count": len(warning_violations),
1875
+ },
1876
+ )
1877
+
1878
+ def _get_or_create_container(self) -> ModelONEXContainer:
1879
+ """Get the injected container or create a new one.
1880
+
1881
+ Returns:
1882
+ ModelONEXContainer instance for architecture validation.
1883
+
1884
+ Note:
1885
+ If no container was provided at init, a new container is created.
1886
+ This container provides basic infrastructure for node execution
1887
+ but may not have all services wired.
1888
+ """
1889
+ if self._container is not None:
1890
+ return self._container
1891
+
1892
+ # Create container for validation
1893
+ from omnibase_core.models.container.model_onex_container import (
1894
+ ModelONEXContainer,
1895
+ )
1896
+
1897
+ logger.debug(
1898
+ "Creating container for architecture validation "
1899
+ "(no container provided at init)"
1900
+ )
1901
+ return ModelONEXContainer()
1902
+
1903
+ # =========================================================================
1904
+ # Idempotency Guard Methods (OMN-945)
1905
+ # =========================================================================
1906
+
1907
+ async def _initialize_idempotency_store(self) -> None:
1908
+ """Initialize idempotency store from configuration.
1909
+
1910
+ Reads idempotency configuration from the runtime config and wires
1911
+ the appropriate store implementation. If not configured or disabled,
1912
+ idempotency checking is skipped.
1913
+
1914
+ Supported store types:
1915
+ - "postgres": PostgreSQL-backed durable store (production)
1916
+ - "memory": In-memory store (testing only)
1917
+
1918
+ Configuration keys:
1919
+ - idempotency.enabled: bool (default: False)
1920
+ - idempotency.store_type: "postgres" | "memory" (default: "postgres")
1921
+ - idempotency.domain_from_operation: bool (default: True)
1922
+ - idempotency.skip_operations: list[str] (default: [])
1923
+ - idempotency_database: dict (PostgreSQL connection config)
1924
+ """
1925
+ # Check if config exists
1926
+ if self._config is None:
1927
+ logger.debug("No runtime config provided, skipping idempotency setup")
1928
+ return
1929
+
1930
+ # Check if config has idempotency section
1931
+ idempotency_raw = self._config.get("idempotency")
1932
+ if idempotency_raw is None:
1933
+ logger.debug("Idempotency guard not configured, skipping")
1934
+ return
1935
+
1936
+ try:
1937
+ from omnibase_infra.idempotency import ModelIdempotencyGuardConfig
1938
+
1939
+ if isinstance(idempotency_raw, dict):
1940
+ self._idempotency_config = ModelIdempotencyGuardConfig.model_validate(
1941
+ idempotency_raw
1942
+ )
1943
+ elif isinstance(idempotency_raw, ModelIdempotencyGuardConfig):
1944
+ self._idempotency_config = idempotency_raw
1945
+ else:
1946
+ logger.warning(
1947
+ "Invalid idempotency config type",
1948
+ extra={"type": type(idempotency_raw).__name__},
1949
+ )
1950
+ return
1951
+
1952
+ if not self._idempotency_config.enabled:
1953
+ logger.debug("Idempotency guard disabled in config")
1954
+ return
1955
+
1956
+ # Create store based on store_type
1957
+ if self._idempotency_config.store_type == "postgres":
1958
+ from omnibase_infra.idempotency import (
1959
+ ModelPostgresIdempotencyStoreConfig,
1960
+ StoreIdempotencyPostgres,
1961
+ )
1962
+
1963
+ # Get database config from container or config
1964
+ db_config_raw = self._config.get("idempotency_database", {})
1965
+ if isinstance(db_config_raw, dict):
1966
+ db_config = ModelPostgresIdempotencyStoreConfig.model_validate(
1967
+ db_config_raw
1968
+ )
1969
+ elif isinstance(db_config_raw, ModelPostgresIdempotencyStoreConfig):
1970
+ db_config = db_config_raw
1971
+ else:
1972
+ logger.warning(
1973
+ "Invalid idempotency_database config type",
1974
+ extra={"type": type(db_config_raw).__name__},
1975
+ )
1976
+ return
1977
+
1978
+ self._idempotency_store = StoreIdempotencyPostgres(config=db_config)
1979
+ await self._idempotency_store.initialize()
1980
+
1981
+ elif self._idempotency_config.store_type == "memory":
1982
+ from omnibase_infra.idempotency import StoreIdempotencyInmemory
1983
+
1984
+ self._idempotency_store = StoreIdempotencyInmemory()
1985
+
1986
+ else:
1987
+ logger.warning(
1988
+ "Unknown idempotency store type",
1989
+ extra={"store_type": self._idempotency_config.store_type},
1990
+ )
1991
+ return
1992
+
1993
+ logger.info(
1994
+ "Idempotency guard initialized",
1995
+ extra={
1996
+ "store_type": self._idempotency_config.store_type,
1997
+ "domain_from_operation": self._idempotency_config.domain_from_operation,
1998
+ "skip_operations": self._idempotency_config.skip_operations,
1999
+ },
2000
+ )
2001
+
2002
+ except Exception as e:
2003
+ logger.warning(
2004
+ "Failed to initialize idempotency store, proceeding without",
2005
+ extra={"error": str(e)},
2006
+ )
2007
+ self._idempotency_store = None
2008
+ self._idempotency_config = None
2009
+
2010
+ # =========================================================================
2011
+ # WARNING: FAIL-OPEN BEHAVIOR
2012
+ # =========================================================================
2013
+ # This method implements FAIL-OPEN semantics: if the idempotency store
2014
+ # is unavailable or errors, messages are ALLOWED THROUGH for processing.
2015
+ #
2016
+ # This is an intentional design decision prioritizing availability over
2017
+ # exactly-once guarantees. See docstring below for full trade-off analysis.
2018
+ #
2019
+ # IMPORTANT: Downstream handlers MUST be designed for at-least-once delivery
2020
+ # and implement their own idempotency for critical operations.
2021
+ # =========================================================================
2022
+ async def _check_idempotency(
2023
+ self,
2024
+ envelope: dict[str, object],
2025
+ correlation_id: UUID,
2026
+ ) -> bool:
2027
+ """Check if envelope should be processed (idempotency guard).
2028
+
2029
+ Extracts message_id from envelope headers and checks against the
2030
+ idempotency store. If duplicate detected, publishes a duplicate
2031
+ response and returns False.
2032
+
2033
+ Fail-Open Semantics:
2034
+ This method implements **fail-open** error handling: if the
2035
+ idempotency store is unavailable or throws an error, the message
2036
+ is allowed through for processing (with a warning log).
2037
+
2038
+ **Design Rationale**: In distributed event-driven systems, the
2039
+ idempotency store (e.g., Redis/Valkey) is a supporting service,
2040
+ not a critical path dependency. A temporary store outage should
2041
+ not halt message processing entirely, as this would cascade into
2042
+ broader system unavailability.
2043
+
2044
+ **Trade-offs**:
2045
+ - Pro: High availability - processing continues during store outages
2046
+ - Pro: Graceful degradation - system remains functional
2047
+ - Con: May result in duplicate message processing during outages
2048
+ - Con: Downstream handlers must be designed for at-least-once delivery
2049
+
2050
+ **Mitigation**: Handlers consuming messages should implement their
2051
+ own idempotency logic for critical operations (e.g., using database
2052
+ constraints or transaction guards) to ensure correctness even when
2053
+ duplicates slip through.
2054
+
2055
+ Args:
2056
+ envelope: Validated envelope dict.
2057
+ correlation_id: Normalized correlation ID (UUID).
2058
+
2059
+ Returns:
2060
+ True if message should be processed (new message).
2061
+ False if message is duplicate (skip processing).
2062
+ """
2063
+ # Skip check if idempotency not configured
2064
+ if self._idempotency_store is None or self._idempotency_config is None:
2065
+ return True
2066
+
2067
+ if not self._idempotency_config.enabled:
2068
+ return True
2069
+
2070
+ # Check if operation is in skip list
2071
+ operation = envelope.get("operation")
2072
+ if isinstance(operation, str):
2073
+ if not self._idempotency_config.should_check_idempotency(operation):
2074
+ logger.debug(
2075
+ "Skipping idempotency check for operation",
2076
+ extra={
2077
+ "operation": operation,
2078
+ "correlation_id": str(correlation_id),
2079
+ },
2080
+ )
2081
+ return True
2082
+
2083
+ # Extract message_id from envelope
2084
+ message_id = self._extract_message_id(envelope, correlation_id)
2085
+
2086
+ # Extract domain from operation if configured
2087
+ domain = self._extract_idempotency_domain(envelope)
2088
+
2089
+ # Check and record in store
2090
+ try:
2091
+ is_new = await self._idempotency_store.check_and_record(
2092
+ message_id=message_id,
2093
+ domain=domain,
2094
+ correlation_id=correlation_id,
2095
+ )
2096
+
2097
+ if not is_new:
2098
+ # Duplicate detected - publish duplicate response (NOT an error)
2099
+ logger.info(
2100
+ "Duplicate message detected, skipping processing",
2101
+ extra={
2102
+ "message_id": str(message_id),
2103
+ "domain": domain,
2104
+ "correlation_id": str(correlation_id),
2105
+ },
2106
+ )
2107
+
2108
+ duplicate_response = self._create_duplicate_response(
2109
+ message_id=message_id,
2110
+ correlation_id=correlation_id,
2111
+ )
2112
+ # duplicate_response is already a dict from _create_duplicate_response
2113
+ await self._publish_envelope_safe(
2114
+ duplicate_response, self._output_topic
2115
+ )
2116
+ return False
2117
+
2118
+ return True
2119
+
2120
+ except Exception as e:
2121
+ # FAIL-OPEN: Allow message through on idempotency store errors.
2122
+ # Rationale: Availability over exactly-once. Store outages should not
2123
+ # halt processing. Downstream handlers must tolerate duplicates.
2124
+ # See docstring for full trade-off analysis.
2125
+ logger.warning(
2126
+ "Idempotency check failed, allowing message through (fail-open)",
2127
+ extra={
2128
+ "error": str(e),
2129
+ "error_type": type(e).__name__,
2130
+ "message_id": str(message_id),
2131
+ "domain": domain,
2132
+ "correlation_id": str(correlation_id),
2133
+ },
2134
+ )
2135
+ return True
2136
+
2137
+ def _extract_message_id(
2138
+ self,
2139
+ envelope: dict[str, object],
2140
+ correlation_id: UUID,
2141
+ ) -> UUID:
2142
+ """Extract message_id from envelope, falling back to correlation_id.
2143
+
2144
+ Priority:
2145
+ 1. envelope["headers"]["message_id"]
2146
+ 2. envelope["message_id"]
2147
+ 3. Use correlation_id as message_id (fallback)
2148
+
2149
+ Args:
2150
+ envelope: Envelope dict to extract message_id from.
2151
+ correlation_id: Fallback UUID if message_id not found.
2152
+
2153
+ Returns:
2154
+ UUID representing the message_id.
2155
+ """
2156
+ # Try headers first
2157
+ headers = envelope.get("headers")
2158
+ if isinstance(headers, dict):
2159
+ header_msg_id = headers.get("message_id")
2160
+ if header_msg_id is not None:
2161
+ if isinstance(header_msg_id, UUID):
2162
+ return header_msg_id
2163
+ if isinstance(header_msg_id, str):
2164
+ try:
2165
+ return UUID(header_msg_id)
2166
+ except ValueError:
2167
+ pass
2168
+
2169
+ # Try top-level message_id
2170
+ top_level_msg_id = envelope.get("message_id")
2171
+ if top_level_msg_id is not None:
2172
+ if isinstance(top_level_msg_id, UUID):
2173
+ return top_level_msg_id
2174
+ if isinstance(top_level_msg_id, str):
2175
+ try:
2176
+ return UUID(top_level_msg_id)
2177
+ except ValueError:
2178
+ pass
2179
+
2180
+ # Fallback: use correlation_id as message_id
2181
+ return correlation_id
2182
+
2183
+ def _extract_idempotency_domain(
2184
+ self,
2185
+ envelope: dict[str, object],
2186
+ ) -> str | None:
2187
+ """Extract domain for idempotency key from envelope.
2188
+
2189
+ If domain_from_operation is enabled in config, extracts domain
2190
+ from the operation prefix (e.g., "db.query" -> "db").
2191
+
2192
+ Args:
2193
+ envelope: Envelope dict to extract domain from.
2194
+
2195
+ Returns:
2196
+ Domain string if found and configured, None otherwise.
2197
+ """
2198
+ if self._idempotency_config is None:
2199
+ return None
2200
+
2201
+ if not self._idempotency_config.domain_from_operation:
2202
+ return None
2203
+
2204
+ operation = envelope.get("operation")
2205
+ if isinstance(operation, str):
2206
+ return self._idempotency_config.extract_domain(operation)
2207
+
2208
+ return None
2209
+
2210
+ def _create_duplicate_response(
2211
+ self,
2212
+ message_id: UUID,
2213
+ correlation_id: UUID,
2214
+ ) -> dict[str, object]:
2215
+ """Create response for duplicate message detection.
2216
+
2217
+ This is NOT an error response - duplicates are expected under
2218
+ at-least-once delivery. The response indicates successful
2219
+ deduplication.
2220
+
2221
+ Args:
2222
+ message_id: UUID of the duplicate message.
2223
+ correlation_id: Correlation ID for tracing.
2224
+
2225
+ Returns:
2226
+ Dict representation of ModelDuplicateResponse for envelope publishing.
2227
+ """
2228
+ return ModelDuplicateResponse(
2229
+ message_id=message_id,
2230
+ correlation_id=correlation_id,
2231
+ ).model_dump()
2232
+
2233
+ async def _cleanup_idempotency_store(self) -> None:
2234
+ """Cleanup idempotency store during shutdown.
2235
+
2236
+ Closes the idempotency store connection if initialized.
2237
+ Called during stop() to release resources.
2238
+ """
2239
+ if self._idempotency_store is None:
2240
+ return
2241
+
2242
+ try:
2243
+ if hasattr(self._idempotency_store, "shutdown"):
2244
+ await self._idempotency_store.shutdown()
2245
+ elif hasattr(self._idempotency_store, "close"):
2246
+ await self._idempotency_store.close()
2247
+ logger.debug("Idempotency store shutdown complete")
2248
+ except Exception as e:
2249
+ logger.warning(
2250
+ "Failed to shutdown idempotency store",
2251
+ extra={"error": str(e)},
2252
+ )
2253
+ finally:
2254
+ self._idempotency_store = None
2255
+
2256
+
2257
+ __all__: list[str] = [
2258
+ "RuntimeHostProcess",
2259
+ "wire_handlers",
2260
+ ]