omnibase_infra 0.2.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (833) hide show
  1. omnibase_infra/__init__.py +101 -0
  2. omnibase_infra/adapters/adapter_onex_tool_execution.py +451 -0
  3. omnibase_infra/capabilities/__init__.py +15 -0
  4. omnibase_infra/capabilities/capability_inference_rules.py +211 -0
  5. omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
  6. omnibase_infra/capabilities/intent_type_extractor.py +160 -0
  7. omnibase_infra/cli/__init__.py +1 -0
  8. omnibase_infra/cli/commands.py +216 -0
  9. omnibase_infra/clients/__init__.py +0 -0
  10. omnibase_infra/configs/widget_mapping.yaml +176 -0
  11. omnibase_infra/constants_topic_patterns.py +26 -0
  12. omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +264 -0
  13. omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +141 -0
  14. omnibase_infra/decorators/__init__.py +29 -0
  15. omnibase_infra/decorators/allow_any.py +109 -0
  16. omnibase_infra/dlq/__init__.py +90 -0
  17. omnibase_infra/dlq/constants_dlq.py +57 -0
  18. omnibase_infra/dlq/models/__init__.py +26 -0
  19. omnibase_infra/dlq/models/enum_replay_status.py +37 -0
  20. omnibase_infra/dlq/models/model_dlq_replay_record.py +135 -0
  21. omnibase_infra/dlq/models/model_dlq_tracking_config.py +184 -0
  22. omnibase_infra/dlq/service_dlq_tracking.py +611 -0
  23. omnibase_infra/enums/__init__.py +132 -0
  24. omnibase_infra/enums/enum_any_type_violation.py +104 -0
  25. omnibase_infra/enums/enum_backend_type.py +27 -0
  26. omnibase_infra/enums/enum_capture_outcome.py +42 -0
  27. omnibase_infra/enums/enum_capture_state.py +88 -0
  28. omnibase_infra/enums/enum_chain_violation_type.py +119 -0
  29. omnibase_infra/enums/enum_circuit_state.py +51 -0
  30. omnibase_infra/enums/enum_confirmation_event_type.py +27 -0
  31. omnibase_infra/enums/enum_consumer_group_purpose.py +92 -0
  32. omnibase_infra/enums/enum_contract_type.py +84 -0
  33. omnibase_infra/enums/enum_dedupe_strategy.py +46 -0
  34. omnibase_infra/enums/enum_dispatch_status.py +191 -0
  35. omnibase_infra/enums/enum_environment.py +46 -0
  36. omnibase_infra/enums/enum_execution_shape_violation.py +103 -0
  37. omnibase_infra/enums/enum_handler_error_type.py +111 -0
  38. omnibase_infra/enums/enum_handler_loader_error.py +178 -0
  39. omnibase_infra/enums/enum_handler_source_mode.py +86 -0
  40. omnibase_infra/enums/enum_handler_source_type.py +87 -0
  41. omnibase_infra/enums/enum_handler_type.py +77 -0
  42. omnibase_infra/enums/enum_handler_type_category.py +61 -0
  43. omnibase_infra/enums/enum_infra_transport_type.py +73 -0
  44. omnibase_infra/enums/enum_introspection_reason.py +154 -0
  45. omnibase_infra/enums/enum_kafka_acks.py +99 -0
  46. omnibase_infra/enums/enum_message_category.py +213 -0
  47. omnibase_infra/enums/enum_node_archetype.py +74 -0
  48. omnibase_infra/enums/enum_node_output_type.py +185 -0
  49. omnibase_infra/enums/enum_non_retryable_error_category.py +224 -0
  50. omnibase_infra/enums/enum_policy_type.py +32 -0
  51. omnibase_infra/enums/enum_registration_state.py +261 -0
  52. omnibase_infra/enums/enum_registration_status.py +33 -0
  53. omnibase_infra/enums/enum_registry_response_status.py +28 -0
  54. omnibase_infra/enums/enum_response_status.py +26 -0
  55. omnibase_infra/enums/enum_retry_error_category.py +98 -0
  56. omnibase_infra/enums/enum_security_rule_id.py +103 -0
  57. omnibase_infra/enums/enum_selection_strategy.py +91 -0
  58. omnibase_infra/enums/enum_topic_standard.py +42 -0
  59. omnibase_infra/enums/enum_validation_severity.py +78 -0
  60. omnibase_infra/errors/__init__.py +160 -0
  61. omnibase_infra/errors/error_architecture_violation.py +152 -0
  62. omnibase_infra/errors/error_binding_resolution.py +128 -0
  63. omnibase_infra/errors/error_chain_propagation.py +188 -0
  64. omnibase_infra/errors/error_compute_registry.py +95 -0
  65. omnibase_infra/errors/error_consul.py +132 -0
  66. omnibase_infra/errors/error_container_wiring.py +243 -0
  67. omnibase_infra/errors/error_event_bus_registry.py +105 -0
  68. omnibase_infra/errors/error_infra.py +610 -0
  69. omnibase_infra/errors/error_message_type_registry.py +101 -0
  70. omnibase_infra/errors/error_policy_registry.py +115 -0
  71. omnibase_infra/errors/error_vault.py +123 -0
  72. omnibase_infra/event_bus/__init__.py +72 -0
  73. omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +84 -0
  74. omnibase_infra/event_bus/event_bus_inmemory.py +797 -0
  75. omnibase_infra/event_bus/event_bus_kafka.py +1716 -0
  76. omnibase_infra/event_bus/mixin_kafka_broadcast.py +180 -0
  77. omnibase_infra/event_bus/mixin_kafka_dlq.py +771 -0
  78. omnibase_infra/event_bus/models/__init__.py +29 -0
  79. omnibase_infra/event_bus/models/config/__init__.py +20 -0
  80. omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +693 -0
  81. omnibase_infra/event_bus/models/model_dlq_event.py +206 -0
  82. omnibase_infra/event_bus/models/model_dlq_metrics.py +304 -0
  83. omnibase_infra/event_bus/models/model_event_headers.py +115 -0
  84. omnibase_infra/event_bus/models/model_event_message.py +60 -0
  85. omnibase_infra/event_bus/testing/__init__.py +26 -0
  86. omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
  87. omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
  88. omnibase_infra/event_bus/topic_constants.py +376 -0
  89. omnibase_infra/handlers/__init__.py +82 -0
  90. omnibase_infra/handlers/filesystem/__init__.py +48 -0
  91. omnibase_infra/handlers/filesystem/enum_file_system_operation.py +35 -0
  92. omnibase_infra/handlers/filesystem/model_file_system_request.py +298 -0
  93. omnibase_infra/handlers/filesystem/model_file_system_result.py +166 -0
  94. omnibase_infra/handlers/handler_consul.py +795 -0
  95. omnibase_infra/handlers/handler_db.py +1046 -0
  96. omnibase_infra/handlers/handler_filesystem.py +1478 -0
  97. omnibase_infra/handlers/handler_graph.py +2015 -0
  98. omnibase_infra/handlers/handler_http.py +926 -0
  99. omnibase_infra/handlers/handler_intent.py +387 -0
  100. omnibase_infra/handlers/handler_manifest_persistence.contract.yaml +184 -0
  101. omnibase_infra/handlers/handler_manifest_persistence.py +1539 -0
  102. omnibase_infra/handlers/handler_mcp.py +1430 -0
  103. omnibase_infra/handlers/handler_qdrant.py +1076 -0
  104. omnibase_infra/handlers/handler_vault.py +428 -0
  105. omnibase_infra/handlers/mcp/__init__.py +19 -0
  106. omnibase_infra/handlers/mcp/adapter_onex_to_mcp.py +446 -0
  107. omnibase_infra/handlers/mcp/protocols.py +178 -0
  108. omnibase_infra/handlers/mcp/transport_streamable_http.py +352 -0
  109. omnibase_infra/handlers/mixins/__init__.py +47 -0
  110. omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
  111. omnibase_infra/handlers/mixins/mixin_consul_kv.py +338 -0
  112. omnibase_infra/handlers/mixins/mixin_consul_service.py +542 -0
  113. omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -0
  114. omnibase_infra/handlers/mixins/mixin_vault_initialization.py +338 -0
  115. omnibase_infra/handlers/mixins/mixin_vault_retry.py +412 -0
  116. omnibase_infra/handlers/mixins/mixin_vault_secrets.py +450 -0
  117. omnibase_infra/handlers/mixins/mixin_vault_token.py +365 -0
  118. omnibase_infra/handlers/models/__init__.py +286 -0
  119. omnibase_infra/handlers/models/consul/__init__.py +81 -0
  120. omnibase_infra/handlers/models/consul/enum_consul_operation_type.py +57 -0
  121. omnibase_infra/handlers/models/consul/model_consul_deregister_payload.py +51 -0
  122. omnibase_infra/handlers/models/consul/model_consul_handler_config.py +153 -0
  123. omnibase_infra/handlers/models/consul/model_consul_handler_payload.py +89 -0
  124. omnibase_infra/handlers/models/consul/model_consul_kv_get_found_payload.py +55 -0
  125. omnibase_infra/handlers/models/consul/model_consul_kv_get_not_found_payload.py +49 -0
  126. omnibase_infra/handlers/models/consul/model_consul_kv_get_recurse_payload.py +50 -0
  127. omnibase_infra/handlers/models/consul/model_consul_kv_item.py +33 -0
  128. omnibase_infra/handlers/models/consul/model_consul_kv_put_payload.py +41 -0
  129. omnibase_infra/handlers/models/consul/model_consul_register_payload.py +53 -0
  130. omnibase_infra/handlers/models/consul/model_consul_retry_config.py +66 -0
  131. omnibase_infra/handlers/models/consul/model_payload_consul.py +66 -0
  132. omnibase_infra/handlers/models/consul/registry_payload_consul.py +214 -0
  133. omnibase_infra/handlers/models/graph/__init__.py +35 -0
  134. omnibase_infra/handlers/models/graph/enum_graph_operation_type.py +20 -0
  135. omnibase_infra/handlers/models/graph/model_graph_execute_payload.py +38 -0
  136. omnibase_infra/handlers/models/graph/model_graph_handler_config.py +54 -0
  137. omnibase_infra/handlers/models/graph/model_graph_handler_payload.py +44 -0
  138. omnibase_infra/handlers/models/graph/model_graph_query_payload.py +40 -0
  139. omnibase_infra/handlers/models/graph/model_graph_record.py +22 -0
  140. omnibase_infra/handlers/models/http/__init__.py +50 -0
  141. omnibase_infra/handlers/models/http/enum_http_operation_type.py +29 -0
  142. omnibase_infra/handlers/models/http/model_http_body_content.py +45 -0
  143. omnibase_infra/handlers/models/http/model_http_get_payload.py +88 -0
  144. omnibase_infra/handlers/models/http/model_http_handler_payload.py +90 -0
  145. omnibase_infra/handlers/models/http/model_http_post_payload.py +88 -0
  146. omnibase_infra/handlers/models/http/model_payload_http.py +66 -0
  147. omnibase_infra/handlers/models/http/registry_payload_http.py +212 -0
  148. omnibase_infra/handlers/models/mcp/__init__.py +23 -0
  149. omnibase_infra/handlers/models/mcp/enum_mcp_operation_type.py +24 -0
  150. omnibase_infra/handlers/models/mcp/model_mcp_handler_config.py +40 -0
  151. omnibase_infra/handlers/models/mcp/model_mcp_tool_call.py +32 -0
  152. omnibase_infra/handlers/models/mcp/model_mcp_tool_result.py +45 -0
  153. omnibase_infra/handlers/models/model_consul_handler_response.py +96 -0
  154. omnibase_infra/handlers/models/model_db_describe_response.py +83 -0
  155. omnibase_infra/handlers/models/model_db_query_payload.py +95 -0
  156. omnibase_infra/handlers/models/model_db_query_response.py +60 -0
  157. omnibase_infra/handlers/models/model_filesystem_config.py +98 -0
  158. omnibase_infra/handlers/models/model_filesystem_delete_payload.py +54 -0
  159. omnibase_infra/handlers/models/model_filesystem_delete_result.py +77 -0
  160. omnibase_infra/handlers/models/model_filesystem_directory_entry.py +75 -0
  161. omnibase_infra/handlers/models/model_filesystem_ensure_directory_payload.py +54 -0
  162. omnibase_infra/handlers/models/model_filesystem_ensure_directory_result.py +60 -0
  163. omnibase_infra/handlers/models/model_filesystem_list_directory_payload.py +60 -0
  164. omnibase_infra/handlers/models/model_filesystem_list_directory_result.py +68 -0
  165. omnibase_infra/handlers/models/model_filesystem_read_payload.py +62 -0
  166. omnibase_infra/handlers/models/model_filesystem_read_result.py +61 -0
  167. omnibase_infra/handlers/models/model_filesystem_write_payload.py +70 -0
  168. omnibase_infra/handlers/models/model_filesystem_write_result.py +55 -0
  169. omnibase_infra/handlers/models/model_graph_handler_response.py +98 -0
  170. omnibase_infra/handlers/models/model_handler_response.py +103 -0
  171. omnibase_infra/handlers/models/model_http_handler_response.py +101 -0
  172. omnibase_infra/handlers/models/model_manifest_metadata.py +75 -0
  173. omnibase_infra/handlers/models/model_manifest_persistence_config.py +62 -0
  174. omnibase_infra/handlers/models/model_manifest_query_payload.py +90 -0
  175. omnibase_infra/handlers/models/model_manifest_query_result.py +97 -0
  176. omnibase_infra/handlers/models/model_manifest_retrieve_payload.py +44 -0
  177. omnibase_infra/handlers/models/model_manifest_retrieve_result.py +98 -0
  178. omnibase_infra/handlers/models/model_manifest_store_payload.py +47 -0
  179. omnibase_infra/handlers/models/model_manifest_store_result.py +67 -0
  180. omnibase_infra/handlers/models/model_operation_context.py +187 -0
  181. omnibase_infra/handlers/models/model_qdrant_handler_response.py +98 -0
  182. omnibase_infra/handlers/models/model_retry_state.py +162 -0
  183. omnibase_infra/handlers/models/model_vault_handler_response.py +98 -0
  184. omnibase_infra/handlers/models/qdrant/__init__.py +44 -0
  185. omnibase_infra/handlers/models/qdrant/enum_qdrant_operation_type.py +26 -0
  186. omnibase_infra/handlers/models/qdrant/model_qdrant_collection_payload.py +42 -0
  187. omnibase_infra/handlers/models/qdrant/model_qdrant_delete_payload.py +36 -0
  188. omnibase_infra/handlers/models/qdrant/model_qdrant_handler_config.py +42 -0
  189. omnibase_infra/handlers/models/qdrant/model_qdrant_handler_payload.py +54 -0
  190. omnibase_infra/handlers/models/qdrant/model_qdrant_search_payload.py +42 -0
  191. omnibase_infra/handlers/models/qdrant/model_qdrant_search_result.py +30 -0
  192. omnibase_infra/handlers/models/qdrant/model_qdrant_upsert_payload.py +36 -0
  193. omnibase_infra/handlers/models/vault/__init__.py +69 -0
  194. omnibase_infra/handlers/models/vault/enum_vault_operation_type.py +35 -0
  195. omnibase_infra/handlers/models/vault/model_payload_vault.py +66 -0
  196. omnibase_infra/handlers/models/vault/model_vault_delete_payload.py +57 -0
  197. omnibase_infra/handlers/models/vault/model_vault_handler_config.py +148 -0
  198. omnibase_infra/handlers/models/vault/model_vault_handler_payload.py +101 -0
  199. omnibase_infra/handlers/models/vault/model_vault_list_payload.py +58 -0
  200. omnibase_infra/handlers/models/vault/model_vault_renew_token_payload.py +67 -0
  201. omnibase_infra/handlers/models/vault/model_vault_retry_config.py +66 -0
  202. omnibase_infra/handlers/models/vault/model_vault_secret_payload.py +106 -0
  203. omnibase_infra/handlers/models/vault/model_vault_write_payload.py +66 -0
  204. omnibase_infra/handlers/models/vault/registry_payload_vault.py +213 -0
  205. omnibase_infra/handlers/registration_storage/__init__.py +43 -0
  206. omnibase_infra/handlers/registration_storage/handler_registration_storage_mock.py +392 -0
  207. omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +922 -0
  208. omnibase_infra/handlers/registration_storage/models/__init__.py +23 -0
  209. omnibase_infra/handlers/registration_storage/models/model_delete_registration_request.py +58 -0
  210. omnibase_infra/handlers/registration_storage/models/model_update_registration_request.py +73 -0
  211. omnibase_infra/handlers/registration_storage/protocol_registration_persistence.py +191 -0
  212. omnibase_infra/handlers/service_discovery/__init__.py +43 -0
  213. omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +1051 -0
  214. omnibase_infra/handlers/service_discovery/handler_service_discovery_mock.py +258 -0
  215. omnibase_infra/handlers/service_discovery/models/__init__.py +22 -0
  216. omnibase_infra/handlers/service_discovery/models/model_discovery_result.py +64 -0
  217. omnibase_infra/handlers/service_discovery/models/model_registration_result.py +138 -0
  218. omnibase_infra/handlers/service_discovery/models/model_service_info.py +109 -0
  219. omnibase_infra/handlers/service_discovery/protocol_discovery_operations.py +170 -0
  220. omnibase_infra/idempotency/__init__.py +94 -0
  221. omnibase_infra/idempotency/models/__init__.py +43 -0
  222. omnibase_infra/idempotency/models/model_idempotency_check_result.py +85 -0
  223. omnibase_infra/idempotency/models/model_idempotency_guard_config.py +130 -0
  224. omnibase_infra/idempotency/models/model_idempotency_record.py +86 -0
  225. omnibase_infra/idempotency/models/model_idempotency_store_health_check_result.py +81 -0
  226. omnibase_infra/idempotency/models/model_idempotency_store_metrics.py +140 -0
  227. omnibase_infra/idempotency/models/model_postgres_idempotency_store_config.py +299 -0
  228. omnibase_infra/idempotency/protocol_idempotency_store.py +184 -0
  229. omnibase_infra/idempotency/store_inmemory.py +265 -0
  230. omnibase_infra/idempotency/store_postgres.py +923 -0
  231. omnibase_infra/infrastructure/__init__.py +0 -0
  232. omnibase_infra/migrations/001_create_event_ledger.sql +166 -0
  233. omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
  234. omnibase_infra/mixins/__init__.py +71 -0
  235. omnibase_infra/mixins/mixin_async_circuit_breaker.py +656 -0
  236. omnibase_infra/mixins/mixin_dict_like_accessors.py +146 -0
  237. omnibase_infra/mixins/mixin_envelope_extraction.py +119 -0
  238. omnibase_infra/mixins/mixin_node_introspection.py +2670 -0
  239. omnibase_infra/mixins/mixin_retry_execution.py +386 -0
  240. omnibase_infra/mixins/protocol_circuit_breaker_aware.py +133 -0
  241. omnibase_infra/models/__init__.py +144 -0
  242. omnibase_infra/models/bindings/__init__.py +59 -0
  243. omnibase_infra/models/bindings/constants.py +144 -0
  244. omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
  245. omnibase_infra/models/bindings/model_operation_binding.py +44 -0
  246. omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
  247. omnibase_infra/models/bindings/model_parsed_binding.py +52 -0
  248. omnibase_infra/models/corpus/__init__.py +17 -0
  249. omnibase_infra/models/corpus/model_capture_config.py +133 -0
  250. omnibase_infra/models/corpus/model_capture_result.py +86 -0
  251. omnibase_infra/models/discovery/__init__.py +42 -0
  252. omnibase_infra/models/discovery/model_dependency_spec.py +319 -0
  253. omnibase_infra/models/discovery/model_discovered_capabilities.py +50 -0
  254. omnibase_infra/models/discovery/model_introspection_config.py +330 -0
  255. omnibase_infra/models/discovery/model_introspection_performance_metrics.py +169 -0
  256. omnibase_infra/models/discovery/model_introspection_task_config.py +116 -0
  257. omnibase_infra/models/dispatch/__init__.py +155 -0
  258. omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -0
  259. omnibase_infra/models/dispatch/model_dispatch_context.py +439 -0
  260. omnibase_infra/models/dispatch/model_dispatch_error.py +336 -0
  261. omnibase_infra/models/dispatch/model_dispatch_log_context.py +400 -0
  262. omnibase_infra/models/dispatch/model_dispatch_metadata.py +228 -0
  263. omnibase_infra/models/dispatch/model_dispatch_metrics.py +496 -0
  264. omnibase_infra/models/dispatch/model_dispatch_outcome.py +317 -0
  265. omnibase_infra/models/dispatch/model_dispatch_outputs.py +231 -0
  266. omnibase_infra/models/dispatch/model_dispatch_result.py +436 -0
  267. omnibase_infra/models/dispatch/model_dispatch_route.py +279 -0
  268. omnibase_infra/models/dispatch/model_dispatcher_metrics.py +275 -0
  269. omnibase_infra/models/dispatch/model_dispatcher_registration.py +352 -0
  270. omnibase_infra/models/dispatch/model_materialized_dispatch.py +141 -0
  271. omnibase_infra/models/dispatch/model_parsed_topic.py +135 -0
  272. omnibase_infra/models/dispatch/model_topic_parser.py +725 -0
  273. omnibase_infra/models/dispatch/model_tracing_context.py +285 -0
  274. omnibase_infra/models/errors/__init__.py +45 -0
  275. omnibase_infra/models/errors/model_handler_validation_error.py +594 -0
  276. omnibase_infra/models/errors/model_infra_error_context.py +99 -0
  277. omnibase_infra/models/errors/model_message_type_registry_error_context.py +71 -0
  278. omnibase_infra/models/errors/model_timeout_error_context.py +110 -0
  279. omnibase_infra/models/handlers/__init__.py +80 -0
  280. omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
  281. omnibase_infra/models/handlers/model_contract_discovery_result.py +82 -0
  282. omnibase_infra/models/handlers/model_handler_descriptor.py +200 -0
  283. omnibase_infra/models/handlers/model_handler_identifier.py +215 -0
  284. omnibase_infra/models/handlers/model_handler_source_config.py +220 -0
  285. omnibase_infra/models/health/__init__.py +9 -0
  286. omnibase_infra/models/health/model_health_check_result.py +40 -0
  287. omnibase_infra/models/lifecycle/__init__.py +39 -0
  288. omnibase_infra/models/logging/__init__.py +51 -0
  289. omnibase_infra/models/logging/model_log_context.py +756 -0
  290. omnibase_infra/models/mcp/__init__.py +15 -0
  291. omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
  292. omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
  293. omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
  294. omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
  295. omnibase_infra/models/model_node_identity.py +126 -0
  296. omnibase_infra/models/model_retry_error_classification.py +78 -0
  297. omnibase_infra/models/projection/__init__.py +43 -0
  298. omnibase_infra/models/projection/model_capability_fields.py +112 -0
  299. omnibase_infra/models/projection/model_registration_projection.py +434 -0
  300. omnibase_infra/models/projection/model_registration_snapshot.py +322 -0
  301. omnibase_infra/models/projection/model_sequence_info.py +182 -0
  302. omnibase_infra/models/projection/model_snapshot_topic_config.py +591 -0
  303. omnibase_infra/models/projectors/__init__.py +41 -0
  304. omnibase_infra/models/projectors/model_projector_column.py +289 -0
  305. omnibase_infra/models/projectors/model_projector_discovery_result.py +65 -0
  306. omnibase_infra/models/projectors/model_projector_index.py +270 -0
  307. omnibase_infra/models/projectors/model_projector_schema.py +415 -0
  308. omnibase_infra/models/projectors/model_projector_validation_error.py +63 -0
  309. omnibase_infra/models/projectors/util_sql_identifiers.py +115 -0
  310. omnibase_infra/models/registration/__init__.py +68 -0
  311. omnibase_infra/models/registration/commands/__init__.py +15 -0
  312. omnibase_infra/models/registration/commands/model_node_registration_acked.py +108 -0
  313. omnibase_infra/models/registration/events/__init__.py +56 -0
  314. omnibase_infra/models/registration/events/model_node_became_active.py +103 -0
  315. omnibase_infra/models/registration/events/model_node_liveness_expired.py +103 -0
  316. omnibase_infra/models/registration/events/model_node_registration_accepted.py +98 -0
  317. omnibase_infra/models/registration/events/model_node_registration_ack_received.py +98 -0
  318. omnibase_infra/models/registration/events/model_node_registration_ack_timed_out.py +112 -0
  319. omnibase_infra/models/registration/events/model_node_registration_initiated.py +107 -0
  320. omnibase_infra/models/registration/events/model_node_registration_rejected.py +104 -0
  321. omnibase_infra/models/registration/model_event_bus_topic_entry.py +59 -0
  322. omnibase_infra/models/registration/model_introspection_metrics.py +253 -0
  323. omnibase_infra/models/registration/model_node_capabilities.py +190 -0
  324. omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
  325. omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
  326. omnibase_infra/models/registration/model_node_introspection_event.py +195 -0
  327. omnibase_infra/models/registration/model_node_metadata.py +79 -0
  328. omnibase_infra/models/registration/model_node_registration.py +162 -0
  329. omnibase_infra/models/registration/model_node_registration_record.py +162 -0
  330. omnibase_infra/models/registry/__init__.py +29 -0
  331. omnibase_infra/models/registry/model_domain_constraint.py +202 -0
  332. omnibase_infra/models/registry/model_message_type_entry.py +271 -0
  333. omnibase_infra/models/resilience/__init__.py +9 -0
  334. omnibase_infra/models/resilience/model_circuit_breaker_config.py +227 -0
  335. omnibase_infra/models/routing/__init__.py +25 -0
  336. omnibase_infra/models/routing/model_routing_entry.py +52 -0
  337. omnibase_infra/models/routing/model_routing_subcontract.py +70 -0
  338. omnibase_infra/models/runtime/__init__.py +49 -0
  339. omnibase_infra/models/runtime/model_contract_security_config.py +41 -0
  340. omnibase_infra/models/runtime/model_discovery_error.py +81 -0
  341. omnibase_infra/models/runtime/model_discovery_result.py +162 -0
  342. omnibase_infra/models/runtime/model_discovery_warning.py +74 -0
  343. omnibase_infra/models/runtime/model_failed_plugin_load.py +63 -0
  344. omnibase_infra/models/runtime/model_handler_contract.py +296 -0
  345. omnibase_infra/models/runtime/model_loaded_handler.py +129 -0
  346. omnibase_infra/models/runtime/model_plugin_load_context.py +93 -0
  347. omnibase_infra/models/runtime/model_plugin_load_summary.py +124 -0
  348. omnibase_infra/models/security/__init__.py +50 -0
  349. omnibase_infra/models/security/classification_levels.py +99 -0
  350. omnibase_infra/models/security/model_environment_policy.py +145 -0
  351. omnibase_infra/models/security/model_handler_security_policy.py +107 -0
  352. omnibase_infra/models/security/model_security_error.py +81 -0
  353. omnibase_infra/models/security/model_security_validation_result.py +328 -0
  354. omnibase_infra/models/security/model_security_warning.py +67 -0
  355. omnibase_infra/models/snapshot/__init__.py +27 -0
  356. omnibase_infra/models/snapshot/model_field_change.py +65 -0
  357. omnibase_infra/models/snapshot/model_snapshot.py +270 -0
  358. omnibase_infra/models/snapshot/model_snapshot_diff.py +203 -0
  359. omnibase_infra/models/snapshot/model_subject_ref.py +81 -0
  360. omnibase_infra/models/types/__init__.py +71 -0
  361. omnibase_infra/models/validation/__init__.py +89 -0
  362. omnibase_infra/models/validation/model_any_type_validation_result.py +118 -0
  363. omnibase_infra/models/validation/model_any_type_violation.py +141 -0
  364. omnibase_infra/models/validation/model_category_match_result.py +345 -0
  365. omnibase_infra/models/validation/model_chain_violation.py +166 -0
  366. omnibase_infra/models/validation/model_coverage_metrics.py +316 -0
  367. omnibase_infra/models/validation/model_execution_shape_rule.py +159 -0
  368. omnibase_infra/models/validation/model_execution_shape_validation.py +208 -0
  369. omnibase_infra/models/validation/model_execution_shape_validation_result.py +294 -0
  370. omnibase_infra/models/validation/model_execution_shape_violation.py +122 -0
  371. omnibase_infra/models/validation/model_localhandler_validation_result.py +139 -0
  372. omnibase_infra/models/validation/model_localhandler_violation.py +100 -0
  373. omnibase_infra/models/validation/model_output_validation_params.py +74 -0
  374. omnibase_infra/models/validation/model_validate_and_raise_params.py +84 -0
  375. omnibase_infra/models/validation/model_validation_error_params.py +84 -0
  376. omnibase_infra/models/validation/model_validation_outcome.py +287 -0
  377. omnibase_infra/nodes/__init__.py +57 -0
  378. omnibase_infra/nodes/architecture_validator/__init__.py +79 -0
  379. omnibase_infra/nodes/architecture_validator/contract.yaml +252 -0
  380. omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +203 -0
  381. omnibase_infra/nodes/architecture_validator/mixins/__init__.py +16 -0
  382. omnibase_infra/nodes/architecture_validator/mixins/mixin_file_path_rule.py +92 -0
  383. omnibase_infra/nodes/architecture_validator/models/__init__.py +36 -0
  384. omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_request.py +56 -0
  385. omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_result.py +311 -0
  386. omnibase_infra/nodes/architecture_validator/models/model_architecture_violation.py +163 -0
  387. omnibase_infra/nodes/architecture_validator/models/model_rule_check_result.py +265 -0
  388. omnibase_infra/nodes/architecture_validator/models/model_validation_request.py +105 -0
  389. omnibase_infra/nodes/architecture_validator/models/model_validation_result.py +314 -0
  390. omnibase_infra/nodes/architecture_validator/node.py +262 -0
  391. omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +383 -0
  392. omnibase_infra/nodes/architecture_validator/protocols/__init__.py +9 -0
  393. omnibase_infra/nodes/architecture_validator/protocols/protocol_architecture_rule.py +225 -0
  394. omnibase_infra/nodes/architecture_validator/registry/__init__.py +28 -0
  395. omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +106 -0
  396. omnibase_infra/nodes/architecture_validator/validators/__init__.py +104 -0
  397. omnibase_infra/nodes/architecture_validator/validators/validator_no_direct_dispatch.py +422 -0
  398. omnibase_infra/nodes/architecture_validator/validators/validator_no_handler_publishing.py +481 -0
  399. omnibase_infra/nodes/architecture_validator/validators/validator_no_orchestrator_fsm.py +491 -0
  400. omnibase_infra/nodes/contract_registry_reducer/__init__.py +29 -0
  401. omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
  402. omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
  403. omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
  404. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
  405. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
  406. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
  407. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
  408. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
  409. omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
  410. omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
  411. omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
  412. omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
  413. omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
  414. omnibase_infra/nodes/effects/README.md +358 -0
  415. omnibase_infra/nodes/effects/__init__.py +26 -0
  416. omnibase_infra/nodes/effects/contract.yaml +167 -0
  417. omnibase_infra/nodes/effects/models/__init__.py +32 -0
  418. omnibase_infra/nodes/effects/models/model_backend_result.py +190 -0
  419. omnibase_infra/nodes/effects/models/model_effect_idempotency_config.py +92 -0
  420. omnibase_infra/nodes/effects/models/model_registry_request.py +132 -0
  421. omnibase_infra/nodes/effects/models/model_registry_response.py +263 -0
  422. omnibase_infra/nodes/effects/protocol_consul_client.py +89 -0
  423. omnibase_infra/nodes/effects/protocol_effect_idempotency_store.py +143 -0
  424. omnibase_infra/nodes/effects/protocol_postgres_adapter.py +96 -0
  425. omnibase_infra/nodes/effects/registry_effect.py +525 -0
  426. omnibase_infra/nodes/effects/store_effect_idempotency_inmemory.py +425 -0
  427. omnibase_infra/nodes/handlers/consul/contract.yaml +85 -0
  428. omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
  429. omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
  430. omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
  431. omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
  432. omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
  433. omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
  434. omnibase_infra/nodes/node_intent_storage_effect/__init__.py +50 -0
  435. omnibase_infra/nodes/node_intent_storage_effect/contract.yaml +194 -0
  436. omnibase_infra/nodes/node_intent_storage_effect/models/__init__.py +24 -0
  437. omnibase_infra/nodes/node_intent_storage_effect/models/model_intent_storage_input.py +141 -0
  438. omnibase_infra/nodes/node_intent_storage_effect/models/model_intent_storage_output.py +130 -0
  439. omnibase_infra/nodes/node_intent_storage_effect/node.py +94 -0
  440. omnibase_infra/nodes/node_intent_storage_effect/registry/__init__.py +35 -0
  441. omnibase_infra/nodes/node_intent_storage_effect/registry/registry_infra_intent_storage.py +294 -0
  442. omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
  443. omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
  444. omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
  445. omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
  446. omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
  447. omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
  448. omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
  449. omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
  450. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
  451. omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
  452. omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
  453. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
  454. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
  455. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
  456. omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
  457. omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
  458. omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
  459. omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
  460. omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
  461. omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -0
  462. omnibase_infra/nodes/node_registration_orchestrator/README.md +542 -0
  463. omnibase_infra/nodes/node_registration_orchestrator/__init__.py +120 -0
  464. omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +482 -0
  465. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/__init__.py +53 -0
  466. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_introspected.py +376 -0
  467. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_registration_acked.py +376 -0
  468. omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_runtime_tick.py +373 -0
  469. omnibase_infra/nodes/node_registration_orchestrator/handlers/__init__.py +62 -0
  470. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_heartbeat.py +376 -0
  471. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +694 -0
  472. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_registration_acked.py +458 -0
  473. omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_runtime_tick.py +364 -0
  474. omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +544 -0
  475. omnibase_infra/nodes/node_registration_orchestrator/models/__init__.py +75 -0
  476. omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_intent_payload.py +194 -0
  477. omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_registration_intent.py +67 -0
  478. omnibase_infra/nodes/node_registration_orchestrator/models/model_intent_execution_result.py +50 -0
  479. omnibase_infra/nodes/node_registration_orchestrator/models/model_node_liveness_expired.py +107 -0
  480. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_config.py +67 -0
  481. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_input.py +41 -0
  482. omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_output.py +166 -0
  483. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +235 -0
  484. omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_upsert_intent.py +68 -0
  485. omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_execution_result.py +384 -0
  486. omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_state.py +60 -0
  487. omnibase_infra/nodes/node_registration_orchestrator/models/model_registration_intent.py +177 -0
  488. omnibase_infra/nodes/node_registration_orchestrator/models/model_registry_intent.py +247 -0
  489. omnibase_infra/nodes/node_registration_orchestrator/node.py +195 -0
  490. omnibase_infra/nodes/node_registration_orchestrator/plugin.py +909 -0
  491. omnibase_infra/nodes/node_registration_orchestrator/protocols.py +439 -0
  492. omnibase_infra/nodes/node_registration_orchestrator/registry/__init__.py +41 -0
  493. omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +528 -0
  494. omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +393 -0
  495. omnibase_infra/nodes/node_registration_orchestrator/wiring.py +743 -0
  496. omnibase_infra/nodes/node_registration_reducer/__init__.py +15 -0
  497. omnibase_infra/nodes/node_registration_reducer/contract.yaml +301 -0
  498. omnibase_infra/nodes/node_registration_reducer/models/__init__.py +38 -0
  499. omnibase_infra/nodes/node_registration_reducer/models/model_validation_result.py +113 -0
  500. omnibase_infra/nodes/node_registration_reducer/node.py +139 -0
  501. omnibase_infra/nodes/node_registration_reducer/registry/__init__.py +9 -0
  502. omnibase_infra/nodes/node_registration_reducer/registry/registry_infra_node_registration_reducer.py +79 -0
  503. omnibase_infra/nodes/node_registration_storage_effect/__init__.py +41 -0
  504. omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +220 -0
  505. omnibase_infra/nodes/node_registration_storage_effect/models/__init__.py +44 -0
  506. omnibase_infra/nodes/node_registration_storage_effect/models/model_delete_result.py +132 -0
  507. omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_record.py +199 -0
  508. omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_update.py +155 -0
  509. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_details.py +123 -0
  510. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_result.py +117 -0
  511. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_query.py +100 -0
  512. omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_result.py +136 -0
  513. omnibase_infra/nodes/node_registration_storage_effect/models/model_upsert_result.py +127 -0
  514. omnibase_infra/nodes/node_registration_storage_effect/node.py +112 -0
  515. omnibase_infra/nodes/node_registration_storage_effect/protocols/__init__.py +22 -0
  516. omnibase_infra/nodes/node_registration_storage_effect/protocols/protocol_registration_persistence.py +333 -0
  517. omnibase_infra/nodes/node_registration_storage_effect/registry/__init__.py +23 -0
  518. omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +215 -0
  519. omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
  520. omnibase_infra/nodes/node_registry_effect/contract.yaml +677 -0
  521. omnibase_infra/nodes/node_registry_effect/handlers/__init__.py +70 -0
  522. omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_deregister.py +211 -0
  523. omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_register.py +212 -0
  524. omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +417 -0
  525. omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_deactivate.py +215 -0
  526. omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_upsert.py +208 -0
  527. omnibase_infra/nodes/node_registry_effect/models/__init__.py +43 -0
  528. omnibase_infra/nodes/node_registry_effect/models/model_partial_retry_request.py +92 -0
  529. omnibase_infra/nodes/node_registry_effect/node.py +165 -0
  530. omnibase_infra/nodes/node_registry_effect/registry/__init__.py +27 -0
  531. omnibase_infra/nodes/node_registry_effect/registry/registry_infra_registry_effect.py +196 -0
  532. omnibase_infra/nodes/node_service_discovery_effect/__init__.py +111 -0
  533. omnibase_infra/nodes/node_service_discovery_effect/contract.yaml +246 -0
  534. omnibase_infra/nodes/node_service_discovery_effect/models/__init__.py +67 -0
  535. omnibase_infra/nodes/node_service_discovery_effect/models/enum_health_status.py +72 -0
  536. omnibase_infra/nodes/node_service_discovery_effect/models/enum_service_discovery_operation.py +58 -0
  537. omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_query.py +99 -0
  538. omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_result.py +98 -0
  539. omnibase_infra/nodes/node_service_discovery_effect/models/model_health_check_config.py +121 -0
  540. omnibase_infra/nodes/node_service_discovery_effect/models/model_query_metadata.py +63 -0
  541. omnibase_infra/nodes/node_service_discovery_effect/models/model_registration_result.py +130 -0
  542. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_details.py +111 -0
  543. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_result.py +119 -0
  544. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_info.py +106 -0
  545. omnibase_infra/nodes/node_service_discovery_effect/models/model_service_registration.py +121 -0
  546. omnibase_infra/nodes/node_service_discovery_effect/node.py +111 -0
  547. omnibase_infra/nodes/node_service_discovery_effect/protocols/__init__.py +14 -0
  548. omnibase_infra/nodes/node_service_discovery_effect/protocols/protocol_discovery_operations.py +279 -0
  549. omnibase_infra/nodes/node_service_discovery_effect/registry/__init__.py +13 -0
  550. omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +222 -0
  551. omnibase_infra/nodes/reducers/__init__.py +30 -0
  552. omnibase_infra/nodes/reducers/models/__init__.py +37 -0
  553. omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +87 -0
  554. omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -0
  555. omnibase_infra/nodes/reducers/models/model_payload_postgres_upsert_registration.py +60 -0
  556. omnibase_infra/nodes/reducers/models/model_registration_confirmation.py +166 -0
  557. omnibase_infra/nodes/reducers/models/model_registration_state.py +433 -0
  558. omnibase_infra/nodes/reducers/registration_reducer.py +1138 -0
  559. omnibase_infra/observability/__init__.py +143 -0
  560. omnibase_infra/observability/constants_metrics.py +91 -0
  561. omnibase_infra/observability/factory_observability_sink.py +525 -0
  562. omnibase_infra/observability/handlers/__init__.py +118 -0
  563. omnibase_infra/observability/handlers/handler_logging_structured.py +967 -0
  564. omnibase_infra/observability/handlers/handler_metrics_prometheus.py +1120 -0
  565. omnibase_infra/observability/handlers/model_logging_handler_config.py +71 -0
  566. omnibase_infra/observability/handlers/model_logging_handler_response.py +77 -0
  567. omnibase_infra/observability/handlers/model_metrics_handler_config.py +172 -0
  568. omnibase_infra/observability/handlers/model_metrics_handler_payload.py +135 -0
  569. omnibase_infra/observability/handlers/model_metrics_handler_response.py +101 -0
  570. omnibase_infra/observability/hooks/__init__.py +74 -0
  571. omnibase_infra/observability/hooks/hook_observability.py +1223 -0
  572. omnibase_infra/observability/models/__init__.py +30 -0
  573. omnibase_infra/observability/models/enum_required_log_context_key.py +77 -0
  574. omnibase_infra/observability/models/model_buffered_log_entry.py +117 -0
  575. omnibase_infra/observability/models/model_logging_sink_config.py +73 -0
  576. omnibase_infra/observability/models/model_metrics_sink_config.py +156 -0
  577. omnibase_infra/observability/sinks/__init__.py +69 -0
  578. omnibase_infra/observability/sinks/sink_logging_structured.py +809 -0
  579. omnibase_infra/observability/sinks/sink_metrics_prometheus.py +710 -0
  580. omnibase_infra/plugins/__init__.py +27 -0
  581. omnibase_infra/plugins/examples/__init__.py +28 -0
  582. omnibase_infra/plugins/examples/plugin_json_normalizer.py +271 -0
  583. omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +210 -0
  584. omnibase_infra/plugins/models/__init__.py +21 -0
  585. omnibase_infra/plugins/models/model_plugin_context.py +76 -0
  586. omnibase_infra/plugins/models/model_plugin_input_data.py +58 -0
  587. omnibase_infra/plugins/models/model_plugin_output_data.py +62 -0
  588. omnibase_infra/plugins/plugin_compute_base.py +449 -0
  589. omnibase_infra/projectors/__init__.py +30 -0
  590. omnibase_infra/projectors/contracts/__init__.py +63 -0
  591. omnibase_infra/projectors/contracts/registration_projector.yaml +370 -0
  592. omnibase_infra/projectors/projection_reader_registration.py +1559 -0
  593. omnibase_infra/projectors/snapshot_publisher_registration.py +1329 -0
  594. omnibase_infra/protocols/__init__.py +104 -0
  595. omnibase_infra/protocols/protocol_capability_projection.py +253 -0
  596. omnibase_infra/protocols/protocol_capability_query.py +251 -0
  597. omnibase_infra/protocols/protocol_container_aware.py +200 -0
  598. omnibase_infra/protocols/protocol_dispatch_engine.py +152 -0
  599. omnibase_infra/protocols/protocol_event_bus_like.py +127 -0
  600. omnibase_infra/protocols/protocol_event_projector.py +96 -0
  601. omnibase_infra/protocols/protocol_idempotency_store.py +142 -0
  602. omnibase_infra/protocols/protocol_message_dispatcher.py +247 -0
  603. omnibase_infra/protocols/protocol_message_type_registry.py +306 -0
  604. omnibase_infra/protocols/protocol_plugin_compute.py +368 -0
  605. omnibase_infra/protocols/protocol_projector_schema_validator.py +82 -0
  606. omnibase_infra/protocols/protocol_registry_metrics.py +215 -0
  607. omnibase_infra/protocols/protocol_snapshot_publisher.py +396 -0
  608. omnibase_infra/protocols/protocol_snapshot_store.py +567 -0
  609. omnibase_infra/runtime/__init__.py +445 -0
  610. omnibase_infra/runtime/binding_config_resolver.py +2771 -0
  611. omnibase_infra/runtime/binding_resolver.py +753 -0
  612. omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
  613. omnibase_infra/runtime/constants_notification.py +75 -0
  614. omnibase_infra/runtime/constants_security.py +70 -0
  615. omnibase_infra/runtime/contract_handler_discovery.py +587 -0
  616. omnibase_infra/runtime/contract_loaders/__init__.py +51 -0
  617. omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
  618. omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
  619. omnibase_infra/runtime/dispatch_context_enforcer.py +427 -0
  620. omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
  621. omnibase_infra/runtime/emit_daemon/cli.py +844 -0
  622. omnibase_infra/runtime/emit_daemon/client.py +811 -0
  623. omnibase_infra/runtime/emit_daemon/config.py +535 -0
  624. omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
  625. omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
  626. omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
  627. omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
  628. omnibase_infra/runtime/emit_daemon/queue.py +618 -0
  629. omnibase_infra/runtime/enums/__init__.py +18 -0
  630. omnibase_infra/runtime/enums/enum_config_ref_scheme.py +33 -0
  631. omnibase_infra/runtime/enums/enum_scheduler_status.py +170 -0
  632. omnibase_infra/runtime/envelope_validator.py +179 -0
  633. omnibase_infra/runtime/event_bus_subcontract_wiring.py +466 -0
  634. omnibase_infra/runtime/handler_bootstrap_source.py +507 -0
  635. omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
  636. omnibase_infra/runtime/handler_contract_source.py +750 -0
  637. omnibase_infra/runtime/handler_identity.py +81 -0
  638. omnibase_infra/runtime/handler_plugin_loader.py +2046 -0
  639. omnibase_infra/runtime/handler_registry.py +329 -0
  640. omnibase_infra/runtime/handler_source_resolver.py +367 -0
  641. omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
  642. omnibase_infra/runtime/kafka_contract_source.py +984 -0
  643. omnibase_infra/runtime/kernel.py +40 -0
  644. omnibase_infra/runtime/mixin_policy_validation.py +522 -0
  645. omnibase_infra/runtime/mixin_semver_cache.py +402 -0
  646. omnibase_infra/runtime/mixins/__init__.py +24 -0
  647. omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
  648. omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +778 -0
  649. omnibase_infra/runtime/models/__init__.py +229 -0
  650. omnibase_infra/runtime/models/model_batch_lifecycle_result.py +217 -0
  651. omnibase_infra/runtime/models/model_binding_config.py +168 -0
  652. omnibase_infra/runtime/models/model_binding_config_cache_stats.py +135 -0
  653. omnibase_infra/runtime/models/model_binding_config_resolver_config.py +329 -0
  654. omnibase_infra/runtime/models/model_cached_secret.py +138 -0
  655. omnibase_infra/runtime/models/model_compute_key.py +138 -0
  656. omnibase_infra/runtime/models/model_compute_registration.py +97 -0
  657. omnibase_infra/runtime/models/model_config_cache_entry.py +61 -0
  658. omnibase_infra/runtime/models/model_config_ref.py +331 -0
  659. omnibase_infra/runtime/models/model_config_ref_parse_result.py +125 -0
  660. omnibase_infra/runtime/models/model_contract_load_result.py +224 -0
  661. omnibase_infra/runtime/models/model_domain_plugin_config.py +92 -0
  662. omnibase_infra/runtime/models/model_domain_plugin_result.py +270 -0
  663. omnibase_infra/runtime/models/model_duplicate_response.py +54 -0
  664. omnibase_infra/runtime/models/model_enabled_protocols_config.py +61 -0
  665. omnibase_infra/runtime/models/model_event_bus_config.py +54 -0
  666. omnibase_infra/runtime/models/model_failed_component.py +55 -0
  667. omnibase_infra/runtime/models/model_health_check_response.py +168 -0
  668. omnibase_infra/runtime/models/model_health_check_result.py +229 -0
  669. omnibase_infra/runtime/models/model_lifecycle_result.py +245 -0
  670. omnibase_infra/runtime/models/model_logging_config.py +42 -0
  671. omnibase_infra/runtime/models/model_optional_correlation_id.py +167 -0
  672. omnibase_infra/runtime/models/model_optional_string.py +94 -0
  673. omnibase_infra/runtime/models/model_optional_uuid.py +110 -0
  674. omnibase_infra/runtime/models/model_policy_context.py +100 -0
  675. omnibase_infra/runtime/models/model_policy_key.py +138 -0
  676. omnibase_infra/runtime/models/model_policy_registration.py +139 -0
  677. omnibase_infra/runtime/models/model_policy_result.py +103 -0
  678. omnibase_infra/runtime/models/model_policy_type_filter.py +157 -0
  679. omnibase_infra/runtime/models/model_projector_notification_config.py +171 -0
  680. omnibase_infra/runtime/models/model_projector_plugin_loader_config.py +47 -0
  681. omnibase_infra/runtime/models/model_protocol_registration_config.py +65 -0
  682. omnibase_infra/runtime/models/model_retry_policy.py +105 -0
  683. omnibase_infra/runtime/models/model_runtime_config.py +150 -0
  684. omnibase_infra/runtime/models/model_runtime_contract_config.py +268 -0
  685. omnibase_infra/runtime/models/model_runtime_scheduler_config.py +625 -0
  686. omnibase_infra/runtime/models/model_runtime_scheduler_metrics.py +233 -0
  687. omnibase_infra/runtime/models/model_runtime_tick.py +193 -0
  688. omnibase_infra/runtime/models/model_secret_cache_stats.py +82 -0
  689. omnibase_infra/runtime/models/model_secret_mapping.py +63 -0
  690. omnibase_infra/runtime/models/model_secret_resolver_config.py +107 -0
  691. omnibase_infra/runtime/models/model_secret_resolver_metrics.py +111 -0
  692. omnibase_infra/runtime/models/model_secret_source_info.py +72 -0
  693. omnibase_infra/runtime/models/model_secret_source_spec.py +66 -0
  694. omnibase_infra/runtime/models/model_security_config.py +109 -0
  695. omnibase_infra/runtime/models/model_shutdown_batch_result.py +75 -0
  696. omnibase_infra/runtime/models/model_shutdown_config.py +94 -0
  697. omnibase_infra/runtime/models/model_transition_notification_outbox_config.py +112 -0
  698. omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
  699. omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -0
  700. omnibase_infra/runtime/projector_plugin_loader.py +1462 -0
  701. omnibase_infra/runtime/projector_schema_manager.py +565 -0
  702. omnibase_infra/runtime/projector_shell.py +1330 -0
  703. omnibase_infra/runtime/protocol_contract_descriptor.py +92 -0
  704. omnibase_infra/runtime/protocol_contract_source.py +92 -0
  705. omnibase_infra/runtime/protocol_domain_plugin.py +474 -0
  706. omnibase_infra/runtime/protocol_handler_discovery.py +221 -0
  707. omnibase_infra/runtime/protocol_handler_plugin_loader.py +327 -0
  708. omnibase_infra/runtime/protocol_lifecycle_executor.py +435 -0
  709. omnibase_infra/runtime/protocol_policy.py +366 -0
  710. omnibase_infra/runtime/protocols/__init__.py +37 -0
  711. omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -0
  712. omnibase_infra/runtime/publisher_topic_scoped.py +294 -0
  713. omnibase_infra/runtime/registry/__init__.py +93 -0
  714. omnibase_infra/runtime/registry/mixin_message_type_query.py +326 -0
  715. omnibase_infra/runtime/registry/mixin_message_type_registration.py +354 -0
  716. omnibase_infra/runtime/registry/registry_event_bus_binding.py +268 -0
  717. omnibase_infra/runtime/registry/registry_message_type.py +542 -0
  718. omnibase_infra/runtime/registry/registry_protocol_binding.py +445 -0
  719. omnibase_infra/runtime/registry_compute.py +1143 -0
  720. omnibase_infra/runtime/registry_contract_source.py +693 -0
  721. omnibase_infra/runtime/registry_dispatcher.py +678 -0
  722. omnibase_infra/runtime/registry_policy.py +1185 -0
  723. omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
  724. omnibase_infra/runtime/runtime_scheduler.py +1070 -0
  725. omnibase_infra/runtime/secret_resolver.py +2112 -0
  726. omnibase_infra/runtime/security_metadata_validator.py +776 -0
  727. omnibase_infra/runtime/service_kernel.py +1651 -0
  728. omnibase_infra/runtime/service_message_dispatch_engine.py +2350 -0
  729. omnibase_infra/runtime/service_runtime_host_process.py +3493 -0
  730. omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
  731. omnibase_infra/runtime/transition_notification_publisher.py +765 -0
  732. omnibase_infra/runtime/util_container_wiring.py +1124 -0
  733. omnibase_infra/runtime/util_validation.py +314 -0
  734. omnibase_infra/runtime/util_version.py +98 -0
  735. omnibase_infra/runtime/util_wiring.py +723 -0
  736. omnibase_infra/schemas/schema_registration_projection.sql +320 -0
  737. omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
  738. omnibase_infra/services/__init__.py +89 -0
  739. omnibase_infra/services/corpus_capture.py +684 -0
  740. omnibase_infra/services/mcp/__init__.py +31 -0
  741. omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -0
  742. omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
  743. omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
  744. omnibase_infra/services/mcp/service_mcp_tool_sync.py +565 -0
  745. omnibase_infra/services/registry_api/__init__.py +40 -0
  746. omnibase_infra/services/registry_api/main.py +261 -0
  747. omnibase_infra/services/registry_api/models/__init__.py +66 -0
  748. omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
  749. omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
  750. omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
  751. omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
  752. omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
  753. omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
  754. omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
  755. omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
  756. omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
  757. omnibase_infra/services/registry_api/models/model_warning.py +49 -0
  758. omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
  759. omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
  760. omnibase_infra/services/registry_api/routes.py +371 -0
  761. omnibase_infra/services/registry_api/service.py +837 -0
  762. omnibase_infra/services/service_capability_query.py +945 -0
  763. omnibase_infra/services/service_health.py +898 -0
  764. omnibase_infra/services/service_node_selector.py +530 -0
  765. omnibase_infra/services/service_timeout_emitter.py +699 -0
  766. omnibase_infra/services/service_timeout_scanner.py +394 -0
  767. omnibase_infra/services/session/__init__.py +56 -0
  768. omnibase_infra/services/session/config_consumer.py +137 -0
  769. omnibase_infra/services/session/config_store.py +139 -0
  770. omnibase_infra/services/session/consumer.py +1007 -0
  771. omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
  772. omnibase_infra/services/session/store.py +997 -0
  773. omnibase_infra/services/snapshot/__init__.py +31 -0
  774. omnibase_infra/services/snapshot/service_snapshot.py +647 -0
  775. omnibase_infra/services/snapshot/store_inmemory.py +637 -0
  776. omnibase_infra/services/snapshot/store_postgres.py +1279 -0
  777. omnibase_infra/shared/__init__.py +8 -0
  778. omnibase_infra/testing/__init__.py +10 -0
  779. omnibase_infra/testing/utils.py +23 -0
  780. omnibase_infra/topics/__init__.py +45 -0
  781. omnibase_infra/topics/platform_topic_suffixes.py +140 -0
  782. omnibase_infra/topics/util_topic_composition.py +95 -0
  783. omnibase_infra/types/__init__.py +48 -0
  784. omnibase_infra/types/type_cache_info.py +49 -0
  785. omnibase_infra/types/type_dsn.py +173 -0
  786. omnibase_infra/types/type_infra_aliases.py +60 -0
  787. omnibase_infra/types/typed_dict/__init__.py +29 -0
  788. omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -0
  789. omnibase_infra/types/typed_dict/typed_dict_introspection_cache.py +128 -0
  790. omnibase_infra/types/typed_dict/typed_dict_performance_metrics_cache.py +140 -0
  791. omnibase_infra/types/typed_dict_capabilities.py +64 -0
  792. omnibase_infra/utils/__init__.py +117 -0
  793. omnibase_infra/utils/correlation.py +208 -0
  794. omnibase_infra/utils/util_atomic_file.py +261 -0
  795. omnibase_infra/utils/util_consumer_group.py +232 -0
  796. omnibase_infra/utils/util_datetime.py +372 -0
  797. omnibase_infra/utils/util_db_transaction.py +239 -0
  798. omnibase_infra/utils/util_dsn_validation.py +333 -0
  799. omnibase_infra/utils/util_env_parsing.py +264 -0
  800. omnibase_infra/utils/util_error_sanitization.py +457 -0
  801. omnibase_infra/utils/util_pydantic_validators.py +477 -0
  802. omnibase_infra/utils/util_retry_optimistic.py +281 -0
  803. omnibase_infra/utils/util_semver.py +233 -0
  804. omnibase_infra/validation/__init__.py +307 -0
  805. omnibase_infra/validation/contracts/security.validation.yaml +114 -0
  806. omnibase_infra/validation/enums/__init__.py +11 -0
  807. omnibase_infra/validation/enums/enum_contract_violation_severity.py +13 -0
  808. omnibase_infra/validation/infra_validators.py +1514 -0
  809. omnibase_infra/validation/linter_contract.py +907 -0
  810. omnibase_infra/validation/mixin_any_type_classification.py +120 -0
  811. omnibase_infra/validation/mixin_any_type_exemption.py +580 -0
  812. omnibase_infra/validation/mixin_any_type_reporting.py +106 -0
  813. omnibase_infra/validation/mixin_execution_shape_violation_checks.py +596 -0
  814. omnibase_infra/validation/mixin_node_archetype_detection.py +254 -0
  815. omnibase_infra/validation/models/__init__.py +15 -0
  816. omnibase_infra/validation/models/model_contract_lint_result.py +101 -0
  817. omnibase_infra/validation/models/model_contract_violation.py +41 -0
  818. omnibase_infra/validation/service_validation_aggregator.py +395 -0
  819. omnibase_infra/validation/validation_exemptions.yaml +2033 -0
  820. omnibase_infra/validation/validator_any_type.py +715 -0
  821. omnibase_infra/validation/validator_chain_propagation.py +839 -0
  822. omnibase_infra/validation/validator_execution_shape.py +465 -0
  823. omnibase_infra/validation/validator_localhandler.py +261 -0
  824. omnibase_infra/validation/validator_registration_security.py +410 -0
  825. omnibase_infra/validation/validator_routing_coverage.py +1020 -0
  826. omnibase_infra/validation/validator_runtime_shape.py +915 -0
  827. omnibase_infra/validation/validator_security.py +513 -0
  828. omnibase_infra/validation/validator_topic_category.py +1152 -0
  829. omnibase_infra-0.2.6.dist-info/METADATA +197 -0
  830. omnibase_infra-0.2.6.dist-info/RECORD +833 -0
  831. omnibase_infra-0.2.6.dist-info/WHEEL +4 -0
  832. omnibase_infra-0.2.6.dist-info/entry_points.txt +5 -0
  833. omnibase_infra-0.2.6.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,2771 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Binding configuration resolver for ONEX infrastructure.
