omnibase_infra 0.2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (833) hide show
  1. omnibase_infra/__init__.py +101 -0
  2. omnibase_infra/adapters/adapter_onex_tool_execution.py +451 -0
  3. omnibase_infra/capabilities/__init__.py +15 -0
  4. omnibase_infra/capabilities/capability_inference_rules.py +211 -0
  5. omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
  6. omnibase_infra/capabilities/intent_type_extractor.py +160 -0
  7. omnibase_infra/cli/__init__.py +1 -0
  8. omnibase_infra/cli/commands.py +216 -0
  9. omnibase_infra/clients/__init__.py +0 -0
  10. omnibase_infra/configs/widget_mapping.yaml +176 -0
  11. omnibase_infra/constants_topic_patterns.py +26 -0
  12. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +264 -0
  13. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +141 -0
  14. omnibase_infra/decorators/__init__.py +29 -0
  15. omnibase_infra/decorators/allow_any.py +109 -0
  16. omnibase_infra/dlq/__init__.py +90 -0
  17. omnibase_infra/dlq/constants_dlq.py +57 -0
  18. omnibase_infra/dlq/models/__init__.py +26 -0
  19. omnibase_infra/dlq/models/enum_replay_status.py +37 -0
  20. omnibase_infra/dlq/models/model_dlq_replay_record.py +135 -0
  21. omnibase_infra/dlq/models/model_dlq_tracking_config.py +184 -0
  22. omnibase_infra/dlq/service_dlq_tracking.py +611 -0
  23. omnibase_infra/enums/__init__.py +132 -0
  24. omnibase_infra/enums/enum_any_type_violation.py +104 -0
  25. omnibase_infra/enums/enum_backend_type.py +27 -0
  26. omnibase_infra/enums/enum_capture_outcome.py +42 -0
  27. omnibase_infra/enums/enum_capture_state.py +88 -0
  28. omnibase_infra/enums/enum_chain_violation_type.py +119 -0
  29. omnibase_infra/enums/enum_circuit_state.py +51 -0
  30. omnibase_infra/enums/enum_confirmation_event_type.py +27 -0
  31. omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
  32. omnibase_infra/enums/enum_contract_type.py +84 -0
  33. omnibase_infra/enums/enum_dedupe_strategy.py +46 -0
  34. omnibase_infra/enums/enum_dispatch_status.py +191 -0
  35. omnibase_infra/enums/enum_environment.py +46 -0
  36. omnibase_infra/enums/enum_execution_shape_violation.py +103 -0
  37. omnibase_infra/enums/enum_handler_error_type.py +111 -0
  38. omnibase_infra/enums/enum_handler_loader_error.py +178 -0
  39. omnibase_infra/enums/enum_handler_source_mode.py +86 -0
  40. omnibase_infra/enums/enum_handler_source_type.py +87 -0
  41. omnibase_infra/enums/enum_handler_type.py +77 -0
  42. omnibase_infra/enums/enum_handler_type_category.py +61 -0
  43. omnibase_infra/enums/enum_infra_transport_type.py +73 -0
  44. omnibase_infra/enums/enum_introspection_reason.py +154 -0
  45. omnibase_infra/enums/enum_kafka_acks.py +99 -0
  46. omnibase_infra/enums/enum_message_category.py +213 -0
  47. omnibase_infra/enums/enum_node_archetype.py +74 -0
  48. omnibase_infra/enums/enum_node_output_type.py +185 -0
  49. omnibase_infra/enums/enum_non_retryable_error_category.py +224 -0
  50. omnibase_infra/enums/enum_policy_type.py +32 -0
  51. omnibase_infra/enums/enum_registration_state.py +261 -0
  52. omnibase_infra/enums/enum_registration_status.py +33 -0
  53. omnibase_infra/enums/enum_registry_response_status.py +28 -0
  54. omnibase_infra/enums/enum_response_status.py +26 -0
  55. omnibase_infra/enums/enum_retry_error_category.py +98 -0
  56. omnibase_infra/enums/enum_security_rule_id.py +103 -0
  57. omnibase_infra/enums/enum_selection_strategy.py +91 -0
  58. omnibase_infra/enums/enum_topic_standard.py +42 -0
  59. omnibase_infra/enums/enum_validation_severity.py +78 -0
  60. omnibase_infra/errors/__init__.py +160 -0
  61. omnibase_infra/errors/error_architecture_violation.py +152 -0
  62. omnibase_infra/errors/error_binding_resolution.py +128 -0
  63. omnibase_infra/errors/error_chain_propagation.py +188 -0
  64. omnibase_infra/errors/error_compute_registry.py +95 -0
  65. omnibase_infra/errors/error_consul.py +132 -0
  66. omnibase_infra/errors/error_container_wiring.py +243 -0
  67. omnibase_infra/errors/error_event_bus_registry.py +105 -0
  68. omnibase_infra/errors/error_infra.py +610 -0
  69. omnibase_infra/errors/error_message_type_registry.py +101 -0
  70. omnibase_infra/errors/error_policy_registry.py +115 -0
  71. omnibase_infra/errors/error_vault.py +123 -0
  72. omnibase_infra/event_bus/__init__.py +72 -0
  73. omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +84 -0
  74. omnibase_infra/event_bus/event_bus_inmemory.py +797 -0
  75. omnibase_infra/event_bus/event_bus_kafka.py +1716 -0
  76. omnibase_infra/event_bus/mixin_kafka_broadcast.py +180 -0
  77. omnibase_infra/event_bus/mixin_kafka_dlq.py +771 -0
  78. omnibase_infra/event_bus/models/__init__.py +29 -0
  79. omnibase_infra/event_bus/models/config/__init__.py +20 -0
  80. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +693 -0
  81. omnibase_infra/event_bus/models/model_dlq_event.py +206 -0
  82. omnibase_infra/event_bus/models/model_dlq_metrics.py +304 -0
  83. omnibase_infra/event_bus/models/model_event_headers.py +115 -0
  84. omnibase_infra/event_bus/models/model_event_message.py +60 -0
  85. omnibase_infra/event_bus/testing/__init__.py +26 -0
  86. omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
  87. omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
  88. omnibase_infra/event_bus/topic_constants.py +376 -0
  89. omnibase_infra/handlers/__init__.py +82 -0
  90. omnibase_infra/handlers/filesystem/__init__.py +48 -0
  91. omnibase_infra/handlers/filesystem/enum_file_system_operation.py +35 -0
  92. omnibase_infra/handlers/filesystem/model_file_system_request.py +298 -0
  93. omnibase_infra/handlers/filesystem/model_file_system_result.py +166 -0
  94. omnibase_infra/handlers/handler_consul.py +795 -0
  95. omnibase_infra/handlers/handler_db.py +1046 -0
  96. omnibase_infra/handlers/handler_filesystem.py +1478 -0
  97. omnibase_infra/handlers/handler_graph.py +2015 -0
  98. omnibase_infra/handlers/handler_http.py +926 -0
  99. omnibase_infra/handlers/handler_intent.py +387 -0
  100. omnibase_infra/handlers/handler_manifest_persistence.contract.yaml +184 -0
  101. omnibase_infra/handlers/handler_manifest_persistence.py +1539 -0
  102. omnibase_infra/handlers/handler_mcp.py +1430 -0
  103. omnibase_infra/handlers/handler_qdrant.py +1076 -0
  104. omnibase_infra/handlers/handler_vault.py +428 -0
  105. omnibase_infra/handlers/mcp/__init__.py +19 -0
  106. omnibase_infra/handlers/mcp/adapter_onex_to_mcp.py +446 -0
  107. omnibase_infra/handlers/mcp/protocols.py +178 -0
  108. omnibase_infra/handlers/mcp/transport_streamable_http.py +352 -0
  109. omnibase_infra/handlers/mixins/__init__.py +47 -0
  110. omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
  111. omnibase_infra/handlers/mixins/mixin_consul_kv.py +338 -0
  112. omnibase_infra/handlers/mixins/mixin_consul_service.py +542 -0
  113. omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
  114. omnibase_infra/handlers/mixins/mixin_vault_initialization.py +338 -0
  115. omnibase_infra/handlers/mixins/mixin_vault_retry.py +412 -0
  116. omnibase_infra/handlers/mixins/mixin_vault_secrets.py +450 -0
  117. omnibase_infra/handlers/mixins/mixin_vault_token.py +365 -0
  118. omnibase_infra/handlers/models/__init__.py +286 -0
  119. omnibase_infra/handlers/models/consul/__init__.py +81 -0
  120. omnibase_infra/handlers/models/consul/enum_consul_operation_type.py +57 -0
  121. omnibase_infra/handlers/models/consul/model_consul_deregister_payload.py +51 -0
  122. omnibase_infra/handlers/models/consul/model_consul_handler_config.py +153 -0
  123. omnibase_infra/handlers/models/consul/model_consul_handler_payload.py +89 -0
  124. omnibase_infra/handlers/models/consul/model_consul_kv_get_found_payload.py +55 -0
  125. omnibase_infra/handlers/models/consul/model_consul_kv_get_not_found_payload.py +49 -0
  126. omnibase_infra/handlers/models/consul/model_consul_kv_get_recurse_payload.py +50 -0
  127. omnibase_infra/handlers/models/consul/model_consul_kv_item.py +33 -0
  128. omnibase_infra/handlers/models/consul/model_consul_kv_put_payload.py +41 -0
  129. omnibase_infra/handlers/models/consul/model_consul_register_payload.py +53 -0
  130. omnibase_infra/handlers/models/consul/model_consul_retry_config.py +66 -0
  131. omnibase_infra/handlers/models/consul/model_payload_consul.py +66 -0
  132. omnibase_infra/handlers/models/consul/registry_payload_consul.py +214 -0
  133. omnibase_infra/handlers/models/graph/__init__.py +35 -0
  134. omnibase_infra/handlers/models/graph/enum_graph_operation_type.py +20 -0
  135. omnibase_infra/handlers/models/graph/model_graph_execute_payload.py +38 -0
  136. omnibase_infra/handlers/models/graph/model_graph_handler_config.py +54 -0
  137. omnibase_infra/handlers/models/graph/model_graph_handler_payload.py +44 -0
  138. omnibase_infra/handlers/models/graph/model_graph_query_payload.py +40 -0
  139. omnibase_infra/handlers/models/graph/model_graph_record.py +22 -0
  140. omnibase_infra/handlers/models/http/__init__.py +50 -0
  141. omnibase_infra/handlers/models/http/enum_http_operation_type.py +29 -0
  142. omnibase_infra/handlers/models/http/model_http_body_content.py +45 -0
  143. omnibase_infra/handlers/models/http/model_http_get_payload.py +88 -0
  144. omnibase_infra/handlers/models/http/model_http_handler_payload.py +90 -0
  145. omnibase_infra/handlers/models/http/model_http_post_payload.py +88 -0
  146. omnibase_infra/handlers/models/http/model_payload_http.py +66 -0
  147. omnibase_infra/handlers/models/http/registry_payload_http.py +212 -0
  148. omnibase_infra/handlers/models/mcp/__init__.py +23 -0
  149. omnibase_infra/handlers/models/mcp/enum_mcp_operation_type.py +24 -0
  150. omnibase_infra/handlers/models/mcp/model_mcp_handler_config.py +40 -0
  151. omnibase_infra/handlers/models/mcp/model_mcp_tool_call.py +32 -0
  152. omnibase_infra/handlers/models/mcp/model_mcp_tool_result.py +45 -0
  153. omnibase_infra/handlers/models/model_consul_handler_response.py +96 -0
  154. omnibase_infra/handlers/models/model_db_describe_response.py +83 -0
  155. omnibase_infra/handlers/models/model_db_query_payload.py +95 -0
  156. omnibase_infra/handlers/models/model_db_query_response.py +60 -0
  157. omnibase_infra/handlers/models/model_filesystem_config.py +98 -0
  158. omnibase_infra/handlers/models/model_filesystem_delete_payload.py +54 -0
  159. omnibase_infra/handlers/models/model_filesystem_delete_result.py +77 -0
  160. omnibase_infra/handlers/models/model_filesystem_directory_entry.py +75 -0
  161. omnibase_infra/handlers/models/model_filesystem_ensure_directory_payload.py +54 -0
  162. omnibase_infra/handlers/models/model_filesystem_ensure_directory_result.py +60 -0
  163. omnibase_infra/handlers/models/model_filesystem_list_directory_payload.py +60 -0
  164. omnibase_infra/handlers/models/model_filesystem_list_directory_result.py +68 -0
  165. omnibase_infra/handlers/models/model_filesystem_read_payload.py +62 -0
  166. omnibase_infra/handlers/models/model_filesystem_read_result.py +61 -0
  167. omnibase_infra/handlers/models/model_filesystem_write_payload.py +70 -0
  168. omnibase_infra/handlers/models/model_filesystem_write_result.py +55 -0
  169. omnibase_infra/handlers/models/model_graph_handler_response.py +98 -0
  170. omnibase_infra/handlers/models/model_handler_response.py +103 -0
  171. omnibase_infra/handlers/models/model_http_handler_response.py +101 -0
  172. omnibase_infra/handlers/models/model_manifest_metadata.py +75 -0
  173. omnibase_infra/handlers/models/model_manifest_persistence_config.py +62 -0
  174. omnibase_infra/handlers/models/model_manifest_query_payload.py +90 -0
  175. omnibase_infra/handlers/models/model_manifest_query_result.py +97 -0
  176. omnibase_infra/handlers/models/model_manifest_retrieve_payload.py +44 -0
  177. omnibase_infra/handlers/models/model_manifest_retrieve_result.py +98 -0
  178. omnibase_infra/handlers/models/model_manifest_store_payload.py +47 -0
  179. omnibase_infra/handlers/models/model_manifest_store_result.py +67 -0
  180. omnibase_infra/handlers/models/model_operation_context.py +187 -0
  181. omnibase_infra/handlers/models/model_qdrant_handler_response.py +98 -0
  182. omnibase_infra/handlers/models/model_retry_state.py +162 -0
  183. omnibase_infra/handlers/models/model_vault_handler_response.py +98 -0
  184. omnibase_infra/handlers/models/qdrant/__init__.py +44 -0
  185. omnibase_infra/handlers/models/qdrant/enum_qdrant_operation_type.py +26 -0
  186. omnibase_infra/handlers/models/qdrant/model_qdrant_collection_payload.py +42 -0
  187. omnibase_infra/handlers/models/qdrant/model_qdrant_delete_payload.py +36 -0
  188. omnibase_infra/handlers/models/qdrant/model_qdrant_handler_config.py +42 -0
  189. omnibase_infra/handlers/models/qdrant/model_qdrant_handler_payload.py +54 -0
  190. omnibase_infra/handlers/models/qdrant/model_qdrant_search_payload.py +42 -0
  191. omnibase_infra/handlers/models/qdrant/model_qdrant_search_result.py +30 -0
  192. omnibase_infra/handlers/models/qdrant/model_qdrant_upsert_payload.py +36 -0
  193. omnibase_infra/handlers/models/vault/__init__.py +69 -0
  194. omnibase_infra/handlers/models/vault/enum_vault_operation_type.py +35 -0
  195. omnibase_infra/handlers/models/vault/model_payload_vault.py +66 -0
  196. omnibase_infra/handlers/models/vault/model_vault_delete_payload.py +57 -0
  197. omnibase_infra/handlers/models/vault/model_vault_handler_config.py +148 -0
  198. omnibase_infra/handlers/models/vault/model_vault_handler_payload.py +101 -0
  199. omnibase_infra/handlers/models/vault/model_vault_list_payload.py +58 -0
  200. omnibase_infra/handlers/models/vault/model_vault_renew_token_payload.py +67 -0
  201. omnibase_infra/handlers/models/vault/model_vault_retry_config.py +66 -0
  202. omnibase_infra/handlers/models/vault/model_vault_secret_payload.py +106 -0
  203. omnibase_infra/handlers/models/vault/model_vault_write_payload.py +66 -0
  204. omnibase_infra/handlers/models/vault/registry_payload_vault.py +213 -0
  205. omnibase_infra/handlers/registration_storage/__init__.py +43 -0
  206. omnibase_infra/handlers/registration_storage/handler_registration_storage_mock.py +392 -0
  207. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +922 -0
  208. omnibase_infra/handlers/registration_storage/models/__init__.py +23 -0
  209. omnibase_infra/handlers/registration_storage/models/model_delete_registration_request.py +58 -0
  210. omnibase_infra/handlers/registration_storage/models/model_update_registration_request.py +73 -0
  211. omnibase_infra/handlers/registration_storage/protocol_registration_persistence.py +191 -0
  212. omnibase_infra/handlers/service_discovery/__init__.py +43 -0
  213. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +1051 -0
  214. omnibase_infra/handlers/service_discovery/handler_service_discovery_mock.py +258 -0
  215. omnibase_infra/handlers/service_discovery/models/__init__.py +22 -0
  216. omnibase_infra/handlers/service_discovery/models/model_discovery_result.py +64 -0
  217. omnibase_infra/handlers/service_discovery/models/model_registration_result.py +138 -0
  218. omnibase_infra/handlers/service_discovery/models/model_service_info.py +109 -0
  219. omnibase_infra/handlers/service_discovery/protocol_discovery_operations.py +170 -0
  220. omnibase_infra/idempotency/__init__.py +94 -0
  221. omnibase_infra/idempotency/models/__init__.py +43 -0
  222. omnibase_infra/idempotency/models/model_idempotency_check_result.py +85 -0
  223. omnibase_infra/idempotency/models/model_idempotency_guard_config.py +130 -0
  224. omnibase_infra/idempotency/models/model_idempotency_record.py +86 -0
  225. omnibase_infra/idempotency/models/model_idempotency_store_health_check_result.py +81 -0
  226. omnibase_infra/idempotency/models/model_idempotency_store_metrics.py +140 -0
  227. omnibase_infra/idempotency/models/model_postgres_idempotency_store_config.py +299 -0
  228. omnibase_infra/idempotency/protocol_idempotency_store.py +184 -0
  229. omnibase_infra/idempotency/store_inmemory.py +265 -0
  230. omnibase_infra/idempotency/store_postgres.py +923 -0
  231. omnibase_infra/infrastructure/__init__.py +0 -0
  232. omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
  233. omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
  234. omnibase_infra/mixins/__init__.py +71 -0
  235. omnibase_infra/mixins/mixin_async_circuit_breaker.py +656 -0
  236. omnibase_infra/mixins/mixin_dict_like_accessors.py +146 -0
  237. omnibase_infra/mixins/mixin_envelope_extraction.py +119 -0
  238. omnibase_infra/mixins/mixin_node_introspection.py +2670 -0
  239. omnibase_infra/mixins/mixin_retry_execution.py +386 -0
  240. omnibase_infra/mixins/protocol_circuit_breaker_aware.py +133 -0
  241. omnibase_infra/models/__init__.py +144 -0
  242. omnibase_infra/models/bindings/__init__.py +59 -0
  243. omnibase_infra/models/bindings/constants.py +144 -0
  244. omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
  245. omnibase_infra/models/bindings/model_operation_binding.py +44 -0
  246. omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
  247. omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
  248. omnibase_infra/models/corpus/__init__.py +17 -0
  249. omnibase_infra/models/corpus/model_capture_config.py +133 -0
  250. omnibase_infra/models/corpus/model_capture_result.py +86 -0
  251. omnibase_infra/models/discovery/__init__.py +42 -0
  252. omnibase_infra/models/discovery/model_dependency_spec.py +319 -0
  253. omnibase_infra/models/discovery/model_discovered_capabilities.py +50 -0
  254. omnibase_infra/models/discovery/model_introspection_config.py +330 -0
  255. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +169 -0
  256. omnibase_infra/models/discovery/model_introspection_task_config.py +116 -0
  257. omnibase_infra/models/dispatch/__init__.py +155 -0
  258. omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
  259. omnibase_infra/models/dispatch/model_dispatch_context.py +439 -0
  260. omnibase_infra/models/dispatch/model_dispatch_error.py +336 -0
  261. omnibase_infra/models/dispatch/model_dispatch_log_context.py +400 -0
  262. omnibase_infra/models/dispatch/model_dispatch_metadata.py +228 -0
  263. omnibase_infra/models/dispatch/model_dispatch_metrics.py +496 -0
  264. omnibase_infra/models/dispatch/model_dispatch_outcome.py +317 -0
  265. omnibase_infra/models/dispatch/model_dispatch_outputs.py +231 -0
  266. omnibase_infra/models/dispatch/model_dispatch_result.py +436 -0
  267. omnibase_infra/models/dispatch/model_dispatch_route.py +279 -0
  268. omnibase_infra/models/dispatch/model_dispatcher_metrics.py +275 -0
  269. omnibase_infra/models/dispatch/model_dispatcher_registration.py +352 -0
  270. omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
  271. omnibase_infra/models/dispatch/model_parsed_topic.py +135 -0
  272. omnibase_infra/models/dispatch/model_topic_parser.py +725 -0
  273. omnibase_infra/models/dispatch/model_tracing_context.py +285 -0
  274. omnibase_infra/models/errors/__init__.py +45 -0
  275. omnibase_infra/models/errors/model_handler_validation_error.py +594 -0
  276. omnibase_infra/models/errors/model_infra_error_context.py +99 -0
  277. omnibase_infra/models/errors/model_message_type_registry_error_context.py +71 -0
  278. omnibase_infra/models/errors/model_timeout_error_context.py +110 -0
  279. omnibase_infra/models/handlers/__init__.py +80 -0
  280. omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
  281. omnibase_infra/models/handlers/model_contract_discovery_result.py +82 -0
  282. omnibase_infra/models/handlers/model_handler_descriptor.py +200 -0
  283. omnibase_infra/models/handlers/model_handler_identifier.py +215 -0
  284. omnibase_infra/models/handlers/model_handler_source_config.py +220 -0
  285. omnibase_infra/models/health/__init__.py +9 -0
  286. omnibase_infra/models/health/model_health_check_result.py +40 -0
  287. omnibase_infra/models/lifecycle/__init__.py +39 -0
  288. omnibase_infra/models/logging/__init__.py +51 -0
  289. omnibase_infra/models/logging/model_log_context.py +756 -0
  290. omnibase_infra/models/mcp/__init__.py +15 -0
  291. omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
  292. omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
  293. omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
  294. omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
  295. omnibase_infra/models/model_node_identity.py +126 -0
  296. omnibase_infra/models/model_retry_error_classification.py +78 -0
  297. omnibase_infra/models/projection/__init__.py +43 -0
  298. omnibase_infra/models/projection/model_capability_fields.py +112 -0
  299. omnibase_infra/models/projection/model_registration_projection.py +434 -0
  300. omnibase_infra/models/projection/model_registration_snapshot.py +322 -0
  301. omnibase_infra/models/projection/model_sequence_info.py +182 -0
  302. omnibase_infra/models/projection/model_snapshot_topic_config.py +591 -0
  303. omnibase_infra/models/projectors/__init__.py +41 -0
  304. omnibase_infra/models/projectors/model_projector_column.py +289 -0
  305. omnibase_infra/models/projectors/model_projector_discovery_result.py +65 -0
  306. omnibase_infra/models/projectors/model_projector_index.py +270 -0
  307. omnibase_infra/models/projectors/model_projector_schema.py +415 -0
  308. omnibase_infra/models/projectors/model_projector_validation_error.py +63 -0
  309. omnibase_infra/models/projectors/util_sql_identifiers.py +115 -0
  310. omnibase_infra/models/registration/__init__.py +68 -0
  311. omnibase_infra/models/registration/commands/__init__.py +15 -0
  312. omnibase_infra/models/registration/commands/model_node_registration_acked.py +108 -0
  313. omnibase_infra/models/registration/events/__init__.py +56 -0
  314. omnibase_infra/models/registration/events/model_node_became_active.py +103 -0
  315. omnibase_infra/models/registration/events/model_node_liveness_expired.py +103 -0
  316. omnibase_infra/models/registration/events/model_node_registration_accepted.py +98 -0
  317. omnibase_infra/models/registration/events/model_node_registration_ack_received.py +98 -0
  318. omnibase_infra/models/registration/events/model_node_registration_ack_timed_out.py +112 -0
  319. omnibase_infra/models/registration/events/model_node_registration_initiated.py +107 -0
  320. omnibase_infra/models/registration/events/model_node_registration_rejected.py +104 -0
  321. omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
  322. omnibase_infra/models/registration/model_introspection_metrics.py +253 -0
  323. omnibase_infra/models/registration/model_node_capabilities.py +190 -0
  324. omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
  325. omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
  326. omnibase_infra/models/registration/model_node_introspection_event.py +195 -0
  327. omnibase_infra/models/registration/model_node_metadata.py +79 -0
  328. omnibase_infra/models/registration/model_node_registration.py +162 -0
  329. omnibase_infra/models/registration/model_node_registration_record.py +162 -0
  330. omnibase_infra/models/registry/__init__.py +29 -0
  331. omnibase_infra/models/registry/model_domain_constraint.py +202 -0
  332. omnibase_infra/models/registry/model_message_type_entry.py +271 -0
  333. omnibase_infra/models/resilience/__init__.py +9 -0
  334. omnibase_infra/models/resilience/model_circuit_breaker_config.py +227 -0
  335. omnibase_infra/models/routing/__init__.py +25 -0
  336. omnibase_infra/models/routing/model_routing_entry.py +52 -0
  337. omnibase_infra/models/routing/model_routing_subcontract.py +70 -0
  338. omnibase_infra/models/runtime/__init__.py +49 -0
  339. omnibase_infra/models/runtime/model_contract_security_config.py +41 -0
  340. omnibase_infra/models/runtime/model_discovery_error.py +81 -0
  341. omnibase_infra/models/runtime/model_discovery_result.py +162 -0
  342. omnibase_infra/models/runtime/model_discovery_warning.py +74 -0
  343. omnibase_infra/models/runtime/model_failed_plugin_load.py +63 -0
  344. omnibase_infra/models/runtime/model_handler_contract.py +296 -0
  345. omnibase_infra/models/runtime/model_loaded_handler.py +129 -0
  346. omnibase_infra/models/runtime/model_plugin_load_context.py +93 -0
  347. omnibase_infra/models/runtime/model_plugin_load_summary.py +124 -0
  348. omnibase_infra/models/security/__init__.py +50 -0
  349. omnibase_infra/models/security/classification_levels.py +99 -0
  350. omnibase_infra/models/security/model_environment_policy.py +145 -0
  351. omnibase_infra/models/security/model_handler_security_policy.py +107 -0
  352. omnibase_infra/models/security/model_security_error.py +81 -0
  353. omnibase_infra/models/security/model_security_validation_result.py +328 -0
  354. omnibase_infra/models/security/model_security_warning.py +67 -0
  355. omnibase_infra/models/snapshot/__init__.py +27 -0
  356. omnibase_infra/models/snapshot/model_field_change.py +65 -0
  357. omnibase_infra/models/snapshot/model_snapshot.py +270 -0
  358. omnibase_infra/models/snapshot/model_snapshot_diff.py +203 -0
  359. omnibase_infra/models/snapshot/model_subject_ref.py +81 -0
  360. omnibase_infra/models/types/__init__.py +71 -0
  361. omnibase_infra/models/validation/__init__.py +89 -0
  362. omnibase_infra/models/validation/model_any_type_validation_result.py +118 -0
  363. omnibase_infra/models/validation/model_any_type_violation.py +141 -0
  364. omnibase_infra/models/validation/model_category_match_result.py +345 -0
  365. omnibase_infra/models/validation/model_chain_violation.py +166 -0
  366. omnibase_infra/models/validation/model_coverage_metrics.py +316 -0
  367. omnibase_infra/models/validation/model_execution_shape_rule.py +159 -0
  368. omnibase_infra/models/validation/model_execution_shape_validation.py +208 -0
  369. omnibase_infra/models/validation/model_execution_shape_validation_result.py +294 -0
  370. omnibase_infra/models/validation/model_execution_shape_violation.py +122 -0
  371. omnibase_infra/models/validation/model_localhandler_validation_result.py +139 -0
  372. omnibase_infra/models/validation/model_localhandler_violation.py +100 -0
  373. omnibase_infra/models/validation/model_output_validation_params.py +74 -0
  374. omnibase_infra/models/validation/model_validate_and_raise_params.py +84 -0
  375. omnibase_infra/models/validation/model_validation_error_params.py +84 -0
  376. omnibase_infra/models/validation/model_validation_outcome.py +287 -0
  377. omnibase_infra/nodes/__init__.py +57 -0
  378. omnibase_infra/nodes/architecture_validator/__init__.py +79 -0
  379. omnibase_infra/nodes/architecture_validator/contract.yaml +252 -0
  380. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +203 -0
  381. omnibase_infra/nodes/architecture_validator/mixins/__init__.py +16 -0
  382. omnibase_infra/nodes/architecture_validator/mixins/mixin_file_path_rule.py +92 -0
  383. omnibase_infra/nodes/architecture_validator/models/__init__.py +36 -0
  384. omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_request.py +56 -0
  385. omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_result.py +311 -0
  386. omnibase_infra/nodes/architecture_validator/models/model_architecture_violation.py +163 -0
  387. omnibase_infra/nodes/architecture_validator/models/model_rule_check_result.py +265 -0
  388. omnibase_infra/nodes/architecture_validator/models/model_validation_request.py +105 -0
  389. omnibase_infra/nodes/architecture_validator/models/model_validation_result.py +314 -0
  390. omnibase_infra/nodes/architecture_validator/node.py +262 -0
  391. omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +383 -0
  392. omnibase_infra/nodes/architecture_validator/protocols/__init__.py +9 -0
  393. omnibase_infra/nodes/architecture_validator/protocols/protocol_architecture_rule.py +225 -0
  394. omnibase_infra/nodes/architecture_validator/registry/__init__.py +28 -0
  395. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +106 -0
  396. omnibase_infra/nodes/architecture_validator/validators/__init__.py +104 -0
  397. omnibase_infra/nodes/architecture_validator/validators/validator_no_direct_dispatch.py +422 -0
  398. omnibase_infra/nodes/architecture_validator/validators/validator_no_handler_publishing.py +481 -0
  399. omnibase_infra/nodes/architecture_validator/validators/validator_no_orchestrator_fsm.py +491 -0
  400. omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
  401. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
  402. omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
  403. omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
  404. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
  405. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
  406. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
  407. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
  408. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
  409. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
  410. omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
  411. omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
  412. omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
  413. omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
  414. omnibase_infra/nodes/effects/README.md +358 -0
  415. omnibase_infra/nodes/effects/__init__.py +26 -0
  416. omnibase_infra/nodes/effects/contract.yaml +167 -0
  417. omnibase_infra/nodes/effects/models/__init__.py +32 -0
  418. omnibase_infra/nodes/effects/models/model_backend_result.py +190 -0
  419. omnibase_infra/nodes/effects/models/model_effect_idempotency_config.py +92 -0
  420. omnibase_infra/nodes/effects/models/model_registry_request.py +132 -0
  421. omnibase_infra/nodes/effects/models/model_registry_response.py +263 -0
  422. omnibase_infra/nodes/effects/protocol_consul_client.py +89 -0
  423. omnibase_infra/nodes/effects/protocol_effect_idempotency_store.py +143 -0
  424. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +96 -0
  425. omnibase_infra/nodes/effects/registry_effect.py +525 -0
  426. omnibase_infra/nodes/effects/store_effect_idempotency_inmemory.py +425 -0
  427. omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
  428. omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
  429. omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
  430. omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
  431. omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
  432. omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
  433. omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
  434. omnibase_infra/nodes/node_intent_storage_effect/__init__.py +50 -0
  435. omnibase_infra/nodes/node_intent_storage_effect/contract.yaml +194 -0
  436. omnibase_infra/nodes/node_intent_storage_effect/models/__init__.py +24 -0
  437. omnibase_infra/nodes/node_intent_storage_effect/models/model_intent_storage_input.py +141 -0
  438. omnibase_infra/nodes/node_intent_storage_effect/models/model_intent_storage_output.py +130 -0
  439. omnibase_infra/nodes/node_intent_storage_effect/node.py +94 -0
  440. omnibase_infra/nodes/node_intent_storage_effect/registry/__init__.py +35 -0
  441. omnibase_infra/nodes/node_intent_storage_effect/registry/registry_infra_intent_storage.py +294 -0
  442. omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
  443. omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
  444. omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
  445. omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
  446. omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
  447. omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
  448. omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
  449. omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
  450. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
  451. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
  452. omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
  453. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
  454. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
  455. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
  456. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
  457. omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
  458. omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
  459. omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
  460. omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
  461. omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
  462. omnibase_infra/nodes/node_registration_orchestrator/README.md +542 -0
  463. omnibase_infra/nodes/node_registration_orchestrator/__init__.py +120 -0
  464. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +482 -0
  465. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/__init__.py +53 -0
  466. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_introspected.py +376 -0
  467. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_registration_acked.py +376 -0
  468. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_runtime_tick.py +373 -0
  469. omnibase_infra/nodes/node_registration_orchestrator/handlers/__init__.py +62 -0
  470. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_heartbeat.py +376 -0
  471. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +694 -0
  472. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_registration_acked.py +458 -0
  473. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_runtime_tick.py +364 -0
  474. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +544 -0
  475. omnibase_infra/nodes/node_registration_orchestrator/models/__init__.py +75 -0
  476. omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_intent_payload.py +194 -0
  477. omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_registration_intent.py +67 -0
  478. omnibase_infra/nodes/node_registration_orchestrator/models/model_intent_execution_result.py +50 -0
  479. omnibase_infra/nodes/node_registration_orchestrator/models/model_node_liveness_expired.py +107 -0
  480. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_config.py +67 -0
  481. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_input.py +41 -0
  482. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_output.py +166 -0
  483. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +235 -0
  484. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_upsert_intent.py +68 -0
  485. omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_execution_result.py +384 -0
  486. omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_state.py +60 -0
  487. omnibase_infra/nodes/node_registration_orchestrator/models/model_registration_intent.py +177 -0
  488. omnibase_infra/nodes/node_registration_orchestrator/models/model_registry_intent.py +247 -0
  489. omnibase_infra/nodes/node_registration_orchestrator/node.py +195 -0
  490. omnibase_infra/nodes/node_registration_orchestrator/plugin.py +909 -0
  491. omnibase_infra/nodes/node_registration_orchestrator/protocols.py +439 -0
  492. omnibase_infra/nodes/node_registration_orchestrator/registry/__init__.py +41 -0
  493. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +528 -0
  494. omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +393 -0
  495. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +743 -0
  496. omnibase_infra/nodes/node_registration_reducer/__init__.py +15 -0
  497. omnibase_infra/nodes/node_registration_reducer/contract.yaml +301 -0
  498. omnibase_infra/nodes/node_registration_reducer/models/__init__.py +38 -0
  499. omnibase_infra/nodes/node_registration_reducer/models/model_validation_result.py +113 -0
  500. omnibase_infra/nodes/node_registration_reducer/node.py +139 -0
  501. omnibase_infra/nodes/node_registration_reducer/registry/__init__.py +9 -0
  502. omnibase_infra/nodes/node_registration_reducer/registry/registry_infra_node_registration_reducer.py +79 -0
  503. omnibase_infra/nodes/node_registration_storage_effect/__init__.py +41 -0
  504. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +220 -0
  505. omnibase_infra/nodes/node_registration_storage_effect/models/__init__.py +44 -0
  506. omnibase_infra/nodes/node_registration_storage_effect/models/model_delete_result.py +132 -0
  507. omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_record.py +199 -0
  508. omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_update.py +155 -0
  509. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_details.py +123 -0
  510. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_result.py +117 -0
  511. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_query.py +100 -0
  512. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_result.py +136 -0
  513. omnibase_infra/nodes/node_registration_storage_effect/models/model_upsert_result.py +127 -0
  514. omnibase_infra/nodes/node_registration_storage_effect/node.py +112 -0
  515. omnibase_infra/nodes/node_registration_storage_effect/protocols/__init__.py +22 -0
  516. omnibase_infra/nodes/node_registration_storage_effect/protocols/protocol_registration_persistence.py +333 -0
  517. omnibase_infra/nodes/node_registration_storage_effect/registry/__init__.py +23 -0
  518. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +215 -0
  519. omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
  520. omnibase_infra/nodes/node_registry_effect/contract.yaml +677 -0
  521. omnibase_infra/nodes/node_registry_effect/handlers/__init__.py +70 -0
  522. omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_deregister.py +211 -0
  523. omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_register.py +212 -0
  524. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +417 -0
  525. omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_deactivate.py +215 -0
  526. omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_upsert.py +208 -0
  527. omnibase_infra/nodes/node_registry_effect/models/__init__.py +43 -0
  528. omnibase_infra/nodes/node_registry_effect/models/model_partial_retry_request.py +92 -0
  529. omnibase_infra/nodes/node_registry_effect/node.py +165 -0
  530. omnibase_infra/nodes/node_registry_effect/registry/__init__.py +27 -0
  531. omnibase_infra/nodes/node_registry_effect/registry/registry_infra_registry_effect.py +196 -0
  532. omnibase_infra/nodes/node_service_discovery_effect/__init__.py +111 -0
  533. omnibase_infra/nodes/node_service_discovery_effect/contract.yaml +246 -0
  534. omnibase_infra/nodes/node_service_discovery_effect/models/__init__.py +67 -0
  535. omnibase_infra/nodes/node_service_discovery_effect/models/enum_health_status.py +72 -0
  536. omnibase_infra/nodes/node_service_discovery_effect/models/enum_service_discovery_operation.py +58 -0
  537. omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_query.py +99 -0
  538. omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_result.py +98 -0
  539. omnibase_infra/nodes/node_service_discovery_effect/models/model_health_check_config.py +121 -0
  540. omnibase_infra/nodes/node_service_discovery_effect/models/model_query_metadata.py +63 -0
  541. omnibase_infra/nodes/node_service_discovery_effect/models/model_registration_result.py +130 -0
  542. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_details.py +111 -0
  543. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_result.py +119 -0
  544. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_info.py +106 -0
  545. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_registration.py +121 -0
  546. omnibase_infra/nodes/node_service_discovery_effect/node.py +111 -0
  547. omnibase_infra/nodes/node_service_discovery_effect/protocols/__init__.py +14 -0
  548. omnibase_infra/nodes/node_service_discovery_effect/protocols/protocol_discovery_operations.py +279 -0
  549. omnibase_infra/nodes/node_service_discovery_effect/registry/__init__.py +13 -0
  550. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +222 -0
  551. omnibase_infra/nodes/reducers/__init__.py +30 -0
  552. omnibase_infra/nodes/reducers/models/__init__.py +37 -0
  553. omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +87 -0
  554. omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
  555. omnibase_infra/nodes/reducers/models/model_payload_postgres_upsert_registration.py +60 -0
  556. omnibase_infra/nodes/reducers/models/model_registration_confirmation.py +166 -0
  557. omnibase_infra/nodes/reducers/models/model_registration_state.py +433 -0
  558. omnibase_infra/nodes/reducers/registration_reducer.py +1138 -0
  559. omnibase_infra/observability/__init__.py +143 -0
  560. omnibase_infra/observability/constants_metrics.py +91 -0
  561. omnibase_infra/observability/factory_observability_sink.py +525 -0
  562. omnibase_infra/observability/handlers/__init__.py +118 -0
  563. omnibase_infra/observability/handlers/handler_logging_structured.py +967 -0
  564. omnibase_infra/observability/handlers/handler_metrics_prometheus.py +1120 -0
  565. omnibase_infra/observability/handlers/model_logging_handler_config.py +71 -0
  566. omnibase_infra/observability/handlers/model_logging_handler_response.py +77 -0
  567. omnibase_infra/observability/handlers/model_metrics_handler_config.py +172 -0
  568. omnibase_infra/observability/handlers/model_metrics_handler_payload.py +135 -0
  569. omnibase_infra/observability/handlers/model_metrics_handler_response.py +101 -0
  570. omnibase_infra/observability/hooks/__init__.py +74 -0
  571. omnibase_infra/observability/hooks/hook_observability.py +1223 -0
  572. omnibase_infra/observability/models/__init__.py +30 -0
  573. omnibase_infra/observability/models/enum_required_log_context_key.py +77 -0
  574. omnibase_infra/observability/models/model_buffered_log_entry.py +117 -0
  575. omnibase_infra/observability/models/model_logging_sink_config.py +73 -0
  576. omnibase_infra/observability/models/model_metrics_sink_config.py +156 -0
  577. omnibase_infra/observability/sinks/__init__.py +69 -0
  578. omnibase_infra/observability/sinks/sink_logging_structured.py +809 -0
  579. omnibase_infra/observability/sinks/sink_metrics_prometheus.py +710 -0
  580. omnibase_infra/plugins/__init__.py +27 -0
  581. omnibase_infra/plugins/examples/__init__.py +28 -0
  582. omnibase_infra/plugins/examples/plugin_json_normalizer.py +271 -0
  583. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +210 -0
  584. omnibase_infra/plugins/models/__init__.py +21 -0
  585. omnibase_infra/plugins/models/model_plugin_context.py +76 -0
  586. omnibase_infra/plugins/models/model_plugin_input_data.py +58 -0
  587. omnibase_infra/plugins/models/model_plugin_output_data.py +62 -0
  588. omnibase_infra/plugins/plugin_compute_base.py +449 -0
  589. omnibase_infra/projectors/__init__.py +30 -0
  590. omnibase_infra/projectors/contracts/__init__.py +63 -0
  591. omnibase_infra/projectors/contracts/registration_projector.yaml +370 -0
  592. omnibase_infra/projectors/projection_reader_registration.py +1559 -0
  593. omnibase_infra/projectors/snapshot_publisher_registration.py +1329 -0
  594. omnibase_infra/protocols/__init__.py +104 -0
  595. omnibase_infra/protocols/protocol_capability_projection.py +253 -0
  596. omnibase_infra/protocols/protocol_capability_query.py +251 -0
  597. omnibase_infra/protocols/protocol_container_aware.py +200 -0
  598. omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
  599. omnibase_infra/protocols/protocol_event_bus_like.py +127 -0
  600. omnibase_infra/protocols/protocol_event_projector.py +96 -0
  601. omnibase_infra/protocols/protocol_idempotency_store.py +142 -0
  602. omnibase_infra/protocols/protocol_message_dispatcher.py +247 -0
  603. omnibase_infra/protocols/protocol_message_type_registry.py +306 -0
  604. omnibase_infra/protocols/protocol_plugin_compute.py +368 -0
  605. omnibase_infra/protocols/protocol_projector_schema_validator.py +82 -0
  606. omnibase_infra/protocols/protocol_registry_metrics.py +215 -0
  607. omnibase_infra/protocols/protocol_snapshot_publisher.py +396 -0
  608. omnibase_infra/protocols/protocol_snapshot_store.py +567 -0
  609. omnibase_infra/runtime/__init__.py +445 -0
  610. omnibase_infra/runtime/binding_config_resolver.py +2771 -0
  611. omnibase_infra/runtime/binding_resolver.py +753 -0
  612. omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
  613. omnibase_infra/runtime/constants_notification.py +75 -0
  614. omnibase_infra/runtime/constants_security.py +70 -0
  615. omnibase_infra/runtime/contract_handler_discovery.py +587 -0
  616. omnibase_infra/runtime/contract_loaders/__init__.py +51 -0
  617. omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
  618. omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
  619. omnibase_infra/runtime/dispatch_context_enforcer.py +427 -0
  620. omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
  621. omnibase_infra/runtime/emit_daemon/cli.py +844 -0
  622. omnibase_infra/runtime/emit_daemon/client.py +811 -0
  623. omnibase_infra/runtime/emit_daemon/config.py +535 -0
  624. omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
  625. omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
  626. omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
  627. omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
  628. omnibase_infra/runtime/emit_daemon/queue.py +618 -0
  629. omnibase_infra/runtime/enums/__init__.py +18 -0
  630. omnibase_infra/runtime/enums/enum_config_ref_scheme.py +33 -0
  631. omnibase_infra/runtime/enums/enum_scheduler_status.py +170 -0
  632. omnibase_infra/runtime/envelope_validator.py +179 -0
  633. omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
  634. omnibase_infra/runtime/handler_bootstrap_source.py +507 -0
  635. omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
  636. omnibase_infra/runtime/handler_contract_source.py +750 -0
  637. omnibase_infra/runtime/handler_identity.py +81 -0
  638. omnibase_infra/runtime/handler_plugin_loader.py +2046 -0
  639. omnibase_infra/runtime/handler_registry.py +329 -0
  640. omnibase_infra/runtime/handler_source_resolver.py +367 -0
  641. omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
  642. omnibase_infra/runtime/kafka_contract_source.py +984 -0
  643. omnibase_infra/runtime/kernel.py +40 -0
  644. omnibase_infra/runtime/mixin_policy_validation.py +522 -0
  645. omnibase_infra/runtime/mixin_semver_cache.py +402 -0
  646. omnibase_infra/runtime/mixins/__init__.py +24 -0
  647. omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
  648. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +778 -0
  649. omnibase_infra/runtime/models/__init__.py +229 -0
  650. omnibase_infra/runtime/models/model_batch_lifecycle_result.py +217 -0
  651. omnibase_infra/runtime/models/model_binding_config.py +168 -0
  652. omnibase_infra/runtime/models/model_binding_config_cache_stats.py +135 -0
  653. omnibase_infra/runtime/models/model_binding_config_resolver_config.py +329 -0
  654. omnibase_infra/runtime/models/model_cached_secret.py +138 -0
  655. omnibase_infra/runtime/models/model_compute_key.py +138 -0
  656. omnibase_infra/runtime/models/model_compute_registration.py +97 -0
  657. omnibase_infra/runtime/models/model_config_cache_entry.py +61 -0
  658. omnibase_infra/runtime/models/model_config_ref.py +331 -0
  659. omnibase_infra/runtime/models/model_config_ref_parse_result.py +125 -0
  660. omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
  661. omnibase_infra/runtime/models/model_domain_plugin_config.py +92 -0
  662. omnibase_infra/runtime/models/model_domain_plugin_result.py +270 -0
  663. omnibase_infra/runtime/models/model_duplicate_response.py +54 -0
  664. omnibase_infra/runtime/models/model_enabled_protocols_config.py +61 -0
  665. omnibase_infra/runtime/models/model_event_bus_config.py +54 -0
  666. omnibase_infra/runtime/models/model_failed_component.py +55 -0
  667. omnibase_infra/runtime/models/model_health_check_response.py +168 -0
  668. omnibase_infra/runtime/models/model_health_check_result.py +229 -0
  669. omnibase_infra/runtime/models/model_lifecycle_result.py +245 -0
  670. omnibase_infra/runtime/models/model_logging_config.py +42 -0
  671. omnibase_infra/runtime/models/model_optional_correlation_id.py +167 -0
  672. omnibase_infra/runtime/models/model_optional_string.py +94 -0
  673. omnibase_infra/runtime/models/model_optional_uuid.py +110 -0
  674. omnibase_infra/runtime/models/model_policy_context.py +100 -0
  675. omnibase_infra/runtime/models/model_policy_key.py +138 -0
  676. omnibase_infra/runtime/models/model_policy_registration.py +139 -0
  677. omnibase_infra/runtime/models/model_policy_result.py +103 -0
  678. omnibase_infra/runtime/models/model_policy_type_filter.py +157 -0
  679. omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
  680. omnibase_infra/runtime/models/model_projector_plugin_loader_config.py +47 -0
  681. omnibase_infra/runtime/models/model_protocol_registration_config.py +65 -0
  682. omnibase_infra/runtime/models/model_retry_policy.py +105 -0
  683. omnibase_infra/runtime/models/model_runtime_config.py +150 -0
  684. omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
  685. omnibase_infra/runtime/models/model_runtime_scheduler_config.py +625 -0
  686. omnibase_infra/runtime/models/model_runtime_scheduler_metrics.py +233 -0
  687. omnibase_infra/runtime/models/model_runtime_tick.py +193 -0
  688. omnibase_infra/runtime/models/model_secret_cache_stats.py +82 -0
  689. omnibase_infra/runtime/models/model_secret_mapping.py +63 -0
  690. omnibase_infra/runtime/models/model_secret_resolver_config.py +107 -0
  691. omnibase_infra/runtime/models/model_secret_resolver_metrics.py +111 -0
  692. omnibase_infra/runtime/models/model_secret_source_info.py +72 -0
  693. omnibase_infra/runtime/models/model_secret_source_spec.py +66 -0
  694. omnibase_infra/runtime/models/model_security_config.py +109 -0
  695. omnibase_infra/runtime/models/model_shutdown_batch_result.py +75 -0
  696. omnibase_infra/runtime/models/model_shutdown_config.py +94 -0
  697. omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
  698. omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
  699. omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
  700. omnibase_infra/runtime/projector_plugin_loader.py +1462 -0
  701. omnibase_infra/runtime/projector_schema_manager.py +565 -0
  702. omnibase_infra/runtime/projector_shell.py +1330 -0
  703. omnibase_infra/runtime/protocol_contract_descriptor.py +92 -0
  704. omnibase_infra/runtime/protocol_contract_source.py +92 -0
  705. omnibase_infra/runtime/protocol_domain_plugin.py +474 -0
  706. omnibase_infra/runtime/protocol_handler_discovery.py +221 -0
  707. omnibase_infra/runtime/protocol_handler_plugin_loader.py +327 -0
  708. omnibase_infra/runtime/protocol_lifecycle_executor.py +435 -0
  709. omnibase_infra/runtime/protocol_policy.py +366 -0
  710. omnibase_infra/runtime/protocols/__init__.py +37 -0
  711. omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -0
  712. omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
  713. omnibase_infra/runtime/registry/__init__.py +93 -0
  714. omnibase_infra/runtime/registry/mixin_message_type_query.py +326 -0
  715. omnibase_infra/runtime/registry/mixin_message_type_registration.py +354 -0
  716. omnibase_infra/runtime/registry/registry_event_bus_binding.py +268 -0
  717. omnibase_infra/runtime/registry/registry_message_type.py +542 -0
  718. omnibase_infra/runtime/registry/registry_protocol_binding.py +445 -0
  719. omnibase_infra/runtime/registry_compute.py +1143 -0
  720. omnibase_infra/runtime/registry_contract_source.py +693 -0
  721. omnibase_infra/runtime/registry_dispatcher.py +678 -0
  722. omnibase_infra/runtime/registry_policy.py +1185 -0
  723. omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
  724. omnibase_infra/runtime/runtime_scheduler.py +1070 -0
  725. omnibase_infra/runtime/secret_resolver.py +2112 -0
  726. omnibase_infra/runtime/security_metadata_validator.py +776 -0
  727. omnibase_infra/runtime/service_kernel.py +1651 -0
  728. omnibase_infra/runtime/service_message_dispatch_engine.py +2350 -0
  729. omnibase_infra/runtime/service_runtime_host_process.py +3493 -0
  730. omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
  731. omnibase_infra/runtime/transition_notification_publisher.py +765 -0
  732. omnibase_infra/runtime/util_container_wiring.py +1124 -0
  733. omnibase_infra/runtime/util_validation.py +314 -0
  734. omnibase_infra/runtime/util_version.py +98 -0
  735. omnibase_infra/runtime/util_wiring.py +723 -0
  736. omnibase_infra/schemas/schema_registration_projection.sql +320 -0
  737. omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
  738. omnibase_infra/services/__init__.py +89 -0
  739. omnibase_infra/services/corpus_capture.py +684 -0
  740. omnibase_infra/services/mcp/__init__.py +31 -0
  741. omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -0
  742. omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
  743. omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
  744. omnibase_infra/services/mcp/service_mcp_tool_sync.py +565 -0
  745. omnibase_infra/services/registry_api/__init__.py +40 -0
  746. omnibase_infra/services/registry_api/main.py +261 -0
  747. omnibase_infra/services/registry_api/models/__init__.py +66 -0
  748. omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
  749. omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
  750. omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
  751. omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
  752. omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
  753. omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
  754. omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
  755. omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
  756. omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
  757. omnibase_infra/services/registry_api/models/model_warning.py +49 -0
  758. omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
  759. omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
  760. omnibase_infra/services/registry_api/routes.py +371 -0
  761. omnibase_infra/services/registry_api/service.py +837 -0
  762. omnibase_infra/services/service_capability_query.py +945 -0
  763. omnibase_infra/services/service_health.py +898 -0
  764. omnibase_infra/services/service_node_selector.py +530 -0
  765. omnibase_infra/services/service_timeout_emitter.py +699 -0
  766. omnibase_infra/services/service_timeout_scanner.py +394 -0
  767. omnibase_infra/services/session/__init__.py +56 -0
  768. omnibase_infra/services/session/config_consumer.py +137 -0
  769. omnibase_infra/services/session/config_store.py +139 -0
  770. omnibase_infra/services/session/consumer.py +1007 -0
  771. omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
  772. omnibase_infra/services/session/store.py +997 -0
  773. omnibase_infra/services/snapshot/__init__.py +31 -0
  774. omnibase_infra/services/snapshot/service_snapshot.py +647 -0
  775. omnibase_infra/services/snapshot/store_inmemory.py +637 -0
  776. omnibase_infra/services/snapshot/store_postgres.py +1279 -0
  777. omnibase_infra/shared/__init__.py +8 -0
  778. omnibase_infra/testing/__init__.py +10 -0
  779. omnibase_infra/testing/utils.py +23 -0
  780. omnibase_infra/topics/__init__.py +45 -0
  781. omnibase_infra/topics/platform_topic_suffixes.py +140 -0
  782. omnibase_infra/topics/util_topic_composition.py +95 -0
  783. omnibase_infra/types/__init__.py +48 -0
  784. omnibase_infra/types/type_cache_info.py +49 -0
  785. omnibase_infra/types/type_dsn.py +173 -0
  786. omnibase_infra/types/type_infra_aliases.py +60 -0
  787. omnibase_infra/types/typed_dict/__init__.py +29 -0
  788. omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
  789. omnibase_infra/types/typed_dict/typed_dict_introspection_cache.py +128 -0
  790. omnibase_infra/types/typed_dict/typed_dict_performance_metrics_cache.py +140 -0
  791. omnibase_infra/types/typed_dict_capabilities.py +64 -0
  792. omnibase_infra/utils/__init__.py +117 -0
  793. omnibase_infra/utils/correlation.py +208 -0
  794. omnibase_infra/utils/util_atomic_file.py +261 -0
  795. omnibase_infra/utils/util_consumer_group.py +232 -0
  796. omnibase_infra/utils/util_datetime.py +372 -0
  797. omnibase_infra/utils/util_db_transaction.py +239 -0
  798. omnibase_infra/utils/util_dsn_validation.py +333 -0
  799. omnibase_infra/utils/util_env_parsing.py +264 -0
  800. omnibase_infra/utils/util_error_sanitization.py +457 -0
  801. omnibase_infra/utils/util_pydantic_validators.py +477 -0
  802. omnibase_infra/utils/util_retry_optimistic.py +281 -0
  803. omnibase_infra/utils/util_semver.py +233 -0
  804. omnibase_infra/validation/__init__.py +307 -0
  805. omnibase_infra/validation/contracts/security.validation.yaml +114 -0
  806. omnibase_infra/validation/enums/__init__.py +11 -0
  807. omnibase_infra/validation/enums/enum_contract_violation_severity.py +13 -0
  808. omnibase_infra/validation/infra_validators.py +1514 -0
  809. omnibase_infra/validation/linter_contract.py +907 -0
  810. omnibase_infra/validation/mixin_any_type_classification.py +120 -0
  811. omnibase_infra/validation/mixin_any_type_exemption.py +580 -0
  812. omnibase_infra/validation/mixin_any_type_reporting.py +106 -0
  813. omnibase_infra/validation/mixin_execution_shape_violation_checks.py +596 -0
  814. omnibase_infra/validation/mixin_node_archetype_detection.py +254 -0
  815. omnibase_infra/validation/models/__init__.py +15 -0
  816. omnibase_infra/validation/models/model_contract_lint_result.py +101 -0
  817. omnibase_infra/validation/models/model_contract_violation.py +41 -0
  818. omnibase_infra/validation/service_validation_aggregator.py +395 -0
  819. omnibase_infra/validation/validation_exemptions.yaml +2033 -0
  820. omnibase_infra/validation/validator_any_type.py +715 -0
  821. omnibase_infra/validation/validator_chain_propagation.py +839 -0
  822. omnibase_infra/validation/validator_execution_shape.py +465 -0
  823. omnibase_infra/validation/validator_localhandler.py +261 -0
  824. omnibase_infra/validation/validator_registration_security.py +410 -0
  825. omnibase_infra/validation/validator_routing_coverage.py +1020 -0
  826. omnibase_infra/validation/validator_runtime_shape.py +915 -0
  827. omnibase_infra/validation/validator_security.py +513 -0
  828. omnibase_infra/validation/validator_topic_category.py +1152 -0
  829. omnibase_infra-0.2.6.dist-info/METADATA +197 -0
  830. omnibase_infra-0.2.6.dist-info/RECORD +833 -0
  831. omnibase_infra-0.2.6.dist-info/WHEEL +4 -0
  832. omnibase_infra-0.2.6.dist-info/entry_points.txt +5 -0
  833. omnibase_infra-0.2.6.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,2046 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Handler Plugin Loader for Contract-Driven Discovery.
