omnibase_infra 0.2.1__py3-none-any.whl

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