4
+
5
+ BindingConfigResolver provides a unified interface for resolving handler configurations
6
+ from multiple sources with proper priority ordering (highest to lowest):
7
+
8
+ 1. Environment variables (HANDLER_{TYPE}_{FIELD}) - **highest priority**, always wins
9
+ 2. Inline config (passed directly to resolve()) - overrides config_ref values
10
+ 3. Config reference (file:, env:, vault:) - **base configuration** (lowest priority)
11
+
12
+ Resolution Process:
13
+ The resolver builds the final configuration by layering sources from lowest to highest
14
+ priority. Later sources override earlier ones for overlapping keys:
15
+
16
+ 1. Load base config from config_ref (if provided)
17
+ 2. Merge inline_config on top (inline values override config_ref values)
18
+ 3. Apply environment variable overrides on top (env values override everything)
19
+
20
+ This means: config_ref provides defaults, inline_config can override those defaults,
21
+ and environment variables can override both for operational flexibility.
22
+
23
+ Important: config_ref Schemes are Mutually Exclusive
24
+ The config_ref schemes (file:, env:, vault:) are **mutually exclusive** - only ONE
25
+ config_ref can be provided per resolution call. The scheme determines WHERE to load
26
+ the base configuration from, not a priority ordering between schemes.
27
+
28
+ Correct usage::
29
+
30
+ # Load from file
31
+ resolver.resolve(handler_type="db", config_ref="file:configs/db.yaml")
32
+
33
+ # OR load from environment variable
34
+ resolver.resolve(handler_type="db", config_ref="env:DB_CONFIG_JSON")
35
+
36
+ # OR load from Vault
37
+ resolver.resolve(handler_type="db", config_ref="vault:secret/data/db")
38
+
39
+ Invalid usage (would use only ONE, ignoring others)::
40
+
41
+ # WRONG: Cannot combine multiple config_ref schemes in a single call
42
+ # config_ref="file:..." AND config_ref="env:..." is NOT supported
43
+ # Pass only ONE config_ref string per resolve() call
44
+
45
+ Design Philosophy:
46
+ - Dumb and deterministic: resolves and caches, does not discover or mutate
47
+ - Environment overrides always take precedence for operational flexibility
48
+ - Caching is optional and TTL-controlled for performance vs freshness tradeoff
49
+
50
+ Example:
51
+ Basic usage with container-based dependency injection::
52
+
53
+ from omnibase_core.container import ModelONEXContainer
54
+ from omnibase_infra.runtime.util_container_wiring import wire_infrastructure_services
55
+
56
+ # Bootstrap container and register config
57
+ container = ModelONEXContainer()
58
+ config = ModelBindingConfigResolverConfig(env_prefix="HANDLER")
59
+ from omnibase_core.enums import EnumInjectionScope
60
+
61
+ await container.service_registry.register_instance(
62
+ interface=ModelBindingConfigResolverConfig,
63
+ instance=config,
64
+ scope=EnumInjectionScope.GLOBAL,
65
+ )
66
+
67
+ # Create resolver with container injection
68
+ resolver = BindingConfigResolver(container)
69
+
70
+ # Resolve from inline config
71
+ binding = resolver.resolve(
72
+ handler_type="db",
73
+ inline_config={"pool_size": 10, "timeout_ms": 5000}
74
+ )
75
+
76
+ # Resolve from file reference
77
+ binding = resolver.resolve(
78
+ handler_type="vault",
79
+ config_ref="file:configs/vault.yaml"
80
+ )
81
+
82
+ With environment overrides::
83
+
84
+ # Set HANDLER_DB_TIMEOUT_MS=10000 in environment
85
+ binding = resolver.resolve(
86
+ handler_type="db",
87
+ inline_config={"timeout_ms": 5000} # Will be overridden to 10000
88
+ )
89
+
90
+ Security Considerations:
91
+ - File paths are validated to prevent path traversal attacks
92
+ - Error messages are sanitized to exclude configuration values
93
+ - Vault secrets are resolved through SecretResolver (not accessed directly)
94
+ - File size limits prevent memory exhaustion attacks
95
+
96
+ Thread Safety:
97
+ This class supports concurrent access from both sync and async contexts
98
+ using a two-level locking strategy:
99
+
100
+ 1. ``threading.Lock`` (``_lock``): Protects all cache reads/writes and
101
+ stats updates. This lock is held briefly for in-memory operations.
102
+
103
+ 2. Per-key ``asyncio.Lock`` (``_async_key_locks``): Prevents duplicate
104
+ async fetches for the SAME handler type. When multiple async callers
105
+ request the same config simultaneously, only one performs the fetch
106
+ while others wait and reuse the cached result.
107
+
108
+ .. versionadded:: 0.8.0
109
+ Initial implementation for OMN-765.
110
+ """
111
+
112
+ from __future__ import annotations
113
+
114
+ import asyncio
115
+ import json
116
+ import logging
117
+ import os
118
+ import threading
119
+ import time
120
+ from collections import OrderedDict
121
+ from datetime import UTC, datetime, timedelta
122
+ from pathlib import Path
123
+ from typing import TYPE_CHECKING, Final
124
+ from uuid import UUID, uuid4
125
+
126
+ import yaml
127
+ from pydantic import ValidationError
128
+
129
+ from omnibase_core.types import JsonType
130
+ from omnibase_infra.enums import EnumInfraTransportType
131
+ from omnibase_infra.errors import (
132
+ ModelInfraErrorContext,
133
+ ProtocolConfigurationError,
134
+ SecretResolutionError,
135
+ )
136
+ from omnibase_infra.runtime.models.model_binding_config import ModelBindingConfig
137
+ from omnibase_infra.runtime.models.model_binding_config_cache_stats import (
138
+ ModelBindingConfigCacheStats,
139
+ )
140
+ from omnibase_infra.runtime.models.model_binding_config_resolver_config import (
141
+ ModelBindingConfigResolverConfig,
142
+ )
143
+ from omnibase_infra.runtime.models.model_config_cache_entry import ModelConfigCacheEntry
144
+ from omnibase_infra.runtime.models.model_config_ref import (
145
+ EnumConfigRefScheme,
146
+ ModelConfigRef,
147
+ )
148
+ from omnibase_infra.runtime.models.model_retry_policy import ModelRetryPolicy
149
+
150
+ if TYPE_CHECKING:
151
+ from omnibase_core.container import ModelONEXContainer
152
+ from omnibase_infra.runtime.secret_resolver import SecretResolver
153
+
154
+ logger = logging.getLogger(__name__)
155
+
156
+ # Maximum file size for config files (1MB)
157
+ # Prevents memory exhaustion from accidentally pointing at large files
158
+ MAX_CONFIG_FILE_SIZE: Final[int] = 1024 * 1024
159
+
160
+ # Maximum recursion depth for nested config resolution
161
+ # Prevents stack overflow on deeply nested or circular configs
162
+ _MAX_NESTED_CONFIG_DEPTH: Final[int] = 20
163
+
164
+ # Fields that can be overridden via environment variables
165
+ # Maps from environment variable field name (uppercase) to model field name
166
+ _ENV_OVERRIDE_FIELDS: Final[dict[str, str]] = {
167
+ "ENABLED": "enabled",
168
+ "PRIORITY": "priority",
169
+ "TIMEOUT_MS": "timeout_ms",
170
+ "RATE_LIMIT_PER_SECOND": "rate_limit_per_second",
171
+ "MAX_RETRIES": "max_retries",
172
+ "BACKOFF_STRATEGY": "backoff_strategy",
173
+ "BASE_DELAY_MS": "base_delay_ms",
174
+ "MAX_DELAY_MS": "max_delay_ms",
175
+ "NAME": "name",
176
+ }
177
+
178
+ # Retry policy fields (nested under retry_policy)
179
+ _RETRY_POLICY_FIELDS: Final[frozenset[str]] = frozenset(
180
+ {"MAX_RETRIES", "BACKOFF_STRATEGY", "BASE_DELAY_MS", "MAX_DELAY_MS"}
181
+ )
182
+
183
+ # Async key lock cleanup configuration (default values)
184
+ # These values are now configurable via ModelBindingConfigResolverConfig.
185
+ # These constants are kept as fallbacks and for backward compatibility with tests
186
+ # that directly manipulate internal state without going through config.
187
+ # Prevents unbounded growth of _async_key_locks dict in long-running processes.
188
+ _ASYNC_KEY_LOCK_CLEANUP_THRESHOLD: Final[int] = (
189
+ 1000 # Trigger cleanup when > 1000 locks (default, can be overridden via config)
190
+ )
191
+ _ASYNC_KEY_LOCK_MAX_AGE_SECONDS: Final[float] = (
192
+ 3600.0 # Clean locks older than 1 hour (default, can be overridden via config)
193
+ )
194
+
195
+
196
+ def _split_path_and_fragment(path: str) -> tuple[str, str | None]:
197
+ """Split a path into path and optional fragment at '#'.
198
+
199
+ This is a common helper used by both BindingConfigResolver and SecretResolver
200
+ to parse vault paths that may contain a fragment identifier (e.g., "path#field").
201
+
202
+ Args:
203
+ path: The path string, optionally containing a '#' separator.
204
+
205
+ Returns:
206
+ Tuple of (path, fragment) where fragment may be None if no '#' present.
207
+
208
+ Example:
209
+ >>> _split_path_and_fragment("secret/data/db#password")
210
+ ("secret/data/db", "password")
211
+ >>> _split_path_and_fragment("secret/data/config")
212
+ ("secret/data/config", None)
213
+ """
214
+ if "#" in path:
215
+ path_part, fragment = path.rsplit("#", 1)
216
+ return path_part, fragment
217
+ return path, None
218
+
219
+
220
+ class BindingConfigResolver: # ONEX_EXCLUDE: method_count - follows SecretResolver pattern
221
+ """Resolver that normalizes handler configs from multiple sources.
222
+
223
+ The BindingConfigResolver provides a unified interface for resolving handler
224
+ configurations with proper priority ordering and caching support.
225
+
226
+ Resolution Order:
227
+ 1. Check cache (if enabled and not expired)
228
+ 2. Parse config_ref if present (file:, env:, vault:)
229
+ 3. Load base config from ref, then merge inline_config (inline takes precedence)
230
+ 4. Apply environment variable overrides (highest priority)
231
+ 5. Resolve any vault: references in config values
232
+ 6. Validate and construct ModelBindingConfig
233
+
234
+ Thread Safety:
235
+ This class is thread-safe for concurrent access from both sync and
236
+ async contexts. See module docstring for details on the locking strategy.
237
+
238
+ Example:
239
+ >>> # Container setup (async context required)
240
+ >>> from omnibase_core.enums import EnumInjectionScope
241
+ >>> container = ModelONEXContainer()
242
+ >>> config = ModelBindingConfigResolverConfig(env_prefix="HANDLER")
243
+ >>> await container.service_registry.register_instance(
244
+ ... interface=ModelBindingConfigResolverConfig,
245
+ ... instance=config,
246
+ ... scope=EnumInjectionScope.GLOBAL,
247
+ ... )
248
+ >>> resolver = await BindingConfigResolver.create(container)
249
+ >>> binding = resolver.resolve(
250
+ ... handler_type="db",
251
+ ... inline_config={"pool_size": 10}
252
+ ... )
253
+ """
254
+
255
+ def __init__(
256
+ self,
257
+ container: ModelONEXContainer,
258
+ *,
259
+ _config: ModelBindingConfigResolverConfig | None = None,
260
+ _secret_resolver: SecretResolver | None = None,
261
+ ) -> None:
262
+ """Initialize BindingConfigResolver with container-based dependency injection.
263
+
264
+ Follows ONEX mandatory container injection pattern per CLAUDE.md.
265
+ Config is resolved from container's service registry, and SecretResolver
266
+ is resolved as an optional dependency.
267
+
268
+ Note:
269
+ Prefer using the async factory method ``create()`` for initialization,
270
+ which properly resolves dependencies from the container's service registry.
271
+ Direct ``__init__`` usage requires pre-resolved config and secret_resolver.
272
+
273
+ Args:
274
+ container: ONEX container for dependency resolution.
275
+ _config: Pre-resolved config (used by create() factory). If None, raises error.
276
+ _secret_resolver: Pre-resolved secret resolver (optional, used by create() factory).
277
+
278
+ Raises:
279
+ ProtocolConfigurationError: If _config is not provided (use create() instead).
280
+ """
281
+ self._container = container
282
+
283
+ # Validate that config was provided (either via create() or directly)
284
+ if _config is None:
285
+ context = ModelInfraErrorContext.with_correlation(
286
+ transport_type=EnumInfraTransportType.RUNTIME,
287
+ operation="init",
288
+ target_name="binding_config_resolver",
289
+ )
290
+ raise ProtocolConfigurationError(
291
+ "BindingConfigResolver requires config to be provided. "
292
+ "Use the async factory method BindingConfigResolver.create(container) "
293
+ "for proper initialization with dependency resolution.",
294
+ context=context,
295
+ )
296
+
297
+ self._config: ModelBindingConfigResolverConfig = _config
298
+ self._secret_resolver: SecretResolver | None = _secret_resolver
299
+
300
+ # Use OrderedDict for LRU eviction support - entries are moved to end on access
301
+ self._cache: OrderedDict[str, ModelConfigCacheEntry] = OrderedDict()
302
+ # Track mutable stats internally since ModelBindingConfigCacheStats is frozen
303
+ self._hits = 0
304
+ self._misses = 0
305
+ self._expired_evictions = 0
306
+ self._lru_evictions = 0
307
+ self._refreshes = 0
308
+ self._file_loads = 0
309
+ self._env_loads = 0
310
+ self._vault_loads = 0
311
+ self._async_key_lock_cleanups = 0 # Track cleanup events for observability
312
+
313
+ # RLock (Reentrant Lock) is REQUIRED - DO NOT CHANGE TO REGULAR LOCK.
314
+ #
315
+ # Why RLock is necessary:
316
+ # -----------------------
317
+ # The sync path (resolve()) holds the lock while calling internal methods
318
+ # that also need to update counters protected by the same lock:
319
+ #
320
+ # resolve() [holds _lock]
321
+ # -> _get_from_cache() [updates _hits, _expired_evictions]
322
+ # -> _resolve_config() [no lock needed directly]
323
+ # -> _load_from_file() [updates _file_loads]
324
+ # -> _load_from_env() [updates _env_loads]
325
+ # -> _resolve_vault_refs() [updates _vault_loads]
326
+ # -> _cache_config() [updates _misses, _lru_evictions]
327
+ #
328
+ # With a regular threading.Lock, this would cause DEADLOCK because:
329
+ # - Thread A calls resolve() and acquires _lock
330
+ # - Thread A calls _get_from_cache() which tries to update _hits
331
+ # - Since Thread A already holds _lock, a regular Lock would block forever
332
+ #
333
+ # RLock allows the same thread to acquire the lock multiple times,
334
+ # with a release count that must match the acquisition count.
335
+ #
336
+ # Alternative considered: Move counter updates outside the critical section.
337
+ # This was rejected because:
338
+ # 1. Counters must be updated atomically with cache operations for consistency
339
+ # 2. Would require significant refactoring with subtle race condition risks
340
+ # 3. RLock performance overhead is minimal for in-memory operations
341
+ #
342
+ # See PR #168 review for detailed analysis of this design decision.
343
+ self._lock = threading.RLock()
344
+
345
+ # Per-key async locks to allow parallel fetches for different handler types
346
+ # while preventing duplicate fetches for the same handler type.
347
+ # Timestamps track when each lock was created for periodic cleanup.
348
+ self._async_key_locks: dict[str, asyncio.Lock] = {}
349
+ self._async_key_lock_timestamps: dict[str, float] = {}
350
+
351
+ @classmethod
352
+ async def create(
353
+ cls,
354
+ container: ModelONEXContainer,
355
+ ) -> BindingConfigResolver:
356
+ """Async factory method for creating BindingConfigResolver with proper DI.
357
+
358
+ This is the preferred method for creating BindingConfigResolver instances.
359
+ It properly resolves dependencies from the container's async service registry.
360
+
361
+ Args:
362
+ container: ONEX container for dependency resolution.
363
+
364
+ Returns:
365
+ Fully initialized BindingConfigResolver instance.
366
+
367
+ Raises:
368
+ ProtocolConfigurationError: If ModelBindingConfigResolverConfig is not registered
369
+ in the container's service registry.
370
+
371
+ Example:
372
+ >>> from omnibase_core.enums import EnumInjectionScope
373
+ >>> container = ModelONEXContainer()
374
+ >>> config = ModelBindingConfigResolverConfig(env_prefix="HANDLER")
375
+ >>> await container.service_registry.register_instance(
376
+ ... interface=ModelBindingConfigResolverConfig,
377
+ ... instance=config,
378
+ ... scope=EnumInjectionScope.GLOBAL,
379
+ ... )
380
+ >>> resolver = await BindingConfigResolver.create(container)
381
+ """
382
+ # Resolve config from container's service registry
383
+ try:
384
+ config: ModelBindingConfigResolverConfig = (
385
+ await container.service_registry.resolve_service(
386
+ ModelBindingConfigResolverConfig
387
+ )
388
+ )
389
+ except (LookupError, KeyError, TypeError, AttributeError) as e:
390
+ # LookupError/KeyError: service not registered
391
+ # TypeError: invalid interface specification
392
+ # AttributeError: container/registry missing expected methods
393
+ context = ModelInfraErrorContext.with_correlation(
394
+ transport_type=EnumInfraTransportType.RUNTIME,
395
+ operation="create",
396
+ target_name="binding_config_resolver",
397
+ )
398
+ raise ProtocolConfigurationError(
399
+ "Failed to resolve ModelBindingConfigResolverConfig from container. "
400
+ "Ensure config is registered via container.service_registry.register_instance().",
401
+ context=context,
402
+ ) from e
403
+
404
+ # Resolve SecretResolver from container (optional dependency)
405
+ secret_resolver: SecretResolver | None = None
406
+ try:
407
+ from omnibase_infra.runtime.secret_resolver import SecretResolver
408
+
409
+ secret_resolver = await container.service_registry.resolve_service(
410
+ SecretResolver
411
+ )
412
+ except (ImportError, KeyError, AttributeError):
413
+ # SecretResolver is optional - if not registered, vault: schemes won't work
414
+ # ImportError: SecretResolver module not available
415
+ # KeyError: SecretResolver not registered in service registry
416
+ # AttributeError: service_registry missing resolve_service method (test mocks)
417
+ pass
418
+
419
+ return cls(
420
+ container,
421
+ _config=config,
422
+ _secret_resolver=secret_resolver,
423
+ )
424
+
425
+ # === Primary API (Sync) ===
426
+
427
+ def resolve(
428
+ self,
429
+ handler_type: str,
430
+ config_ref: str | None = None,
431
+ inline_config: dict[str, JsonType] | None = None,
432
+ correlation_id: UUID | None = None,
433
+ ) -> ModelBindingConfig:
434
+ """Resolve handler configuration synchronously.
435
+
436
+ Resolution order:
437
+ 1. Check cache (if enabled and not expired)
438
+ 2. Load from config_ref (if provided)
439
+ 3. Merge with inline_config (inline takes precedence)
440
+ 4. Apply environment variable overrides (highest priority)
441
+ 5. Validate and construct ModelBindingConfig
442
+
443
+ Args:
444
+ handler_type: Handler type identifier (e.g., "db", "vault", "consul").
445
+ config_ref: Optional reference to external configuration.
446
+ Supported schemes: file:, env:, vault: (mutually exclusive - use only ONE)
447
+ Examples: file:configs/db.yaml, env:DB_CONFIG, vault:secret/data/db#password
448
+ inline_config: Optional inline configuration dictionary.
449
+ Takes precedence over config_ref for overlapping keys.
450
+ correlation_id: Optional correlation ID for error tracking.
451
+
452
+ Returns:
453
+ Resolved and validated ModelBindingConfig.
454
+
455
+ Raises:
456
+ ProtocolConfigurationError: If configuration is invalid or cannot be loaded.
457
+ """
458
+ correlation_id = correlation_id or uuid4()
459
+
460
+ with self._lock:
461
+ # Check cache first
462
+ cached = self._get_from_cache(handler_type)
463
+ if cached is not None:
464
+ return cached
465
+
466
+ # Resolve from sources
467
+ result = self._resolve_config(
468
+ handler_type=handler_type,
469
+ config_ref=config_ref,
470
+ inline_config=inline_config,
471
+ correlation_id=correlation_id,
472
+ )
473
+
474
+ # Cache the result if caching is enabled
475
+ if self._config.enable_caching:
476
+ source = self._describe_source(config_ref, inline_config)
477
+ self._cache_config(handler_type, result, source)
478
+ else:
479
+ # Count miss when caching is disabled since _cache_config won't be called
480
+ self._misses += 1
481
+
482
+ return result
483
+
484
+ def resolve_many(
485
+ self,
486
+ bindings: list[dict[str, JsonType]],
487
+ correlation_id: UUID | None = None,
488
+ ) -> list[ModelBindingConfig]:
489
+ """Resolve multiple handler configurations.
490
+
491
+ Each binding dict must contain at least "handler_type" key.
492
+ Optionally can include "config_ref" and "config" (inline_config).
493
+
494
+ Args:
495
+ bindings: List of binding specifications. Each dict should contain:
496
+ - handler_type (required): Handler type identifier
497
+ - config_ref (optional): Reference to external configuration
498
+ - config (optional): Inline configuration dictionary
499
+ correlation_id: Optional correlation ID for error tracking.
500
+
501
+ Returns:
502
+ List of resolved ModelBindingConfig instances.
503
+
504
+ Raises:
505
+ ProtocolConfigurationError: If any configuration is invalid.
506
+
507
+ Note:
508
+ This sync method resolves configurations sequentially. For better
509
+ latency when resolving multiple configurations that involve I/O
510
+ (file or Vault), prefer using ``resolve_many_async()``.
511
+ """
512
+ correlation_id = correlation_id or uuid4()
513
+ results: list[ModelBindingConfig] = []
514
+
515
+ for binding in bindings:
516
+ handler_type = binding.get("handler_type")
517
+ if not isinstance(handler_type, str):
518
+ context = ModelInfraErrorContext.with_correlation(
519
+ correlation_id=correlation_id,
520
+ transport_type=EnumInfraTransportType.RUNTIME,
521
+ operation="resolve_many",
522
+ target_name="binding_config_resolver",
523
+ )
524
+ raise ProtocolConfigurationError(
525
+ "Each binding must have a 'handler_type' string field",
526
+ context=context,
527
+ )
528
+
529
+ config_ref = binding.get("config_ref")
530
+ if config_ref is not None and not isinstance(config_ref, str):
531
+ config_ref = None
532
+
533
+ inline_config = binding.get("config")
534
+ if inline_config is not None and not isinstance(inline_config, dict):
535
+ inline_config = None
536
+
537
+ result = self.resolve(
538
+ handler_type=handler_type,
539
+ config_ref=config_ref,
540
+ inline_config=inline_config,
541
+ correlation_id=correlation_id,
542
+ )
543
+ results.append(result)
544
+
545
+ return results
546
+
547
+ # === Primary API (Async) ===
548
+
549
+ async def resolve_async(
550
+ self,
551
+ handler_type: str,
552
+ config_ref: str | None = None,
553
+ inline_config: dict[str, JsonType] | None = None,
554
+ correlation_id: UUID | None = None,
555
+ ) -> ModelBindingConfig:
556
+ """Resolve handler configuration asynchronously.
557
+
558
+ For file-based configs, this uses async file I/O. For Vault secrets,
559
+ this uses the SecretResolver's async interface.
560
+
561
+ Thread Safety:
562
+ Uses threading.Lock for cache access to prevent race conditions
563
+ with sync callers. Per-key async locks serialize resolution for the
564
+ same handler type while allowing parallel fetches for different types.
565
+
566
+ Args:
567
+ handler_type: Handler type identifier (e.g., "db", "vault", "consul").
568
+ config_ref: Optional reference to external configuration.
569
+ Supported schemes: file:, env:, vault: (mutually exclusive - use only ONE)
570
+ inline_config: Optional inline configuration dictionary.
571
+ correlation_id: Optional correlation ID for error tracking.
572
+
573
+ Returns:
574
+ Resolved and validated ModelBindingConfig.
575
+
576
+ Raises:
577
+ ProtocolConfigurationError: If configuration is invalid or cannot be loaded.
578
+ """
579
+ correlation_id = correlation_id or uuid4()
580
+
581
+ # Use threading lock for cache check (fast operation, prevents race with sync)
582
+ with self._lock:
583
+ cached = self._get_from_cache(handler_type)
584
+ if cached is not None:
585
+ return cached
586
+
587
+ # Get or create per-key async lock for this handler_type
588
+ key_lock = self._get_async_key_lock(handler_type)
589
+
590
+ async with key_lock:
591
+ # Double-check cache after acquiring async lock
592
+ with self._lock:
593
+ cached = self._get_from_cache(handler_type)
594
+ if cached is not None:
595
+ return cached
596
+
597
+ # Resolve from sources asynchronously
598
+ result = await self._resolve_config_async(
599
+ handler_type=handler_type,
600
+ config_ref=config_ref,
601
+ inline_config=inline_config, # type: ignore[arg-type]
602
+ correlation_id=correlation_id,
603
+ )
604
+
605
+ # Cache the result if caching is enabled
606
+ if self._config.enable_caching:
607
+ with self._lock:
608
+ if handler_type not in self._cache:
609
+ source = self._describe_source(config_ref, inline_config)
610
+ self._cache_config(handler_type, result, source)
611
+ else:
612
+ # Count miss when caching is disabled since _cache_config won't be called
613
+ with self._lock:
614
+ self._misses += 1
615
+
616
+ return result
617
+
618
+ async def resolve_many_async(
619
+ self,
620
+ bindings: list[dict[str, JsonType]],
621
+ correlation_id: UUID | None = None,
622
+ ) -> list[ModelBindingConfig]:
623
+ """Resolve multiple configurations asynchronously in parallel.
624
+
625
+ Uses asyncio.gather() to fetch multiple configurations concurrently,
626
+ improving performance when resolving multiple configs that may involve
627
+ I/O (e.g., file or Vault-based secrets).
628
+
629
+ Thread Safety:
630
+ Each configuration resolution uses per-key async locks, so fetches
631
+ for different handler types proceed in parallel while fetches for
632
+ the same handler type are serialized.
633
+
634
+ Args:
635
+ bindings: List of binding specifications.
636
+ correlation_id: Optional correlation ID for error tracking.
637
+
638
+ Returns:
639
+ List of resolved ModelBindingConfig instances.
640
+
641
+ Raises:
642
+ ProtocolConfigurationError: If any configuration is invalid.
643
+ """
644
+ correlation_id = correlation_id or uuid4()
645
+
646
+ if not bindings:
647
+ return []
648
+
649
+ # Build tasks for parallel resolution
650
+ tasks: list[asyncio.Task[ModelBindingConfig]] = []
651
+
652
+ for binding in bindings:
653
+ handler_type = binding.get("handler_type")
654
+ if not isinstance(handler_type, str):
655
+ context = ModelInfraErrorContext.with_correlation(
656
+ correlation_id=correlation_id,
657
+ transport_type=EnumInfraTransportType.RUNTIME,
658
+ operation="resolve_many_async",
659
+ target_name="binding_config_resolver",
660
+ )
661
+ raise ProtocolConfigurationError(
662
+ "Each binding must have a 'handler_type' string field",
663
+ context=context,
664
+ )
665
+
666
+ config_ref = binding.get("config_ref")
667
+ if config_ref is not None and not isinstance(config_ref, str):
668
+ config_ref = None
669
+
670
+ inline_config = binding.get("config")
671
+ if inline_config is not None and not isinstance(inline_config, dict):
672
+ inline_config = None
673
+
674
+ task = asyncio.create_task(
675
+ self.resolve_async(
676
+ handler_type=handler_type,
677
+ config_ref=config_ref,
678
+ inline_config=inline_config,
679
+ correlation_id=correlation_id,
680
+ )
681
+ )
682
+ tasks.append(task)
683
+
684
+ # Gather results - collect all exceptions for better error reporting
685
+ results = await asyncio.gather(*tasks, return_exceptions=True)
686
+
687
+ # Check for exceptions and aggregate them
688
+ failed_handler_types: list[str] = []
689
+ configs: list[ModelBindingConfig] = []
690
+
691
+ for i, result in enumerate(results):
692
+ if isinstance(result, BaseException):
693
+ handler_type = bindings[i].get("handler_type", f"binding[{i}]")
694
+ failed_handler_types.append(str(handler_type))
695
+ # Log the detailed error for debugging, but don't expose in exception
696
+ # (exception message could contain sensitive config values)
697
+ logger.debug(
698
+ "Configuration resolution failed for handler '%s': %s",
699
+ handler_type,
700
+ result,
701
+ extra={"correlation_id": str(correlation_id)},
702
+ )
703
+ else:
704
+ # Type narrowing: result is ModelBindingConfig after BaseException check
705
+ configs.append(result)
706
+
707
+ if failed_handler_types:
708
+ context = ModelInfraErrorContext.with_correlation(
709
+ correlation_id=correlation_id,
710
+ transport_type=EnumInfraTransportType.RUNTIME,
711
+ operation="resolve_many_async",
712
+ target_name="binding_config_resolver",
713
+ )
714
+ raise ProtocolConfigurationError(
715
+ f"Failed to resolve {len(failed_handler_types)} configuration(s) "
716
+ f"for handlers: {', '.join(failed_handler_types)}",
717
+ context=context,
718
+ )
719
+
720
+ return configs
721
+
722
+ # === Cache Management ===
723
+
724
+ def refresh(self, handler_type: str) -> None:
725
+ """Invalidate cached config for a handler type.
726
+
727
+ Args:
728
+ handler_type: The handler type to refresh.
729
+ """
730
+ with self._lock:
731
+ if handler_type in self._cache:
732
+ del self._cache[handler_type]
733
+ self._refreshes += 1
734
+
735
+ def refresh_all(self) -> None:
736
+ """Invalidate all cached configs."""
737
+ with self._lock:
738
+ count = len(self._cache)
739
+ self._cache.clear()
740
+ self._refreshes += count
741
+
742
+ def get_cache_stats(self) -> ModelBindingConfigCacheStats:
743
+ """Get cache statistics.
744
+
745
+ Returns:
746
+ ModelBindingConfigCacheStats with hit/miss/load counts and lock stats.
747
+ """
748
+ with self._lock:
749
+ return ModelBindingConfigCacheStats(
750
+ total_entries=len(self._cache),
751
+ hits=self._hits,
752
+ misses=self._misses,
753
+ refreshes=self._refreshes,
754
+ expired_evictions=self._expired_evictions,
755
+ lru_evictions=self._lru_evictions,
756
+ file_loads=self._file_loads,
757
+ env_loads=self._env_loads,
758
+ vault_loads=self._vault_loads,
759
+ async_key_lock_count=len(self._async_key_locks),
760
+ async_key_lock_cleanups=self._async_key_lock_cleanups,
761
+ )
762
+
763
+ # === Internal Methods ===
764
+
765
+ def _get_async_key_lock(self, handler_type: str) -> asyncio.Lock:
766
+ """Get or create an async lock for a specific handler_type.
767
+
768
+ Includes periodic cleanup of stale locks to prevent unbounded memory
769
+ growth in long-running processes. Cleanup is triggered when the number
770
+ of locks exceeds the configured threshold (async_lock_cleanup_threshold).
771
+
772
+ Thread Safety:
773
+ Uses threading.Lock to safely access the key locks dictionary,
774
+ ensuring thread-safe creation of new locks. Cleanup only removes
775
+ locks that are not currently held.
776
+
777
+ Args:
778
+ handler_type: The handler type to get a lock for.
779
+
780
+ Returns:
781
+ asyncio.Lock for the given handler_type.
782
+
783
+ Note:
784
+ The cleanup threshold is configurable via
785
+ ModelBindingConfigResolverConfig.async_lock_cleanup_threshold.
786
+ Default is 1000 locks.
787
+ """
788
+ with self._lock:
789
+ # Periodic cleanup when threshold exceeded (uses config value)
790
+ threshold = self._config.async_lock_cleanup_threshold
791
+ if len(self._async_key_locks) > threshold:
792
+ self._cleanup_stale_async_key_locks()
793
+
794
+ if handler_type not in self._async_key_locks:
795
+ self._async_key_locks[handler_type] = asyncio.Lock()
796
+ self._async_key_lock_timestamps[handler_type] = time.monotonic()
797
+ return self._async_key_locks[handler_type]
798
+
799
+ def _cleanup_stale_async_key_locks(self) -> None:
800
+ """Remove async key locks that have not been used recently.
801
+
802
+ Only removes locks that are:
803
+ 1. Older than the configured max age (async_lock_max_age_seconds)
804
+ 2. Not currently held (not locked)
805
+
806
+ Thread Safety:
807
+ Must be called while holding self._lock. Safe to call from
808
+ any thread as it only modifies internal state.
809
+
810
+ Note:
811
+ This method is called periodically from _get_async_key_lock()
812
+ when the lock count exceeds the threshold. It does not require
813
+ external scheduling.
814
+
815
+ The max age is configurable via
816
+ ModelBindingConfigResolverConfig.async_lock_max_age_seconds.
817
+ Default is 3600 seconds (1 hour).
818
+ """
819
+ current_time = time.monotonic()
820
+ stale_keys: list[str] = []
821
+ max_age = self._config.async_lock_max_age_seconds
822
+
823
+ for key, timestamp in self._async_key_lock_timestamps.items():
824
+ age = current_time - timestamp
825
+ if age > max_age:
826
+ lock = self._async_key_locks.get(key)
827
+ # Only remove locks that are not currently held
828
+ if lock is not None and not lock.locked():
829
+ stale_keys.append(key)
830
+
831
+ for key in stale_keys:
832
+ del self._async_key_locks[key]
833
+ del self._async_key_lock_timestamps[key]
834
+
835
+ if stale_keys:
836
+ self._async_key_lock_cleanups += 1
837
+ logger.debug(
838
+ "Cleaned up stale async key locks",
839
+ extra={
840
+ "cleaned_count": len(stale_keys),
841
+ "remaining_count": len(self._async_key_locks),
842
+ "max_age_seconds": max_age,
843
+ },
844
+ )
845
+
846
+ def _cleanup_async_key_lock_for_eviction(self, key: str) -> None:
847
+ """Clean up async key lock when its associated cache entry is evicted.
848
+
849
+ This method is called during LRU eviction or TTL expiration to ensure
850
+ async locks don't leak when their corresponding cache entries are removed.
851
+
852
+ Thread Safety:
853
+ Must be called while holding self._lock. Only removes locks that
854
+ are not currently held to prevent race conditions with concurrent
855
+ async operations.
856
+
857
+ Args:
858
+ key: The handler_type key whose async lock should be cleaned up.
859
+
860
+ Note:
861
+ If the lock is currently held (e.g., an async operation is in progress),
862
+ it will NOT be removed. The lock will be cleaned up later during
863
+ periodic cleanup or when the operation completes.
864
+ """
865
+ if key in self._async_key_locks:
866
+ lock = self._async_key_locks[key]
867
+ # Only remove locks that are not currently held
868
+ # If locked, an async operation is in progress and will need the lock
869
+ if not lock.locked():
870
+ del self._async_key_locks[key]
871
+ if key in self._async_key_lock_timestamps:
872
+ del self._async_key_lock_timestamps[key]
873
+ logger.debug(
874
+ "Cleaned up async key lock for evicted cache entry",
875
+ extra={"handler_type": key},
876
+ )
877
+
878
+ def _get_from_cache(self, handler_type: str) -> ModelBindingConfig | None:
879
+ """Get config from cache if present and not expired.
880
+
881
+ Args:
882
+ handler_type: The handler type to look up.
883
+
884
+ Returns:
885
+ ModelBindingConfig if cached and valid, None otherwise.
886
+
887
+ Note:
888
+ This method does NOT increment the miss counter. Misses are counted
889
+ at the point where resolution from sources occurs (either in
890
+ _cache_config when caching is enabled, or in resolve/resolve_async
891
+ when caching is disabled). This ensures accurate miss counting in
892
+ the async path which uses a double-check locking pattern.
893
+
894
+ When max_cache_entries is configured, this method moves accessed entries
895
+ to the end of the OrderedDict to maintain LRU ordering. This ensures
896
+ the least recently used entry is at the front for eviction.
897
+
898
+ When a cache entry is evicted due to TTL expiration, this method also
899
+ cleans up the associated async key lock to prevent memory leaks.
900
+ """
901
+ if not self._config.enable_caching:
902
+ return None
903
+
904
+ cached = self._cache.get(handler_type)
905
+ if cached is None:
906
+ return None
907
+
908
+ if cached.is_expired():
909
+ del self._cache[handler_type]
910
+ self._expired_evictions += 1
911
+ # Clean up the async lock for this evicted entry
912
+ self._cleanup_async_key_lock_for_eviction(handler_type)
913
+ return None
914
+
915
+ # Move to end for LRU tracking (most recently used)
916
+ # This is a no-op if max_cache_entries is None, but we do it anyway
917
+ # for consistency since OrderedDict.move_to_end() is O(1)
918
+ self._cache.move_to_end(handler_type)
919
+ self._hits += 1
920
+ return cached.config
921
+
922
+ def _cache_config(
923
+ self,
924
+ handler_type: str,
925
+ config: ModelBindingConfig,
926
+ source: str,
927
+ ) -> None:
928
+ """Cache a resolved configuration with TTL.
929
+
930
+ Args:
931
+ handler_type: The handler type being cached.
932
+ config: The configuration to cache.
933
+ source: Description of the configuration source.
934
+
935
+ Note:
936
+ When max_cache_entries is configured and the cache is at capacity,
937
+ this method evicts the least recently used (LRU) entry before adding
938
+ the new one. The LRU entry is the first entry in the OrderedDict
939
+ since entries are moved to the end on access.
940
+
941
+ When a cache entry is evicted via LRU, this method also cleans up
942
+ the associated async key lock to prevent memory leaks. The lock is
943
+ only removed if it is not currently held by an async operation.
944
+
945
+ Thread Safety:
946
+ This method is ALWAYS called while holding self._lock (from resolve()
947
+ or resolve_async()), ensuring that LRU eviction and cache write are
948
+ atomic. There is no race window where another thread could observe
949
+ an inconsistent cache state. See PR #168 review for analysis.
950
+ """
951
+ # Evict LRU entry if cache is at capacity (before adding new entry)
952
+ # NOTE: This entire operation is atomic because the caller holds self._lock
953
+ max_entries = self._config.max_cache_entries
954
+ if max_entries is not None and handler_type not in self._cache:
955
+ # Only evict if adding a NEW entry (not updating existing)
956
+ while len(self._cache) >= max_entries:
957
+ # popitem(last=False) removes the first (oldest/LRU) entry
958
+ evicted_key, _ = self._cache.popitem(last=False)
959
+ self._lru_evictions += 1
960
+ # Clean up the async lock for this evicted entry
961
+ self._cleanup_async_key_lock_for_eviction(evicted_key)
962
+ logger.debug(
963
+ "LRU eviction: removed cache entry",
964
+ extra={
965
+ "evicted_handler_type": evicted_key,
966
+ "new_handler_type": handler_type,
967
+ "max_cache_entries": max_entries,
968
+ },
969
+ )
970
+
971
+ now = datetime.now(UTC)
972
+ ttl_seconds = self._config.cache_ttl_seconds
973
+ expires_at = now + timedelta(seconds=ttl_seconds)
974
+
975
+ self._cache[handler_type] = ModelConfigCacheEntry(
976
+ config=config,
977
+ expires_at=expires_at,
978
+ source=source,
979
+ )
980
+ self._misses += 1
981
+
982
+ def _describe_source(
983
+ self,
984
+ config_ref: str | None,
985
+ inline_config: dict[str, JsonType] | None,
986
+ ) -> str:
987
+ """Create a description of the configuration source for debugging.
988
+
989
+ Args:
990
+ config_ref: The config reference, if any.
991
+ inline_config: The inline config, if any.
992
+
993
+ Returns:
994
+ Human-readable source description.
995
+ """
996
+ sources: list[str] = []
997
+ if config_ref:
998
+ # Don't expose full path - just scheme
999
+ if ":" in config_ref:
1000
+ scheme = config_ref.split(":")[0]
1001
+ sources.append(f"{scheme}:...")
1002
+ else:
1003
+ sources.append("unknown")
1004
+ if inline_config:
1005
+ sources.append("inline")
1006
+ sources.append("env_overrides")
1007
+ return "+".join(sources) if sources else "default"
1008
+
1009
+ def _resolve_config(
1010
+ self,
1011
+ handler_type: str,
1012
+ config_ref: str | None,
1013
+ inline_config: dict[str, JsonType] | None,
1014
+ correlation_id: UUID,
1015
+ ) -> ModelBindingConfig:
1016
+ """Resolve configuration from sources synchronously.
1017
+
1018
+ Args:
1019
+ handler_type: Handler type identifier.
1020
+ config_ref: Optional external configuration reference.
1021
+ inline_config: Optional inline configuration.
1022
+ correlation_id: Correlation ID for error tracking.
1023
+
1024
+ Returns:
1025
+ Resolved ModelBindingConfig.
1026
+
1027
+ Raises:
1028
+ ProtocolConfigurationError: If configuration is invalid.
1029
+ """
1030
+ # Start with empty config
1031
+ merged_config: dict[str, object] = {}
1032
+
1033
+ # Load from config_ref if provided
1034
+ if config_ref:
1035
+ ref_config = self._load_from_ref(config_ref, correlation_id)
1036
+ merged_config.update(ref_config)
1037
+
1038
+ # Merge inline config (takes precedence over ref)
1039
+ if inline_config:
1040
+ merged_config.update(inline_config)
1041
+
1042
+ # Ensure handler_type is set
1043
+ merged_config["handler_type"] = handler_type
1044
+
1045
+ # Apply environment variable overrides (highest priority)
1046
+ merged_config = self._apply_env_overrides(
1047
+ merged_config, handler_type, correlation_id
1048
+ )
1049
+
1050
+ # Resolve any vault: references in the config
1051
+ merged_config = self._resolve_vault_refs(merged_config, correlation_id)
1052
+
1053
+ # Validate and construct the final config
1054
+ return self._validate_config(merged_config, handler_type, correlation_id)
1055
+
1056
+ async def _resolve_config_async(
1057
+ self,
1058
+ handler_type: str,
1059
+ config_ref: str | None,
1060
+ inline_config: dict[str, object] | None,
1061
+ correlation_id: UUID,
1062
+ ) -> ModelBindingConfig:
1063
+ """Resolve configuration from sources asynchronously.
1064
+
1065
+ Args:
1066
+ handler_type: Handler type identifier.
1067
+ config_ref: Optional external configuration reference.
1068
+ inline_config: Optional inline configuration.
1069
+ correlation_id: Correlation ID for error tracking.
1070
+
1071
+ Returns:
1072
+ Resolved ModelBindingConfig.
1073
+
1074
+ Raises:
1075
+ ProtocolConfigurationError: If configuration is invalid.
1076
+ """
1077
+ # Start with empty config
1078
+ merged_config: dict[str, object] = {}
1079
+
1080
+ # Load from config_ref if provided
1081
+ if config_ref:
1082
+ ref_config = await self._load_from_ref_async(config_ref, correlation_id)
1083
+ merged_config.update(ref_config)
1084
+
1085
+ # Merge inline config (takes precedence over ref)
1086
+ if inline_config:
1087
+ merged_config.update(inline_config)
1088
+
1089
+ # Ensure handler_type is set
1090
+ merged_config["handler_type"] = handler_type
1091
+
1092
+ # Apply environment variable overrides (highest priority)
1093
+ merged_config = self._apply_env_overrides(
1094
+ merged_config, handler_type, correlation_id
1095
+ )
1096
+
1097
+ # Resolve any vault: references in the config (async)
1098
+ merged_config = await self._resolve_vault_refs_async(
1099
+ merged_config, correlation_id
1100
+ )
1101
+
1102
+ # Validate and construct the final config
1103
+ return self._validate_config(merged_config, handler_type, correlation_id)
1104
+
1105
+ def _load_from_ref(
1106
+ self,
1107
+ config_ref: str,
1108
+ correlation_id: UUID,
1109
+ ) -> dict[str, object]:
1110
+ """Load configuration from a config_ref.
1111
+
1112
+ Args:
1113
+ config_ref: Configuration reference using scheme format (file:, env:, vault:).
1114
+ Examples: file:configs/db.yaml, env:DB_CONFIG, vault:secret/data/db#password
1115
+ correlation_id: Correlation ID for error tracking.
1116
+
1117
+ Returns:
1118
+ Loaded configuration dictionary.
1119
+
1120
+ Raises:
1121
+ ProtocolConfigurationError: If reference is invalid or cannot be loaded.
1122
+ """
1123
+ # Parse the config reference
1124
+ parse_result = ModelConfigRef.parse(config_ref)
1125
+ if not parse_result:
1126
+ context = ModelInfraErrorContext.with_correlation(
1127
+ correlation_id=correlation_id,
1128
+ transport_type=EnumInfraTransportType.RUNTIME,
1129
+ operation="load_from_ref",
1130
+ target_name="binding_config_resolver",
1131
+ )
1132
+ # Log the detailed error for debugging, but don't expose parse details
1133
+ # in the exception message (config_ref could contain sensitive paths)
1134
+ logger.debug(
1135
+ "Config reference parsing failed: %s",
1136
+ parse_result.error_message,
1137
+ extra={"correlation_id": str(correlation_id)},
1138
+ )
1139
+ raise ProtocolConfigurationError(
1140
+ "Invalid config reference format",
1141
+ context=context,
1142
+ )
1143
+
1144
+ ref = parse_result.config_ref
1145
+ if ref is None:
1146
+ context = ModelInfraErrorContext.with_correlation(
1147
+ correlation_id=correlation_id,
1148
+ transport_type=EnumInfraTransportType.RUNTIME,
1149
+ operation="load_from_ref",
1150
+ target_name="binding_config_resolver",
1151
+ )
1152
+ raise ProtocolConfigurationError(
1153
+ "Config reference parse result has no config_ref",
1154
+ context=context,
1155
+ )
1156
+
1157
+ # Check scheme is allowed
1158
+ if ref.scheme.value not in self._config.allowed_schemes:
1159
+ context = ModelInfraErrorContext.with_correlation(
1160
+ correlation_id=correlation_id,
1161
+ transport_type=EnumInfraTransportType.RUNTIME,
1162
+ operation="load_from_ref",
1163
+ target_name="binding_config_resolver",
1164
+ )
1165
+ raise ProtocolConfigurationError(
1166
+ f"Scheme '{ref.scheme.value}' is not in allowed schemes",
1167
+ context=context,
1168
+ )
1169
+
1170
+ # Load based on scheme
1171
+ if ref.scheme == EnumConfigRefScheme.FILE:
1172
+ return self._load_from_file(Path(ref.path), correlation_id)
1173
+ elif ref.scheme == EnumConfigRefScheme.ENV:
1174
+ return self._load_from_env(ref.path, correlation_id)
1175
+ elif ref.scheme == EnumConfigRefScheme.VAULT:
1176
+ return self._load_from_vault(ref.path, ref.fragment, correlation_id)
1177
+ else:
1178
+ context = ModelInfraErrorContext.with_correlation(
1179
+ correlation_id=correlation_id,
1180
+ transport_type=EnumInfraTransportType.RUNTIME,
1181
+ operation="load_from_ref",
1182
+ target_name="binding_config_resolver",
1183
+ )
1184
+ raise ProtocolConfigurationError(
1185
+ f"Unsupported scheme: {ref.scheme.value}",
1186
+ context=context,
1187
+ )
1188
+
1189
+ async def _load_from_ref_async(
1190
+ self,
1191
+ config_ref: str,
1192
+ correlation_id: UUID,
1193
+ ) -> dict[str, object]:
1194
+ """Load configuration from a config_ref asynchronously.
1195
+
1196
+ Args:
1197
+ config_ref: Configuration reference using scheme format (file:, env:, vault:).
1198
+ Examples: file:configs/db.yaml, env:DB_CONFIG, vault:secret/data/db#password
1199
+ correlation_id: Correlation ID for error tracking.
1200
+
1201
+ Returns:
1202
+ Loaded configuration dictionary.
1203
+
1204
+ Raises:
1205
+ ProtocolConfigurationError: If reference is invalid or cannot be loaded.
1206
+ """
1207
+ # Parse the config reference
1208
+ parse_result = ModelConfigRef.parse(config_ref)
1209
+ if not parse_result:
1210
+ context = ModelInfraErrorContext.with_correlation(
1211
+ correlation_id=correlation_id,
1212
+ transport_type=EnumInfraTransportType.RUNTIME,
1213
+ operation="load_from_ref_async",
1214
+ target_name="binding_config_resolver",
1215
+ )
1216
+ # Log the detailed error for debugging, but don't expose parse details
1217
+ # in the exception message (config_ref could contain sensitive paths)
1218
+ logger.debug(
1219
+ "Config reference parsing failed: %s",
1220
+ parse_result.error_message,
1221
+ extra={"correlation_id": str(correlation_id)},
1222
+ )
1223
+ raise ProtocolConfigurationError(
1224
+ "Invalid config reference format",
1225
+ context=context,
1226
+ )
1227
+
1228
+ ref = parse_result.config_ref
1229
+ if ref is None:
1230
+ context = ModelInfraErrorContext.with_correlation(
1231
+ correlation_id=correlation_id,
1232
+ transport_type=EnumInfraTransportType.RUNTIME,
1233
+ operation="load_from_ref_async",
1234
+ target_name="binding_config_resolver",
1235
+ )
1236
+ raise ProtocolConfigurationError(
1237
+ "Config reference parse result has no config_ref",
1238
+ context=context,
1239
+ )
1240
+
1241
+ # Check scheme is allowed
1242
+ if ref.scheme.value not in self._config.allowed_schemes:
1243
+ context = ModelInfraErrorContext.with_correlation(
1244
+ correlation_id=correlation_id,
1245
+ transport_type=EnumInfraTransportType.RUNTIME,
1246
+ operation="load_from_ref_async",
1247
+ target_name="binding_config_resolver",
1248
+ )
1249
+ raise ProtocolConfigurationError(
1250
+ f"Scheme '{ref.scheme.value}' is not in allowed schemes",
1251
+ context=context,
1252
+ )
1253
+
1254
+ # Load based on scheme
1255
+ if ref.scheme == EnumConfigRefScheme.FILE:
1256
+ return await asyncio.to_thread(
1257
+ self._load_from_file, Path(ref.path), correlation_id
1258
+ )
1259
+ elif ref.scheme == EnumConfigRefScheme.ENV:
1260
+ # Env var access is fast, no need for thread
1261
+ return self._load_from_env(ref.path, correlation_id)
1262
+ elif ref.scheme == EnumConfigRefScheme.VAULT:
1263
+ return await self._load_from_vault_async(
1264
+ ref.path, ref.fragment, correlation_id
1265
+ )
1266
+ else:
1267
+ context = ModelInfraErrorContext.with_correlation(
1268
+ correlation_id=correlation_id,
1269
+ transport_type=EnumInfraTransportType.RUNTIME,
1270
+ operation="load_from_ref_async",
1271
+ target_name="binding_config_resolver",
1272
+ )
1273
+ raise ProtocolConfigurationError(
1274
+ f"Unsupported scheme: {ref.scheme.value}",
1275
+ context=context,
1276
+ )
1277
+
1278
+ def _load_from_file(
1279
+ self,
1280
+ path: Path,
1281
+ correlation_id: UUID,
1282
+ ) -> dict[str, object]:
1283
+ """Load config from YAML or JSON file.
1284
+
1285
+ Args:
1286
+ path: Path to the configuration file.
1287
+ correlation_id: Correlation ID for error tracking.
1288
+
1289
+ Returns:
1290
+ Loaded configuration dictionary.
1291
+
1292
+ Raises:
1293
+ ProtocolConfigurationError: If file cannot be read or parsed.
1294
+ """
1295
+ # Resolve relative paths against config_dir
1296
+ if not path.is_absolute():
1297
+ if self._config.config_dir is not None:
1298
+ # Validate config_dir at use-time (deferred from model construction)
1299
+ try:
1300
+ config_dir_exists = self._config.config_dir.exists()
1301
+ config_dir_is_dir = self._config.config_dir.is_dir()
1302
+ except ValueError:
1303
+ # config_dir contains null bytes (defense-in-depth)
1304
+ context = ModelInfraErrorContext.with_correlation(
1305
+ correlation_id=correlation_id,
1306
+ transport_type=EnumInfraTransportType.RUNTIME,
1307
+ operation="load_from_file",
1308
+ target_name="binding_config_resolver",
1309
+ )
1310
+ raise ProtocolConfigurationError(
1311
+ "Invalid config_dir path: contains invalid characters",
1312
+ context=context,
1313
+ )
1314
+ if not config_dir_exists:
1315
+ context = ModelInfraErrorContext.with_correlation(
1316
+ correlation_id=correlation_id,
1317
+ transport_type=EnumInfraTransportType.RUNTIME,
1318
+ operation="load_from_file",
1319
+ target_name="binding_config_resolver",
1320
+ )
1321
+ raise ProtocolConfigurationError(
1322
+ f"config_dir does not exist: path='{self._config.config_dir}'",
1323
+ context=context,
1324
+ )
1325
+ if not config_dir_is_dir:
1326
+ context = ModelInfraErrorContext.with_correlation(
1327
+ correlation_id=correlation_id,
1328
+ transport_type=EnumInfraTransportType.RUNTIME,
1329
+ operation="load_from_file",
1330
+ target_name="binding_config_resolver",
1331
+ )
1332
+ raise ProtocolConfigurationError(
1333
+ f"config_dir exists but is not a directory: path='{self._config.config_dir}'",
1334
+ context=context,
1335
+ )
1336
+ path = self._config.config_dir / path
1337
+ else:
1338
+ context = ModelInfraErrorContext.with_correlation(
1339
+ correlation_id=correlation_id,
1340
+ transport_type=EnumInfraTransportType.RUNTIME,
1341
+ operation="load_from_file",
1342
+ target_name="binding_config_resolver",
1343
+ )
1344
+ raise ProtocolConfigurationError(
1345
+ "Relative path provided but no config_dir configured",
1346
+ context=context,
1347
+ )
1348
+
1349
+ # Security: Check for symlinks if not allowed
1350
+ # Check before resolve() to detect symlinks in the original path
1351
+ try:
1352
+ is_symlink = not self._config.allow_symlinks and path.is_symlink()
1353
+ except ValueError:
1354
+ # Path contains null bytes or other invalid characters
1355
+ context = ModelInfraErrorContext.with_correlation(
1356
+ correlation_id=correlation_id,
1357
+ transport_type=EnumInfraTransportType.RUNTIME,
1358
+ operation="load_from_file",
1359
+ target_name="binding_config_resolver",
1360
+ )
1361
+ raise ProtocolConfigurationError(
1362
+ "Invalid configuration file path: contains invalid characters",
1363
+ context=context,
1364
+ )
1365
+ if is_symlink:
1366
+ logger.warning(
1367
+ "Symlink rejected in config file path",
1368
+ extra={"correlation_id": str(correlation_id)},
1369
+ )
1370
+ context = ModelInfraErrorContext.with_correlation(
1371
+ correlation_id=correlation_id,
1372
+ transport_type=EnumInfraTransportType.RUNTIME,
1373
+ operation="load_from_file",
1374
+ target_name="binding_config_resolver",
1375
+ )
1376
+ raise ProtocolConfigurationError(
1377
+ "Configuration file symlinks not allowed",
1378
+ context=context,
1379
+ )
1380
+
1381
+ # Resolve to absolute path for security validation
1382
+ try:
1383
+ resolved_path = path.resolve()
1384
+ except (OSError, RuntimeError, ValueError):
1385
+ # ValueError: path contains null bytes or other invalid characters
1386
+ # OSError/RuntimeError: filesystem/symlink resolution errors
1387
+ context = ModelInfraErrorContext.with_correlation(
1388
+ correlation_id=correlation_id,
1389
+ transport_type=EnumInfraTransportType.RUNTIME,
1390
+ operation="load_from_file",
1391
+ target_name="binding_config_resolver",
1392
+ )
1393
+ raise ProtocolConfigurationError(
1394
+ "Invalid configuration file path",
1395
+ context=context,
1396
+ )
1397
+
1398
+ # Security: Check if any path component is a symlink (when symlinks disallowed)
1399
+ # This catches symlinks in parent directories (e.g., /etc/configs -> /tmp/evil)
1400
+ if not self._config.allow_symlinks:
1401
+ current = path
1402
+ while current != current.parent:
1403
+ try:
1404
+ is_current_symlink = current.is_symlink()
1405
+ except ValueError:
1406
+ # Path contains null bytes or other invalid characters
1407
+ context = ModelInfraErrorContext.with_correlation(
1408
+ correlation_id=correlation_id,
1409
+ transport_type=EnumInfraTransportType.RUNTIME,
1410
+ operation="load_from_file",
1411
+ target_name="binding_config_resolver",
1412
+ )
1413
+ raise ProtocolConfigurationError(
1414
+ "Invalid configuration file path: contains invalid characters",
1415
+ context=context,
1416
+ )
1417
+ if is_current_symlink:
1418
+ logger.warning(
1419
+ "Symlink detected in path hierarchy",
1420
+ extra={"correlation_id": str(correlation_id)},
1421
+ )
1422
+ context = ModelInfraErrorContext.with_correlation(
1423
+ correlation_id=correlation_id,
1424
+ transport_type=EnumInfraTransportType.RUNTIME,
1425
+ operation="load_from_file",
1426
+ target_name="binding_config_resolver",
1427
+ )
1428
+ raise ProtocolConfigurationError(
1429
+ "Configuration file path contains symlink",
1430
+ context=context,
1431
+ )
1432
+ current = current.parent
1433
+
1434
+ # Security: Validate path is within config_dir if configured
1435
+ if self._config.config_dir is not None:
1436
+ try:
1437
+ config_dir_resolved = self._config.config_dir.resolve()
1438
+ except (OSError, RuntimeError, ValueError):
1439
+ # ValueError: config_dir contains null bytes (defense-in-depth)
1440
+ # OSError/RuntimeError: filesystem/symlink resolution errors
1441
+ context = ModelInfraErrorContext.with_correlation(
1442
+ correlation_id=correlation_id,
1443
+ transport_type=EnumInfraTransportType.RUNTIME,
1444
+ operation="load_from_file",
1445
+ target_name="binding_config_resolver",
1446
+ )
1447
+ raise ProtocolConfigurationError(
1448
+ "Invalid config_dir path",
1449
+ context=context,
1450
+ )
1451
+ try:
1452
+ resolved_path.relative_to(config_dir_resolved)
1453
+ except ValueError:
1454
+ # Path escapes config_dir - this is a path traversal attempt
1455
+ logger.warning(
1456
+ "Path traversal detected in config file path",
1457
+ extra={"correlation_id": str(correlation_id)},
1458
+ )
1459
+ context = ModelInfraErrorContext.with_correlation(
1460
+ correlation_id=correlation_id,
1461
+ transport_type=EnumInfraTransportType.RUNTIME,
1462
+ operation="load_from_file",
1463
+ target_name="binding_config_resolver",
1464
+ )
1465
+ raise ProtocolConfigurationError(
1466
+ "Configuration file path traversal not allowed",
1467
+ context=context,
1468
+ )
1469
+
1470
+ # Read file with size limit
1471
+ try:
1472
+ with resolved_path.open("r") as f:
1473
+ content = f.read(MAX_CONFIG_FILE_SIZE + 1)
1474
+ if len(content) > MAX_CONFIG_FILE_SIZE:
1475
+ context = ModelInfraErrorContext.with_correlation(
1476
+ correlation_id=correlation_id,
1477
+ transport_type=EnumInfraTransportType.RUNTIME,
1478
+ operation="load_from_file",
1479
+ target_name="binding_config_resolver",
1480
+ )
1481
+ raise ProtocolConfigurationError(
1482
+ "Configuration file exceeds size limit",
1483
+ context=context,
1484
+ )
1485
+ except FileNotFoundError:
1486
+ context = ModelInfraErrorContext.with_correlation(
1487
+ correlation_id=correlation_id,
1488
+ transport_type=EnumInfraTransportType.RUNTIME,
1489
+ operation="load_from_file",
1490
+ target_name="binding_config_resolver",
1491
+ )
1492
+ raise ProtocolConfigurationError(
1493
+ "Configuration file not found",
1494
+ context=context,
1495
+ )
1496
+ except IsADirectoryError:
1497
+ context = ModelInfraErrorContext.with_correlation(
1498
+ correlation_id=correlation_id,
1499
+ transport_type=EnumInfraTransportType.RUNTIME,
1500
+ operation="load_from_file",
1501
+ target_name="binding_config_resolver",
1502
+ )
1503
+ raise ProtocolConfigurationError(
1504
+ "Configuration path is a directory, not a file",
1505
+ context=context,
1506
+ )
1507
+ except PermissionError:
1508
+ context = ModelInfraErrorContext.with_correlation(
1509
+ correlation_id=correlation_id,
1510
+ transport_type=EnumInfraTransportType.RUNTIME,
1511
+ operation="load_from_file",
1512
+ target_name="binding_config_resolver",
1513
+ )
1514
+ raise ProtocolConfigurationError(
1515
+ "Permission denied reading configuration file",
1516
+ context=context,
1517
+ )
1518
+ except OSError:
1519
+ context = ModelInfraErrorContext.with_correlation(
1520
+ correlation_id=correlation_id,
1521
+ transport_type=EnumInfraTransportType.RUNTIME,
1522
+ operation="load_from_file",
1523
+ target_name="binding_config_resolver",
1524
+ )
1525
+ raise ProtocolConfigurationError(
1526
+ "OS error reading configuration file",
1527
+ context=context,
1528
+ )
1529
+
1530
+ # Parse based on extension
1531
+ suffix = resolved_path.suffix.lower()
1532
+ try:
1533
+ if suffix in {".yaml", ".yml"}:
1534
+ data = yaml.safe_load(content)
1535
+ elif suffix == ".json":
1536
+ data = json.loads(content)
1537
+ else:
1538
+ context = ModelInfraErrorContext.with_correlation(
1539
+ correlation_id=correlation_id,
1540
+ transport_type=EnumInfraTransportType.RUNTIME,
1541
+ operation="load_from_file",
1542
+ target_name="binding_config_resolver",
1543
+ )
1544
+ raise ProtocolConfigurationError(
1545
+ f"Unsupported configuration file format: {suffix}",
1546
+ context=context,
1547
+ )
1548
+ except yaml.YAMLError:
1549
+ context = ModelInfraErrorContext.with_correlation(
1550
+ correlation_id=correlation_id,
1551
+ transport_type=EnumInfraTransportType.RUNTIME,
1552
+ operation="load_from_file",
1553
+ target_name="binding_config_resolver",
1554
+ )
1555
+ raise ProtocolConfigurationError(
1556
+ "Invalid YAML in configuration file",
1557
+ context=context,
1558
+ )
1559
+ except json.JSONDecodeError:
1560
+ context = ModelInfraErrorContext.with_correlation(
1561
+ correlation_id=correlation_id,
1562
+ transport_type=EnumInfraTransportType.RUNTIME,
1563
+ operation="load_from_file",
1564
+ target_name="binding_config_resolver",
1565
+ )
1566
+ raise ProtocolConfigurationError(
1567
+ "Invalid JSON in configuration file",
1568
+ context=context,
1569
+ )
1570
+
1571
+ if not isinstance(data, dict):
1572
+ context = ModelInfraErrorContext.with_correlation(
1573
+ correlation_id=correlation_id,
1574
+ transport_type=EnumInfraTransportType.RUNTIME,
1575
+ operation="load_from_file",
1576
+ target_name="binding_config_resolver",
1577
+ )
1578
+ raise ProtocolConfigurationError(
1579
+ "Configuration file must contain a dictionary",
1580
+ context=context,
1581
+ )
1582
+
1583
+ with self._lock:
1584
+ self._file_loads += 1
1585
+
1586
+ return data
1587
+
1588
+ def _load_from_env(
1589
+ self,
1590
+ env_var: str,
1591
+ correlation_id: UUID,
1592
+ ) -> dict[str, object]:
1593
+ """Load config from environment variable (JSON or YAML).
1594
+
1595
+ Args:
1596
+ env_var: Environment variable name containing configuration.
1597
+ correlation_id: Correlation ID for error tracking.
1598
+
1599
+ Returns:
1600
+ Loaded configuration dictionary.
1601
+
1602
+ Raises:
1603
+ ProtocolConfigurationError: If env var is missing or contains invalid data.
1604
+ """
1605
+ value = os.environ.get(env_var)
1606
+ if value is None:
1607
+ context = ModelInfraErrorContext.with_correlation(
1608
+ correlation_id=correlation_id,
1609
+ transport_type=EnumInfraTransportType.RUNTIME,
1610
+ operation="load_from_env",
1611
+ target_name="binding_config_resolver",
1612
+ )
1613
+ raise ProtocolConfigurationError(
1614
+ f"Environment variable not set: {env_var}",
1615
+ context=context,
1616
+ )
1617
+
1618
+ # Try JSON first, then YAML
1619
+ data: object = None
1620
+ try:
1621
+ data = json.loads(value)
1622
+ except json.JSONDecodeError:
1623
+ try:
1624
+ data = yaml.safe_load(value)
1625
+ except yaml.YAMLError:
1626
+ context = ModelInfraErrorContext.with_correlation(
1627
+ correlation_id=correlation_id,
1628
+ transport_type=EnumInfraTransportType.RUNTIME,
1629
+ operation="load_from_env",
1630
+ target_name="binding_config_resolver",
1631
+ )
1632
+ raise ProtocolConfigurationError(
1633
+ f"Environment variable {env_var} contains invalid JSON/YAML",
1634
+ context=context,
1635
+ )
1636
+
1637
+ if not isinstance(data, dict):
1638
+ context = ModelInfraErrorContext.with_correlation(
1639
+ correlation_id=correlation_id,
1640
+ transport_type=EnumInfraTransportType.RUNTIME,
1641
+ operation="load_from_env",
1642
+ target_name="binding_config_resolver",
1643
+ )
1644
+ raise ProtocolConfigurationError(
1645
+ f"Environment variable {env_var} must contain a dictionary",
1646
+ context=context,
1647
+ )
1648
+
1649
+ with self._lock:
1650
+ self._env_loads += 1
1651
+
1652
+ return data
1653
+
1654
+ def _load_from_vault(
1655
+ self,
1656
+ vault_path: str,
1657
+ fragment: str | None,
1658
+ correlation_id: UUID,
1659
+ ) -> dict[str, object]:
1660
+ """Load config from Vault secret.
1661
+
1662
+ Args:
1663
+ vault_path: Vault secret path.
1664
+ fragment: Optional field within the secret.
1665
+ correlation_id: Correlation ID for error tracking.
1666
+
1667
+ Returns:
1668
+ Loaded configuration dictionary.
1669
+
1670
+ Raises:
1671
+ ProtocolConfigurationError: If Vault is not configured or secret cannot be read.
1672
+ """
1673
+ secret_resolver = self._get_secret_resolver()
1674
+ if secret_resolver is None:
1675
+ context = ModelInfraErrorContext.with_correlation(
1676
+ correlation_id=correlation_id,
1677
+ transport_type=EnumInfraTransportType.RUNTIME,
1678
+ operation="load_from_vault",
1679
+ target_name="binding_config_resolver",
1680
+ )
1681
+ raise ProtocolConfigurationError(
1682
+ "Vault scheme used but no SecretResolver configured",
1683
+ context=context,
1684
+ )
1685
+
1686
+ # Build logical name for secret resolver
1687
+ logical_name = vault_path
1688
+ if fragment:
1689
+ logical_name = f"{vault_path}#{fragment}"
1690
+
1691
+ try:
1692
+ secret = secret_resolver.get_secret(logical_name, required=True)
1693
+ except (SecretResolutionError, NotImplementedError) as e:
1694
+ # SecretResolutionError: secret not found or resolution failed
1695
+ # NotImplementedError: Vault integration not yet implemented
1696
+ # SECURITY: Log at DEBUG level only - exception may contain vault paths
1697
+ logger.debug(
1698
+ "Vault configuration retrieval failed (correlation_id=%s): %s",
1699
+ correlation_id,
1700
+ e,
1701
+ extra={"correlation_id": str(correlation_id)},
1702
+ )
1703
+ context = ModelInfraErrorContext.with_correlation(
1704
+ correlation_id=correlation_id,
1705
+ transport_type=EnumInfraTransportType.VAULT,
1706
+ operation="load_from_vault",
1707
+ target_name="binding_config_resolver",
1708
+ )
1709
+ # SECURITY: Do NOT chain original exception (from e) - it may
1710
+ # contain vault paths in its message.
1711
+ raise ProtocolConfigurationError(
1712
+ f"Failed to retrieve configuration from Vault. "
1713
+ f"correlation_id={correlation_id}",
1714
+ context=context,
1715
+ )
1716
+
1717
+ if secret is None:
1718
+ context = ModelInfraErrorContext.with_correlation(
1719
+ correlation_id=correlation_id,
1720
+ transport_type=EnumInfraTransportType.VAULT,
1721
+ operation="load_from_vault",
1722
+ target_name="binding_config_resolver",
1723
+ )
1724
+ raise ProtocolConfigurationError(
1725
+ "Vault secret not found",
1726
+ context=context,
1727
+ )
1728
+
1729
+ # Parse secret value as JSON or YAML
1730
+ secret_value = secret.get_secret_value()
1731
+ data: object = None
1732
+ try:
1733
+ data = json.loads(secret_value)
1734
+ except json.JSONDecodeError:
1735
+ try:
1736
+ data = yaml.safe_load(secret_value)
1737
+ except yaml.YAMLError:
1738
+ context = ModelInfraErrorContext.with_correlation(
1739
+ correlation_id=correlation_id,
1740
+ transport_type=EnumInfraTransportType.VAULT,
1741
+ operation="load_from_vault",
1742
+ target_name="binding_config_resolver",
1743
+ )
1744
+ raise ProtocolConfigurationError(
1745
+ "Vault secret contains invalid JSON/YAML",
1746
+ context=context,
1747
+ )
1748
+
1749
+ if not isinstance(data, dict):
1750
+ context = ModelInfraErrorContext.with_correlation(
1751
+ correlation_id=correlation_id,
1752
+ transport_type=EnumInfraTransportType.VAULT,
1753
+ operation="load_from_vault",
1754
+ target_name="binding_config_resolver",
1755
+ )
1756
+ raise ProtocolConfigurationError(
1757
+ "Vault secret must contain a dictionary",
1758
+ context=context,
1759
+ )
1760
+
1761
+ with self._lock:
1762
+ self._vault_loads += 1
1763
+
1764
+ return data
1765
+
1766
+ async def _load_from_vault_async(
1767
+ self,
1768
+ vault_path: str,
1769
+ fragment: str | None,
1770
+ correlation_id: UUID,
1771
+ ) -> dict[str, object]:
1772
+ """Load config from Vault secret asynchronously.
1773
+
1774
+ Args:
1775
+ vault_path: Vault secret path.
1776
+ fragment: Optional field within the secret.
1777
+ correlation_id: Correlation ID for error tracking.
1778
+
1779
+ Returns:
1780
+ Loaded configuration dictionary.
1781
+
1782
+ Raises:
1783
+ ProtocolConfigurationError: If Vault is not configured or secret cannot be read.
1784
+ """
1785
+ secret_resolver = self._get_secret_resolver()
1786
+ if secret_resolver is None:
1787
+ context = ModelInfraErrorContext.with_correlation(
1788
+ correlation_id=correlation_id,
1789
+ transport_type=EnumInfraTransportType.RUNTIME,
1790
+ operation="load_from_vault_async",
1791
+ target_name="binding_config_resolver",
1792
+ )
1793
+ raise ProtocolConfigurationError(
1794
+ "Vault scheme used but no SecretResolver configured",
1795
+ context=context,
1796
+ )
1797
+
1798
+ # Build logical name for secret resolver
1799
+ logical_name = vault_path
1800
+ if fragment:
1801
+ logical_name = f"{vault_path}#{fragment}"
1802
+
1803
+ try:
1804
+ secret = await secret_resolver.get_secret_async(logical_name, required=True)
1805
+ except (SecretResolutionError, NotImplementedError) as e:
1806
+ # SecretResolutionError: secret not found or resolution failed
1807
+ # NotImplementedError: Vault integration not yet implemented
1808
+ # SECURITY: Log at DEBUG level only - exception may contain vault paths
1809
+ logger.debug(
1810
+ "Vault configuration retrieval failed async (correlation_id=%s): %s",
1811
+ correlation_id,
1812
+ e,
1813
+ extra={"correlation_id": str(correlation_id)},
1814
+ )
1815
+ context = ModelInfraErrorContext.with_correlation(
1816
+ correlation_id=correlation_id,
1817
+ transport_type=EnumInfraTransportType.VAULT,
1818
+ operation="load_from_vault_async",
1819
+ target_name="binding_config_resolver",
1820
+ )
1821
+ # SECURITY: Do NOT chain original exception (from e) - it may
1822
+ # contain vault paths in its message.
1823
+ raise ProtocolConfigurationError(
1824
+ f"Failed to retrieve configuration from Vault. "
1825
+ f"correlation_id={correlation_id}",
1826
+ context=context,
1827
+ )
1828
+
1829
+ if secret is None:
1830
+ context = ModelInfraErrorContext.with_correlation(
1831
+ correlation_id=correlation_id,
1832
+ transport_type=EnumInfraTransportType.VAULT,
1833
+ operation="load_from_vault_async",
1834
+ target_name="binding_config_resolver",
1835
+ )
1836
+ raise ProtocolConfigurationError(
1837
+ "Vault secret not found",
1838
+ context=context,
1839
+ )
1840
+
1841
+ # Parse secret value as JSON or YAML
1842
+ secret_value = secret.get_secret_value()
1843
+ data: object = None
1844
+ try:
1845
+ data = json.loads(secret_value)
1846
+ except json.JSONDecodeError:
1847
+ try:
1848
+ data = yaml.safe_load(secret_value)
1849
+ except yaml.YAMLError:
1850
+ context = ModelInfraErrorContext.with_correlation(
1851
+ correlation_id=correlation_id,
1852
+ transport_type=EnumInfraTransportType.VAULT,
1853
+ operation="load_from_vault_async",
1854
+ target_name="binding_config_resolver",
1855
+ )
1856
+ raise ProtocolConfigurationError(
1857
+ "Vault secret contains invalid JSON/YAML",
1858
+ context=context,
1859
+ )
1860
+
1861
+ if not isinstance(data, dict):
1862
+ context = ModelInfraErrorContext.with_correlation(
1863
+ correlation_id=correlation_id,
1864
+ transport_type=EnumInfraTransportType.VAULT,
1865
+ operation="load_from_vault_async",
1866
+ target_name="binding_config_resolver",
1867
+ )
1868
+ raise ProtocolConfigurationError(
1869
+ "Vault secret must contain a dictionary",
1870
+ context=context,
1871
+ )
1872
+
1873
+ with self._lock:
1874
+ self._vault_loads += 1
1875
+
1876
+ return data
1877
+
1878
+ def _get_secret_resolver(self) -> SecretResolver | None:
1879
+ """Get the container-resolved SecretResolver instance.
1880
+
1881
+ The SecretResolver is resolved from the container during __init__.
1882
+ This method provides access to the cached instance.
1883
+
1884
+ Returns:
1885
+ SecretResolver if registered in container, None otherwise.
1886
+ """
1887
+ return self._secret_resolver
1888
+
1889
+ def _parse_vault_reference(self, value: str) -> tuple[str, str | None]:
1890
+ """Parse a vault: reference string into path and optional fragment.
1891
+
1892
+ Extracts the vault path and optional fragment from a vault reference.
1893
+ The fragment is specified after a '#' character in the reference.
1894
+
1895
+ Args:
1896
+ value: The vault reference string (e.g., "vault:secret/path#field").
1897
+
1898
+ Returns:
1899
+ Tuple of (vault_path, fragment) where fragment may be None.
1900
+ The vault_path has the "vault:" prefix removed.
1901
+
1902
+ Example:
1903
+ >>> self._parse_vault_reference("vault:secret/data/db#password")
1904
+ ("secret/data/db", "password")
1905
+ >>> self._parse_vault_reference("vault:secret/data/config")
1906
+ ("secret/data/config", None)
1907
+ """
1908
+ vault_path = value[6:] # Remove "vault:" prefix
1909
+ return _split_path_and_fragment(vault_path)
1910
+
1911
+ def _has_vault_references(self, config: dict[str, object]) -> bool:
1912
+ """Check if config contains any vault: references (including nested dicts and lists).
1913
+
1914
+ Recursively scans the configuration dictionary to detect any string
1915
+ values that start with "vault:".
1916
+
1917
+ Args:
1918
+ config: Configuration dictionary to check.
1919
+
1920
+ Returns:
1921
+ True if any vault: references are found, False otherwise.
1922
+ """
1923
+ for value in config.values():
1924
+ if isinstance(value, str) and value.startswith("vault:"):
1925
+ return True
1926
+ if isinstance(value, dict):
1927
+ if self._has_vault_references(value):
1928
+ return True
1929
+ if isinstance(value, list):
1930
+ if self._has_vault_references_in_list(value):
1931
+ return True
1932
+ return False
1933
+
1934
+ def _has_vault_references_in_list(self, items: list[object]) -> bool:
1935
+ """Check if a list contains any vault: references (including nested structures).
1936
+
1937
+ Args:
1938
+ items: List to check for vault references.
1939
+
1940
+ Returns:
1941
+ True if any vault: references are found, False otherwise.
1942
+ """
1943
+ for item in items:
1944
+ if isinstance(item, str) and item.startswith("vault:"):
1945
+ return True
1946
+ if isinstance(item, dict):
1947
+ if self._has_vault_references(item):
1948
+ return True
1949
+ if isinstance(item, list):
1950
+ if self._has_vault_references_in_list(item):
1951
+ return True
1952
+ return False
1953
+
1954
+ def _apply_env_overrides(
1955
+ self,
1956
+ config: dict[str, object],
1957
+ handler_type: str,
1958
+ correlation_id: UUID,
1959
+ ) -> dict[str, object]:
1960
+ """Apply environment variable overrides.
1961
+
1962
+ Environment variables follow the pattern:
1963
+ {env_prefix}_{HANDLER_TYPE}_{FIELD}
1964
+
1965
+ For example: HANDLER_DB_TIMEOUT_MS=10000
1966
+
1967
+ Args:
1968
+ config: Base configuration dictionary.
1969
+ handler_type: Handler type for env var name construction.
1970
+ correlation_id: Correlation ID for error tracking.
1971
+
1972
+ Returns:
1973
+ Configuration with environment overrides applied.
1974
+ """
1975
+ result = dict(config)
1976
+ prefix = self._config.env_prefix
1977
+ handler_upper = handler_type.upper()
1978
+
1979
+ # Track retry policy overrides separately
1980
+ retry_overrides: dict[str, object] = {}
1981
+
1982
+ for env_field, model_field in _ENV_OVERRIDE_FIELDS.items():
1983
+ env_name = f"{prefix}_{handler_upper}_{env_field}"
1984
+ env_value = os.environ.get(env_name)
1985
+
1986
+ if env_value is not None:
1987
+ # Convert value based on expected type
1988
+ converted = self._convert_env_value(
1989
+ env_value, model_field, env_name, correlation_id
1990
+ )
1991
+ if converted is not None:
1992
+ if env_field in _RETRY_POLICY_FIELDS:
1993
+ retry_overrides[model_field] = converted
1994
+ else:
1995
+ result[model_field] = converted
1996
+
1997
+ # Merge retry policy overrides if any
1998
+ if retry_overrides:
1999
+ existing_retry = result.get("retry_policy")
2000
+ if isinstance(existing_retry, dict):
2001
+ merged_retry = dict(existing_retry)
2002
+ merged_retry.update(retry_overrides)
2003
+ result["retry_policy"] = merged_retry
2004
+ elif isinstance(existing_retry, ModelRetryPolicy):
2005
+ # Convert to dict, update, leave as dict for later construction
2006
+ merged_retry = existing_retry.model_dump()
2007
+ merged_retry.update(retry_overrides)
2008
+ result["retry_policy"] = merged_retry
2009
+ else:
2010
+ result["retry_policy"] = retry_overrides
2011
+
2012
+ return result
2013
+
2014
+ def _convert_env_value(
2015
+ self,
2016
+ value: str,
2017
+ field: str,
2018
+ env_name: str,
2019
+ correlation_id: UUID,
2020
+ ) -> object | None:
2021
+ """Convert environment variable string to appropriate type.
2022
+
2023
+ This method handles type coercion for environment variable overrides.
2024
+ The behavior on invalid values depends on the ``strict_env_coercion``
2025
+ configuration setting:
2026
+
2027
+ **Strict mode** (``strict_env_coercion=True``):
2028
+ Raises ``ProtocolConfigurationError`` immediately when a value
2029
+ cannot be converted to the expected type. This is appropriate
2030
+ for production environments where configuration errors should
2031
+ fail fast.
2032
+
2033
+ **Non-strict mode** (``strict_env_coercion=False``, default):
2034
+ Logs a warning and returns ``None``. When ``None`` is returned,
2035
+ the calling code skips the override entirely, leaving the
2036
+ original configuration value unchanged. This is intentional
2037
+ conservative behavior: rather than applying a potentially
2038
+ incorrect default, the system preserves the existing
2039
+ configuration when an environment variable contains an
2040
+ invalid value.
2041
+
2042
+ Args:
2043
+ value: String value from environment.
2044
+ field: Field name to determine type.
2045
+ env_name: Full environment variable name for error messages.
2046
+ correlation_id: Correlation ID for error tracking.
2047
+
2048
+ Returns:
2049
+ The converted value if conversion succeeds, or ``None`` if
2050
+ conversion fails in non-strict mode. When ``None`` is returned,
2051
+ the override is skipped and the original configuration value
2052
+ is preserved (the invalid environment variable is not applied).
2053
+
2054
+ Raises:
2055
+ ProtocolConfigurationError: If ``strict_env_coercion`` is enabled
2056
+ and the value cannot be converted to the expected type.
2057
+
2058
+ Example:
2059
+ Boolean coercion accepts case-insensitive values::
2060
+
2061
+ # All these evaluate to True:
2062
+ # HANDLER_DB_ENABLED=true, HANDLER_DB_ENABLED=1
2063
+ # HANDLER_DB_ENABLED=yes, HANDLER_DB_ENABLED=on
2064
+
2065
+ # All these evaluate to False:
2066
+ # HANDLER_DB_ENABLED=false, HANDLER_DB_ENABLED=0
2067
+ # HANDLER_DB_ENABLED=no, HANDLER_DB_ENABLED=off
2068
+
2069
+ Integer and float fields accept standard numeric strings::
2070
+
2071
+ # Integer: HANDLER_DB_TIMEOUT_MS=5000
2072
+ # Float: HANDLER_DB_RATE_LIMIT_PER_SECOND=100.5
2073
+
2074
+ Invalid values in non-strict mode are skipped (original preserved)::
2075
+
2076
+ # HANDLER_DB_ENABLED=invalid -> warning logged, original kept
2077
+ # HANDLER_DB_TIMEOUT_MS=not_a_number -> warning logged, original kept
2078
+ """
2079
+ # Boolean fields
2080
+ if field == "enabled":
2081
+ valid_true = {"true", "1", "yes", "on"}
2082
+ valid_false = {"false", "0", "no", "off"}
2083
+ value_lower = value.lower()
2084
+
2085
+ if value_lower in valid_true:
2086
+ return True
2087
+ if value_lower in valid_false:
2088
+ return False
2089
+
2090
+ # Invalid boolean value - handle based on strict mode
2091
+ self._handle_conversion_error(
2092
+ env_name=env_name,
2093
+ field=field,
2094
+ expected_type="boolean (true/false/1/0/yes/no/on/off)",
2095
+ correlation_id=correlation_id,
2096
+ )
2097
+ # In non-strict mode, skip override (return None) to match other types
2098
+ return None
2099
+
2100
+ # Integer fields
2101
+ if field in {
2102
+ "priority",
2103
+ "timeout_ms",
2104
+ "max_retries",
2105
+ "base_delay_ms",
2106
+ "max_delay_ms",
2107
+ }:
2108
+ try:
2109
+ return int(value)
2110
+ except ValueError:
2111
+ self._handle_conversion_error(
2112
+ env_name=env_name,
2113
+ field=field,
2114
+ expected_type="integer",
2115
+ correlation_id=correlation_id,
2116
+ )
2117
+ return None
2118
+
2119
+ # Float fields
2120
+ if field == "rate_limit_per_second":
2121
+ try:
2122
+ return float(value)
2123
+ except ValueError:
2124
+ self._handle_conversion_error(
2125
+ env_name=env_name,
2126
+ field=field,
2127
+ expected_type="float",
2128
+ correlation_id=correlation_id,
2129
+ )
2130
+ return None
2131
+
2132
+ # String fields
2133
+ if field in {"name", "backoff_strategy"}:
2134
+ return value
2135
+
2136
+ return value
2137
+
2138
+ def _handle_conversion_error(
2139
+ self,
2140
+ env_name: str,
2141
+ field: str,
2142
+ expected_type: str,
2143
+ correlation_id: UUID,
2144
+ ) -> None:
2145
+ """Handle type conversion error based on strict_env_coercion setting.
2146
+
2147
+ In strict mode, raises ProtocolConfigurationError.
2148
+ In lenient mode, logs a warning with structured context.
2149
+
2150
+ Args:
2151
+ env_name: Full environment variable name.
2152
+ field: Field name that was being set.
2153
+ expected_type: Expected type name (e.g., "integer", "float").
2154
+ correlation_id: Correlation ID for error tracking.
2155
+
2156
+ Raises:
2157
+ ProtocolConfigurationError: If strict_env_coercion is enabled.
2158
+ """
2159
+ if self._config.strict_env_coercion:
2160
+ context = ModelInfraErrorContext.with_correlation(
2161
+ correlation_id=correlation_id,
2162
+ transport_type=EnumInfraTransportType.RUNTIME,
2163
+ operation="convert_env_value",
2164
+ target_name="binding_config_resolver",
2165
+ )
2166
+ raise ProtocolConfigurationError(
2167
+ f"Invalid {expected_type} value in environment variable "
2168
+ f"'{env_name}' for field '{field}'",
2169
+ context=context,
2170
+ )
2171
+
2172
+ logger.warning(
2173
+ "Invalid %s value in environment variable '%s' for field '%s'; "
2174
+ "override will be skipped",
2175
+ expected_type,
2176
+ env_name,
2177
+ field,
2178
+ extra={
2179
+ "correlation_id": str(correlation_id),
2180
+ "env_var": env_name,
2181
+ "field": field,
2182
+ "expected_type": expected_type,
2183
+ },
2184
+ )
2185
+
2186
+ def _resolve_vault_refs(
2187
+ self,
2188
+ config: dict[str, object],
2189
+ correlation_id: UUID,
2190
+ depth: int = 0,
2191
+ ) -> dict[str, object]:
2192
+ """Resolve any vault: references in config values.
2193
+
2194
+ Scans all string values for vault: prefix and resolves them
2195
+ using the SecretResolver.
2196
+
2197
+ Args:
2198
+ config: Configuration dictionary.
2199
+ correlation_id: Correlation ID for error tracking.
2200
+ depth: Current recursion depth (default 0).
2201
+
2202
+ Returns:
2203
+ Configuration with vault references resolved.
2204
+
2205
+ Raises:
2206
+ ProtocolConfigurationError: If recursion depth exceeds maximum,
2207
+ or if fail_on_vault_error is True and a vault reference fails.
2208
+ """
2209
+ if depth > _MAX_NESTED_CONFIG_DEPTH:
2210
+ context = ModelInfraErrorContext.with_correlation(
2211
+ correlation_id=correlation_id,
2212
+ transport_type=EnumInfraTransportType.RUNTIME,
2213
+ operation="resolve_vault_refs",
2214
+ target_name="binding_config_resolver",
2215
+ )
2216
+ raise ProtocolConfigurationError(
2217
+ f"Configuration nesting exceeds maximum depth of {_MAX_NESTED_CONFIG_DEPTH}",
2218
+ context=context,
2219
+ )
2220
+
2221
+ secret_resolver = self._get_secret_resolver()
2222
+ if secret_resolver is None:
2223
+ # Check if there are vault references that need resolution
2224
+ # If fail_on_vault_error is True and vault refs exist, this is a security issue
2225
+ if self._config.fail_on_vault_error and self._has_vault_references(config):
2226
+ context = ModelInfraErrorContext.with_correlation(
2227
+ correlation_id=correlation_id,
2228
+ transport_type=EnumInfraTransportType.RUNTIME,
2229
+ operation="resolve_vault_refs",
2230
+ target_name="binding_config_resolver",
2231
+ )
2232
+ raise ProtocolConfigurationError(
2233
+ "Config contains vault: references but no SecretResolver is configured",
2234
+ context=context,
2235
+ )
2236
+ return config
2237
+
2238
+ result: dict[str, object] = {}
2239
+ for key, value in config.items():
2240
+ if isinstance(value, str) and value.startswith("vault:"):
2241
+ # Parse vault reference using helper method
2242
+ vault_path, fragment = self._parse_vault_reference(value)
2243
+ logical_name = f"{vault_path}#{fragment}" if fragment else vault_path
2244
+
2245
+ try:
2246
+ secret = secret_resolver.get_secret(logical_name, required=False)
2247
+ if secret is not None:
2248
+ result[key] = secret.get_secret_value()
2249
+ else:
2250
+ # Secret not found - check fail_on_vault_error
2251
+ if self._config.fail_on_vault_error:
2252
+ logger.error(
2253
+ "Vault secret not found for config key '%s'",
2254
+ key,
2255
+ extra={
2256
+ "correlation_id": str(correlation_id),
2257
+ "config_key": key,
2258
+ },
2259
+ )
2260
+ context = ModelInfraErrorContext.with_correlation(
2261
+ correlation_id=correlation_id,
2262
+ transport_type=EnumInfraTransportType.VAULT,
2263
+ operation="resolve_vault_refs",
2264
+ target_name="binding_config_resolver",
2265
+ )
2266
+ raise ProtocolConfigurationError(
2267
+ f"Vault secret not found for config key '{key}'",
2268
+ context=context,
2269
+ )
2270
+ result[key] = value # Keep original if not found
2271
+ except (SecretResolutionError, NotImplementedError) as e:
2272
+ # SecretResolutionError: secret not found or resolution failed
2273
+ # NotImplementedError: Vault integration not yet implemented
2274
+ # SECURITY: Log at DEBUG level only - exception may contain vault paths
2275
+ # Use DEBUG to capture details for troubleshooting without exposing
2276
+ # sensitive paths in production logs
2277
+ logger.debug(
2278
+ "Vault resolution failed for config key '%s' "
2279
+ "(correlation_id=%s): %s",
2280
+ key,
2281
+ correlation_id,
2282
+ e,
2283
+ extra={
2284
+ "correlation_id": str(correlation_id),
2285
+ "config_key": key,
2286
+ },
2287
+ )
2288
+ # Respect fail_on_vault_error config option
2289
+ if self._config.fail_on_vault_error:
2290
+ context = ModelInfraErrorContext.with_correlation(
2291
+ correlation_id=correlation_id,
2292
+ transport_type=EnumInfraTransportType.VAULT,
2293
+ operation="resolve_vault_refs",
2294
+ target_name="binding_config_resolver",
2295
+ )
2296
+ # SECURITY: Do NOT chain original exception (from e) - it may
2297
+ # contain vault paths in its message. Original error is logged
2298
+ # at DEBUG level above for troubleshooting.
2299
+ raise ProtocolConfigurationError(
2300
+ f"Failed to resolve Vault secret reference for config key "
2301
+ f"'{key}'. correlation_id={correlation_id}",
2302
+ context=context,
2303
+ )
2304
+ # Keep original on error (may be insecure - logged above)
2305
+ result[key] = value
2306
+ elif isinstance(value, dict):
2307
+ # Recursively resolve nested dicts
2308
+ result[key] = self._resolve_vault_refs(value, correlation_id, depth + 1)
2309
+ elif isinstance(value, list):
2310
+ # Recursively resolve vault references in list items
2311
+ result[key] = self._resolve_vault_refs_in_list(
2312
+ value, secret_resolver, correlation_id, depth + 1
2313
+ )
2314
+ else:
2315
+ result[key] = value
2316
+
2317
+ return result
2318
+
2319
+ def _resolve_vault_refs_in_list(
2320
+ self,
2321
+ items: list[object],
2322
+ secret_resolver: SecretResolver,
2323
+ correlation_id: UUID,
2324
+ depth: int,
2325
+ ) -> list[object]:
2326
+ """Resolve vault: references within a list.
2327
+
2328
+ Processes each item in the list, resolving any vault: references found
2329
+ in strings, nested dicts, or nested lists.
2330
+
2331
+ Args:
2332
+ items: List of items to process.
2333
+ secret_resolver: SecretResolver instance for vault lookups.
2334
+ correlation_id: Correlation ID for error tracking.
2335
+ depth: Current recursion depth.
2336
+
2337
+ Returns:
2338
+ List with vault references resolved.
2339
+
2340
+ Raises:
2341
+ ProtocolConfigurationError: If recursion depth exceeds maximum,
2342
+ or if fail_on_vault_error is True and a vault reference fails.
2343
+ """
2344
+ if depth > _MAX_NESTED_CONFIG_DEPTH:
2345
+ context = ModelInfraErrorContext.with_correlation(
2346
+ correlation_id=correlation_id,
2347
+ transport_type=EnumInfraTransportType.RUNTIME,
2348
+ operation="resolve_vault_refs_in_list",
2349
+ target_name="binding_config_resolver",
2350
+ )
2351
+ raise ProtocolConfigurationError(
2352
+ f"Configuration nesting exceeds maximum depth of {_MAX_NESTED_CONFIG_DEPTH}",
2353
+ context=context,
2354
+ )
2355
+
2356
+ result: list[object] = []
2357
+ for i, item in enumerate(items):
2358
+ if isinstance(item, str) and item.startswith("vault:"):
2359
+ # Parse vault reference using helper method
2360
+ vault_path, fragment = self._parse_vault_reference(item)
2361
+ logical_name = f"{vault_path}#{fragment}" if fragment else vault_path
2362
+
2363
+ try:
2364
+ secret = secret_resolver.get_secret(logical_name, required=False)
2365
+ if secret is not None:
2366
+ result.append(secret.get_secret_value())
2367
+ else:
2368
+ # Secret not found - check fail_on_vault_error
2369
+ if self._config.fail_on_vault_error:
2370
+ logger.error(
2371
+ "Vault secret not found at list index %d",
2372
+ i,
2373
+ extra={
2374
+ "correlation_id": str(correlation_id),
2375
+ "list_index": i,
2376
+ },
2377
+ )
2378
+ context = ModelInfraErrorContext.with_correlation(
2379
+ correlation_id=correlation_id,
2380
+ transport_type=EnumInfraTransportType.VAULT,
2381
+ operation="resolve_vault_refs_in_list",
2382
+ target_name="binding_config_resolver",
2383
+ )
2384
+ raise ProtocolConfigurationError(
2385
+ f"Vault secret not found at list index {i}",
2386
+ context=context,
2387
+ )
2388
+ result.append(item) # Keep original if not found
2389
+ except (SecretResolutionError, NotImplementedError) as e:
2390
+ # SecretResolutionError: secret not found or resolution failed
2391
+ # NotImplementedError: Vault integration not yet implemented
2392
+ # SECURITY: Log at DEBUG level only - exception may contain vault paths
2393
+ # Do not log vault_path - reveals secret structure
2394
+ logger.debug(
2395
+ "Vault resolution failed at list index %d "
2396
+ "(correlation_id=%s): %s",
2397
+ i,
2398
+ correlation_id,
2399
+ e,
2400
+ extra={
2401
+ "correlation_id": str(correlation_id),
2402
+ "list_index": i,
2403
+ },
2404
+ )
2405
+ if self._config.fail_on_vault_error:
2406
+ context = ModelInfraErrorContext.with_correlation(
2407
+ correlation_id=correlation_id,
2408
+ transport_type=EnumInfraTransportType.VAULT,
2409
+ operation="resolve_vault_refs_in_list",
2410
+ target_name="binding_config_resolver",
2411
+ )
2412
+ # SECURITY: Do NOT chain original exception (from e) - it may
2413
+ # contain vault paths in its message.
2414
+ raise ProtocolConfigurationError(
2415
+ f"Failed to resolve Vault secret reference at list index {i}. "
2416
+ f"correlation_id={correlation_id}",
2417
+ context=context,
2418
+ )
2419
+ result.append(item)
2420
+ elif isinstance(item, dict):
2421
+ result.append(self._resolve_vault_refs(item, correlation_id, depth + 1))
2422
+ elif isinstance(item, list):
2423
+ result.append(
2424
+ self._resolve_vault_refs_in_list(
2425
+ item, secret_resolver, correlation_id, depth + 1
2426
+ )
2427
+ )
2428
+ else:
2429
+ result.append(item)
2430
+
2431
+ return result
2432
+
2433
+ async def _resolve_vault_refs_async(
2434
+ self,
2435
+ config: dict[str, object],
2436
+ correlation_id: UUID,
2437
+ depth: int = 0,
2438
+ ) -> dict[str, object]:
2439
+ """Resolve any vault: references in config values asynchronously.
2440
+
2441
+ Args:
2442
+ config: Configuration dictionary.
2443
+ correlation_id: Correlation ID for error tracking.
2444
+ depth: Current recursion depth (default 0).
2445
+
2446
+ Returns:
2447
+ Configuration with vault references resolved.
2448
+
2449
+ Raises:
2450
+ ProtocolConfigurationError: If recursion depth exceeds maximum,
2451
+ or if fail_on_vault_error is True and a vault reference fails.
2452
+ """
2453
+ if depth > _MAX_NESTED_CONFIG_DEPTH:
2454
+ context = ModelInfraErrorContext.with_correlation(
2455
+ correlation_id=correlation_id,
2456
+ transport_type=EnumInfraTransportType.RUNTIME,
2457
+ operation="resolve_vault_refs_async",
2458
+ target_name="binding_config_resolver",
2459
+ )
2460
+ raise ProtocolConfigurationError(
2461
+ f"Configuration nesting exceeds maximum depth of {_MAX_NESTED_CONFIG_DEPTH}",
2462
+ context=context,
2463
+ )
2464
+
2465
+ secret_resolver = self._get_secret_resolver()
2466
+ if secret_resolver is None:
2467
+ # Check if there are vault references that need resolution
2468
+ # If fail_on_vault_error is True and vault refs exist, this is a security issue
2469
+ if self._config.fail_on_vault_error and self._has_vault_references(config):
2470
+ context = ModelInfraErrorContext.with_correlation(
2471
+ correlation_id=correlation_id,
2472
+ transport_type=EnumInfraTransportType.RUNTIME,
2473
+ operation="resolve_vault_refs_async",
2474
+ target_name="binding_config_resolver",
2475
+ )
2476
+ raise ProtocolConfigurationError(
2477
+ "Config contains vault: references but no SecretResolver is configured",
2478
+ context=context,
2479
+ )
2480
+ return config
2481
+
2482
+ result: dict[str, object] = {}
2483
+ for key, value in config.items():
2484
+ if isinstance(value, str) and value.startswith("vault:"):
2485
+ # Parse vault reference using helper method
2486
+ vault_path, fragment = self._parse_vault_reference(value)
2487
+ logical_name = f"{vault_path}#{fragment}" if fragment else vault_path
2488
+
2489
+ try:
2490
+ secret = await secret_resolver.get_secret_async(
2491
+ logical_name, required=False
2492
+ )
2493
+ if secret is not None:
2494
+ result[key] = secret.get_secret_value()
2495
+ else:
2496
+ # Secret not found - check fail_on_vault_error
2497
+ if self._config.fail_on_vault_error:
2498
+ logger.error(
2499
+ "Vault secret not found for config key '%s'",
2500
+ key,
2501
+ extra={
2502
+ "correlation_id": str(correlation_id),
2503
+ "config_key": key,
2504
+ },
2505
+ )
2506
+ context = ModelInfraErrorContext.with_correlation(
2507
+ correlation_id=correlation_id,
2508
+ transport_type=EnumInfraTransportType.VAULT,
2509
+ operation="resolve_vault_refs_async",
2510
+ target_name="binding_config_resolver",
2511
+ )
2512
+ raise ProtocolConfigurationError(
2513
+ f"Vault secret not found for config key '{key}'",
2514
+ context=context,
2515
+ )
2516
+ result[key] = value # Keep original if not found
2517
+ except (SecretResolutionError, NotImplementedError) as e:
2518
+ # SecretResolutionError: secret not found or resolution failed
2519
+ # NotImplementedError: Vault integration not yet implemented
2520
+ # SECURITY: Log at DEBUG level only - exception may contain vault paths
2521
+ # Use DEBUG to capture details for troubleshooting without exposing
2522
+ # sensitive paths in production logs
2523
+ logger.debug(
2524
+ "Vault resolution failed for config key '%s' "
2525
+ "(correlation_id=%s): %s",
2526
+ key,
2527
+ correlation_id,
2528
+ e,
2529
+ extra={
2530
+ "correlation_id": str(correlation_id),
2531
+ "config_key": key,
2532
+ },
2533
+ )
2534
+ # Respect fail_on_vault_error config option
2535
+ if self._config.fail_on_vault_error:
2536
+ context = ModelInfraErrorContext.with_correlation(
2537
+ correlation_id=correlation_id,
2538
+ transport_type=EnumInfraTransportType.VAULT,
2539
+ operation="resolve_vault_refs_async",
2540
+ target_name="binding_config_resolver",
2541
+ )
2542
+ # SECURITY: Do NOT chain original exception (from e) - it may
2543
+ # contain vault paths in its message. Original error is logged
2544
+ # at DEBUG level above for troubleshooting.
2545
+ raise ProtocolConfigurationError(
2546
+ f"Failed to resolve Vault secret reference for config key "
2547
+ f"'{key}'. correlation_id={correlation_id}",
2548
+ context=context,
2549
+ )
2550
+ # Keep original on error (may be insecure - logged above)
2551
+ result[key] = value
2552
+ elif isinstance(value, dict):
2553
+ # Recursively resolve nested dicts
2554
+ result[key] = await self._resolve_vault_refs_async(
2555
+ value, correlation_id, depth + 1
2556
+ )
2557
+ elif isinstance(value, list):
2558
+ # Recursively resolve vault references in list items
2559
+ result[key] = await self._resolve_vault_refs_in_list_async(
2560
+ value, secret_resolver, correlation_id, depth + 1
2561
+ )
2562
+ else:
2563
+ result[key] = value
2564
+
2565
+ return result
2566
+
2567
+ async def _resolve_vault_refs_in_list_async(
2568
+ self,
2569
+ items: list[object],
2570
+ secret_resolver: SecretResolver,
2571
+ correlation_id: UUID,
2572
+ depth: int,
2573
+ ) -> list[object]:
2574
+ """Resolve vault: references within a list asynchronously.
2575
+
2576
+ Processes each item in the list, resolving any vault: references found
2577
+ in strings, nested dicts, or nested lists.
2578
+
2579
+ Args:
2580
+ items: List of items to process.
2581
+ secret_resolver: SecretResolver instance for vault lookups.
2582
+ correlation_id: Correlation ID for error tracking.
2583
+ depth: Current recursion depth.
2584
+
2585
+ Returns:
2586
+ List with vault references resolved.
2587
+
2588
+ Raises:
2589
+ ProtocolConfigurationError: If recursion depth exceeds maximum,
2590
+ or if fail_on_vault_error is True and a vault reference fails.
2591
+ """
2592
+ if depth > _MAX_NESTED_CONFIG_DEPTH:
2593
+ context = ModelInfraErrorContext.with_correlation(
2594
+ correlation_id=correlation_id,
2595
+ transport_type=EnumInfraTransportType.RUNTIME,
2596
+ operation="resolve_vault_refs_in_list_async",
2597
+ target_name="binding_config_resolver",
2598
+ )
2599
+ raise ProtocolConfigurationError(
2600
+ f"Configuration nesting exceeds maximum depth of {_MAX_NESTED_CONFIG_DEPTH}",
2601
+ context=context,
2602
+ )
2603
+
2604
+ result: list[object] = []
2605
+ for i, item in enumerate(items):
2606
+ if isinstance(item, str) and item.startswith("vault:"):
2607
+ # Parse vault reference using helper method
2608
+ vault_path, fragment = self._parse_vault_reference(item)
2609
+ logical_name = f"{vault_path}#{fragment}" if fragment else vault_path
2610
+
2611
+ try:
2612
+ secret = await secret_resolver.get_secret_async(
2613
+ logical_name, required=False
2614
+ )
2615
+ if secret is not None:
2616
+ result.append(secret.get_secret_value())
2617
+ else:
2618
+ # Secret not found - check fail_on_vault_error
2619
+ if self._config.fail_on_vault_error:
2620
+ logger.error(
2621
+ "Vault secret not found at list index %d",
2622
+ i,
2623
+ extra={
2624
+ "correlation_id": str(correlation_id),
2625
+ "list_index": i,
2626
+ },
2627
+ )
2628
+ context = ModelInfraErrorContext.with_correlation(
2629
+ correlation_id=correlation_id,
2630
+ transport_type=EnumInfraTransportType.VAULT,
2631
+ operation="resolve_vault_refs_in_list_async",
2632
+ target_name="binding_config_resolver",
2633
+ )
2634
+ raise ProtocolConfigurationError(
2635
+ f"Vault secret not found at list index {i}",
2636
+ context=context,
2637
+ )
2638
+ result.append(item) # Keep original if not found
2639
+ except (SecretResolutionError, NotImplementedError) as e:
2640
+ # SecretResolutionError: secret not found or resolution failed
2641
+ # NotImplementedError: Vault integration not yet implemented
2642
+ # SECURITY: Log at DEBUG level only - exception may contain vault paths
2643
+ # Do not log vault_path - reveals secret structure
2644
+ logger.debug(
2645
+ "Vault resolution failed at list index %d "
2646
+ "(correlation_id=%s): %s",
2647
+ i,
2648
+ correlation_id,
2649
+ e,
2650
+ extra={
2651
+ "correlation_id": str(correlation_id),
2652
+ "list_index": i,
2653
+ },
2654
+ )
2655
+ if self._config.fail_on_vault_error:
2656
+ context = ModelInfraErrorContext.with_correlation(
2657
+ correlation_id=correlation_id,
2658
+ transport_type=EnumInfraTransportType.VAULT,
2659
+ operation="resolve_vault_refs_in_list_async",
2660
+ target_name="binding_config_resolver",
2661
+ )
2662
+ # SECURITY: Do NOT chain original exception (from e) - it may
2663
+ # contain vault paths in its message.
2664
+ raise ProtocolConfigurationError(
2665
+ f"Failed to resolve Vault secret reference at list index {i}. "
2666
+ f"correlation_id={correlation_id}",
2667
+ context=context,
2668
+ )
2669
+ result.append(item)
2670
+ elif isinstance(item, dict):
2671
+ result.append(
2672
+ await self._resolve_vault_refs_async(
2673
+ item, correlation_id, depth + 1
2674
+ )
2675
+ )
2676
+ elif isinstance(item, list):
2677
+ result.append(
2678
+ await self._resolve_vault_refs_in_list_async(
2679
+ item, secret_resolver, correlation_id, depth + 1
2680
+ )
2681
+ )
2682
+ else:
2683
+ result.append(item)
2684
+
2685
+ return result
2686
+
2687
+ def _validate_config(
2688
+ self,
2689
+ config: dict[str, object],
2690
+ handler_type: str,
2691
+ correlation_id: UUID,
2692
+ ) -> ModelBindingConfig:
2693
+ """Validate and construct the final config model.
2694
+
2695
+ Args:
2696
+ config: Merged configuration dictionary.
2697
+ handler_type: Handler type identifier.
2698
+ correlation_id: Correlation ID for error tracking.
2699
+
2700
+ Returns:
2701
+ Validated ModelBindingConfig.
2702
+
2703
+ Raises:
2704
+ ProtocolConfigurationError: If configuration is invalid.
2705
+ """
2706
+ # Handle retry_policy construction if it's a dict
2707
+ retry_policy = config.get("retry_policy")
2708
+ if isinstance(retry_policy, dict):
2709
+ try:
2710
+ config["retry_policy"] = ModelRetryPolicy.model_validate(retry_policy)
2711
+ except ValidationError as e:
2712
+ # ValidationError: Pydantic model validation failed
2713
+ context = ModelInfraErrorContext.with_correlation(
2714
+ correlation_id=correlation_id,
2715
+ transport_type=EnumInfraTransportType.RUNTIME,
2716
+ operation="validate_config",
2717
+ target_name=f"handler:{handler_type}",
2718
+ )
2719
+ # SECURITY: Log at DEBUG level only - validation error may contain
2720
+ # config values which could include secrets
2721
+ logger.debug(
2722
+ "Retry policy validation failed for handler '%s' "
2723
+ "(correlation_id=%s): %s",
2724
+ handler_type,
2725
+ correlation_id,
2726
+ e,
2727
+ extra={"correlation_id": str(correlation_id)},
2728
+ )
2729
+ # SECURITY: Do NOT chain original exception (from e) - Pydantic
2730
+ # validation errors may contain config values in their message.
2731
+ raise ProtocolConfigurationError(
2732
+ f"Invalid retry policy configuration for handler '{handler_type}'. "
2733
+ f"correlation_id={correlation_id}",
2734
+ context=context,
2735
+ )
2736
+
2737
+ # Filter to only known fields if strict validation is disabled
2738
+ if not self._config.strict_validation:
2739
+ known_fields = set(ModelBindingConfig.model_fields.keys())
2740
+ config = {k: v for k, v in config.items() if k in known_fields}
2741
+
2742
+ try:
2743
+ return ModelBindingConfig.model_validate(config)
2744
+ except ValidationError as e:
2745
+ # ValidationError: Pydantic model validation failed
2746
+ context = ModelInfraErrorContext.with_correlation(
2747
+ correlation_id=correlation_id,
2748
+ transport_type=EnumInfraTransportType.RUNTIME,
2749
+ operation="validate_config",
2750
+ target_name=f"handler:{handler_type}",
2751
+ )
2752
+ # SECURITY: Log at DEBUG level only - validation error may contain
2753
+ # config values which could include secrets or sensitive data
2754
+ logger.debug(
2755
+ "Handler configuration validation failed for '%s' "
2756
+ "(correlation_id=%s): %s",
2757
+ handler_type,
2758
+ correlation_id,
2759
+ e,
2760
+ extra={"correlation_id": str(correlation_id)},
2761
+ )
2762
+ # SECURITY: Do NOT chain original exception (from e) - Pydantic
2763
+ # validation errors may contain config values in their message.
2764
+ raise ProtocolConfigurationError(
2765
+ f"Invalid handler configuration for type '{handler_type}'. "
2766
+ f"correlation_id={correlation_id}",
2767
+ context=context,
2768
+ )
2769
+
2770
+
2771
+ __all__: Final[list[str]] = ["BindingConfigResolver"]