omnibase_infra 0.2.1__py3-none-any.whl

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