4
+
5
+ This module provides HandlerPluginLoader, which discovers handler contracts
6
+ from the filesystem, validates handlers against protocols, and creates
7
+ ModelLoadedHandler instances for runtime registration.
8
+
9
+ Part of OMN-1132: Handler Plugin Loader implementation.
10
+
11
+ The loader implements ProtocolHandlerPluginLoader and supports:
12
+ - Single contract loading from a specific path
13
+ - Directory-based discovery with recursive scanning
14
+ - Glob pattern-based discovery for flexible matching
15
+
16
+ Error Codes:
17
+ This module uses structured error codes from ``EnumHandlerLoaderError`` for
18
+ precise error classification. Error codes are organized by category:
19
+
20
+ **File-Level Errors (HANDLER_LOADER_001 - HANDLER_LOADER_009)**:
21
+ - ``HANDLER_LOADER_001`` (FILE_NOT_FOUND): Contract file path does not exist
22
+ - ``HANDLER_LOADER_002`` (INVALID_YAML_SYNTAX): Contract file has invalid YAML
23
+ - ``HANDLER_LOADER_003`` (SCHEMA_VALIDATION_FAILED): Contract fails Pydantic validation
24
+ - ``HANDLER_LOADER_004`` (MISSING_REQUIRED_FIELDS): Required contract fields missing
25
+ - ``HANDLER_LOADER_005`` (FILE_SIZE_EXCEEDED): Contract exceeds 10MB size limit
26
+ - ``HANDLER_LOADER_006`` (PROTOCOL_NOT_IMPLEMENTED): Handler missing protocol methods
27
+ - ``HANDLER_LOADER_007`` (NOT_A_FILE): Path exists but is not a regular file
28
+ - ``HANDLER_LOADER_008`` (FILE_READ_ERROR): Failed to read contract file (I/O)
29
+ - ``HANDLER_LOADER_009`` (FILE_STAT_ERROR): Failed to stat contract file (I/O)
30
+
31
+ **Import Errors (HANDLER_LOADER_010 - HANDLER_LOADER_013)**:
32
+ - ``HANDLER_LOADER_010`` (MODULE_NOT_FOUND): Handler module not found
33
+ - ``HANDLER_LOADER_011`` (CLASS_NOT_FOUND): Handler class not found in module
34
+ - ``HANDLER_LOADER_012`` (IMPORT_ERROR): Import error (syntax/dependency)
35
+ - ``HANDLER_LOADER_013`` (NAMESPACE_NOT_ALLOWED): Handler module namespace not allowed
36
+
37
+ **Directory Errors (HANDLER_LOADER_020 - HANDLER_LOADER_022)**:
38
+ - ``HANDLER_LOADER_020`` (DIRECTORY_NOT_FOUND): Directory does not exist
39
+ - ``HANDLER_LOADER_021`` (PERMISSION_DENIED): Permission denied accessing directory
40
+ - ``HANDLER_LOADER_022`` (NOT_A_DIRECTORY): Path exists but is not a directory
41
+
42
+ **Pattern Errors (HANDLER_LOADER_030 - HANDLER_LOADER_031)**:
43
+ - ``HANDLER_LOADER_030`` (EMPTY_PATTERNS_LIST): Patterns list cannot be empty
44
+ - ``HANDLER_LOADER_031`` (INVALID_GLOB_PATTERN): Invalid glob pattern syntax
45
+ (logged only, not raised - pattern is skipped and discovery continues)
46
+
47
+ **Configuration Errors (HANDLER_LOADER_040)**:
48
+ - ``HANDLER_LOADER_040`` (AMBIGUOUS_CONTRACT_CONFIGURATION): Both contract types
49
+ exist in the same directory
50
+
51
+ Error codes are accessible via ``error.model.context.get("loader_error")`` on
52
+ raised exceptions. Note: HANDLER_LOADER_031 is logged but not raised as an
53
+ exception to allow graceful continuation during discovery operations.
54
+
55
+ Concurrency Notes:
56
+ The loader is stateless and reentrant - each load operation is independent:
57
+
58
+ - No instance state is stored after ``__init__`` (empty constructor)
59
+ - All method variables are local to each call
60
+ - ``importlib.import_module()`` is thread-safe in CPython (uses GIL and import lock)
61
+ - File operations use independent file handles per call
62
+
63
+ **Thread Safety Guarantees**:
64
+ - Multiple threads can safely call any loader method concurrently
65
+ - Concurrent imports of the SAME module are serialized by Python's import lock
66
+ - Repeated loads of the same handler are idempotent (cached by Python)
67
+
68
+ **Thread Safety Limitations**:
69
+ - The loader does NOT provide transactional semantics across multiple calls
70
+ - If contracts change on disk during concurrent loading, results may be inconsistent
71
+ - The ``discover_and_load()`` method's default ``Path.cwd()`` behavior is
72
+ process-global; if cwd changes between calls, results will differ
73
+
74
+ Working Directory Dependency:
75
+ The ``discover_and_load()`` method uses ``Path.cwd()`` by default,
76
+ which reads process-level state. For deterministic behavior when cwd
77
+ may change between calls, provide an explicit ``base_path`` parameter.
78
+
79
+ See Also:
80
+ - ProtocolHandlerPluginLoader: Protocol definition for plugin loaders
81
+ - HandlerContractSource: Contract discovery and parsing
82
+ - ModelLoadedHandler: Model representing loaded handler metadata
83
+ - EnumHandlerLoaderError: Structured error codes for loader operations
84
+
85
+ Security Considerations:
86
+ This loader dynamically imports Python classes specified in YAML contracts.
87
+ Contract files should be treated as code and protected accordingly:
88
+ - Only load contracts from trusted sources
89
+ - Validate contract file permissions in production environments
90
+ - Be aware that module side effects execute during import
91
+ - Use the ``allowed_namespaces`` parameter to restrict imports to trusted packages
92
+
93
+ Namespace Allowlisting:
94
+ The loader supports namespace-based import restrictions via the
95
+ ``allowed_namespaces`` parameter. When configured, only handler modules
96
+ whose fully-qualified path starts with one of the allowed namespace
97
+ prefixes will be imported. This provides defense-in-depth against
98
+ malicious contract files attempting to load untrusted code.
99
+
100
+ Example:
101
+ >>> loader = HandlerPluginLoader(
102
+ ... allowed_namespaces=["omnibase_infra.", "omnibase_core.", "mycompany."]
103
+ ... )
104
+ >>> # This would succeed:
105
+ >>> loader.load_from_contract(Path("contract.yaml")) # handler_class: omnibase_infra.handlers.HandlerAuth
106
+ >>> # This would fail with NAMESPACE_NOT_ALLOWED:
107
+ >>> loader.load_from_contract(Path("malicious.yaml")) # handler_class: malicious_pkg.EvilHandler
108
+
109
+ **Error Message Sanitization**:
110
+ Error messages are designed to be safe for end users and prevent
111
+ information disclosure:
112
+
113
+ - User-facing error messages use filename only (not full filesystem paths)
114
+ - System exception details are sanitized to prevent path disclosure
115
+ - Full paths are stored in error context for internal debugging only
116
+ - Correlation IDs enable tracing without exposing sensitive information
117
+ - Exception messages from underlying libraries are sanitized before inclusion
118
+
119
+ The ``_sanitize_exception_message()`` helper strips filesystem paths from
120
+ exception messages while preserving useful diagnostic information like
121
+ line numbers and error types.
122
+
123
+ .. versionadded:: 0.7.0
124
+ Created as part of OMN-1132 handler plugin loader implementation.
125
+ """
126
+
127
+ from __future__ import annotations
128
+
129
+ import importlib
130
+ import logging
131
+ import re
132
+ import time
133
+ from datetime import UTC, datetime
134
+ from pathlib import Path
135
+ from uuid import UUID, uuid4
136
+
137
+ import yaml
138
+ from pydantic import ValidationError
139
+
140
+ from omnibase_infra.enums import EnumHandlerLoaderError, EnumInfraTransportType
141
+ from omnibase_infra.errors import InfraConnectionError, ProtocolConfigurationError
142
+ from omnibase_infra.models.errors import ModelInfraErrorContext
143
+ from omnibase_infra.models.runtime import (
144
+ ModelFailedPluginLoad,
145
+ ModelHandlerContract,
146
+ ModelLoadedHandler,
147
+ ModelPluginLoadContext,
148
+ ModelPluginLoadSummary,
149
+ )
150
+ from omnibase_infra.runtime.protocol_handler_plugin_loader import (
151
+ ProtocolHandlerPluginLoader,
152
+ )
153
+
154
+ logger = logging.getLogger(__name__)
155
+
156
+ # Regex pattern for detecting filesystem paths in error messages
157
+ # Matches Unix paths (/path/to/file) and Windows paths (C:\path\to\file)
158
+ _PATH_PATTERN = re.compile(
159
+ r"""
160
+ (?: # Non-capturing group for path types
161
+ /(?:[\w.-]+/)+[\w.-]+ # Unix absolute path: /path/to/file
162
+ |
163
+ [A-Za-z]:\\(?:[\w.-]+\\)*[\w.-]+ # Windows path: C:\path\to\file
164
+ |
165
+ \.\.?/(?:[\w.-]+/)*[\w.-]* # Relative path: ./path or ../path
166
+ )
167
+ """,
168
+ re.VERBOSE,
169
+ )
170
+
171
+
172
+ def _sanitize_exception_message(exception: BaseException) -> str:
173
+ """Sanitize exception message to prevent information disclosure.
174
+
175
+ Removes or masks filesystem paths from exception messages to prevent
176
+ exposing internal directory structures in user-facing error messages.
177
+ Preserves useful diagnostic information like line numbers and error types.
178
+
179
+ Args:
180
+ exception: The exception whose message should be sanitized.
181
+
182
+ Returns:
183
+ A sanitized version of the exception message with paths removed.
184
+
185
+ Example:
186
+ >>> e = OSError("[Errno 13] Permission denied: '/etc/secrets/key.pem'")
187
+ >>> _sanitize_exception_message(e)
188
+ "[Errno 13] Permission denied: '<path>'"
189
+
190
+ >>> e = yaml.YAMLError("expected ... in '/home/user/config.yaml', line 10")
191
+ >>> _sanitize_exception_message(e)
192
+ "expected ... in '<path>', line 10"
193
+ """
194
+ message = str(exception)
195
+
196
+ # Replace filesystem paths with <path> placeholder
197
+ sanitized = _PATH_PATTERN.sub("<path>", message)
198
+
199
+ # Also handle quoted paths that might have been missed
200
+ # Pattern: 'path/to/file' or "path/to/file"
201
+ sanitized = re.sub(r"['\"](?:[^'\"]*[/\\][^'\"]+)['\"]", "'<path>'", sanitized)
202
+
203
+ return sanitized
204
+
205
+
206
+ # File pattern for handler contracts
207
+ HANDLER_CONTRACT_FILENAME = "handler_contract.yaml"
208
+ CONTRACT_YAML_FILENAME = "contract.yaml"
209
+
210
+ # Maximum contract file size (10MB) to prevent memory exhaustion
211
+ MAX_CONTRACT_SIZE = 10 * 1024 * 1024
212
+
213
+ # ---------------------------------------------------------------------------
214
+ # Correlation ID Design Decision: UUID Type
215
+ # ---------------------------------------------------------------------------
216
+ # The correlation_id parameter is typed as `UUID | None` to comply with ONEX
217
+ # standards requiring typed models rather than primitives. This aligns with:
218
+ #
219
+ # 1. **ONEX Protocol Conventions**: All other protocols in the codebase use
220
+ # `correlation_id: UUID | None` (see ProtocolIdempotencyStore,
221
+ # ProtocolServiceDiscoveryHandler, etc.).
222
+ #
223
+ # 2. **Type Safety**: UUID type ensures valid correlation IDs at compile time.
224
+ #
225
+ # 3. **Auto-Generation Pattern**: Methods auto-generate correlation IDs via
226
+ # `uuid4()` when not provided, ensuring all operations are traceable.
227
+ #
228
+ # For external system compatibility (OpenTelemetry, Zipkin, etc.), convert
229
+ # string-based correlation IDs to UUID at the call boundary, or use
230
+ # uuid.UUID(external_id) if the external ID is UUID-compatible.
231
+ #
232
+ # See: ONEX correlation ID conventions in omnibase_core.
233
+ # ---------------------------------------------------------------------------
234
+
235
+
236
+ class HandlerPluginLoader(ProtocolHandlerPluginLoader):
237
+ """Load handlers as plugins from contracts.
238
+
239
+ Discovers handler contracts, validates handlers against protocols,
240
+ and registers them with the handler registry.
241
+
242
+ This class implements ProtocolHandlerPluginLoader by scanning filesystem
243
+ paths for handler_contract.yaml or contract.yaml files, parsing them,
244
+ dynamically importing the handler classes, and creating ModelLoadedHandler
245
+ instances.
246
+
247
+ Protocol Compliance:
248
+ This class explicitly implements ProtocolHandlerPluginLoader and provides
249
+ all required methods: load_from_contract(), load_from_directory(), and
250
+ discover_and_load(). Protocol compliance is verified via duck typing.
251
+
252
+ Example:
253
+ >>> # Load a single handler from contract
254
+ >>> loader = HandlerPluginLoader()
255
+ >>> handler = loader.load_from_contract(
256
+ ... Path("src/handlers/auth/handler_contract.yaml")
257
+ ... )
258
+ >>> print(f"Loaded: {handler.handler_name}")
259
+
260
+ >>> # Load all handlers from a directory
261
+ >>> handlers = loader.load_from_directory(Path("src/handlers"))
262
+ >>> print(f"Loaded {len(handlers)} handlers")
263
+
264
+ >>> # Discover with glob patterns
265
+ >>> handlers = loader.discover_and_load([
266
+ ... "src/**/handler_contract.yaml",
267
+ ... "plugins/**/contract.yaml",
268
+ ... ])
269
+
270
+ .. versionadded:: 0.7.0
271
+ Created as part of OMN-1132 handler plugin loader implementation.
272
+ """
273
+
274
+ def __init__(self, allowed_namespaces: list[str] | None = None) -> None:
275
+ """Initialize the handler plugin loader.
276
+
277
+ Args:
278
+ allowed_namespaces: Optional list of allowed namespace prefixes for
279
+ handler module imports. When provided, only handler modules whose
280
+ fully-qualified class path starts with one of these prefixes will
281
+ be loaded. This provides defense-in-depth security by restricting
282
+ which packages can be dynamically imported.
283
+
284
+ Each prefix should end with a period for explicit package boundary
285
+ matching (e.g., "omnibase_infra." not "omnibase_infra"). Prefixes
286
+ without a trailing period are validated at package boundaries to
287
+ prevent unintended matches (e.g., "omnibase" matches "omnibase" or
288
+ "omnibase.handlers" but NOT "omnibase_other").
289
+
290
+ If None (default), no namespace restriction is applied and any
291
+ importable module can be loaded.
292
+
293
+ If an empty list is provided, NO namespaces are allowed, effectively
294
+ blocking all handler imports.
295
+
296
+ Example:
297
+ >>> # Restrict to trusted packages
298
+ >>> loader = HandlerPluginLoader(
299
+ ... allowed_namespaces=["omnibase_infra.", "omnibase_core.", "mycompany.handlers."]
300
+ ... )
301
+ >>>
302
+ >>> # No restriction (default)
303
+ >>> loader = HandlerPluginLoader()
304
+ >>>
305
+ >>> # Block all imports (empty list)
306
+ >>> loader = HandlerPluginLoader(allowed_namespaces=[])
307
+
308
+ Security Note:
309
+ Namespace validation occurs BEFORE ``importlib.import_module()`` is
310
+ called, preventing any module-level side effects from untrusted packages.
311
+ """
312
+ self._allowed_namespaces: list[str] | None = allowed_namespaces
313
+
314
+ # Security best practice: warn if no namespace restriction is configured
315
+ if allowed_namespaces is None:
316
+ logger.info(
317
+ "HandlerPluginLoader initialized without namespace restrictions. "
318
+ "For production environments, consider setting allowed_namespaces to "
319
+ "restrict handler imports to trusted packages (e.g., "
320
+ "allowed_namespaces=['omnibase_infra.', 'omnibase_core.', 'mycompany.']).",
321
+ )
322
+ # Warn if empty list is provided - this blocks ALL handler imports
323
+ elif len(allowed_namespaces) == 0:
324
+ logger.warning(
325
+ "HandlerPluginLoader initialized with empty allowed_namespaces list. "
326
+ "This will block ALL handler imports. If this is intentional, ignore "
327
+ "this warning. Otherwise, set allowed_namespaces=None to allow all "
328
+ "namespaces or provide a list of allowed namespace prefixes.",
329
+ )
330
+
331
+ def _validate_correlation_id(
332
+ self,
333
+ correlation_id: UUID | None,
334
+ operation: str,
335
+ ) -> None:
336
+ """Validate correlation_id is a UUID or None at runtime.
337
+
338
+ Provides runtime type validation for correlation_id parameters at public
339
+ API entry points. While type hints provide static checking, runtime
340
+ validation catches cases where callers bypass type checking (e.g.,
341
+ dynamically constructed calls, JSON deserialization without validation).
342
+
343
+ Args:
344
+ correlation_id: The correlation ID to validate. Must be UUID or None.
345
+ operation: Name of the calling operation (for error context).
346
+
347
+ Raises:
348
+ ProtocolConfigurationError: If correlation_id is not a UUID instance
349
+ or None. The error message includes the actual type received and
350
+ the operation name for debugging, along with guidance on how to
351
+ convert string IDs to UUID.
352
+
353
+ Example:
354
+ >>> loader = HandlerPluginLoader()
355
+ >>> loader._validate_correlation_id(UUID("..."), "load_from_contract") # OK
356
+ >>> loader._validate_correlation_id(None, "load_from_contract") # OK
357
+ >>> loader._validate_correlation_id("not-a-uuid", "load_from_contract")
358
+ ProtocolConfigurationError: correlation_id must be UUID or None...
359
+ """
360
+ if correlation_id is not None and not isinstance(correlation_id, UUID):
361
+ context = ModelInfraErrorContext(
362
+ transport_type=EnumInfraTransportType.RUNTIME,
363
+ operation=operation,
364
+ correlation_id=None, # Cannot use invalid correlation_id in context
365
+ )
366
+ raise ProtocolConfigurationError(
367
+ f"correlation_id must be UUID or None, got {type(correlation_id).__name__} "
368
+ f"in {operation}(). Convert string IDs using uuid.UUID(your_string).",
369
+ context=context,
370
+ loader_error="INVALID_CORRELATION_ID_TYPE",
371
+ received_type=type(correlation_id).__name__,
372
+ )
373
+
374
+ def load_from_contract(
375
+ self,
376
+ contract_path: Path,
377
+ correlation_id: UUID | None = None,
378
+ ) -> ModelLoadedHandler:
379
+ """Load a single handler from a contract file.
380
+
381
+ Parses the contract YAML file at the given path, validates it,
382
+ imports the handler class, validates protocol compliance, and
383
+ returns a ModelLoadedHandler with the loaded metadata.
384
+
385
+ Args:
386
+ contract_path: Path to the handler contract YAML file.
387
+ Must be an absolute or relative path to an existing file.
388
+ correlation_id: Optional correlation ID for tracing and error context.
389
+ If not provided, a new UUID4 is auto-generated to ensure all
390
+ operations have traceable correlation IDs.
391
+
392
+ Returns:
393
+ ModelLoadedHandler containing the loaded handler metadata
394
+ including handler class, version, and contract information.
395
+
396
+ Raises:
397
+ ProtocolConfigurationError: If the contract file is invalid,
398
+ missing required fields, or fails validation. Error codes:
399
+ - HANDLER_LOADER_001: Contract file not found (path doesn't exist)
400
+ - HANDLER_LOADER_002: Invalid YAML syntax
401
+ - HANDLER_LOADER_003: Schema validation failed
402
+ - HANDLER_LOADER_004: Missing required fields
403
+ - HANDLER_LOADER_005: Contract file exceeds size limit
404
+ - HANDLER_LOADER_006: Handler does not implement protocol
405
+ - HANDLER_LOADER_007: Path exists but is not a file (e.g., directory)
406
+ - HANDLER_LOADER_008: Failed to read contract file (I/O error)
407
+ - HANDLER_LOADER_009: Failed to stat contract file (I/O error)
408
+ ProtocolConfigurationError: If namespace validation fails (when
409
+ allowed_namespaces is configured).
410
+ - HANDLER_LOADER_013: Namespace not allowed
411
+ InfraConnectionError: If the handler class cannot be imported.
412
+ Error codes:
413
+ - HANDLER_LOADER_010: Module not found
414
+ - HANDLER_LOADER_011: Class not found in module
415
+ - HANDLER_LOADER_012: Import error (syntax/dependency)
416
+ ProtocolConfigurationError: If correlation_id is not a UUID or None.
417
+ Error code: INVALID_CORRELATION_ID_TYPE
418
+ """
419
+ # Validate correlation_id type at entry point (runtime type check)
420
+ self._validate_correlation_id(correlation_id, "load_from_contract")
421
+
422
+ # Auto-generate correlation_id if not provided (per ONEX guidelines)
423
+ correlation_id = correlation_id or uuid4()
424
+
425
+ # Convert UUID to string for logging and error context
426
+ correlation_id_str = str(correlation_id)
427
+
428
+ # Start timing for performance observability
429
+ start_time = time.perf_counter()
430
+
431
+ logger.debug(
432
+ "Loading handler from contract: %s",
433
+ contract_path,
434
+ extra={
435
+ "contract_path": str(contract_path),
436
+ "correlation_id": correlation_id_str,
437
+ },
438
+ )
439
+
440
+ # Validate contract path exists
441
+ # contract_path.exists() and contract_path.is_file() can raise OSError for:
442
+ # - Permission denied when accessing the path
443
+ # - Filesystem errors (unmounted volumes, network failures)
444
+ # - Broken symlinks where the target cannot be resolved
445
+ try:
446
+ path_exists = contract_path.exists()
447
+ except OSError as e:
448
+ context = ModelInfraErrorContext(
449
+ transport_type=EnumInfraTransportType.RUNTIME,
450
+ operation="load_from_contract",
451
+ correlation_id=correlation_id,
452
+ )
453
+ sanitized_msg = _sanitize_exception_message(e)
454
+ raise ProtocolConfigurationError(
455
+ f"Failed to access contract path: {sanitized_msg}",
456
+ context=context,
457
+ loader_error=EnumHandlerLoaderError.FILE_STAT_ERROR.value,
458
+ contract_path=str(contract_path),
459
+ ) from e
460
+
461
+ if not path_exists:
462
+ context = ModelInfraErrorContext(
463
+ transport_type=EnumInfraTransportType.RUNTIME,
464
+ operation="load_from_contract",
465
+ correlation_id=correlation_id,
466
+ )
467
+ raise ProtocolConfigurationError(
468
+ f"Contract file not found: {contract_path.name}",
469
+ context=context,
470
+ loader_error=EnumHandlerLoaderError.FILE_NOT_FOUND.value,
471
+ contract_path=str(contract_path),
472
+ )
473
+
474
+ try:
475
+ is_file = contract_path.is_file()
476
+ except OSError as e:
477
+ context = ModelInfraErrorContext(
478
+ transport_type=EnumInfraTransportType.RUNTIME,
479
+ operation="load_from_contract",
480
+ correlation_id=correlation_id,
481
+ )
482
+ sanitized_msg = _sanitize_exception_message(e)
483
+ raise ProtocolConfigurationError(
484
+ f"Failed to access contract path: {sanitized_msg}",
485
+ context=context,
486
+ loader_error=EnumHandlerLoaderError.FILE_STAT_ERROR.value,
487
+ contract_path=str(contract_path),
488
+ ) from e
489
+
490
+ if not is_file:
491
+ context = ModelInfraErrorContext(
492
+ transport_type=EnumInfraTransportType.RUNTIME,
493
+ operation="load_from_contract",
494
+ correlation_id=correlation_id,
495
+ )
496
+ raise ProtocolConfigurationError(
497
+ f"Contract path is not a file: {contract_path.name}",
498
+ context=context,
499
+ loader_error=EnumHandlerLoaderError.NOT_A_FILE.value,
500
+ contract_path=str(contract_path),
501
+ )
502
+
503
+ # Validate file size (raises ProtocolConfigurationError on failure)
504
+ self._validate_file_size(
505
+ contract_path,
506
+ correlation_id=correlation_id,
507
+ operation="load_from_contract",
508
+ raise_on_error=True,
509
+ )
510
+
511
+ # Parse YAML contract
512
+ try:
513
+ with contract_path.open("r", encoding="utf-8") as f:
514
+ raw_data = yaml.safe_load(f)
515
+ except yaml.YAMLError as e:
516
+ context = ModelInfraErrorContext(
517
+ transport_type=EnumInfraTransportType.RUNTIME,
518
+ operation="load_from_contract",
519
+ correlation_id=correlation_id,
520
+ )
521
+ # Sanitize exception message to prevent path disclosure
522
+ sanitized_msg = _sanitize_exception_message(e)
523
+ raise ProtocolConfigurationError(
524
+ f"Invalid YAML syntax in contract: {sanitized_msg}",
525
+ context=context,
526
+ loader_error=EnumHandlerLoaderError.INVALID_YAML_SYNTAX.value,
527
+ contract_path=str(contract_path),
528
+ ) from e
529
+ except OSError as e:
530
+ context = ModelInfraErrorContext(
531
+ transport_type=EnumInfraTransportType.RUNTIME,
532
+ operation="load_from_contract",
533
+ correlation_id=correlation_id,
534
+ )
535
+ # Sanitize exception message to prevent path disclosure
536
+ sanitized_msg = _sanitize_exception_message(e)
537
+ raise ProtocolConfigurationError(
538
+ f"Failed to read contract file: {sanitized_msg}",
539
+ context=context,
540
+ loader_error=EnumHandlerLoaderError.FILE_READ_ERROR.value,
541
+ contract_path=str(contract_path),
542
+ ) from e
543
+
544
+ if raw_data is None:
545
+ context = ModelInfraErrorContext(
546
+ transport_type=EnumInfraTransportType.RUNTIME,
547
+ operation="load_from_contract",
548
+ correlation_id=correlation_id,
549
+ )
550
+ raise ProtocolConfigurationError(
551
+ "Contract file is empty",
552
+ context=context,
553
+ loader_error=EnumHandlerLoaderError.SCHEMA_VALIDATION_FAILED.value,
554
+ contract_path=str(contract_path),
555
+ )
556
+
557
+ # Validate contract using Pydantic model
558
+ try:
559
+ contract = ModelHandlerContract.model_validate(raw_data)
560
+ except ValidationError as e:
561
+ context = ModelInfraErrorContext(
562
+ transport_type=EnumInfraTransportType.RUNTIME,
563
+ operation="load_from_contract",
564
+ correlation_id=correlation_id,
565
+ )
566
+ # Convert validation errors to readable message
567
+ error_details = "; ".join(
568
+ f"{'.'.join(str(loc) for loc in err['loc'])}: {err['msg']}"
569
+ for err in e.errors()
570
+ )
571
+ raise ProtocolConfigurationError(
572
+ f"Contract validation failed: {error_details}",
573
+ context=context,
574
+ loader_error=EnumHandlerLoaderError.SCHEMA_VALIDATION_FAILED.value,
575
+ contract_path=str(contract_path),
576
+ validation_errors=[
577
+ {"loc": err["loc"], "msg": err["msg"], "type": err["type"]}
578
+ for err in e.errors()
579
+ ],
580
+ ) from e
581
+
582
+ handler_name = contract.handler_name
583
+ handler_class_path = contract.handler_class
584
+ handler_type = contract.handler_type
585
+ capability_tags = contract.capability_tags
586
+ # protocol_type is the registry key (e.g., "db", "http")
587
+ # The model_validator in ModelHandlerContract sets this from handler_name
588
+ # if not explicitly provided (strips "handler-" prefix)
589
+ protocol_type = contract.protocol_type
590
+ # Should never be None after model_validator, but assert for type safety
591
+ assert protocol_type is not None, (
592
+ "protocol_type should be set by model_validator"
593
+ )
594
+
595
+ # Import and validate handler class
596
+ handler_class = self._import_handler_class(
597
+ handler_class_path, contract_path, correlation_id
598
+ )
599
+
600
+ # Validate handler implements protocol
601
+ is_valid, missing_methods = self._validate_handler_protocol(handler_class)
602
+ if not is_valid:
603
+ context = ModelInfraErrorContext(
604
+ transport_type=EnumInfraTransportType.RUNTIME,
605
+ operation="load_from_contract",
606
+ correlation_id=correlation_id,
607
+ )
608
+ missing_str = ", ".join(missing_methods)
609
+ raise ProtocolConfigurationError(
610
+ f"Handler class {handler_class_path} is missing required "
611
+ f"ProtocolHandler methods: {missing_str}",
612
+ context=context,
613
+ loader_error=EnumHandlerLoaderError.PROTOCOL_NOT_IMPLEMENTED.value,
614
+ contract_path=str(contract_path),
615
+ handler_class=handler_class_path,
616
+ missing_methods=missing_methods,
617
+ )
618
+
619
+ # Resolve the contract path, handling potential filesystem errors
620
+ # path.resolve() can raise OSError for:
621
+ # - Broken symlinks: symlink target no longer exists
622
+ # - Race conditions: file deleted between validation and resolution
623
+ # - Permission issues: lacking read permission on parent directories
624
+ # - Filesystem errors: unmounted volumes, network filesystem failures
625
+ try:
626
+ resolved_contract_path = contract_path.resolve()
627
+ except OSError as e:
628
+ context = ModelInfraErrorContext(
629
+ transport_type=EnumInfraTransportType.RUNTIME,
630
+ operation="load_from_contract",
631
+ correlation_id=correlation_id,
632
+ )
633
+ sanitized_msg = _sanitize_exception_message(e)
634
+ # FILE_STAT_ERROR is used here because path resolution involves
635
+ # filesystem metadata access similar to stat operations
636
+ raise ProtocolConfigurationError(
637
+ f"Failed to access contract file during path resolution: {sanitized_msg}",
638
+ context=context,
639
+ loader_error=EnumHandlerLoaderError.FILE_STAT_ERROR.value,
640
+ contract_path=str(contract_path),
641
+ ) from e
642
+
643
+ # Calculate load duration for performance observability
644
+ load_duration_ms = (time.perf_counter() - start_time) * 1000
645
+
646
+ logger.info(
647
+ "Successfully loaded handler from contract: %s -> %s (%.2fms)",
648
+ handler_name,
649
+ handler_class_path,
650
+ load_duration_ms,
651
+ extra={
652
+ "handler_name": handler_name,
653
+ "handler_class": handler_class_path,
654
+ "handler_type": handler_type.value,
655
+ "protocol_type": protocol_type,
656
+ "contract_path": str(resolved_contract_path),
657
+ "correlation_id": correlation_id_str,
658
+ "load_duration_ms": load_duration_ms,
659
+ },
660
+ )
661
+
662
+ # contract.handler_version is guaranteed non-None by model_validator
663
+ if contract.handler_version is None:
664
+ context = ModelInfraErrorContext.with_correlation(
665
+ correlation_id=correlation_id,
666
+ transport_type=EnumInfraTransportType.RUNTIME,
667
+ operation="load_from_contract",
668
+ )
669
+ raise ProtocolConfigurationError(
670
+ "handler_version should be set by model_validator",
671
+ context=context,
672
+ loader_error=EnumHandlerLoaderError.MISSING_REQUIRED_FIELDS.value,
673
+ contract_path=str(contract_path),
674
+ )
675
+
676
+ return ModelLoadedHandler(
677
+ handler_name=handler_name,
678
+ protocol_type=protocol_type,
679
+ handler_type=handler_type,
680
+ handler_class=handler_class_path,
681
+ contract_path=resolved_contract_path,
682
+ capability_tags=capability_tags,
683
+ loaded_at=datetime.now(UTC),
684
+ handler_version=contract.handler_version,
685
+ )
686
+
687
+ def load_from_directory(
688
+ self,
689
+ directory: Path,
690
+ correlation_id: UUID | None = None,
691
+ max_handlers: int | None = None,
692
+ ) -> list[ModelLoadedHandler]:
693
+ """Load all handlers from contract files in a directory.
694
+
695
+ Recursively scans the given directory for handler contract files
696
+ (handler_contract.yaml or contract.yaml), loads each handler,
697
+ and returns a list of successfully loaded handlers.
698
+
699
+ Failed loads are logged but do not stop processing of other handlers.
700
+ A summary is logged at the end of the operation for observability.
701
+
702
+ Args:
703
+ directory: Path to the directory to scan for contract files.
704
+ Must be an existing directory.
705
+ correlation_id: Optional correlation ID for tracing and error context.
706
+ If not provided, a new UUID4 is auto-generated to ensure all
707
+ operations have traceable correlation IDs. The same correlation_id
708
+ is propagated to all contract loads within the directory scan.
709
+ max_handlers: Optional maximum number of handlers to discover and load.
710
+ If specified, discovery stops after finding this many contract files.
711
+ A warning is logged when the limit is reached. Set to None (default)
712
+ for unlimited discovery. This prevents runaway resource usage when
713
+ scanning directories with unexpectedly large numbers of handlers.
714
+
715
+ Returns:
716
+ List of successfully loaded handlers. May be empty if no
717
+ contracts are found or all fail validation.
718
+
719
+ Raises:
720
+ ProtocolConfigurationError: If the directory does not exist
721
+ or is not accessible. Error codes:
722
+ - HANDLER_LOADER_020: Directory not found
723
+ - HANDLER_LOADER_021: Permission denied
724
+ - HANDLER_LOADER_022: Not a directory
725
+ ProtocolConfigurationError: If correlation_id is not a UUID or None.
726
+ Error code: INVALID_CORRELATION_ID_TYPE
727
+ """
728
+ # Validate correlation_id type at entry point (runtime type check)
729
+ self._validate_correlation_id(correlation_id, "load_from_directory")
730
+
731
+ # Auto-generate correlation_id if not provided (per ONEX guidelines)
732
+ correlation_id = correlation_id or uuid4()
733
+
734
+ # Start timing for observability
735
+ start_time = time.perf_counter()
736
+
737
+ logger.debug(
738
+ "Loading handlers from directory: %s",
739
+ directory,
740
+ extra={
741
+ "directory": str(directory),
742
+ "correlation_id": str(correlation_id),
743
+ "max_handlers": max_handlers,
744
+ },
745
+ )
746
+
747
+ # Validate directory exists
748
+ # directory.exists() and directory.is_dir() can raise OSError for:
749
+ # - Permission denied when accessing the path
750
+ # - Filesystem errors (unmounted volumes, network failures)
751
+ # - Broken symlinks where the target cannot be resolved
752
+ try:
753
+ dir_exists = directory.exists()
754
+ except OSError as e:
755
+ context = ModelInfraErrorContext(
756
+ transport_type=EnumInfraTransportType.RUNTIME,
757
+ operation="load_from_directory",
758
+ correlation_id=correlation_id,
759
+ )
760
+ sanitized_msg = _sanitize_exception_message(e)
761
+ raise ProtocolConfigurationError(
762
+ f"Failed to access directory: {sanitized_msg}",
763
+ context=context,
764
+ loader_error=EnumHandlerLoaderError.PERMISSION_DENIED.value,
765
+ directory=str(directory),
766
+ ) from e
767
+
768
+ if not dir_exists:
769
+ context = ModelInfraErrorContext(
770
+ transport_type=EnumInfraTransportType.RUNTIME,
771
+ operation="load_from_directory",
772
+ correlation_id=correlation_id,
773
+ )
774
+ raise ProtocolConfigurationError(
775
+ f"Directory not found: {directory.name}",
776
+ context=context,
777
+ loader_error=EnumHandlerLoaderError.DIRECTORY_NOT_FOUND.value,
778
+ directory=str(directory),
779
+ )
780
+
781
+ try:
782
+ is_directory = directory.is_dir()
783
+ except OSError as e:
784
+ context = ModelInfraErrorContext(
785
+ transport_type=EnumInfraTransportType.RUNTIME,
786
+ operation="load_from_directory",
787
+ correlation_id=correlation_id,
788
+ )
789
+ sanitized_msg = _sanitize_exception_message(e)
790
+ raise ProtocolConfigurationError(
791
+ f"Failed to access directory: {sanitized_msg}",
792
+ context=context,
793
+ loader_error=EnumHandlerLoaderError.PERMISSION_DENIED.value,
794
+ directory=str(directory),
795
+ ) from e
796
+
797
+ if not is_directory:
798
+ context = ModelInfraErrorContext(
799
+ transport_type=EnumInfraTransportType.RUNTIME,
800
+ operation="load_from_directory",
801
+ correlation_id=correlation_id,
802
+ )
803
+ raise ProtocolConfigurationError(
804
+ f"Path is not a directory: {directory.name}",
805
+ context=context,
806
+ loader_error=EnumHandlerLoaderError.NOT_A_DIRECTORY.value,
807
+ directory=str(directory),
808
+ )
809
+
810
+ # Find all contract files (with optional limit)
811
+ contract_files = self._find_contract_files(
812
+ directory, correlation_id, max_handlers=max_handlers
813
+ )
814
+
815
+ logger.debug(
816
+ "Found %d contract files in directory: %s",
817
+ len(contract_files),
818
+ directory,
819
+ extra={
820
+ "directory": str(directory),
821
+ "contract_count": len(contract_files),
822
+ "correlation_id": str(correlation_id),
823
+ },
824
+ )
825
+
826
+ # Load each contract (graceful mode - continue on errors)
827
+ handlers: list[ModelLoadedHandler] = []
828
+ failed_handlers: list[ModelFailedPluginLoad] = []
829
+
830
+ for contract_path in contract_files:
831
+ try:
832
+ handler = self.load_from_contract(contract_path, correlation_id)
833
+ handlers.append(handler)
834
+ except (ProtocolConfigurationError, InfraConnectionError) as e:
835
+ # Extract error code if available
836
+ error_code: str | None = None
837
+ if hasattr(e, "model") and hasattr(e.model, "context"):
838
+ loader_error = e.model.context.get("loader_error")
839
+ error_code = str(loader_error) if loader_error is not None else None
840
+
841
+ failed_handlers.append(
842
+ ModelFailedPluginLoad(
843
+ contract_path=contract_path,
844
+ error_message=str(e),
845
+ error_code=error_code,
846
+ )
847
+ )
848
+
849
+ logger.warning(
850
+ "Failed to load handler from %s: %s",
851
+ contract_path,
852
+ str(e),
853
+ extra={
854
+ "contract_path": str(contract_path),
855
+ "error": str(e),
856
+ "error_code": error_code,
857
+ "correlation_id": str(correlation_id),
858
+ },
859
+ )
860
+ continue
861
+
862
+ # Calculate duration and log summary
863
+ duration_seconds = time.perf_counter() - start_time
864
+
865
+ self._log_load_summary(
866
+ ModelPluginLoadContext(
867
+ operation="load_from_directory",
868
+ source=str(directory),
869
+ total_discovered=len(contract_files),
870
+ handlers=handlers,
871
+ failed_plugins=failed_handlers,
872
+ duration_seconds=duration_seconds,
873
+ correlation_id=correlation_id,
874
+ caller_correlation_string=str(correlation_id),
875
+ )
876
+ )
877
+
878
+ return handlers
879
+
880
+ def discover_and_load(
881
+ self,
882
+ patterns: list[str],
883
+ correlation_id: UUID | None = None,
884
+ base_path: Path | None = None,
885
+ max_handlers: int | None = None,
886
+ ) -> list[ModelLoadedHandler]:
887
+ """Discover contracts matching glob patterns and load handlers.
888
+
889
+ Searches for contract files matching the given glob patterns,
890
+ deduplicates matches, loads each handler, and returns a list
891
+ of successfully loaded handlers.
892
+
893
+ A summary is logged at the end of the operation for observability.
894
+
895
+ Working Directory Dependency:
896
+ By default, glob patterns are resolved relative to the current
897
+ working directory (``Path.cwd()``). This means results may vary
898
+ if the working directory changes between calls. For deterministic
899
+ behavior in environments where cwd may change (e.g., tests,
900
+ multi-threaded applications), provide an explicit ``base_path``
901
+ parameter.
902
+
903
+ Args:
904
+ patterns: List of glob patterns to match contract files.
905
+ Supports standard glob syntax including ** for recursive.
906
+ correlation_id: Optional correlation ID for tracing and error context.
907
+ If not provided, a new UUID4 is auto-generated to ensure all
908
+ operations have traceable correlation IDs. The same correlation_id
909
+ is propagated to all discovered contract loads.
910
+ base_path: Optional base path for resolving glob patterns.
911
+ If not provided, defaults to ``Path.cwd()``. Providing an
912
+ explicit base path ensures deterministic behavior regardless
913
+ of the current working directory.
914
+ max_handlers: Optional maximum number of handlers to discover and load.
915
+ If specified, discovery stops after finding this many contract files.
916
+ A warning is logged when the limit is reached. Set to None (default)
917
+ for unlimited discovery. This prevents runaway resource usage when
918
+ scanning directories with unexpectedly large numbers of handlers.
919
+
920
+ Returns:
921
+ List of successfully loaded handlers. May be empty if no
922
+ patterns match or all fail validation.
923
+
924
+ Raises:
925
+ ProtocolConfigurationError: If patterns list is empty.
926
+ Error codes:
927
+ - HANDLER_LOADER_030: Empty patterns list
928
+ ProtocolConfigurationError: If correlation_id is not a UUID or None.
929
+ Error code: INVALID_CORRELATION_ID_TYPE
930
+
931
+ Example:
932
+ >>> # Using default cwd-based resolution
933
+ >>> handlers = loader.discover_and_load(["src/**/handler_contract.yaml"])
934
+ >>>
935
+ >>> # Using explicit base path for deterministic behavior
936
+ >>> handlers = loader.discover_and_load(
937
+ ... ["src/**/handler_contract.yaml"],
938
+ ... base_path=Path("/app/project"),
939
+ ... )
940
+ """
941
+ # Validate correlation_id type at entry point (runtime type check)
942
+ self._validate_correlation_id(correlation_id, "discover_and_load")
943
+
944
+ # Auto-generate correlation_id if not provided (per ONEX guidelines)
945
+ correlation_id = correlation_id or uuid4()
946
+
947
+ # Start timing for observability
948
+ start_time = time.perf_counter()
949
+
950
+ logger.debug(
951
+ "Discovering handlers with patterns: %s",
952
+ patterns,
953
+ extra={
954
+ "patterns": patterns,
955
+ "correlation_id": str(correlation_id),
956
+ "max_handlers": max_handlers,
957
+ },
958
+ )
959
+
960
+ if not patterns:
961
+ context = ModelInfraErrorContext(
962
+ transport_type=EnumInfraTransportType.RUNTIME,
963
+ operation="discover_and_load",
964
+ correlation_id=correlation_id,
965
+ )
966
+ raise ProtocolConfigurationError(
967
+ "Patterns list cannot be empty",
968
+ context=context,
969
+ loader_error=EnumHandlerLoaderError.EMPTY_PATTERNS_LIST.value,
970
+ )
971
+
972
+ # Collect all matching contract files, deduplicated by resolved path
973
+ discovered_paths: set[Path] = set()
974
+ limit_reached = False
975
+
976
+ # Use explicit base_path if provided, otherwise fall back to cwd
977
+ # Note: Using cwd can produce different results if the working directory
978
+ # changes between calls. For deterministic behavior, provide base_path.
979
+ # Path.cwd() can raise OSError if:
980
+ # - Current working directory has been deleted
981
+ # - Permission denied accessing current directory
982
+ if base_path is not None:
983
+ glob_base = base_path
984
+ else:
985
+ try:
986
+ glob_base = Path.cwd()
987
+ except OSError as e:
988
+ context = ModelInfraErrorContext(
989
+ transport_type=EnumInfraTransportType.RUNTIME,
990
+ operation="discover_and_load",
991
+ correlation_id=correlation_id,
992
+ )
993
+ sanitized_msg = _sanitize_exception_message(e)
994
+ raise ProtocolConfigurationError(
995
+ f"Failed to access current working directory: {sanitized_msg}",
996
+ context=context,
997
+ loader_error=EnumHandlerLoaderError.PERMISSION_DENIED.value,
998
+ ) from e
999
+
1000
+ for pattern in patterns:
1001
+ if limit_reached:
1002
+ break
1003
+
1004
+ # Path.glob() can raise:
1005
+ # - OSError: Permission denied, filesystem errors, invalid path components
1006
+ # - ValueError: Invalid glob pattern syntax (e.g., ** not at path segment boundary)
1007
+ try:
1008
+ matched_paths = list(glob_base.glob(pattern))
1009
+ except ValueError as e:
1010
+ # ValueError indicates invalid glob pattern syntax
1011
+ # Example: "foo**bar" - ** must be at path segment boundaries
1012
+ sanitized_msg = _sanitize_exception_message(e)
1013
+ logger.warning(
1014
+ "Invalid glob pattern syntax '%s': %s",
1015
+ pattern,
1016
+ sanitized_msg,
1017
+ extra={
1018
+ "pattern": pattern,
1019
+ "base_path": str(glob_base),
1020
+ "error": sanitized_msg,
1021
+ "error_code": EnumHandlerLoaderError.INVALID_GLOB_PATTERN.value,
1022
+ "correlation_id": str(correlation_id),
1023
+ },
1024
+ )
1025
+ continue # Skip to next pattern
1026
+ except OSError as e:
1027
+ sanitized_msg = _sanitize_exception_message(e)
1028
+ logger.warning(
1029
+ "Failed to evaluate glob pattern '%s': %s",
1030
+ pattern,
1031
+ sanitized_msg,
1032
+ extra={
1033
+ "pattern": pattern,
1034
+ "base_path": str(glob_base),
1035
+ "error": sanitized_msg,
1036
+ "correlation_id": str(correlation_id),
1037
+ },
1038
+ )
1039
+ continue # Skip to next pattern
1040
+
1041
+ for path in matched_paths:
1042
+ # Check if we've reached the limit
1043
+ if max_handlers is not None and len(discovered_paths) >= max_handlers:
1044
+ limit_reached = True
1045
+ logger.warning(
1046
+ "Handler discovery limit reached: stopped after discovering %d "
1047
+ "handlers (max_handlers=%d). Some handlers may not be loaded.",
1048
+ len(discovered_paths),
1049
+ max_handlers,
1050
+ extra={
1051
+ "discovered_count": len(discovered_paths),
1052
+ "max_handlers": max_handlers,
1053
+ "patterns": patterns,
1054
+ "correlation_id": str(correlation_id),
1055
+ },
1056
+ )
1057
+ break
1058
+
1059
+ # path.is_file() can raise OSError for:
1060
+ # - Permission denied when stat'ing the file
1061
+ # - File deleted between glob discovery and is_file() check
1062
+ # - Filesystem errors (unmounted volumes, network failures)
1063
+ try:
1064
+ is_file = path.is_file()
1065
+ except OSError as e:
1066
+ sanitized_msg = _sanitize_exception_message(e)
1067
+ logger.warning(
1068
+ "Failed to check if path is file %s: %s",
1069
+ path.name,
1070
+ sanitized_msg,
1071
+ extra={
1072
+ "path": str(path),
1073
+ "error": sanitized_msg,
1074
+ "error_code": EnumHandlerLoaderError.FILE_STAT_ERROR.value,
1075
+ "correlation_id": str(correlation_id),
1076
+ },
1077
+ )
1078
+ continue
1079
+
1080
+ if is_file:
1081
+ # Early size validation to skip oversized files before expensive operations
1082
+ if (
1083
+ self._validate_file_size(
1084
+ path, correlation_id=correlation_id, raise_on_error=False
1085
+ )
1086
+ is None
1087
+ ):
1088
+ continue
1089
+
1090
+ # Early YAML syntax validation to fail fast before expensive resolve operations
1091
+ # This catches malformed YAML immediately after discovery rather than
1092
+ # deferring to load_from_contract, which is more efficient for batch discovery
1093
+ if not self._validate_yaml_syntax(
1094
+ path, correlation_id=correlation_id, raise_on_error=False
1095
+ ):
1096
+ continue
1097
+
1098
+ # path.resolve() can raise OSError for:
1099
+ # - Broken symlinks: symlink target no longer exists
1100
+ # - Race conditions: file deleted between glob discovery and resolution
1101
+ # - Permission issues: lacking read permission on parent directories
1102
+ # - Filesystem errors: unmounted volumes, network filesystem failures
1103
+ try:
1104
+ resolved = path.resolve()
1105
+ except OSError as e:
1106
+ sanitized_msg = _sanitize_exception_message(e)
1107
+ logger.warning(
1108
+ "Failed to resolve path %s: %s",
1109
+ path.name,
1110
+ sanitized_msg,
1111
+ extra={
1112
+ "path": str(path),
1113
+ "error": sanitized_msg,
1114
+ "correlation_id": str(correlation_id),
1115
+ },
1116
+ )
1117
+ continue # Skip to next path
1118
+
1119
+ discovered_paths.add(resolved)
1120
+
1121
+ logger.debug(
1122
+ "Discovered %d unique contract files from %d patterns",
1123
+ len(discovered_paths),
1124
+ len(patterns),
1125
+ extra={
1126
+ "patterns": patterns,
1127
+ "discovered_count": len(discovered_paths),
1128
+ "limit_reached": limit_reached,
1129
+ "correlation_id": str(correlation_id),
1130
+ },
1131
+ )
1132
+
1133
+ # Load each discovered contract (graceful mode)
1134
+ handlers: list[ModelLoadedHandler] = []
1135
+ failed_handlers: list[ModelFailedPluginLoad] = []
1136
+
1137
+ for contract_path in sorted(discovered_paths):
1138
+ try:
1139
+ handler = self.load_from_contract(contract_path, correlation_id)
1140
+ handlers.append(handler)
1141
+ except (ProtocolConfigurationError, InfraConnectionError) as e:
1142
+ # Extract error code if available
1143
+ error_code: str | None = None
1144
+ if hasattr(e, "model") and hasattr(e.model, "context"):
1145
+ loader_error = e.model.context.get("loader_error")
1146
+ error_code = str(loader_error) if loader_error is not None else None
1147
+
1148
+ failed_handlers.append(
1149
+ ModelFailedPluginLoad(
1150
+ contract_path=contract_path,
1151
+ error_message=str(e),
1152
+ error_code=error_code,
1153
+ )
1154
+ )
1155
+
1156
+ logger.warning(
1157
+ "Failed to load handler from %s: %s",
1158
+ contract_path,
1159
+ str(e),
1160
+ extra={
1161
+ "contract_path": str(contract_path),
1162
+ "error": str(e),
1163
+ "error_code": error_code,
1164
+ "correlation_id": str(correlation_id),
1165
+ },
1166
+ )
1167
+ continue
1168
+
1169
+ # Calculate duration and log summary
1170
+ duration_seconds = time.perf_counter() - start_time
1171
+
1172
+ # Format patterns as comma-separated string for source
1173
+ patterns_str = ", ".join(patterns)
1174
+
1175
+ self._log_load_summary(
1176
+ ModelPluginLoadContext(
1177
+ operation="discover_and_load",
1178
+ source=patterns_str,
1179
+ total_discovered=len(discovered_paths),
1180
+ handlers=handlers,
1181
+ failed_plugins=failed_handlers,
1182
+ duration_seconds=duration_seconds,
1183
+ correlation_id=correlation_id,
1184
+ caller_correlation_string=str(correlation_id),
1185
+ )
1186
+ )
1187
+
1188
+ return handlers
1189
+
1190
+ def _log_load_summary(
1191
+ self,
1192
+ context: ModelPluginLoadContext,
1193
+ ) -> ModelPluginLoadSummary:
1194
+ """Log a summary of the handler loading operation for observability.
1195
+
1196
+ Creates a structured summary of the load operation and logs it at
1197
+ an appropriate level (INFO for success, WARNING if there were failures).
1198
+
1199
+ The log message format is designed for easy parsing:
1200
+ - Single line summary with counts and timing
1201
+ - Detailed handler list with class names and modules
1202
+ - Failed handler details with error reasons
1203
+
1204
+ Args:
1205
+ context: The load context containing operation details, handlers,
1206
+ failures, and timing information.
1207
+
1208
+ Returns:
1209
+ ModelPluginLoadSummary containing the structured summary data.
1210
+
1211
+ Example log output:
1212
+ Handler load complete: 5 handlers loaded in 0.23s (source: /app/handlers)
1213
+ - HandlerAuth (myapp.handlers.auth)
1214
+ - HandlerDb (myapp.handlers.db)
1215
+ ...
1216
+ """
1217
+ # Build list of loaded handler details
1218
+ loaded_handler_details = [
1219
+ {
1220
+ "name": h.handler_name,
1221
+ "class": h.handler_class.rsplit(".", 1)[-1],
1222
+ "module": h.handler_class.rsplit(".", 1)[0],
1223
+ }
1224
+ for h in context.handlers
1225
+ ]
1226
+
1227
+ # Create summary model
1228
+ summary = ModelPluginLoadSummary(
1229
+ operation=context.operation,
1230
+ source=context.source,
1231
+ total_discovered=context.total_discovered,
1232
+ total_loaded=len(context.handlers),
1233
+ total_failed=len(context.failed_plugins),
1234
+ loaded_plugins=loaded_handler_details,
1235
+ failed_plugins=context.failed_plugins,
1236
+ duration_seconds=context.duration_seconds,
1237
+ correlation_id=context.correlation_id,
1238
+ completed_at=datetime.now(UTC),
1239
+ )
1240
+
1241
+ # Build log message with handler details
1242
+ handler_lines = [
1243
+ f" - {h['class']} ({h['module']})" for h in loaded_handler_details
1244
+ ]
1245
+ handler_list_str = "\n".join(handler_lines) if handler_lines else " (none)"
1246
+
1247
+ # Build failed handler message if any
1248
+ failed_lines = []
1249
+ for failed in context.failed_plugins:
1250
+ error_code_str = f" [{failed.error_code}]" if failed.error_code else ""
1251
+ failed_lines.append(f" - {failed.contract_path}{error_code_str}")
1252
+
1253
+ failed_list_str = "\n".join(failed_lines) if failed_lines else ""
1254
+
1255
+ # Choose log level based on whether there were failures
1256
+ if context.failed_plugins:
1257
+ log_level = logging.WARNING
1258
+ status = "with failures"
1259
+ else:
1260
+ log_level = logging.INFO
1261
+ status = "successfully"
1262
+
1263
+ # Format duration for readability
1264
+ if context.duration_seconds < 0.001:
1265
+ duration_str = f"{context.duration_seconds * 1000000:.0f}us"
1266
+ elif context.duration_seconds < 1.0:
1267
+ duration_str = f"{context.duration_seconds * 1000:.2f}ms"
1268
+ else:
1269
+ duration_str = f"{context.duration_seconds:.2f}s"
1270
+
1271
+ # Log the summary
1272
+ summary_msg = (
1273
+ f"Handler load complete {status}: "
1274
+ f"{len(context.handlers)} handlers loaded in {duration_str}"
1275
+ )
1276
+ if context.failed_plugins:
1277
+ summary_msg += f" ({len(context.failed_plugins)} failed)"
1278
+
1279
+ # Build detailed message
1280
+ detailed_msg = f"{summary_msg}\nLoaded handlers:\n{handler_list_str}"
1281
+ if failed_list_str:
1282
+ detailed_msg += f"\nFailed handlers:\n{failed_list_str}"
1283
+
1284
+ logger.log(
1285
+ log_level,
1286
+ detailed_msg,
1287
+ extra={
1288
+ "operation": context.operation,
1289
+ "source": context.source,
1290
+ "total_discovered": context.total_discovered,
1291
+ "total_loaded": len(context.handlers),
1292
+ "total_failed": len(context.failed_plugins),
1293
+ "duration_seconds": context.duration_seconds,
1294
+ "correlation_id": context.caller_correlation_string,
1295
+ "handler_names": [h.handler_name for h in context.handlers],
1296
+ "handler_classes": [h.handler_class for h in context.handlers],
1297
+ "failed_paths": [str(f.contract_path) for f in context.failed_plugins],
1298
+ },
1299
+ )
1300
+
1301
+ return summary
1302
+
1303
+ def _validate_handler_protocol(self, handler_class: type) -> tuple[bool, list[str]]:
1304
+ """Validate handler implements required protocol (ProtocolHandler).
1305
+
1306
+ Uses duck typing to verify the handler class has the required
1307
+ methods for ProtocolHandler compliance. Per ONEX conventions, protocol
1308
+ compliance is verified via structural typing (duck typing) rather than
1309
+ isinstance checks or explicit inheritance.
1310
+
1311
+ Protocol Requirements (from omnibase_spi.protocols.handlers.protocol_handler):
1312
+ The ProtocolHandler protocol defines the following required members:
1313
+
1314
+ **Required Methods (validated)**:
1315
+ - ``handler_type`` (property): Returns handler type identifier string
1316
+ - ``initialize(config)``: Async method to initialize connections/pools
1317
+ - ``shutdown(timeout_seconds)``: Async method to release resources
1318
+ - ``execute(request, operation_config)``: Async method for operations
1319
+ - ``describe()``: Sync method returning handler metadata/capabilities
1320
+
1321
+ **Optional Methods (not validated)**:
1322
+ - ``health_check()``: Async method for connectivity verification.
1323
+ While part of the ProtocolHandler protocol, this method is not
1324
+ validated because existing handler implementations (HandlerHttp,
1325
+ HandlerDb, HandlerVault, HandlerConsul) do not implement it.
1326
+ Future handler implementations SHOULD include health_check().
1327
+
1328
+ Validation Approach:
1329
+ This method checks for the presence and callability of all 5 required
1330
+ methods. A handler class must have ALL of these methods to pass validation.
1331
+ This prevents false positives where a class might have only ``describe()``
1332
+ but lack other essential handler functionality.
1333
+
1334
+ The validation uses ``callable(getattr(...))`` for methods and
1335
+ ``hasattr()`` for the ``handler_type`` property to accommodate both
1336
+ instance properties and class-level descriptors.
1337
+
1338
+ Why Duck Typing:
1339
+ ONEX uses duck typing for protocol validation to:
1340
+ 1. Avoid tight coupling to specific base classes
1341
+ 2. Enable flexibility in handler implementation strategies
1342
+ 3. Support mixin-based handler composition
1343
+ 4. Allow testing with mock handlers that satisfy the protocol
1344
+
1345
+ Args:
1346
+ handler_class: The handler class to validate. Must be a class type,
1347
+ not an instance.
1348
+
1349
+ Returns:
1350
+ A tuple of (is_valid, missing_methods) where:
1351
+ - is_valid: True if handler implements all required protocol methods
1352
+ - missing_methods: List of method names that are missing or not callable.
1353
+ Empty list if all methods are present.
1354
+
1355
+ Example:
1356
+ >>> class ValidHandler:
1357
+ ... @property
1358
+ ... def handler_type(self) -> str: return "test"
1359
+ ... async def initialize(self, config): pass
1360
+ ... async def shutdown(self, timeout_seconds=30.0): pass
1361
+ ... async def execute(self, request, config): pass
1362
+ ... def describe(self): return {}
1363
+ ...
1364
+ >>> loader = HandlerPluginLoader()
1365
+ >>> loader._validate_handler_protocol(ValidHandler)
1366
+ (True, [])
1367
+
1368
+ >>> class IncompleteHandler:
1369
+ ... def describe(self): return {}
1370
+ ...
1371
+ >>> loader._validate_handler_protocol(IncompleteHandler)
1372
+ (False, ['handler_type', 'initialize', 'shutdown', 'execute'])
1373
+
1374
+ See Also:
1375
+ - ``omnibase_spi.protocols.handlers.protocol_handler.ProtocolHandler``
1376
+ - ``docs/architecture/RUNTIME_HOST_IMPLEMENTATION_PLAN.md``
1377
+ """
1378
+ # Check for required ProtocolHandler methods via duck typing
1379
+ # All 5 core methods must be present for protocol compliance
1380
+ missing_methods: list[str] = []
1381
+
1382
+ # 1. handler_type property - can be property or method
1383
+ if not hasattr(handler_class, "handler_type"):
1384
+ missing_methods.append("handler_type")
1385
+
1386
+ # 2. initialize() - async method for connection setup
1387
+ if not callable(getattr(handler_class, "initialize", None)):
1388
+ missing_methods.append("initialize")
1389
+
1390
+ # 3. shutdown() - async method for resource cleanup
1391
+ if not callable(getattr(handler_class, "shutdown", None)):
1392
+ missing_methods.append("shutdown")
1393
+
1394
+ # 4. execute() - async method for operation execution
1395
+ if not callable(getattr(handler_class, "execute", None)):
1396
+ missing_methods.append("execute")
1397
+
1398
+ # 5. describe() - sync method for introspection
1399
+ if not callable(getattr(handler_class, "describe", None)):
1400
+ missing_methods.append("describe")
1401
+
1402
+ # Note: health_check() is part of ProtocolHandler but is NOT validated
1403
+ # because existing handlers (HandlerHttp, HandlerDb, etc.) do not
1404
+ # implement it. Future handlers SHOULD implement health_check().
1405
+
1406
+ return (len(missing_methods) == 0, missing_methods)
1407
+
1408
+ def _import_handler_class(
1409
+ self,
1410
+ class_path: str,
1411
+ contract_path: Path,
1412
+ correlation_id: UUID | None = None,
1413
+ ) -> type:
1414
+ """Dynamically import handler class from fully qualified path.
1415
+
1416
+ This method validates the namespace (if allowed_namespaces is configured)
1417
+ BEFORE calling ``importlib.import_module()``, preventing any module-level
1418
+ side effects from untrusted packages.
1419
+
1420
+ Args:
1421
+ class_path: Fully qualified class path (e.g., 'myapp.handlers.AuthHandler').
1422
+ contract_path: Path to the contract file (for error context).
1423
+ correlation_id: Optional correlation ID for tracing and error context.
1424
+
1425
+ Returns:
1426
+ The imported class type.
1427
+
1428
+ Raises:
1429
+ ProtocolConfigurationError: If namespace validation fails.
1430
+ - HANDLER_LOADER_013 (NAMESPACE_NOT_ALLOWED): When the class path
1431
+ does not start with any of the allowed namespace prefixes.
1432
+ InfraConnectionError: If the module or class cannot be imported.
1433
+ Error codes include correlation_id when provided for traceability.
1434
+ - HANDLER_LOADER_010 (MODULE_NOT_FOUND): Handler module not found
1435
+ - HANDLER_LOADER_011 (CLASS_NOT_FOUND): Handler class not found in module
1436
+ - HANDLER_LOADER_012 (IMPORT_ERROR): Import error (syntax/dependency)
1437
+ """
1438
+ # Split class path into module and class name
1439
+ if "." not in class_path:
1440
+ context = ModelInfraErrorContext(
1441
+ transport_type=EnumInfraTransportType.RUNTIME,
1442
+ operation="import_handler_class",
1443
+ correlation_id=correlation_id,
1444
+ )
1445
+ raise InfraConnectionError(
1446
+ f"Invalid class path '{class_path}': must be fully qualified "
1447
+ "(e.g., 'myapp.handlers.AuthHandler')",
1448
+ context=context,
1449
+ loader_error=EnumHandlerLoaderError.MODULE_NOT_FOUND.value,
1450
+ class_path=class_path,
1451
+ contract_path=str(contract_path),
1452
+ )
1453
+
1454
+ module_path, class_name = class_path.rsplit(".", 1)
1455
+
1456
+ # Validate namespace BEFORE importing (defense-in-depth)
1457
+ # This prevents any module-level side effects from untrusted packages
1458
+ self._validate_namespace(class_path, contract_path, correlation_id)
1459
+
1460
+ # Import the module
1461
+ try:
1462
+ module = importlib.import_module(module_path)
1463
+ except ModuleNotFoundError as e:
1464
+ context = ModelInfraErrorContext(
1465
+ transport_type=EnumInfraTransportType.RUNTIME,
1466
+ operation="import_handler_class",
1467
+ correlation_id=correlation_id,
1468
+ )
1469
+ raise InfraConnectionError(
1470
+ f"Module not found: {module_path}",
1471
+ context=context,
1472
+ loader_error=EnumHandlerLoaderError.MODULE_NOT_FOUND.value,
1473
+ module_path=module_path,
1474
+ class_path=class_path,
1475
+ contract_path=str(contract_path),
1476
+ ) from e
1477
+ except ImportError as e:
1478
+ context = ModelInfraErrorContext(
1479
+ transport_type=EnumInfraTransportType.RUNTIME,
1480
+ operation="import_handler_class",
1481
+ correlation_id=correlation_id,
1482
+ )
1483
+ # Sanitize exception message to prevent path disclosure
1484
+ sanitized_msg = _sanitize_exception_message(e)
1485
+ raise InfraConnectionError(
1486
+ f"Import error loading module {module_path}: {sanitized_msg}",
1487
+ context=context,
1488
+ loader_error=EnumHandlerLoaderError.IMPORT_ERROR.value,
1489
+ module_path=module_path,
1490
+ class_path=class_path,
1491
+ contract_path=str(contract_path),
1492
+ ) from e
1493
+ except SyntaxError as e:
1494
+ # SyntaxError can occur during import if the handler module has syntax errors.
1495
+ # This is a subclass of Exception, not ImportError, so must be caught separately.
1496
+ context = ModelInfraErrorContext(
1497
+ transport_type=EnumInfraTransportType.RUNTIME,
1498
+ operation="import_handler_class",
1499
+ correlation_id=correlation_id,
1500
+ )
1501
+ # Sanitize exception message to prevent path disclosure
1502
+ sanitized_msg = _sanitize_exception_message(e)
1503
+ raise InfraConnectionError(
1504
+ f"Syntax error in module {module_path}: {sanitized_msg}",
1505
+ context=context,
1506
+ loader_error=EnumHandlerLoaderError.IMPORT_ERROR.value,
1507
+ module_path=module_path,
1508
+ class_path=class_path,
1509
+ contract_path=str(contract_path),
1510
+ ) from e
1511
+
1512
+ # Get the class from the module
1513
+ if not hasattr(module, class_name):
1514
+ context = ModelInfraErrorContext(
1515
+ transport_type=EnumInfraTransportType.RUNTIME,
1516
+ operation="import_handler_class",
1517
+ correlation_id=correlation_id,
1518
+ )
1519
+ raise InfraConnectionError(
1520
+ f"Class '{class_name}' not found in module '{module_path}'",
1521
+ context=context,
1522
+ loader_error=EnumHandlerLoaderError.CLASS_NOT_FOUND.value,
1523
+ module_path=module_path,
1524
+ class_name=class_name,
1525
+ class_path=class_path,
1526
+ contract_path=str(contract_path),
1527
+ )
1528
+
1529
+ handler_class = getattr(module, class_name)
1530
+
1531
+ # Verify it's actually a class
1532
+ if not isinstance(handler_class, type):
1533
+ context = ModelInfraErrorContext(
1534
+ transport_type=EnumInfraTransportType.RUNTIME,
1535
+ operation="import_handler_class",
1536
+ correlation_id=correlation_id,
1537
+ )
1538
+ raise InfraConnectionError(
1539
+ f"'{class_path}' is not a class",
1540
+ context=context,
1541
+ loader_error=EnumHandlerLoaderError.CLASS_NOT_FOUND.value,
1542
+ class_path=class_path,
1543
+ contract_path=str(contract_path),
1544
+ )
1545
+
1546
+ return handler_class
1547
+
1548
+ def _validate_namespace(
1549
+ self,
1550
+ class_path: str,
1551
+ contract_path: Path,
1552
+ correlation_id: UUID | None = None,
1553
+ ) -> None:
1554
+ """Validate handler class path against allowed namespaces.
1555
+
1556
+ Checks whether the handler's fully-qualified class path starts with one
1557
+ of the allowed namespace prefixes. This validation occurs BEFORE the
1558
+ module is imported, preventing any module-level side effects from
1559
+ untrusted packages.
1560
+
1561
+ Args:
1562
+ class_path: Fully qualified class path (e.g., 'myapp.handlers.AuthHandler').
1563
+ contract_path: Path to the contract file (for error context).
1564
+ correlation_id: Optional correlation ID for tracing and error context.
1565
+
1566
+ Raises:
1567
+ ProtocolConfigurationError: If namespace validation fails.
1568
+ - HANDLER_LOADER_013 (NAMESPACE_NOT_ALLOWED): When the class path
1569
+ does not start with any of the allowed namespace prefixes.
1570
+
1571
+ Note:
1572
+ This method is a no-op when ``allowed_namespaces`` is None, allowing
1573
+ any namespace. When ``allowed_namespaces`` is an empty list, ALL
1574
+ namespaces are blocked.
1575
+
1576
+ Example:
1577
+ >>> loader = HandlerPluginLoader(
1578
+ ... allowed_namespaces=["omnibase_infra.", "mycompany."]
1579
+ ... )
1580
+ >>> # This passes validation:
1581
+ >>> loader._validate_namespace(
1582
+ ... "omnibase_infra.handlers.HandlerAuth",
1583
+ ... Path("contract.yaml"),
1584
+ ... )
1585
+ >>> # This raises ProtocolConfigurationError:
1586
+ >>> loader._validate_namespace(
1587
+ ... "malicious_pkg.EvilHandler",
1588
+ ... Path("malicious.yaml"),
1589
+ ... )
1590
+ """
1591
+ # If no namespace restriction is configured, allow all
1592
+ if self._allowed_namespaces is None:
1593
+ return
1594
+
1595
+ # Check if class_path starts with any allowed namespace with proper
1596
+ # package boundary validation. This prevents "foo" from matching "foobar.module".
1597
+ for namespace in self._allowed_namespaces:
1598
+ if class_path.startswith(namespace):
1599
+ # If namespace ends with ".", we've already matched a package boundary
1600
+ if namespace.endswith("."):
1601
+ return
1602
+ # Otherwise, ensure we're at a package boundary (next char is ".")
1603
+ # This prevents "foo" from matching "foobar.module" - only exact
1604
+ # matches or matches followed by "." are valid.
1605
+ remaining = class_path[len(namespace) :]
1606
+ if remaining == "" or remaining.startswith("."):
1607
+ return
1608
+
1609
+ # Namespace not in allowed list - raise error
1610
+ context = ModelInfraErrorContext(
1611
+ transport_type=EnumInfraTransportType.RUNTIME,
1612
+ operation="validate_namespace",
1613
+ correlation_id=correlation_id,
1614
+ )
1615
+
1616
+ # Format allowed namespaces for error message
1617
+ if self._allowed_namespaces:
1618
+ allowed_str = ", ".join(repr(ns) for ns in self._allowed_namespaces)
1619
+ else:
1620
+ allowed_str = "(none - empty allowlist)"
1621
+
1622
+ raise ProtocolConfigurationError(
1623
+ f"Handler namespace not allowed: '{class_path}' does not start with "
1624
+ f"any of the allowed namespaces: {allowed_str}",
1625
+ context=context,
1626
+ loader_error=EnumHandlerLoaderError.NAMESPACE_NOT_ALLOWED.value,
1627
+ class_path=class_path,
1628
+ contract_path=str(contract_path),
1629
+ allowed_namespaces=list(self._allowed_namespaces),
1630
+ )
1631
+
1632
+ def _validate_yaml_syntax(
1633
+ self,
1634
+ path: Path,
1635
+ correlation_id: UUID | None = None,
1636
+ raise_on_error: bool = True,
1637
+ ) -> bool:
1638
+ """Validate YAML syntax of a contract file for early fail-fast behavior.
1639
+
1640
+ Performs early YAML syntax validation to fail fast before expensive
1641
+ operations like path resolution and handler class loading. This method
1642
+ only validates that the file contains valid YAML syntax; it does not
1643
+ perform schema validation.
1644
+
1645
+ This enables the discover_and_load method to skip malformed YAML files
1646
+ immediately after discovery, rather than deferring the error to
1647
+ load_from_contract which would be less efficient for large discovery
1648
+ operations.
1649
+
1650
+ Args:
1651
+ path: Path to the YAML file to validate. Must be an existing file.
1652
+ correlation_id: Optional correlation ID for error context.
1653
+ raise_on_error: If True (default), raises ProtocolConfigurationError
1654
+ on YAML syntax errors. If False, logs a warning and returns False,
1655
+ allowing the caller to skip the file.
1656
+
1657
+ Returns:
1658
+ True if YAML syntax is valid.
1659
+ False if raise_on_error is False and YAML syntax is invalid.
1660
+
1661
+ Raises:
1662
+ ProtocolConfigurationError: If raise_on_error is True and:
1663
+ - INVALID_YAML_SYNTAX: File contains invalid YAML syntax
1664
+ - FILE_READ_ERROR: Failed to read file (I/O error)
1665
+
1666
+ Note:
1667
+ The error message includes the YAML parser error details which
1668
+ typically contain line and column information for the syntax error.
1669
+ """
1670
+ try:
1671
+ with path.open("r", encoding="utf-8") as f:
1672
+ yaml.safe_load(f)
1673
+ except yaml.YAMLError as e:
1674
+ # Sanitize exception message to prevent path disclosure
1675
+ sanitized_msg = _sanitize_exception_message(e)
1676
+ if raise_on_error:
1677
+ context = ModelInfraErrorContext(
1678
+ transport_type=EnumInfraTransportType.RUNTIME,
1679
+ operation="validate_yaml_syntax",
1680
+ correlation_id=correlation_id,
1681
+ )
1682
+ raise ProtocolConfigurationError(
1683
+ f"Invalid YAML syntax in contract file '{path.name}': {sanitized_msg}",
1684
+ context=context,
1685
+ loader_error=EnumHandlerLoaderError.INVALID_YAML_SYNTAX.value,
1686
+ contract_path=str(path),
1687
+ ) from e
1688
+ logger.warning(
1689
+ "Skipping contract file with invalid YAML syntax %s: %s",
1690
+ path.name,
1691
+ sanitized_msg,
1692
+ extra={
1693
+ "path": str(path),
1694
+ "error": sanitized_msg,
1695
+ "correlation_id": str(correlation_id) if correlation_id else None,
1696
+ },
1697
+ )
1698
+ return False
1699
+ except OSError as e:
1700
+ # Sanitize exception message to prevent path disclosure
1701
+ sanitized_msg = _sanitize_exception_message(e)
1702
+ if raise_on_error:
1703
+ context = ModelInfraErrorContext(
1704
+ transport_type=EnumInfraTransportType.RUNTIME,
1705
+ operation="validate_yaml_syntax",
1706
+ correlation_id=correlation_id,
1707
+ )
1708
+ raise ProtocolConfigurationError(
1709
+ f"Failed to read contract file '{path.name}': {sanitized_msg}",
1710
+ context=context,
1711
+ loader_error=EnumHandlerLoaderError.FILE_READ_ERROR.value,
1712
+ contract_path=str(path),
1713
+ ) from e
1714
+ logger.warning(
1715
+ "Failed to read contract file %s: %s",
1716
+ path.name,
1717
+ sanitized_msg,
1718
+ extra={
1719
+ "path": str(path),
1720
+ "error": sanitized_msg,
1721
+ "correlation_id": str(correlation_id) if correlation_id else None,
1722
+ },
1723
+ )
1724
+ return False
1725
+
1726
+ return True
1727
+
1728
+ def _validate_file_size(
1729
+ self,
1730
+ path: Path,
1731
+ correlation_id: UUID | None = None,
1732
+ operation: str = "load_from_contract",
1733
+ raise_on_error: bool = True,
1734
+ ) -> int | None:
1735
+ """Validate file size is within limits.
1736
+
1737
+ Checks that the file at the given path can be stat'd and does not
1738
+ exceed MAX_CONTRACT_SIZE. Supports both strict mode (raising exceptions)
1739
+ and graceful mode (logging warnings and returning None).
1740
+
1741
+ Args:
1742
+ path: Path to the file to validate. Must be an existing file.
1743
+ correlation_id: Optional correlation ID for error context.
1744
+ operation: The operation name for error context in exceptions.
1745
+ raise_on_error: If True (default), raises ProtocolConfigurationError
1746
+ on stat failure or size exceeded. If False, logs a warning
1747
+ and returns None, allowing the caller to skip the file.
1748
+
1749
+ Returns:
1750
+ File size in bytes if validation passes.
1751
+ None if raise_on_error is False and validation fails (stat error
1752
+ or size exceeded).
1753
+
1754
+ Raises:
1755
+ ProtocolConfigurationError: If raise_on_error is True and:
1756
+ - FILE_STAT_ERROR: Failed to stat the file (I/O error)
1757
+ - FILE_SIZE_EXCEEDED: File exceeds MAX_CONTRACT_SIZE
1758
+ """
1759
+ # Attempt to get file size
1760
+ try:
1761
+ file_size = path.stat().st_size
1762
+ except OSError as e:
1763
+ # Sanitize exception message to prevent path disclosure
1764
+ sanitized_msg = _sanitize_exception_message(e)
1765
+ if raise_on_error:
1766
+ context = ModelInfraErrorContext(
1767
+ transport_type=EnumInfraTransportType.RUNTIME,
1768
+ operation=operation,
1769
+ correlation_id=correlation_id,
1770
+ )
1771
+ raise ProtocolConfigurationError(
1772
+ f"Failed to stat contract file: {sanitized_msg}",
1773
+ context=context,
1774
+ loader_error=EnumHandlerLoaderError.FILE_STAT_ERROR.value,
1775
+ contract_path=str(path),
1776
+ ) from e
1777
+ logger.warning(
1778
+ "Failed to stat contract file %s: %s",
1779
+ path.name,
1780
+ sanitized_msg,
1781
+ extra={
1782
+ "path": str(path),
1783
+ "error": sanitized_msg,
1784
+ "correlation_id": str(correlation_id) if correlation_id else None,
1785
+ },
1786
+ )
1787
+ return None
1788
+
1789
+ # Check size limit
1790
+ if file_size > MAX_CONTRACT_SIZE:
1791
+ if raise_on_error:
1792
+ context = ModelInfraErrorContext(
1793
+ transport_type=EnumInfraTransportType.RUNTIME,
1794
+ operation=operation,
1795
+ correlation_id=correlation_id,
1796
+ )
1797
+ raise ProtocolConfigurationError(
1798
+ f"Contract file exceeds size limit: {file_size} bytes "
1799
+ f"(max: {MAX_CONTRACT_SIZE} bytes)",
1800
+ context=context,
1801
+ loader_error=EnumHandlerLoaderError.FILE_SIZE_EXCEEDED.value,
1802
+ contract_path=str(path),
1803
+ file_size=file_size,
1804
+ max_size=MAX_CONTRACT_SIZE,
1805
+ )
1806
+ logger.warning(
1807
+ "Skipping oversized contract file %s: %d bytes exceeds limit of %d bytes",
1808
+ path.name,
1809
+ file_size,
1810
+ MAX_CONTRACT_SIZE,
1811
+ extra={
1812
+ "path": str(path),
1813
+ "file_size": file_size,
1814
+ "max_size": MAX_CONTRACT_SIZE,
1815
+ "correlation_id": str(correlation_id) if correlation_id else None,
1816
+ },
1817
+ )
1818
+ return None
1819
+
1820
+ return file_size
1821
+
1822
+ def _find_contract_files(
1823
+ self,
1824
+ directory: Path,
1825
+ correlation_id: UUID | None = None,
1826
+ max_handlers: int | None = None,
1827
+ ) -> list[Path]:
1828
+ """Find all handler contract files under a directory.
1829
+
1830
+ Searches for both handler_contract.yaml and contract.yaml files.
1831
+ Files exceeding MAX_CONTRACT_SIZE are skipped during discovery
1832
+ to fail fast before expensive path resolution and loading.
1833
+
1834
+ Ambiguous Contract Detection (Fail-Fast):
1835
+ When BOTH ``handler_contract.yaml`` AND ``contract.yaml`` exist in the
1836
+ same directory, this method raises ``ProtocolConfigurationError`` with
1837
+ error code ``AMBIGUOUS_CONTRACT_CONFIGURATION``. This fail-fast behavior
1838
+ prevents:
1839
+
1840
+ - Duplicate handler registrations
1841
+ - Confusion about which contract is authoritative
1842
+ - Unexpected runtime behavior from conflicting configurations
1843
+
1844
+ Best practice: Use only ONE contract file per handler directory.
1845
+
1846
+ See: docs/patterns/handler_plugin_loader.md#contract-file-precedence
1847
+
1848
+ Args:
1849
+ directory: Directory to search recursively.
1850
+ correlation_id: Optional correlation ID for tracing and error context.
1851
+ max_handlers: Optional maximum number of handlers to discover.
1852
+ If specified, discovery stops after finding this many contract files.
1853
+ Propagated to file size validation for consistent traceability.
1854
+
1855
+ Returns:
1856
+ List of paths to contract files that pass size validation.
1857
+
1858
+ Raises:
1859
+ ProtocolConfigurationError: If both handler_contract.yaml and contract.yaml
1860
+ exist in the same directory. Error code: AMBIGUOUS_CONTRACT_CONFIGURATION
1861
+ (HANDLER_LOADER_040).
1862
+ """
1863
+ contract_files: list[Path] = []
1864
+ # Track if max_handlers limit was reached
1865
+ limit_reached = False
1866
+
1867
+ # Search for valid contract filenames in a single scan
1868
+ # This consolidates two rglob() calls into one for better performance
1869
+ # NOTE: If both handler_contract.yaml and contract.yaml are found in the
1870
+ # same directory, we fail fast with AMBIGUOUS_CONTRACT_CONFIGURATION error
1871
+ # after discovery (see ambiguity check below).
1872
+ valid_filenames = {HANDLER_CONTRACT_FILENAME, CONTRACT_YAML_FILENAME}
1873
+
1874
+ # directory.rglob() can raise OSError for:
1875
+ # - Permission denied when accessing the directory or subdirectories
1876
+ # - Filesystem errors (unmounted volumes, network failures)
1877
+ # - Directory deleted or becomes inaccessible during iteration
1878
+ try:
1879
+ rglob_iterator = directory.rglob("*.yaml")
1880
+ except OSError as e:
1881
+ # If we can't even start iterating, log warning and return empty list
1882
+ # This is graceful degradation - the caller can handle empty results
1883
+ sanitized_msg = _sanitize_exception_message(e)
1884
+ logger.warning(
1885
+ "Failed to scan directory %s for contracts: %s",
1886
+ directory.name,
1887
+ sanitized_msg,
1888
+ extra={
1889
+ "directory": str(directory),
1890
+ "error": sanitized_msg,
1891
+ "error_code": EnumHandlerLoaderError.PERMISSION_DENIED.value,
1892
+ "correlation_id": str(correlation_id) if correlation_id else None,
1893
+ },
1894
+ )
1895
+ return []
1896
+
1897
+ # Iterate over discovered paths, handling per-path errors gracefully
1898
+ try:
1899
+ for path in rglob_iterator:
1900
+ # Check if we've reached the max_handlers limit
1901
+ if max_handlers is not None and len(contract_files) >= max_handlers:
1902
+ limit_reached = True
1903
+ break
1904
+
1905
+ # Filter by filename first (cheap string comparison)
1906
+ if path.name not in valid_filenames:
1907
+ continue
1908
+
1909
+ # path.is_file() can raise OSError for:
1910
+ # - Permission denied when stat'ing the file
1911
+ # - File deleted between rglob discovery and is_file() check
1912
+ # - Filesystem errors (unmounted volumes, network failures)
1913
+ try:
1914
+ is_file = path.is_file()
1915
+ except OSError as e:
1916
+ sanitized_msg = _sanitize_exception_message(e)
1917
+ logger.warning(
1918
+ "Failed to check if path is file %s: %s",
1919
+ path.name,
1920
+ sanitized_msg,
1921
+ extra={
1922
+ "path": str(path),
1923
+ "error": sanitized_msg,
1924
+ "error_code": EnumHandlerLoaderError.FILE_STAT_ERROR.value,
1925
+ "correlation_id": str(correlation_id)
1926
+ if correlation_id
1927
+ else None,
1928
+ },
1929
+ )
1930
+ continue
1931
+
1932
+ if not is_file:
1933
+ continue
1934
+
1935
+ # Early size validation to skip oversized files before expensive operations
1936
+ if (
1937
+ self._validate_file_size(
1938
+ path, correlation_id=correlation_id, raise_on_error=False
1939
+ )
1940
+ is None
1941
+ ):
1942
+ continue
1943
+
1944
+ contract_files.append(path)
1945
+ except OSError as e:
1946
+ # Handle errors that occur during iteration (e.g., directory becomes
1947
+ # inaccessible mid-scan). Return what we've collected so far.
1948
+ sanitized_msg = _sanitize_exception_message(e)
1949
+ logger.warning(
1950
+ "Error during directory scan of %s: %s (returning %d files found so far)",
1951
+ directory.name,
1952
+ sanitized_msg,
1953
+ len(contract_files),
1954
+ extra={
1955
+ "directory": str(directory),
1956
+ "error": sanitized_msg,
1957
+ "error_code": EnumHandlerLoaderError.PERMISSION_DENIED.value,
1958
+ "files_found": len(contract_files),
1959
+ "correlation_id": str(correlation_id) if correlation_id else None,
1960
+ },
1961
+ )
1962
+
1963
+ # Log warning if limit was reached
1964
+ if limit_reached:
1965
+ logger.warning(
1966
+ "Handler discovery limit reached: stopped at %d handlers. "
1967
+ "Increase max_handlers to discover more.",
1968
+ max_handlers,
1969
+ extra={
1970
+ "max_handlers": max_handlers,
1971
+ "directory": str(directory),
1972
+ "correlation_id": str(correlation_id) if correlation_id else None,
1973
+ },
1974
+ )
1975
+
1976
+ # Detect directories with both contract types and fail fast on ambiguity
1977
+ # This is an O(n) check after discovery, not during, to avoid overhead
1978
+ # on every file. Build a map of parent_dir -> set of contract filenames.
1979
+ dir_to_contract_types: dict[Path, set[str]] = {}
1980
+ for path in contract_files:
1981
+ parent = path.parent
1982
+ if parent not in dir_to_contract_types:
1983
+ dir_to_contract_types[parent] = set()
1984
+ dir_to_contract_types[parent].add(path.name)
1985
+
1986
+ # Fail fast if any directory has both contract types (ambiguous configuration)
1987
+ for parent_dir, filenames in dir_to_contract_types.items():
1988
+ if len(filenames) > 1:
1989
+ # Use with_correlation() to ensure correlation_id is always present
1990
+ context = ModelInfraErrorContext.with_correlation(
1991
+ correlation_id=correlation_id,
1992
+ transport_type=EnumInfraTransportType.RUNTIME,
1993
+ operation="find_contract_files",
1994
+ )
1995
+ raise ProtocolConfigurationError(
1996
+ f"Ambiguous contract configuration in '{parent_dir.name}': "
1997
+ f"Found both '{HANDLER_CONTRACT_FILENAME}' and '{CONTRACT_YAML_FILENAME}'. "
1998
+ f"Use only ONE contract file per handler directory to avoid conflicts. "
1999
+ f"Total contracts discovered so far: {len(contract_files)}.",
2000
+ context=context,
2001
+ loader_error=EnumHandlerLoaderError.AMBIGUOUS_CONTRACT_CONFIGURATION.value,
2002
+ directory=str(parent_dir),
2003
+ contract_files=sorted(filenames),
2004
+ total_discovered=len(contract_files),
2005
+ )
2006
+
2007
+ # Deduplicate by resolved path
2008
+ seen: set[Path] = set()
2009
+ deduplicated: list[Path] = []
2010
+ for path in contract_files:
2011
+ # path.resolve() can raise OSError in several scenarios:
2012
+ # - Broken symlinks: symlink target no longer exists
2013
+ # - Race conditions: file deleted between glob discovery and resolution
2014
+ # - Permission issues: lacking read permission on parent directories
2015
+ # - Filesystem errors: unmounted volumes, network filesystem failures
2016
+ try:
2017
+ resolved = path.resolve()
2018
+ except OSError as e:
2019
+ # Sanitize exception message to prevent path disclosure
2020
+ sanitized_msg = _sanitize_exception_message(e)
2021
+ logger.warning(
2022
+ "Failed to resolve path %s: %s",
2023
+ path.name,
2024
+ sanitized_msg,
2025
+ extra={
2026
+ "path": str(path),
2027
+ "error": sanitized_msg,
2028
+ "correlation_id": str(correlation_id)
2029
+ if correlation_id
2030
+ else None,
2031
+ },
2032
+ )
2033
+ continue
2034
+ if resolved not in seen:
2035
+ seen.add(resolved)
2036
+ deduplicated.append(path)
2037
+
2038
+ return deduplicated
2039
+
2040
+
2041
+ __all__ = [
2042
+ "CONTRACT_YAML_FILENAME",
2043
+ "HANDLER_CONTRACT_FILENAME",
2044
+ "HandlerPluginLoader",
2045
+ "MAX_CONTRACT_SIZE",
2046
+ ]