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,2110 @@
1
+ # SPDX-License-Identifier: MIT
2
+ # Copyright (c) 2025 OmniNode Team
3
+ """Centralized secret resolution for ONEX infrastructure.
4
+
5
+ SecretResolver provides a unified interface for accessing secrets from multiple sources:
6
+ - Vault (via HandlerVault for KV v2 secrets engine)
7
+ - Environment variables
8
+ - File-based secrets (K8s /run/secrets)
9
+
10
+ Design Philosophy:
11
+ - Dumb and deterministic: resolves and caches, does not discover or mutate
12
+ - Explicit mappings preferred, convention fallback optional
13
+ - Bootstrap secrets (Vault token/addr) always from env
14
+ - Vault is treated as an injected dependency, SecretResolver owns mapping + caching + policy
15
+
16
+ Example:
17
+ Bootstrap phase (env-only for Vault credentials)::
18
+
19
+ vault_token = os.environ.get("VAULT_TOKEN")
20
+ vault_addr = os.environ.get("VAULT_ADDR")
21
+
22
+ Initialize resolver with Vault handler::
23
+
24
+ vault_handler = HandlerVault()
25
+ await vault_handler.initialize({...})
26
+
27
+ config = ModelSecretResolverConfig(mappings=[...])
28
+ resolver = SecretResolver(config=config, vault_handler=vault_handler)
29
+
30
+ Resolve secrets with correlation ID for tracing::
31
+
32
+ db_password = resolver.get_secret(
33
+ "database.postgres.password",
34
+ correlation_id=request.correlation_id,
35
+ )
36
+ api_key = await resolver.get_secret_async(
37
+ "llm.openai.api_key",
38
+ required=False,
39
+ correlation_id=request.correlation_id,
40
+ )
41
+
42
+ Get resolution metrics::
43
+
44
+ metrics = resolver.get_resolution_metrics()
45
+ # ModelSecretResolverMetrics(success_counts={"env": 5, "vault": 3}, ...)
46
+
47
+ Security Considerations:
48
+ - Secret values are wrapped in SecretStr to prevent accidental logging
49
+ - Cache stores SecretStr values, never raw strings
50
+ - Introspection methods never expose secret values
51
+ - Error messages are sanitized to exclude secret values
52
+ - File paths are never logged (prevents information disclosure)
53
+ - Path traversal attacks are blocked for file-based secrets
54
+ - Bootstrap secrets bypass normal resolution to prevent circular dependencies
55
+ - Vault paths are never logged (could reveal secret structure)
56
+
57
+ Memory Handling:
58
+ Raw secret values (plain strings) are briefly held in local variables during
59
+ resolution before being wrapped in SecretStr. Python's garbage collector will
60
+ reclaim this memory, but there is no explicit secure memory wiping. This is
61
+ acceptable for most use cases, but for high-security environments:
62
+
63
+ - Consider using dedicated secret management libraries with secure memory handling
64
+ - Use short-lived processes for secret-intensive operations
65
+ - Ensure swap is encrypted at the OS level
66
+
67
+ The brief exposure window is minimized by immediately wrapping values in SecretStr
68
+ after retrieval and never storing raw strings in instance attributes.
69
+
70
+ Vault Integration:
71
+ Vault secrets are resolved via HandlerVault (KV v2 secrets engine only).
72
+
73
+ Path Format: "mount_point/path/to/secret#field"
74
+ - mount_point: The secrets engine mount (e.g., "secret")
75
+ - path: The secret path within the mount (e.g., "myapp/db")
76
+ - field: Optional specific field to extract (e.g., "password")
77
+
78
+ Examples:
79
+ - "secret/myapp/db#password" -> Reads "password" field from secret at myapp/db
80
+ - "secret/myapp/db" -> Reads first field value from secret
81
+
82
+ Type Handling:
83
+ All Vault values are converted to strings. This is intentional because
84
+ SecretResolver returns SecretStr values (which only wrap strings) for
85
+ security. Non-string Vault values are converted via Python's str():
86
+
87
+ - Integers: 123 -> "123"
88
+ - Booleans: True -> "True"
89
+ - Lists/Dicts: Python repr (NOT JSON)
90
+
91
+ Best Practice: Store secrets as strings in Vault. For structured data,
92
+ store as JSON strings and parse after resolution.
93
+
94
+ Graceful Degradation:
95
+ - If vault_handler is None: Returns None with a warning log
96
+ - Vault errors are wrapped in SecretResolutionError with correlation ID
97
+
98
+ Error Handling:
99
+ - InfraAuthenticationError: Auth failures (403)
100
+ - InfraTimeoutError: Request timeouts
101
+ - InfraUnavailableError: Circuit breaker open
102
+ - SecretResolutionError: Other Vault errors (sanitized message)
103
+
104
+ Observability (OMN-1374):
105
+ SecretResolver includes built-in metrics tracking:
106
+
107
+ - Resolution latency by source type (env, file, vault, cache)
108
+ - Cache hit/miss rates (via get_cache_stats())
109
+ - Resolution success/failure counts by source type
110
+
111
+ External metrics collection via ProtocolSecretResolverMetrics:
112
+ metrics_collector = MyPrometheusMetrics()
113
+ resolver = SecretResolver(config=config, metrics_collector=metrics_collector)
114
+
115
+ Structured logging includes:
116
+ - logical_name: The secret being resolved
117
+ - source_type: Where the secret came from
118
+ - cache_hit: Whether it was a cache hit
119
+ - correlation_id: For distributed tracing
120
+ - latency_ms: Resolution time (on success)
121
+ """
122
+
123
+ from __future__ import annotations
124
+
125
+ import asyncio
126
+ import logging
127
+ import os
128
+ import random
129
+ import threading
130
+ import time
131
+ from collections import defaultdict, deque
132
+ from datetime import UTC, datetime, timedelta
133
+ from pathlib import Path
134
+ from typing import TYPE_CHECKING, Protocol, runtime_checkable
135
+ from uuid import UUID, uuid4
136
+
137
+ from pydantic import SecretStr
138
+
139
+ from omnibase_core.types import JsonType
140
+ from omnibase_infra.enums import EnumInfraTransportType
141
+ from omnibase_infra.errors import (
142
+ InfraAuthenticationError,
143
+ InfraTimeoutError,
144
+ InfraUnavailableError,
145
+ ModelInfraErrorContext,
146
+ ProtocolConfigurationError,
147
+ SecretResolutionError,
148
+ )
149
+ from omnibase_infra.runtime.models.model_cached_secret import ModelCachedSecret
150
+ from omnibase_infra.runtime.models.model_secret_cache_stats import ModelSecretCacheStats
151
+ from omnibase_infra.runtime.models.model_secret_resolver_config import (
152
+ ModelSecretResolverConfig,
153
+ )
154
+ from omnibase_infra.runtime.models.model_secret_resolver_metrics import (
155
+ ModelSecretResolverMetrics,
156
+ )
157
+ from omnibase_infra.runtime.models.model_secret_source_info import ModelSecretSourceInfo
158
+ from omnibase_infra.runtime.models.model_secret_source_spec import (
159
+ ModelSecretSourceSpec,
160
+ SecretSourceType,
161
+ )
162
+ from omnibase_infra.utils.correlation import generate_correlation_id
163
+
164
+ if TYPE_CHECKING:
165
+ from omnibase_core.container import ModelONEXContainer
166
+ from omnibase_infra.handlers.handler_vault import HandlerVault
167
+
168
+
169
+ logger = logging.getLogger(__name__)
170
+
171
+
172
+ @runtime_checkable
173
+ class ProtocolSecretResolverMetrics(Protocol):
174
+ """Protocol for SecretResolver metrics collection.
175
+
176
+ Implementations can hook into secret resolution operations to collect:
177
+ - Resolution latency by source type (env, file, vault, cache)
178
+ - Cache hit/miss rates
179
+ - Resolution failure counts by source type
180
+ - Success counts by source type
181
+
182
+ All methods are optional (duck-typed). If a method is not implemented,
183
+ the metric simply won't be recorded.
184
+
185
+ Thread Safety:
186
+ Implementations MUST be thread-safe because SecretResolver's
187
+ ``_record_resolution_success`` and ``_record_resolution_failure`` methods
188
+ may be called concurrently from multiple threads during parallel
189
+ secret resolution operations. This includes:
190
+
191
+ - Multiple sync callers resolving different secrets simultaneously
192
+ - Async callers running in parallel via ``asyncio.gather``
193
+ - Mixed sync/async access patterns during bootstrap and runtime
194
+
195
+ Thread-Safe Primitives (recommended):
196
+ - ``threading.Lock``: Protects counter increments and dict updates
197
+ - ``threading.RLock``: For reentrant access (if metrics methods call each other)
198
+ - ``collections.Counter`` with lock protection: Convenient for source_type counts
199
+ - ``prometheus_client``: Inherently thread-safe (Counter, Histogram, Gauge)
200
+ - ``queue.Queue``: For async metric collection (producer-consumer pattern)
201
+
202
+ Example Implementation (threading.Lock)::
203
+
204
+ import threading
205
+ from collections import defaultdict
206
+
207
+ class ThreadSafeSecretResolverMetrics:
208
+ '''Minimal thread-safe metrics using threading.Lock.'''
209
+
210
+ def __init__(self) -> None:
211
+ self._lock = threading.Lock()
212
+ self._latencies: list[tuple[str, float]] = []
213
+ self._cache_hits = 0
214
+ self._cache_misses = 0
215
+ self._success_counts: defaultdict[str, int] = defaultdict(int)
216
+ self._failure_counts: defaultdict[str, int] = defaultdict(int)
217
+
218
+ def record_resolution_latency(
219
+ self, source_type: str, latency_ms: float
220
+ ) -> None:
221
+ with self._lock:
222
+ self._latencies.append((source_type, latency_ms))
223
+
224
+ def record_cache_hit(self) -> None:
225
+ with self._lock:
226
+ self._cache_hits += 1
227
+
228
+ def record_cache_miss(self) -> None:
229
+ with self._lock:
230
+ self._cache_misses += 1
231
+
232
+ def record_resolution_success(self, source_type: str) -> None:
233
+ with self._lock:
234
+ self._success_counts[source_type] += 1
235
+
236
+ def record_resolution_failure(self, source_type: str) -> None:
237
+ with self._lock:
238
+ self._failure_counts[source_type] += 1
239
+
240
+ Example Implementation (prometheus_client)::
241
+
242
+ from prometheus_client import Counter, Histogram
243
+
244
+ class PrometheusSecretResolverMetrics:
245
+ def __init__(self) -> None:
246
+ self._latency = Histogram(
247
+ 'secret_resolution_latency_ms',
248
+ 'Latency of secret resolution in milliseconds',
249
+ ['source_type'],
250
+ )
251
+ self._cache_hits = Counter('secret_cache_hits_total', 'Cache hits')
252
+ self._cache_misses = Counter('secret_cache_misses_total', 'Cache misses')
253
+ self._failures = Counter(
254
+ 'secret_resolution_failures_total',
255
+ 'Resolution failures',
256
+ ['source_type'],
257
+ )
258
+
259
+ def record_resolution_latency(
260
+ self, source_type: str, latency_ms: float
261
+ ) -> None:
262
+ self._latency.labels(source_type=source_type).observe(latency_ms)
263
+
264
+ def record_cache_hit(self) -> None:
265
+ self._cache_hits.inc()
266
+
267
+ def record_cache_miss(self) -> None:
268
+ self._cache_misses.inc()
269
+
270
+ def record_resolution_failure(self, source_type: str) -> None:
271
+ self._failures.labels(source_type=source_type).inc()
272
+ """
273
+
274
+ def record_resolution_latency(self, source_type: str, latency_ms: float) -> None:
275
+ """Record latency for a secret resolution operation.
276
+
277
+ Args:
278
+ source_type: Source type (env, file, vault, cache)
279
+ latency_ms: Time in milliseconds for the operation
280
+ """
281
+ ...
282
+
283
+ def record_cache_hit(self) -> None:
284
+ """Record a cache hit."""
285
+ ...
286
+
287
+ def record_cache_miss(self) -> None:
288
+ """Record a cache miss."""
289
+ ...
290
+
291
+ def record_resolution_success(self, source_type: str) -> None:
292
+ """Record a successful resolution.
293
+
294
+ Args:
295
+ source_type: Source type (env, file, vault)
296
+ """
297
+ ...
298
+
299
+ def record_resolution_failure(self, source_type: str) -> None:
300
+ """Record a resolution failure.
301
+
302
+ Args:
303
+ source_type: Source type (env, file, vault)
304
+ """
305
+ ...
306
+
307
+
308
+ # Maximum file size for secret files (1MB)
309
+ # Prevents memory exhaustion from accidentally pointing at large files
310
+ MAX_SECRET_FILE_SIZE = 1024 * 1024
311
+
312
+ # Maximum latency samples to retain for metrics (rolling window)
313
+ MAX_LATENCY_SAMPLES = 1000
314
+
315
+ # Warning threshold for async key locks dictionary size (DoS mitigation)
316
+ ASYNC_KEY_LOCKS_WARNING_THRESHOLD = 1000
317
+
318
+ # Maximum async key locks before LRU eviction (memory leak prevention)
319
+ # When this limit is reached, oldest 10% of entries are evicted
320
+ MAX_ASYNC_KEY_LOCKS = 1000
321
+
322
+ # Cache TTL jitter percentage for symmetric ±10% jitter (stampede prevention)
323
+ CACHE_TTL_JITTER_PERCENT = 0.1
324
+
325
+ # Rate limit interval for LRU eviction warnings (prevents log flooding)
326
+ EVICTION_WARNING_INTERVAL_SECONDS = 60.0
327
+
328
+
329
+ class SecretResolver:
330
+ """Centralized secret resolution. Dumb and deterministic.
331
+
332
+ The SecretResolver provides a unified interface for accessing secrets from
333
+ multiple sources with caching and optional convention-based fallback.
334
+
335
+ Resolution Order:
336
+ 1. Check cache (if not expired)
337
+ 2. Try explicit mapping from configuration
338
+ 3. Try convention fallback (if enabled): logical_name -> ENV_VAR
339
+ 4. Raise or return None based on required flag
340
+
341
+ Thread Safety:
342
+ This class supports concurrent access from both sync and async contexts
343
+ using a two-level locking strategy:
344
+
345
+ 1. ``threading.RLock`` (``_lock``): Protects all cache reads/writes and
346
+ stats updates. This lock is held briefly for in-memory operations.
347
+
348
+ 2. Per-key ``asyncio.Lock`` (``_async_key_locks``): Prevents duplicate
349
+ async fetches for the SAME secret. When multiple async callers request
350
+ the same secret simultaneously, only one performs the fetch while
351
+ others wait and reuse the cached result. Different secrets can be
352
+ fetched in parallel.
353
+
354
+ Sync/Async Coordination:
355
+ - Sync ``get_secret``: Holds ``_lock`` for entire operation (cache
356
+ check through cache write). This ensures atomicity but may briefly
357
+ block async callers during cache access.
358
+ - Async ``get_secret_async``: Uses per-key async locks to serialize
359
+ fetches for the same key, with ``_lock`` held only briefly for
360
+ cache access. This allows parallel fetches for different secrets.
361
+
362
+ Edge Case - Sync/Async Race:
363
+ Due to the different locking granularity between sync (holds lock
364
+ during I/O) and async (releases lock during I/O), there's a small
365
+ window where both sync and async code might resolve the same secret
366
+ simultaneously. This is handled by a check-before-write pattern:
367
+ before caching, we verify the key isn't already present. If a sync
368
+ caller won the race, we skip the redundant cache write.
369
+
370
+ Why this race is acceptable:
371
+ 1. Both sync and async resolvers fetch the same value from the same
372
+ source (env var, file, or Vault path), so the resolved values are
373
+ always identical.
374
+ 2. The skip-if-present check prevents wasted cache writes, but even
375
+ without it, last-write-wins produces correct results since all
376
+ writers have the same value.
377
+ 3. There is no correctness issue - only a minor inefficiency of
378
+ potentially resolving the same secret twice in rare cases.
379
+
380
+ Bootstrap Secret Isolation:
381
+ Bootstrap secrets (vault.token, vault.addr, vault.ca_cert) are
382
+ resolved exclusively from environment variables, never from Vault
383
+ or files. This prevents circular dependencies during Vault init.
384
+ The resolution path is isolated from regular secrets, and cache
385
+ writes are always protected by ``_lock`` in both sync and async
386
+ contexts.
387
+
388
+ Example:
389
+ >>> config = ModelSecretResolverConfig(
390
+ ... mappings=[
391
+ ... ModelSecretMapping(
392
+ ... logical_name="database.postgres.password",
393
+ ... source=ModelSecretSourceSpec(
394
+ ... source_type="env",
395
+ ... source_path="POSTGRES_PASSWORD"
396
+ ... )
397
+ ... )
398
+ ... ]
399
+ ... )
400
+ >>> resolver = SecretResolver(config=config)
401
+ >>> password = resolver.get_secret("database.postgres.password")
402
+ """
403
+
404
+ def __init__(
405
+ self,
406
+ config: ModelSecretResolverConfig,
407
+ vault_handler: HandlerVault | None = None,
408
+ metrics_collector: ProtocolSecretResolverMetrics | None = None,
409
+ ) -> None:
410
+ """Initialize SecretResolver.
411
+
412
+ Args:
413
+ config: Resolver configuration with mappings and TTLs
414
+ vault_handler: Optional Vault handler for Vault-sourced secrets
415
+ metrics_collector: Optional external metrics collector for observability
416
+
417
+ Note:
418
+ For ONEX applications using ``ModelONEXContainer``, consider resolving
419
+ dependencies via container-based DI rather than direct constructor
420
+ injection. This enables centralized lifecycle management and consistent
421
+ dependency resolution across the application. The current explicit
422
+ constructor parameters are retained for flexibility in standalone usage
423
+ and testing scenarios.
424
+ """
425
+ self._config = config
426
+ self._vault_handler = vault_handler
427
+ self._metrics_collector = metrics_collector
428
+ self._cache: dict[str, ModelCachedSecret] = {}
429
+ # Track mutable stats internally since ModelSecretCacheStats is frozen
430
+ self._hits = 0
431
+ self._misses = 0
432
+ self._expired_evictions = 0
433
+ self._refreshes = 0
434
+ self._hit_counts: defaultdict[str, int] = defaultdict(int) # per logical_name
435
+ # RLock (reentrant lock) allows the same thread to acquire the lock
436
+ # multiple times, which is needed because get_secret() holds the lock
437
+ # while calling _record_resolution_success() which also needs the lock.
438
+ self._lock = threading.RLock()
439
+ # Per-key async locks to allow parallel fetches for different secrets
440
+ # while preventing duplicate fetches for the same secret
441
+ self._async_key_locks: dict[str, asyncio.Lock] = {}
442
+
443
+ # === Metrics Tracking (OMN-1374) ===
444
+ # Resolution latency tracking (deque of (source_type, latency_ms) tuples)
445
+ self._resolution_latencies: deque[tuple[str, float]] = deque(
446
+ maxlen=MAX_LATENCY_SAMPLES
447
+ )
448
+ # Resolution success/failure counts by source type
449
+ self._resolution_success_counts: defaultdict[str, int] = defaultdict(int)
450
+ self._resolution_failure_counts: defaultdict[str, int] = defaultdict(int)
451
+
452
+ # === Rate Limiting for Warnings ===
453
+ # Track last eviction warning time to rate-limit log output
454
+ self._last_eviction_warning_time: float = 0.0
455
+
456
+ # Build lookup table from mappings
457
+ self._mappings: dict[str, ModelSecretSourceSpec] = {
458
+ m.logical_name: m.source for m in config.mappings
459
+ }
460
+ self._ttl_overrides: dict[str, int] = {
461
+ m.logical_name: m.ttl_seconds
462
+ for m in config.mappings
463
+ if m.ttl_seconds is not None
464
+ }
465
+
466
+ # === Container-Based Factory ===
467
+
468
+ @classmethod
469
+ async def from_container(
470
+ cls,
471
+ container: ModelONEXContainer,
472
+ config: ModelSecretResolverConfig,
473
+ ) -> SecretResolver:
474
+ """Create SecretResolver from ONEX container with dependency injection.
475
+
476
+ This async factory method supports the ONEX container-based dependency
477
+ injection pattern while the regular constructor remains available for
478
+ standalone use and testing scenarios.
479
+
480
+ The factory attempts to resolve optional dependencies (HandlerVault,
481
+ metrics collector) from the container's service registry. If a dependency
482
+ is not registered, the resolver is created without it - this allows
483
+ graceful degradation when optional services are unavailable.
484
+
485
+ Parameters:
486
+ This factory method accepts 2 parameters (both required):
487
+
488
+ - ``container`` (required): The ONEX container with registered services
489
+ - ``config`` (required): Resolver configuration with secret mappings
490
+
491
+ Args:
492
+ container: ONEX dependency injection container. May have HandlerVault
493
+ and/or ProtocolSecretResolverMetrics registered in its service
494
+ registry. These are resolved if available but not required.
495
+ config: Resolver configuration specifying secret mappings, default TTL,
496
+ and convention fallback settings. This is required because secret
497
+ mappings are application-specific and cannot be auto-discovered
498
+ from the container.
499
+
500
+ Returns:
501
+ Configured SecretResolver instance with container-resolved dependencies.
502
+
503
+ Raises:
504
+ ProtocolConfigurationError: If the container is invalid or missing
505
+ the required ``service_registry`` attribute. The error includes
506
+ :class:`ModelInfraErrorContext` with correlation_id for tracing.
507
+
508
+ Example:
509
+ Basic usage with container-resolved dependencies::
510
+
511
+ container = ModelONEXContainer()
512
+ await wire_infrastructure_services(container)
513
+
514
+ config = ModelSecretResolverConfig(
515
+ mappings=[
516
+ ModelSecretMapping(
517
+ logical_name="database.password",
518
+ source=ModelSecretSourceSpec(
519
+ source_type=SecretSourceType.VAULT,
520
+ path="secret/myapp/db#password",
521
+ ),
522
+ ),
523
+ ],
524
+ )
525
+ resolver = await SecretResolver.from_container(container, config)
526
+ password = await resolver.get_secret_async("database.password")
527
+
528
+ Example (Container Without Optional Services):
529
+ If HandlerVault is not registered, Vault-sourced secrets will fail
530
+ at resolution time (not at factory creation)::
531
+
532
+ container = ModelONEXContainer()
533
+ # No wire_infrastructure_services() call - no Vault handler
534
+ resolver = await SecretResolver.from_container(container, config)
535
+ # Works for env/file secrets, fails for Vault secrets
536
+
537
+ Note:
538
+ **Why config is a required parameter:**
539
+
540
+ Unlike other services that can be fully configured via container
541
+ registration, SecretResolver requires explicit secret mappings that
542
+ are application-specific. The config defines:
543
+
544
+ - Which logical names map to which secret sources
545
+ - Default TTL for caching
546
+ - Whether convention fallback is enabled
547
+
548
+ These settings vary per application and cannot be auto-discovered
549
+ from the container's service registry.
550
+
551
+ See Also:
552
+ - :meth:`__init__`: Direct constructor for standalone usage
553
+ - :class:`ModelSecretResolverConfig`: Configuration model
554
+ - :class:`HandlerVault`: Vault handler for Vault-sourced secrets
555
+ """
556
+ from omnibase_infra.handlers.handler_vault import HandlerVault
557
+
558
+ correlation_id = generate_correlation_id()
559
+
560
+ # Validate container has service_registry
561
+ if not hasattr(container, "service_registry"):
562
+ context = ModelInfraErrorContext.with_correlation(
563
+ correlation_id=correlation_id,
564
+ transport_type=EnumInfraTransportType.RUNTIME,
565
+ operation="from_container",
566
+ target_name="SecretResolver",
567
+ )
568
+ raise ProtocolConfigurationError(
569
+ "Container missing required 'service_registry' attribute. "
570
+ "Ensure container is a valid ModelONEXContainer instance.",
571
+ context=context,
572
+ )
573
+
574
+ # Try to resolve optional dependencies from container
575
+ vault_handler: HandlerVault | None = None
576
+ metrics_collector: ProtocolSecretResolverMetrics | None = None
577
+
578
+ # Attempt to resolve HandlerVault (optional)
579
+ try:
580
+ vault_handler = await container.service_registry.resolve_service(
581
+ HandlerVault
582
+ )
583
+ logger.debug(
584
+ "Resolved HandlerVault from container",
585
+ extra={"correlation_id": str(correlation_id)},
586
+ )
587
+ except Exception as e:
588
+ # HandlerVault not registered - this is acceptable
589
+ logger.debug(
590
+ "HandlerVault not available in container, Vault secrets disabled: %s",
591
+ type(e).__name__,
592
+ extra={"correlation_id": str(correlation_id)},
593
+ )
594
+
595
+ # Attempt to resolve metrics collector (optional)
596
+ # Note: We use a broad try/except since the protocol may not be registered
597
+ try:
598
+ metrics_collector = await container.service_registry.resolve_service(
599
+ ProtocolSecretResolverMetrics # type: ignore[type-abstract]
600
+ )
601
+ logger.debug(
602
+ "Resolved ProtocolSecretResolverMetrics from container",
603
+ extra={"correlation_id": str(correlation_id)},
604
+ )
605
+ except Exception as e:
606
+ # Metrics collector not registered - this is acceptable
607
+ logger.debug(
608
+ "Metrics collector not available in container: %s",
609
+ type(e).__name__,
610
+ extra={"correlation_id": str(correlation_id)},
611
+ )
612
+
613
+ return cls(
614
+ config=config,
615
+ vault_handler=vault_handler,
616
+ metrics_collector=metrics_collector,
617
+ )
618
+
619
+ # === Primary API (Sync) ===
620
+
621
+ def get_secret(
622
+ self,
623
+ logical_name: str,
624
+ required: bool = True,
625
+ correlation_id: UUID | None = None,
626
+ ) -> SecretStr | None:
627
+ """Resolve a secret by logical name.
628
+
629
+ Resolution order:
630
+ 1. Check cache (if not expired)
631
+ 2. Try explicit mapping
632
+ 3. Try convention fallback (if enabled)
633
+ 4. Raise or return None based on required flag
634
+
635
+ Warning:
636
+ This synchronous method cannot resolve Vault secrets from within
637
+ an async context (e.g., from inside an async function or coroutine).
638
+ If you need to resolve Vault secrets in async code, use
639
+ ``get_secret_async()`` instead. Calling this method from async
640
+ context when resolving Vault secrets will raise SecretResolutionError.
641
+
642
+ Args:
643
+ logical_name: Dotted path (e.g., "database.postgres.password")
644
+ required: If True, raises SecretResolutionError when not found
645
+ correlation_id: Optional correlation ID for distributed tracing.
646
+ If provided, propagates to error context for debugging.
647
+
648
+ Returns:
649
+ SecretStr if found, None if not found and required=False
650
+
651
+ Raises:
652
+ SecretResolutionError: If required=True and secret not found
653
+ """
654
+ # Generate correlation ID if not provided (for metrics/logging)
655
+ effective_correlation_id = correlation_id or uuid4()
656
+
657
+ with self._lock:
658
+ # Check cache first
659
+ cached = self._get_from_cache(logical_name)
660
+ if cached is not None:
661
+ self._record_resolution_success(
662
+ logical_name, "cache", effective_correlation_id
663
+ )
664
+ return cached
665
+
666
+ # Resolve from source
667
+ result = self._resolve_secret(logical_name, effective_correlation_id)
668
+
669
+ if result is None:
670
+ self._misses += 1
671
+ if required:
672
+ context = ModelInfraErrorContext.with_correlation(
673
+ correlation_id=effective_correlation_id,
674
+ transport_type=EnumInfraTransportType.RUNTIME,
675
+ operation="get_secret",
676
+ target_name="secret_resolver",
677
+ )
678
+ # SECURITY: Log at DEBUG level only to avoid exposing secret identifiers
679
+ # in error messages shown to users/logs
680
+ logger.debug(
681
+ "Secret not found (correlation_id=%s): %s",
682
+ context.correlation_id,
683
+ logical_name,
684
+ extra={
685
+ "correlation_id": str(context.correlation_id),
686
+ "logical_name": logical_name,
687
+ },
688
+ )
689
+ # SECURITY: Do NOT include logical_name in error - it exposes secret identifiers
690
+ # Use correlation_id to trace back to DEBUG logs if needed
691
+ raise SecretResolutionError(
692
+ f"Required secret not found. "
693
+ f"See logs with correlation_id={context.correlation_id} for details.",
694
+ context=context,
695
+ # NOTE: Intentionally NOT passing logical_name to avoid exposing
696
+ # secret identifiers in error messages, logs, or serialized responses.
697
+ )
698
+ return None
699
+
700
+ return result
701
+
702
+ def get_secrets(
703
+ self,
704
+ logical_names: list[str],
705
+ required: bool = True,
706
+ correlation_id: UUID | None = None,
707
+ ) -> dict[str, SecretStr | None]:
708
+ """Resolve multiple secrets.
709
+
710
+ Args:
711
+ logical_names: List of dotted paths
712
+ required: If True, raises on first missing secret
713
+ correlation_id: Optional correlation ID for distributed tracing.
714
+
715
+ Returns:
716
+ Dict mapping logical_name -> SecretStr | None
717
+
718
+ Note:
719
+ This sync method resolves secrets sequentially. For better latency
720
+ when resolving multiple secrets that involve I/O (Vault, file-based),
721
+ prefer using ``get_secrets_async()`` which resolves in parallel via
722
+ ``asyncio.gather()``.
723
+ """
724
+ return {
725
+ name: self.get_secret(
726
+ name, required=required, correlation_id=correlation_id
727
+ )
728
+ for name in logical_names
729
+ }
730
+
731
+ # === Primary API (Async) ===
732
+
733
+ async def get_secret_async(
734
+ self,
735
+ logical_name: str,
736
+ required: bool = True,
737
+ correlation_id: UUID | None = None,
738
+ ) -> SecretStr | None:
739
+ """Async wrapper for get_secret.
740
+
741
+ For Vault secrets, this uses async I/O. For env/file secrets,
742
+ this wraps the sync call in a thread executor.
743
+
744
+ Thread Safety:
745
+ Uses threading.RLock for cache access to prevent race conditions
746
+ with sync callers. Per-key async locks serialize resolution for the
747
+ same secret while allowing parallel fetches for different secrets.
748
+
749
+ Args:
750
+ logical_name: Dotted path (e.g., "database.postgres.password")
751
+ required: If True, raises SecretResolutionError when not found
752
+ correlation_id: Optional correlation ID for distributed tracing.
753
+ If provided, propagates to error context for debugging.
754
+
755
+ Returns:
756
+ SecretStr if found, None if not found and required=False
757
+
758
+ Raises:
759
+ SecretResolutionError: If required=True and secret not found
760
+ """
761
+ # Generate correlation ID if not provided (for metrics/logging)
762
+ effective_correlation_id = correlation_id or uuid4()
763
+
764
+ # Use threading lock for cache check (fast operation, prevents race with sync)
765
+ with self._lock:
766
+ cached = self._get_from_cache(logical_name)
767
+ if cached is not None:
768
+ self._record_resolution_success(
769
+ logical_name, "cache", effective_correlation_id
770
+ )
771
+ return cached
772
+
773
+ # Get or create per-key async lock for this logical_name
774
+ # This allows parallel fetches for different secrets while preventing
775
+ # duplicate fetches for the same secret
776
+ key_lock = self._get_async_key_lock(logical_name)
777
+
778
+ async with key_lock:
779
+ # Double-check cache after acquiring async lock - another coroutine may
780
+ # have resolved this secret while we were waiting on the lock
781
+ with self._lock:
782
+ cached = self._get_from_cache(logical_name)
783
+ if cached is not None:
784
+ self._record_resolution_success(
785
+ logical_name, "cache", effective_correlation_id
786
+ )
787
+ return cached
788
+
789
+ # Resolve from source (potentially async for Vault)
790
+ # Note: _resolve_secret_async handles its own locking for cache writes
791
+ result = await self._resolve_secret_async(
792
+ logical_name, effective_correlation_id
793
+ )
794
+
795
+ if result is None:
796
+ with self._lock:
797
+ self._misses += 1
798
+ if required:
799
+ context = ModelInfraErrorContext.with_correlation(
800
+ correlation_id=effective_correlation_id,
801
+ transport_type=EnumInfraTransportType.RUNTIME,
802
+ operation="get_secret_async",
803
+ target_name="secret_resolver",
804
+ )
805
+ # SECURITY: Log at DEBUG level only to avoid exposing secret identifiers
806
+ # in error messages shown to users/logs
807
+ logger.debug(
808
+ "Secret not found (correlation_id=%s): %s",
809
+ context.correlation_id,
810
+ logical_name,
811
+ extra={
812
+ "correlation_id": str(context.correlation_id),
813
+ "logical_name": logical_name,
814
+ },
815
+ )
816
+ # SECURITY: Do NOT include logical_name in error - it exposes secret identifiers
817
+ # Use correlation_id to trace back to DEBUG logs if needed
818
+ raise SecretResolutionError(
819
+ f"Required secret not found. "
820
+ f"See logs with correlation_id={context.correlation_id} for details.",
821
+ context=context,
822
+ # NOTE: Intentionally NOT passing logical_name to avoid exposing
823
+ # secret identifiers in error messages, logs, or serialized responses.
824
+ )
825
+ return None
826
+
827
+ return result
828
+
829
+ def _maybe_log_eviction_warning(self, evict_count: int) -> None:
830
+ """Log eviction warning with rate limiting (max once per minute).
831
+
832
+ Prevents log flooding in high-throughput scenarios where evictions
833
+ may occur frequently. The warning is only emitted if sufficient time
834
+ has passed since the last warning.
835
+
836
+ Args:
837
+ evict_count: Number of entries that were evicted.
838
+
839
+ Note:
840
+ Uses time.monotonic() for reliable elapsed time measurement
841
+ even if system clock changes.
842
+ """
843
+ current_time = time.monotonic()
844
+ if (
845
+ current_time - self._last_eviction_warning_time
846
+ >= EVICTION_WARNING_INTERVAL_SECONDS
847
+ ):
848
+ self._last_eviction_warning_time = current_time
849
+ logger.warning(
850
+ "Async key locks at capacity (%d). Evicted %d oldest entries. "
851
+ "This may indicate a DoS attack or dynamic logical name generation. "
852
+ "Consider validating logical names against configured mappings. "
853
+ "(Rate-limited: max 1 warning per minute)",
854
+ MAX_ASYNC_KEY_LOCKS,
855
+ evict_count,
856
+ extra={
857
+ "max_locks": MAX_ASYNC_KEY_LOCKS,
858
+ "evicted_count": evict_count,
859
+ "current_count": len(self._async_key_locks),
860
+ },
861
+ )
862
+
863
+ def _get_async_key_lock(self, logical_name: str) -> asyncio.Lock:
864
+ """Get or create an async lock for a specific logical_name.
865
+
866
+ This enables parallel resolution of different secrets while preventing
867
+ duplicate concurrent fetches for the same secret.
868
+
869
+ Thread Safety:
870
+ Uses threading.RLock to safely access the key locks dictionary,
871
+ ensuring thread-safe creation of new locks.
872
+
873
+ LRU Eviction (Memory Leak Prevention):
874
+ The ``_async_key_locks`` dictionary implements LRU eviction to prevent
875
+ unbounded memory growth. When the dictionary reaches ``MAX_ASYNC_KEY_LOCKS``
876
+ entries:
877
+
878
+ 1. The oldest 10% of entries are evicted (based on insertion order)
879
+ 2. A warning is logged indicating potential DoS or misconfiguration
880
+ 3. The new lock is then added
881
+
882
+ This ensures memory usage is bounded while maintaining correctness:
883
+ - Evicted locks are for secrets that were resolved earlier
884
+ - If a secret is resolved again, a new lock will be created
885
+ - The worst case is a brief period of duplicate concurrent fetches
886
+ for recently-evicted secrets, which is acceptable (same value resolved)
887
+
888
+ DoS Mitigation:
889
+ - Warning threshold at ``ASYNC_KEY_LOCKS_WARNING_THRESHOLD`` for early detection
890
+ - Hard cap at ``MAX_ASYNC_KEY_LOCKS`` with LRU eviction
891
+ - Repeated eviction warnings indicate potential attack or misconfiguration
892
+ - Validate logical names against configured mappings to prevent abuse
893
+
894
+ Args:
895
+ logical_name: The secret key to get a lock for
896
+
897
+ Returns:
898
+ asyncio.Lock for the given logical_name
899
+ """
900
+ with self._lock:
901
+ if logical_name not in self._async_key_locks:
902
+ lock_count = len(self._async_key_locks)
903
+
904
+ # DoS mitigation: warn at threshold for early detection
905
+ if lock_count == ASYNC_KEY_LOCKS_WARNING_THRESHOLD:
906
+ logger.warning(
907
+ "Async key locks dictionary reached %d entries - potential DoS risk. "
908
+ "Validate logical names against configured mappings.",
909
+ lock_count,
910
+ extra={"lock_count": lock_count},
911
+ )
912
+
913
+ # LRU eviction: when at capacity, evict oldest 10% of entries
914
+ if lock_count >= MAX_ASYNC_KEY_LOCKS:
915
+ evict_count = max(1, MAX_ASYNC_KEY_LOCKS // 10) # 10%, minimum 1
916
+ # Python 3.7+ dicts maintain insertion order, so first keys are oldest
917
+ keys_to_evict = list(self._async_key_locks.keys())[:evict_count]
918
+ for key in keys_to_evict:
919
+ del self._async_key_locks[key]
920
+
921
+ # Rate-limited warning (max 1 per minute) to prevent log flooding
922
+ self._maybe_log_eviction_warning(evict_count)
923
+
924
+ self._async_key_locks[logical_name] = asyncio.Lock()
925
+
926
+ return self._async_key_locks[logical_name]
927
+
928
+ async def get_secrets_async(
929
+ self,
930
+ logical_names: list[str],
931
+ required: bool = True,
932
+ correlation_id: UUID | None = None,
933
+ ) -> dict[str, SecretStr | None]:
934
+ """Resolve multiple secrets asynchronously in parallel.
935
+
936
+ Uses asyncio.gather() to fetch multiple secrets concurrently, improving
937
+ performance when resolving multiple secrets that may involve I/O (e.g.,
938
+ Vault or file-based secrets).
939
+
940
+ Thread Safety:
941
+ Each secret resolution uses per-key async locks, so fetches for
942
+ different secrets proceed in parallel while fetches for the same
943
+ secret are serialized.
944
+
945
+ Args:
946
+ logical_names: List of dotted paths
947
+ required: If True, aggregates all failures into a single error
948
+ correlation_id: Optional correlation ID for distributed tracing.
949
+
950
+ Returns:
951
+ Dict mapping logical_name -> SecretStr | None
952
+
953
+ Raises:
954
+ SecretResolutionError: If required=True and any secret is not found.
955
+ All secrets are attempted before raising, and all failures are
956
+ reported in a single aggregated error message.
957
+ """
958
+ if not logical_names:
959
+ return {}
960
+
961
+ # Create tasks for parallel resolution
962
+ tasks = [
963
+ self.get_secret_async(
964
+ name, required=required, correlation_id=correlation_id
965
+ )
966
+ for name in logical_names
967
+ ]
968
+
969
+ # Gather results with return_exceptions=True for better error aggregation
970
+ # This ensures all secrets are attempted before any error is raised
971
+ results = await asyncio.gather(*tasks, return_exceptions=True)
972
+
973
+ # Check for and aggregate exceptions
974
+ failed_secrets: list[str] = []
975
+ successful_results: dict[str, SecretStr | None] = {}
976
+
977
+ for name, result in zip(logical_names, results, strict=True):
978
+ if isinstance(result, BaseException):
979
+ failed_secrets.append(name)
980
+ else:
981
+ successful_results[name] = result
982
+
983
+ # If there were failures and required=True, raise aggregated error
984
+ if failed_secrets and required:
985
+ context = ModelInfraErrorContext.with_correlation(
986
+ transport_type=EnumInfraTransportType.RUNTIME,
987
+ operation="get_secrets_async",
988
+ target_name="secret_resolver",
989
+ )
990
+ # SECURITY: Log at INFO level with count only (for operational awareness)
991
+ # Secret identifiers are logged at DEBUG level only to prevent exposure
992
+ logger.info(
993
+ "Secret resolution failed for %d secret(s) (correlation_id=%s). "
994
+ "Enable DEBUG logging to see secret names.",
995
+ len(failed_secrets),
996
+ context.correlation_id,
997
+ extra={
998
+ "correlation_id": str(context.correlation_id),
999
+ "failed_count": len(failed_secrets),
1000
+ },
1001
+ )
1002
+ # SECURITY: Log failed secret names at DEBUG level only (with correlation_id)
1003
+ # to avoid exposing secret structure in error messages shown to users/logs
1004
+ logger.debug(
1005
+ "Failed secret names (correlation_id=%s): %s",
1006
+ context.correlation_id,
1007
+ ", ".join(failed_secrets),
1008
+ extra={
1009
+ "correlation_id": str(context.correlation_id),
1010
+ "failed_count": len(failed_secrets),
1011
+ },
1012
+ )
1013
+ # SECURITY: Do NOT include logical_name in error - it exposes secret identifiers
1014
+ # Use correlation_id to trace back to DEBUG logs if needed
1015
+ raise SecretResolutionError(
1016
+ f"Failed to resolve {len(failed_secrets)} secret(s). "
1017
+ f"See logs with correlation_id={context.correlation_id} for details.",
1018
+ context=context,
1019
+ # NOTE: Intentionally NOT passing logical_name to avoid exposing
1020
+ # secret identifiers in error messages, logs, or serialized responses.
1021
+ # The correlation_id can be used to find secret names in DEBUG logs.
1022
+ )
1023
+
1024
+ return successful_results
1025
+
1026
+ # === Cache Management ===
1027
+
1028
+ def refresh(self, logical_name: str) -> None:
1029
+ """Force refresh a single secret (invalidate cache).
1030
+
1031
+ Args:
1032
+ logical_name: The logical name to refresh
1033
+ """
1034
+ with self._lock:
1035
+ if logical_name in self._cache:
1036
+ del self._cache[logical_name]
1037
+ if logical_name in self._hit_counts:
1038
+ del self._hit_counts[logical_name]
1039
+ self._refreshes += 1
1040
+
1041
+ def refresh_all(self) -> None:
1042
+ """Force refresh all cached secrets."""
1043
+ with self._lock:
1044
+ count = len(self._cache)
1045
+ self._cache.clear()
1046
+ self._hit_counts.clear()
1047
+ self._refreshes += count
1048
+
1049
+ def get_cache_stats(self) -> ModelSecretCacheStats:
1050
+ """Return cache statistics.
1051
+
1052
+ Returns:
1053
+ ModelSecretCacheStats with hit/miss/refresh counts
1054
+ """
1055
+ with self._lock:
1056
+ return ModelSecretCacheStats(
1057
+ total_entries=len(self._cache),
1058
+ hits=self._hits,
1059
+ misses=self._misses,
1060
+ refreshes=self._refreshes,
1061
+ expired_evictions=self._expired_evictions,
1062
+ )
1063
+
1064
+ def get_resolution_metrics(self) -> ModelSecretResolverMetrics:
1065
+ """Return resolution metrics for observability.
1066
+
1067
+ Returns:
1068
+ ModelSecretResolverMetrics with:
1069
+ - success_counts: Dict of source_type -> success count
1070
+ - failure_counts: Dict of source_type -> failure count
1071
+ - latency_samples: Number of latency samples collected
1072
+ - avg_latency_ms: Average resolution latency (if samples > 0)
1073
+ - cache_hits: Total number of cache hits
1074
+ - cache_misses: Total number of cache misses
1075
+ """
1076
+ with self._lock:
1077
+ avg_latency = 0.0
1078
+ if self._resolution_latencies:
1079
+ avg_latency = sum(lat for _, lat in self._resolution_latencies) / len(
1080
+ self._resolution_latencies
1081
+ )
1082
+
1083
+ return ModelSecretResolverMetrics(
1084
+ success_counts=dict(self._resolution_success_counts),
1085
+ failure_counts=dict(self._resolution_failure_counts),
1086
+ latency_samples=len(self._resolution_latencies),
1087
+ avg_latency_ms=avg_latency,
1088
+ cache_hits=self._hits,
1089
+ cache_misses=self._misses,
1090
+ )
1091
+
1092
+ def set_metrics_collector(
1093
+ self, collector: ProtocolSecretResolverMetrics | None
1094
+ ) -> None:
1095
+ """Set the external metrics collector.
1096
+
1097
+ Thread Safety:
1098
+ This method is thread-safe. The collector reference is updated
1099
+ atomically under ``_lock``. Concurrent calls to resolution methods
1100
+ will see either the old or new collector (never a partial state).
1101
+
1102
+ The pattern used in ``_record_resolution_success`` and
1103
+ ``_record_resolution_failure`` captures the collector reference
1104
+ while holding the lock, then uses it outside the lock. This ensures
1105
+ that even if ``set_metrics_collector()`` is called concurrently,
1106
+ each resolution operation uses a consistent collector reference.
1107
+
1108
+ Args:
1109
+ collector: Metrics collector implementing ProtocolSecretResolverMetrics,
1110
+ or None to disable external metrics collection.
1111
+ """
1112
+ with self._lock:
1113
+ self._metrics_collector = collector
1114
+
1115
+ def _record_resolution_success(
1116
+ self,
1117
+ logical_name: str,
1118
+ source_type: str,
1119
+ correlation_id: UUID,
1120
+ start_time: float | None = None,
1121
+ ) -> None:
1122
+ """Record a successful secret resolution.
1123
+
1124
+ Thread Safety:
1125
+ Captures ``_metrics_collector`` reference while holding ``_lock`` to
1126
+ prevent race conditions with concurrent ``set_metrics_collector()``
1127
+ calls. The captured reference is then used outside the lock to avoid
1128
+ holding the lock during potentially slow I/O operations.
1129
+
1130
+ Args:
1131
+ logical_name: The secret's logical name
1132
+ source_type: Source type (env, file, vault, cache)
1133
+ correlation_id: Correlation ID for tracing
1134
+ start_time: Optional start time from time.monotonic() for latency calc
1135
+ """
1136
+ latency_ms = 0.0
1137
+ if start_time is not None:
1138
+ latency_ms = (time.monotonic() - start_time) * 1000
1139
+
1140
+ # Internal tracking + capture collector reference atomically
1141
+ with self._lock:
1142
+ self._resolution_success_counts[source_type] += 1
1143
+ if start_time is not None:
1144
+ # deque with maxlen=1000 automatically rotates (O(1) vs O(n) for list.pop(0))
1145
+ self._resolution_latencies.append((source_type, latency_ms))
1146
+ # THREAD SAFETY: Capture collector reference while holding lock to prevent
1147
+ # race with set_metrics_collector(). Use captured ref outside lock.
1148
+ collector = self._metrics_collector
1149
+
1150
+ # External metrics collector - use captured reference (may be None)
1151
+ if collector is not None:
1152
+ try:
1153
+ if hasattr(collector, "record_resolution_success"):
1154
+ collector.record_resolution_success(source_type)
1155
+ if start_time is not None and hasattr(
1156
+ collector, "record_resolution_latency"
1157
+ ):
1158
+ collector.record_resolution_latency(source_type, latency_ms)
1159
+ if source_type == "cache" and hasattr(collector, "record_cache_hit"):
1160
+ collector.record_cache_hit()
1161
+ except Exception as e:
1162
+ # Never let metrics failures affect secret resolution, but log
1163
+ # at warning level since a configured collector failing indicates
1164
+ # an integration issue worth investigating.
1165
+ logger.warning(
1166
+ "Metrics collector error (ignored, resolution unaffected): %s",
1167
+ e,
1168
+ extra={
1169
+ "logical_name": logical_name,
1170
+ "correlation_id": str(correlation_id),
1171
+ "exception_type": type(e).__name__,
1172
+ },
1173
+ )
1174
+
1175
+ # Structured logging
1176
+ logger.debug(
1177
+ "Secret resolved successfully: %s",
1178
+ logical_name,
1179
+ extra={
1180
+ "logical_name": logical_name,
1181
+ "source_type": source_type,
1182
+ "cache_hit": source_type == "cache",
1183
+ "latency_ms": latency_ms,
1184
+ "correlation_id": str(correlation_id),
1185
+ },
1186
+ )
1187
+
1188
+ def _record_resolution_failure(
1189
+ self,
1190
+ logical_name: str,
1191
+ source_type: str,
1192
+ correlation_id: UUID,
1193
+ reason: str,
1194
+ ) -> None:
1195
+ """Record a failed secret resolution.
1196
+
1197
+ Thread Safety:
1198
+ Captures ``_metrics_collector`` reference while holding ``_lock`` to
1199
+ prevent race conditions with concurrent ``set_metrics_collector()``
1200
+ calls. The captured reference is then used outside the lock to avoid
1201
+ holding the lock during potentially slow I/O operations.
1202
+
1203
+ Args:
1204
+ logical_name: The secret's logical name
1205
+ source_type: Source type (env, file, vault, unknown)
1206
+ correlation_id: Correlation ID for tracing
1207
+ reason: Failure reason (not_found, handler_not_configured, etc.)
1208
+ """
1209
+ # Internal tracking + capture collector reference atomically
1210
+ with self._lock:
1211
+ self._resolution_failure_counts[source_type] += 1
1212
+ # THREAD SAFETY: Capture collector reference while holding lock to prevent
1213
+ # race with set_metrics_collector(). Use captured ref outside lock.
1214
+ collector = self._metrics_collector
1215
+
1216
+ # External metrics collector - use captured reference (may be None)
1217
+ if collector is not None:
1218
+ try:
1219
+ if hasattr(collector, "record_resolution_failure"):
1220
+ collector.record_resolution_failure(source_type)
1221
+ # NOTE: Do NOT call record_cache_miss() here - resolution failures
1222
+ # are distinct from cache misses. Cache misses are already tracked
1223
+ # in get_secret() and get_secret_async() via self._misses += 1
1224
+ except Exception as e:
1225
+ # Never let metrics failures affect secret resolution, but log
1226
+ # at warning level since a configured collector failing indicates
1227
+ # an integration issue worth investigating.
1228
+ logger.warning(
1229
+ "Metrics collector error (ignored, resolution unaffected): %s",
1230
+ e,
1231
+ extra={
1232
+ "logical_name": logical_name,
1233
+ "correlation_id": str(correlation_id),
1234
+ "exception_type": type(e).__name__,
1235
+ },
1236
+ )
1237
+
1238
+ # Structured logging - level depends on failure type
1239
+ # Configuration issues (no_mapping, handler_not_configured) are warnings
1240
+ # since they indicate misconfiguration that should be addressed.
1241
+ # Expected failures (not_found for optional secrets) are debug level.
1242
+ if reason in ("no_mapping", "handler_not_configured"):
1243
+ logger.warning(
1244
+ "Secret resolution failed (configuration issue): %s",
1245
+ logical_name,
1246
+ extra={
1247
+ "logical_name": logical_name,
1248
+ "source_type": source_type,
1249
+ "reason": reason,
1250
+ "correlation_id": str(correlation_id),
1251
+ },
1252
+ )
1253
+ else:
1254
+ # not_found and other expected failures - debug level
1255
+ logger.debug(
1256
+ "Secret resolution failed: %s",
1257
+ logical_name,
1258
+ extra={
1259
+ "logical_name": logical_name,
1260
+ "source_type": source_type,
1261
+ "reason": reason,
1262
+ "correlation_id": str(correlation_id),
1263
+ },
1264
+ )
1265
+
1266
+ # === Introspection (non-sensitive) ===
1267
+
1268
+ def list_configured_secrets(self) -> list[str]:
1269
+ """List all configured logical names (not values).
1270
+
1271
+ Returns:
1272
+ List of logical names from configuration
1273
+ """
1274
+ return list(self._mappings.keys())
1275
+
1276
+ def get_source_info(self, logical_name: str) -> ModelSecretSourceInfo | None:
1277
+ """Return source type and masked path for a logical name.
1278
+
1279
+ This method is safe to use for debugging and monitoring as it
1280
+ never exposes actual secret values.
1281
+
1282
+ Args:
1283
+ logical_name: The logical name to inspect
1284
+
1285
+ Returns:
1286
+ ModelSecretSourceInfo with masked path, or None if not configured
1287
+ """
1288
+ source = self._get_source_spec(logical_name)
1289
+ if source is None:
1290
+ return None
1291
+
1292
+ # Mask sensitive parts of the path
1293
+ masked_path = self._mask_source_path(source)
1294
+
1295
+ # Use lock for thread-safe cache access
1296
+ with self._lock:
1297
+ cached_entry = self._cache.get(logical_name)
1298
+ return ModelSecretSourceInfo(
1299
+ logical_name=logical_name,
1300
+ source_type=source.source_type,
1301
+ source_path_masked=masked_path,
1302
+ is_cached=cached_entry is not None,
1303
+ expires_at=cached_entry.expires_at if cached_entry else None,
1304
+ )
1305
+
1306
+ # === Internal Methods ===
1307
+
1308
+ def _get_from_cache(self, logical_name: str) -> SecretStr | None:
1309
+ """Get secret from cache if present and not expired.
1310
+
1311
+ Args:
1312
+ logical_name: The logical name to look up
1313
+
1314
+ Returns:
1315
+ SecretStr if cached and valid, None otherwise
1316
+ """
1317
+ cached = self._cache.get(logical_name)
1318
+ if cached is None:
1319
+ return None
1320
+
1321
+ if cached.is_expired():
1322
+ del self._cache[logical_name]
1323
+ self._hit_counts.pop(logical_name, None)
1324
+ self._expired_evictions += 1
1325
+ return None
1326
+
1327
+ # Track hits using internal counter (model is frozen)
1328
+ self._hit_counts[logical_name] += 1
1329
+ self._hits += 1
1330
+ return cached.value
1331
+
1332
+ def _is_bootstrap_secret(self, logical_name: str) -> bool:
1333
+ """Check if a logical name is a bootstrap secret.
1334
+
1335
+ Bootstrap secrets are resolved ONLY from environment variables, never from
1336
+ Vault or files. This ensures they're available before Vault is initialized.
1337
+
1338
+ Security:
1339
+ Bootstrap secrets (vault.token, vault.addr, vault.ca_cert) are needed
1340
+ to initialize the Vault connection. They MUST come from env vars to
1341
+ avoid a circular dependency.
1342
+
1343
+ Args:
1344
+ logical_name: The logical name to check
1345
+
1346
+ Returns:
1347
+ True if this is a bootstrap secret that bypasses normal resolution
1348
+ """
1349
+ return logical_name in self._config.bootstrap_secrets
1350
+
1351
+ def _resolve_bootstrap_secret_value(self, logical_name: str) -> SecretStr | None:
1352
+ """Resolve a bootstrap secret value from environment variables.
1353
+
1354
+ Thread Safety:
1355
+ This method only reads from environment variables (atomic on most platforms)
1356
+ and does NOT write to cache. The caller is responsible for cache writes
1357
+ with proper locking.
1358
+
1359
+ Security:
1360
+ Bootstrap secrets are isolated from the normal resolution chain.
1361
+ They are ALWAYS resolved from environment variables only (never vault/file).
1362
+ If an explicit mapping exists for an env source, that mapping is honored.
1363
+ Otherwise, convention-based naming (logical_name -> ENV_VAR) is used.
1364
+
1365
+ Args:
1366
+ logical_name: The bootstrap secret's logical name
1367
+
1368
+ Returns:
1369
+ SecretStr if found, None if env var is not set
1370
+ """
1371
+ # First, check for explicit env var mapping (same priority as normal secrets)
1372
+ # This ensures that explicit mappings like:
1373
+ # {"vault.token": ModelSecretSourceSpec(source_type="env", source_path="MY_VAULT_TOKEN")}
1374
+ # are respected for bootstrap secrets.
1375
+ if logical_name in self._mappings:
1376
+ mapping = self._mappings[logical_name]
1377
+ if mapping.source_type == "env":
1378
+ # Use the explicitly mapped env var name
1379
+ env_var = mapping.source_path
1380
+ else:
1381
+ # Non-env mappings (vault/file) are invalid for bootstrap secrets
1382
+ # by design - they must come from env to avoid circular dependency.
1383
+ # Fall back to convention for the env var name.
1384
+ env_var = self._logical_name_to_env_var(logical_name)
1385
+ else:
1386
+ # No explicit mapping - use convention fallback
1387
+ env_var = self._logical_name_to_env_var(logical_name)
1388
+
1389
+ value = os.environ.get(env_var)
1390
+
1391
+ if value is None:
1392
+ return None
1393
+
1394
+ return SecretStr(value)
1395
+
1396
+ def _resolve_secret(
1397
+ self, logical_name: str, correlation_id: UUID | None = None
1398
+ ) -> SecretStr | None:
1399
+ """Resolve secret from source and cache it.
1400
+
1401
+ Thread Safety:
1402
+ This method MUST be called while holding _lock. It writes to cache
1403
+ directly without additional locking.
1404
+
1405
+ Security:
1406
+ Bootstrap secrets (vault.token, vault.addr, etc.) are resolved directly
1407
+ from environment variables, bypassing the normal source chain. This
1408
+ prevents circular dependencies when initializing Vault.
1409
+
1410
+ Args:
1411
+ logical_name: The logical name to resolve
1412
+ correlation_id: Optional correlation ID for tracing
1413
+
1414
+ Returns:
1415
+ SecretStr if found, None otherwise
1416
+ """
1417
+ effective_correlation_id = correlation_id or uuid4()
1418
+ start_time = time.monotonic()
1419
+
1420
+ # SECURITY: Bootstrap secrets bypass normal resolution
1421
+ # They must come from env vars to avoid circular dependency with Vault
1422
+ if self._is_bootstrap_secret(logical_name):
1423
+ secret = self._resolve_bootstrap_secret_value(logical_name)
1424
+ if secret is not None:
1425
+ # Cache write is safe here - caller holds _lock
1426
+ self._cache_secret(logical_name, secret, "env")
1427
+ self._record_resolution_success(
1428
+ logical_name, "env", effective_correlation_id, start_time
1429
+ )
1430
+ return secret
1431
+
1432
+ source = self._get_source_spec(logical_name)
1433
+ if source is None:
1434
+ self._record_resolution_failure(
1435
+ logical_name, "unknown", effective_correlation_id, "no_mapping"
1436
+ )
1437
+ return None
1438
+
1439
+ value: str | None = None
1440
+
1441
+ if source.source_type == "env":
1442
+ value = os.environ.get(source.source_path)
1443
+ elif source.source_type == "file":
1444
+ value = self._read_file_secret(source.source_path, logical_name)
1445
+ elif source.source_type == "vault":
1446
+ if self._vault_handler is None:
1447
+ logger.warning(
1448
+ "Vault handler not configured for secret: %s",
1449
+ logical_name,
1450
+ extra={
1451
+ "logical_name": logical_name,
1452
+ "correlation_id": str(effective_correlation_id),
1453
+ },
1454
+ )
1455
+ self._record_resolution_failure(
1456
+ logical_name,
1457
+ "vault",
1458
+ effective_correlation_id,
1459
+ "handler_not_configured",
1460
+ )
1461
+ return None
1462
+ value = self._read_vault_secret_sync(
1463
+ source.source_path, logical_name, effective_correlation_id
1464
+ )
1465
+
1466
+ if value is None:
1467
+ self._record_resolution_failure(
1468
+ logical_name, source.source_type, effective_correlation_id, "not_found"
1469
+ )
1470
+ return None
1471
+
1472
+ secret = SecretStr(value)
1473
+ self._cache_secret(logical_name, secret, source.source_type)
1474
+ self._record_resolution_success(
1475
+ logical_name, source.source_type, effective_correlation_id, start_time
1476
+ )
1477
+ return secret
1478
+
1479
+ async def _resolve_secret_async(
1480
+ self, logical_name: str, correlation_id: UUID | None = None
1481
+ ) -> SecretStr | None:
1482
+ """Resolve secret from source asynchronously.
1483
+
1484
+ Thread Safety:
1485
+ Uses threading.RLock for cache writes to prevent race conditions
1486
+ with sync callers. I/O operations are performed outside the lock.
1487
+ Bootstrap secrets also use _lock for their cache writes to ensure
1488
+ thread-safe access from both sync and async contexts.
1489
+
1490
+ Security:
1491
+ Bootstrap secrets (vault.token, vault.addr, etc.) are resolved directly
1492
+ from environment variables, bypassing the normal source chain. This
1493
+ prevents circular dependencies when initializing Vault.
1494
+
1495
+ Args:
1496
+ logical_name: The logical name to resolve
1497
+ correlation_id: Optional correlation ID for tracing
1498
+
1499
+ Returns:
1500
+ SecretStr if found, None otherwise
1501
+ """
1502
+ effective_correlation_id = correlation_id or uuid4()
1503
+ start_time = time.monotonic()
1504
+
1505
+ # SECURITY: Bootstrap secrets bypass normal resolution
1506
+ # They must come from env vars to avoid circular dependency with Vault
1507
+ if self._is_bootstrap_secret(logical_name):
1508
+ # Resolve value (no cache write in _resolve_bootstrap_secret_value)
1509
+ secret = self._resolve_bootstrap_secret_value(logical_name)
1510
+ if secret is not None:
1511
+ # THREAD SAFETY: Use lock for cache write to prevent race with sync callers
1512
+ # Check-before-write pattern avoids unnecessary overwrites if sync caller
1513
+ # already cached this secret between our cache check and now
1514
+ with self._lock:
1515
+ if logical_name not in self._cache:
1516
+ self._cache_secret(logical_name, secret, "env")
1517
+ self._record_resolution_success(
1518
+ logical_name, "env", effective_correlation_id, start_time
1519
+ )
1520
+ return secret
1521
+
1522
+ source = self._get_source_spec(logical_name)
1523
+ if source is None:
1524
+ self._record_resolution_failure(
1525
+ logical_name, "unknown", effective_correlation_id, "no_mapping"
1526
+ )
1527
+ return None
1528
+
1529
+ value: str | None = None
1530
+
1531
+ # I/O operations - NOT under lock to avoid blocking
1532
+ if source.source_type == "env":
1533
+ value = os.environ.get(source.source_path)
1534
+ elif source.source_type == "file":
1535
+ value = await asyncio.to_thread(
1536
+ self._read_file_secret, source.source_path, logical_name
1537
+ )
1538
+ elif source.source_type == "vault":
1539
+ if self._vault_handler is None:
1540
+ logger.warning(
1541
+ "Vault handler not configured for secret: %s",
1542
+ logical_name,
1543
+ extra={
1544
+ "logical_name": logical_name,
1545
+ "correlation_id": str(effective_correlation_id),
1546
+ },
1547
+ )
1548
+ self._record_resolution_failure(
1549
+ logical_name,
1550
+ "vault",
1551
+ effective_correlation_id,
1552
+ "handler_not_configured",
1553
+ )
1554
+ return None
1555
+ value = await self._read_vault_secret_async(
1556
+ source.source_path, logical_name, effective_correlation_id
1557
+ )
1558
+
1559
+ if value is None:
1560
+ self._record_resolution_failure(
1561
+ logical_name, source.source_type, effective_correlation_id, "not_found"
1562
+ )
1563
+ return None
1564
+
1565
+ secret = SecretStr(value)
1566
+ # THREAD SAFETY: Use lock for cache write to prevent race with sync callers
1567
+ # Check-before-write pattern avoids unnecessary overwrites if sync caller
1568
+ # already cached this secret between our cache check and now
1569
+ with self._lock:
1570
+ if logical_name not in self._cache:
1571
+ self._cache_secret(logical_name, secret, source.source_type)
1572
+ self._record_resolution_success(
1573
+ logical_name, source.source_type, effective_correlation_id, start_time
1574
+ )
1575
+ return secret
1576
+
1577
+ def _get_source_spec(self, logical_name: str) -> ModelSecretSourceSpec | None:
1578
+ """Get source spec from mapping or convention fallback.
1579
+
1580
+ Args:
1581
+ logical_name: The logical name to look up
1582
+
1583
+ Returns:
1584
+ ModelSecretSourceSpec if found, None otherwise
1585
+ """
1586
+ # Try explicit mapping first
1587
+ if logical_name in self._mappings:
1588
+ return self._mappings[logical_name]
1589
+
1590
+ # Try convention fallback
1591
+ if self._config.enable_convention_fallback:
1592
+ env_var = self._logical_name_to_env_var(logical_name)
1593
+ return ModelSecretSourceSpec(source_type="env", source_path=env_var)
1594
+
1595
+ return None
1596
+
1597
+ def _logical_name_to_env_var(self, logical_name: str) -> str:
1598
+ """Convert dotted logical name to environment variable name.
1599
+
1600
+ Example:
1601
+ "database.postgres.password" -> "DATABASE_POSTGRES_PASSWORD"
1602
+ With prefix "ONEX_": "database.postgres.password" -> "ONEX_DATABASE_POSTGRES_PASSWORD"
1603
+
1604
+ Args:
1605
+ logical_name: Dotted path to convert
1606
+
1607
+ Returns:
1608
+ Environment variable name
1609
+ """
1610
+ env_var = logical_name.upper().replace(".", "_")
1611
+ if self._config.convention_env_prefix:
1612
+ env_var = f"{self._config.convention_env_prefix}{env_var}"
1613
+ return env_var
1614
+
1615
+ def _read_file_secret(self, path: str, logical_name: str = "") -> str | None:
1616
+ """Read secret from file.
1617
+
1618
+ Thread Safety:
1619
+ This method avoids TOCTOU race conditions by catching exceptions
1620
+ during the read operation rather than pre-checking file existence.
1621
+
1622
+ Security:
1623
+ - Path traversal attacks are prevented by validating resolved paths
1624
+ stay within the configured secrets_dir
1625
+ - Error messages are sanitized to avoid leaking path information
1626
+ - No secret values are ever logged
1627
+
1628
+ Args:
1629
+ path: Path to the secret file (absolute or relative to secrets_dir)
1630
+ logical_name: The logical name being resolved (for error context only)
1631
+
1632
+ Returns:
1633
+ Secret value with whitespace stripped, or None if not found or unreadable
1634
+ """
1635
+ secret_path = Path(path)
1636
+
1637
+ # Track whether the original path was relative BEFORE combining with secrets_dir
1638
+ # This is critical for path traversal detection
1639
+ original_is_relative = not secret_path.is_absolute()
1640
+
1641
+ # If relative path, resolve against secrets_dir
1642
+ if original_is_relative:
1643
+ secret_path = self._config.secrets_dir / path
1644
+
1645
+ # Resolve to absolute path to detect path traversal
1646
+ try:
1647
+ resolved_path = secret_path.resolve()
1648
+ except (OSError, RuntimeError):
1649
+ # resolve() can fail on invalid paths or symlink loops
1650
+ logger.warning(
1651
+ "Invalid secret path for logical name: %s",
1652
+ logical_name,
1653
+ extra={"logical_name": logical_name},
1654
+ )
1655
+ return None
1656
+
1657
+ # SECURITY: Prevent path traversal attacks
1658
+ # Verify the resolved path is within secrets_dir for relative paths
1659
+ # Absolute paths are trusted (explicitly configured by administrator)
1660
+ if original_is_relative:
1661
+ secrets_dir_resolved = self._config.secrets_dir.resolve()
1662
+ # Relative paths MUST resolve within secrets_dir
1663
+ # Use is_relative_to() to check without raising (Python 3.9+)
1664
+ if not resolved_path.is_relative_to(secrets_dir_resolved):
1665
+ # Path escapes secrets_dir - this is a path traversal attempt
1666
+ # SECURITY: Log at ERROR level - potential attack indicator
1667
+ logger.error(
1668
+ "Path traversal detected for secret: %s",
1669
+ logical_name,
1670
+ extra={"logical_name": logical_name},
1671
+ )
1672
+ return None
1673
+
1674
+ # Avoid TOCTOU race: read atomically with size limit instead of stat() then read()
1675
+ # This prevents an attacker from swapping the file between size check and read
1676
+ try:
1677
+ # Read up to MAX_SECRET_FILE_SIZE + 1 bytes atomically
1678
+ # If we got more than MAX_SECRET_FILE_SIZE, the file is too large
1679
+ with resolved_path.open("r") as f:
1680
+ content = f.read(MAX_SECRET_FILE_SIZE + 1)
1681
+ if len(content) > MAX_SECRET_FILE_SIZE:
1682
+ logger.warning(
1683
+ "Secret file exceeds size limit: %s",
1684
+ logical_name,
1685
+ extra={"logical_name": logical_name},
1686
+ )
1687
+ return None
1688
+ return content.strip()
1689
+ except FileNotFoundError:
1690
+ # File does not exist - this is expected for optional secrets
1691
+ # SECURITY: Don't log the actual path to avoid information disclosure
1692
+ logger.debug(
1693
+ "Secret file not found for logical name: %s",
1694
+ logical_name,
1695
+ extra={"logical_name": logical_name},
1696
+ )
1697
+ return None
1698
+ except IsADirectoryError:
1699
+ # Path exists but is a directory, not a file
1700
+ # SECURITY: Don't log the actual path
1701
+ logger.warning(
1702
+ "Secret path is a directory for logical name: %s",
1703
+ logical_name,
1704
+ extra={"logical_name": logical_name},
1705
+ )
1706
+ return None
1707
+ except PermissionError:
1708
+ # Permission denied - log at warning level since this may indicate
1709
+ # a configuration issue (file exists but is not readable)
1710
+ # SECURITY: Don't log the actual path
1711
+ logger.warning(
1712
+ "Permission denied reading secret for logical name: %s",
1713
+ logical_name,
1714
+ extra={"logical_name": logical_name},
1715
+ )
1716
+ return None
1717
+ except OSError as e:
1718
+ # Catch other OS-level errors (e.g., too many open files, I/O errors)
1719
+ # SECURITY: Don't log the path or detailed OS error which may leak info
1720
+ logger.warning(
1721
+ "OS error reading secret for logical name: %s (error type: %s)",
1722
+ logical_name,
1723
+ type(e).__name__,
1724
+ extra={"logical_name": logical_name, "error_type": type(e).__name__},
1725
+ )
1726
+ return None
1727
+
1728
+ def _read_vault_secret_sync(
1729
+ self, path: str, logical_name: str = "", correlation_id: UUID | None = None
1730
+ ) -> str | None:
1731
+ """Read secret from Vault synchronously.
1732
+
1733
+ This method wraps the async Vault handler for synchronous contexts.
1734
+ It creates a new event loop if one is not running, otherwise raises
1735
+ an error (cannot nest event loops).
1736
+
1737
+ Path format: "mount/path#field" or "mount/path" (returns first field value)
1738
+
1739
+ Security:
1740
+ - This method never logs Vault paths (could reveal secret structure)
1741
+ - Secret values are never logged at any level
1742
+ - Error messages are sanitized to include only logical names
1743
+
1744
+ Args:
1745
+ path: Vault path with optional field specifier
1746
+ logical_name: The logical name being resolved (for error context only)
1747
+ correlation_id: Optional correlation ID for tracing
1748
+
1749
+ Returns:
1750
+ Secret value or None if not found
1751
+
1752
+ Raises:
1753
+ SecretResolutionError: On Vault communication failures or if called
1754
+ from within an async context (cannot nest event loops)
1755
+ InfraAuthenticationError: If authentication fails
1756
+ InfraTimeoutError: If the request times out
1757
+ InfraUnavailableError: If Vault is unavailable (circuit breaker open)
1758
+ """
1759
+ if self._vault_handler is None:
1760
+ return None
1761
+
1762
+ effective_correlation_id = correlation_id or uuid4()
1763
+
1764
+ # Check if we're already in an async context
1765
+ try:
1766
+ asyncio.get_running_loop()
1767
+ # We're in an async context - cannot use asyncio.run()
1768
+ context = ModelInfraErrorContext.with_correlation(
1769
+ correlation_id=effective_correlation_id,
1770
+ transport_type=EnumInfraTransportType.VAULT,
1771
+ operation="read_secret_sync",
1772
+ target_name="secret_resolver",
1773
+ )
1774
+ raise SecretResolutionError(
1775
+ f"Cannot resolve Vault secret synchronously from async context: "
1776
+ f"{logical_name}. Use get_secret_async() instead.",
1777
+ context=context,
1778
+ logical_name=logical_name,
1779
+ )
1780
+ except RuntimeError:
1781
+ # No running event loop - safe to use asyncio.run()
1782
+ pass
1783
+
1784
+ # Run the async method in a new event loop
1785
+ return asyncio.run(
1786
+ self._read_vault_secret_async(path, logical_name, effective_correlation_id)
1787
+ )
1788
+
1789
+ async def _read_vault_secret_async(
1790
+ self, path: str, logical_name: str = "", correlation_id: UUID | None = None
1791
+ ) -> str | None:
1792
+ """Read secret from Vault asynchronously.
1793
+
1794
+ Path format: "mount/path#field" or "mount/path" (returns first field value)
1795
+
1796
+ Examples:
1797
+ "secret/myapp/db#password" -> mount="secret", path="myapp/db", field="password"
1798
+ "secret/myapp/db" -> mount="secret", path="myapp/db", field=None (first value)
1799
+
1800
+ Type Handling:
1801
+ All Vault values are converted to strings via ``str()``. This is intentional
1802
+ because SecretResolver returns ``SecretStr`` values, which only wrap strings.
1803
+ Non-string Vault values (integers, booleans, dicts) are converted as follows:
1804
+
1805
+ - Integers: ``123`` -> ``"123"``
1806
+ - Booleans: ``True`` -> ``"True"``
1807
+ - Lists/Dicts: Python repr (NOT JSON) - avoid storing complex types
1808
+
1809
+ Best Practice: Store secrets as strings in Vault. If you need structured
1810
+ data, store it as a JSON string and parse after resolution.
1811
+
1812
+ Security:
1813
+ - This method never logs Vault paths (could reveal secret structure)
1814
+ - Secret values are never logged at any level
1815
+ - Error messages are sanitized to include only logical names
1816
+
1817
+ Args:
1818
+ path: Vault path with optional field specifier (mount/path#field)
1819
+ logical_name: The logical name being resolved (for error context only)
1820
+ correlation_id: Optional correlation ID for tracing
1821
+
1822
+ Returns:
1823
+ Secret value as string, or None if not found
1824
+
1825
+ Raises:
1826
+ SecretResolutionError: On Vault communication failures
1827
+ InfraAuthenticationError: If authentication fails
1828
+ InfraTimeoutError: If the request times out
1829
+ InfraUnavailableError: If Vault is unavailable (circuit breaker open)
1830
+ """
1831
+ if self._vault_handler is None:
1832
+ return None
1833
+
1834
+ effective_correlation_id = correlation_id or uuid4()
1835
+
1836
+ # Parse path into mount_point, vault_path, and optional field
1837
+ mount_point, vault_path, field = self._parse_vault_path_components(path)
1838
+
1839
+ # Create envelope for vault.read_secret operation
1840
+ envelope: JsonType = {
1841
+ "operation": "vault.read_secret",
1842
+ "payload": {
1843
+ "path": vault_path,
1844
+ "mount_point": mount_point,
1845
+ },
1846
+ "correlation_id": str(effective_correlation_id),
1847
+ }
1848
+
1849
+ try:
1850
+ result = await self._vault_handler.execute(envelope)
1851
+
1852
+ # Extract secret data from handler response
1853
+ # Response format: {"status": "success", "payload": {"data": {...}, "metadata": {...}}}
1854
+ result_dict = result.result
1855
+ if not isinstance(result_dict, dict):
1856
+ logger.warning(
1857
+ "Unexpected Vault response format for secret: %s",
1858
+ logical_name,
1859
+ extra={
1860
+ "logical_name": logical_name,
1861
+ "correlation_id": str(effective_correlation_id),
1862
+ },
1863
+ )
1864
+ return None
1865
+
1866
+ status = result_dict.get("status")
1867
+ if status != "success":
1868
+ logger.debug(
1869
+ "Vault returned non-success status for secret: %s",
1870
+ logical_name,
1871
+ extra={
1872
+ "logical_name": logical_name,
1873
+ "correlation_id": str(effective_correlation_id),
1874
+ },
1875
+ )
1876
+ return None
1877
+
1878
+ payload = result_dict.get("payload", {})
1879
+ if not isinstance(payload, dict):
1880
+ return None
1881
+
1882
+ secret_data = payload.get("data", {})
1883
+ if not isinstance(secret_data, dict) or not secret_data:
1884
+ logger.debug(
1885
+ "No secret data found in Vault for: %s",
1886
+ logical_name,
1887
+ extra={
1888
+ "logical_name": logical_name,
1889
+ "correlation_id": str(effective_correlation_id),
1890
+ },
1891
+ )
1892
+ return None
1893
+
1894
+ # Extract the specific field or first value
1895
+ if field:
1896
+ value = secret_data.get(field)
1897
+ if value is None:
1898
+ logger.debug(
1899
+ "Field not found in Vault secret: %s",
1900
+ logical_name,
1901
+ extra={
1902
+ "logical_name": logical_name,
1903
+ "correlation_id": str(effective_correlation_id),
1904
+ },
1905
+ )
1906
+ return None
1907
+ else:
1908
+ # No field specified - return first value
1909
+ value = next(iter(secret_data.values()), None)
1910
+
1911
+ if value is None:
1912
+ return None
1913
+
1914
+ # SECURITY: String conversion is INTENTIONAL for SecretStr compatibility.
1915
+ #
1916
+ # SecretStr (from Pydantic) only wraps string values to prevent accidental
1917
+ # logging of secrets. Since SecretResolver returns SecretStr, all Vault
1918
+ # values must be converted to strings.
1919
+ #
1920
+ # Type conversion behavior:
1921
+ # - Strings: returned as-is
1922
+ # - Integers: "123" (str representation)
1923
+ # - Booleans: "True" or "False" (Python str representation)
1924
+ # - Lists/Dicts: Python repr (NOT JSON) - avoid storing complex types
1925
+ #
1926
+ # Best Practice: Store secrets as strings in Vault. If you need structured
1927
+ # data, store it as a JSON string and parse after resolution.
1928
+ return str(value)
1929
+
1930
+ except InfraAuthenticationError:
1931
+ # Re-raise auth errors directly - they have proper context
1932
+ raise
1933
+ except InfraTimeoutError:
1934
+ # Re-raise timeout errors directly - they have proper context
1935
+ raise
1936
+ except InfraUnavailableError:
1937
+ # Re-raise unavailable errors (circuit breaker open)
1938
+ raise
1939
+ except Exception as e:
1940
+ # Wrap other errors in SecretResolutionError with sanitized message
1941
+ context = ModelInfraErrorContext.with_correlation(
1942
+ correlation_id=effective_correlation_id,
1943
+ transport_type=EnumInfraTransportType.VAULT,
1944
+ operation="read_secret",
1945
+ target_name="secret_resolver",
1946
+ )
1947
+ raise SecretResolutionError(
1948
+ f"Failed to resolve secret from Vault: {logical_name}",
1949
+ context=context,
1950
+ logical_name=logical_name,
1951
+ ) from e
1952
+
1953
+ def _parse_vault_path(self, path: str) -> tuple[str, str | None]:
1954
+ """Parse Vault path into path and optional field.
1955
+
1956
+ Examples:
1957
+ "secret/data/db#password" -> ("secret/data/db", "password")
1958
+ "secret/data/db" -> ("secret/data/db", None)
1959
+
1960
+ Args:
1961
+ path: Vault path with optional field specifier
1962
+
1963
+ Returns:
1964
+ Tuple of (vault_path, field_name or None)
1965
+ """
1966
+ if "#" in path:
1967
+ vault_path, field = path.rsplit("#", 1)
1968
+ return vault_path, field
1969
+ return path, None
1970
+
1971
+ def _parse_vault_path_components(self, path: str) -> tuple[str, str, str | None]:
1972
+ """Parse Vault path into mount_point, path, and optional field.
1973
+
1974
+ The path format is: "mount_point/path/to/secret#field"
1975
+
1976
+ For KV v2 secrets engine, the path convention is:
1977
+ - mount_point: The secrets engine mount (e.g., "secret")
1978
+ - path: The secret path within the mount (e.g., "myapp/db")
1979
+ - field: Optional specific field to extract (e.g., "password")
1980
+
1981
+ Examples:
1982
+ "secret/myapp/db#password" -> ("secret", "myapp/db", "password")
1983
+ "secret/myapp/db" -> ("secret", "myapp/db", None)
1984
+ "kv/prod/config#api_key" -> ("kv", "prod/config", "api_key")
1985
+ "secret#password" -> ("secret", "", "password") # Edge case - unusual format
1986
+
1987
+ Args:
1988
+ path: Full Vault path with optional field specifier
1989
+
1990
+ Returns:
1991
+ Tuple of (mount_point, vault_path, field_name or None)
1992
+ """
1993
+ # First extract field if present
1994
+ if "#" in path:
1995
+ path_without_field, field = path.rsplit("#", 1)
1996
+ else:
1997
+ path_without_field = path
1998
+ field = None
1999
+
2000
+ # Split into mount_point and rest of path
2001
+ # First component is always the mount_point
2002
+ parts = path_without_field.split("/", 1)
2003
+ if len(parts) == 1:
2004
+ # Edge case: No slash in path - entire path is treated as mount_point
2005
+ # with empty vault_path. This format (e.g., "secret#field") is unusual
2006
+ # and may not be supported by Vault's KV v2 engine which expects
2007
+ # paths like "mount/path". Log a warning to alert operators.
2008
+ logger.warning(
2009
+ "Unusual Vault path format detected. "
2010
+ "Expected 'mount/path#field' format with at least one '/' separator. "
2011
+ "Empty path segment may not work with Vault KV v2.",
2012
+ )
2013
+ return parts[0], "", field
2014
+
2015
+ mount_point = parts[0]
2016
+ vault_path = parts[1]
2017
+
2018
+ return mount_point, vault_path, field
2019
+
2020
+ def _cache_secret(
2021
+ self,
2022
+ logical_name: str,
2023
+ value: SecretStr,
2024
+ source_type: SecretSourceType,
2025
+ ) -> None:
2026
+ """Cache a resolved secret with appropriate TTL and jitter.
2027
+
2028
+ TTL Jitter:
2029
+ A symmetric random jitter of ±10% is added to the base TTL to
2030
+ prevent cache stampede scenarios where many cached entries expire
2031
+ at the same time, causing a thundering herd of resolution requests.
2032
+ The symmetric distribution provides better spread than additive-only
2033
+ jitter, reducing the probability of clustered expirations.
2034
+
2035
+ Args:
2036
+ logical_name: The logical name being cached
2037
+ value: The secret value to cache
2038
+ source_type: Source type for TTL selection
2039
+ """
2040
+ base_ttl_seconds = self._get_ttl(logical_name, source_type)
2041
+ # Add ±10% jitter to prevent cache stampede (thundering herd)
2042
+ jitter_factor = random.uniform(
2043
+ -CACHE_TTL_JITTER_PERCENT, CACHE_TTL_JITTER_PERCENT
2044
+ )
2045
+ ttl_seconds = max(1, int(base_ttl_seconds * (1 + jitter_factor)))
2046
+ now = datetime.now(UTC)
2047
+
2048
+ self._cache[logical_name] = ModelCachedSecret(
2049
+ value=value,
2050
+ source_type=source_type,
2051
+ logical_name=logical_name,
2052
+ cached_at=now,
2053
+ expires_at=now + timedelta(seconds=ttl_seconds),
2054
+ )
2055
+
2056
+ def _get_ttl(self, logical_name: str, source_type: SecretSourceType) -> int:
2057
+ """Get TTL for a secret based on source type or override.
2058
+
2059
+ Args:
2060
+ logical_name: The logical name for TTL override lookup
2061
+ source_type: Source type for default TTL selection
2062
+
2063
+ Returns:
2064
+ TTL in seconds
2065
+ """
2066
+ # Check for explicit override
2067
+ if logical_name in self._ttl_overrides:
2068
+ return self._ttl_overrides[logical_name]
2069
+
2070
+ # Use default based on source type
2071
+ ttl_defaults = {
2072
+ "env": self._config.default_ttl_env_seconds,
2073
+ "file": self._config.default_ttl_file_seconds,
2074
+ "vault": self._config.default_ttl_vault_seconds,
2075
+ }
2076
+ return ttl_defaults.get(source_type, self._config.default_ttl_env_seconds)
2077
+
2078
+ def _mask_source_path(self, source: ModelSecretSourceSpec) -> str:
2079
+ """Mask sensitive parts of source path for introspection.
2080
+
2081
+ This ensures that introspection never reveals sensitive information
2082
+ while still being useful for debugging.
2083
+
2084
+ Args:
2085
+ source: Source specification to mask
2086
+
2087
+ Returns:
2088
+ Masked path string safe for logging/display
2089
+ """
2090
+ if source.source_type == "env":
2091
+ # Show env var name but mask the value context
2092
+ return f"env:{source.source_path}"
2093
+ elif source.source_type == "file":
2094
+ # Show directory but mask filename
2095
+ path = Path(source.source_path)
2096
+ return f"file:{path.parent}/***"
2097
+ elif source.source_type == "vault":
2098
+ # Show mount but mask the rest
2099
+ parts = source.source_path.split("/")
2100
+ if len(parts) > 2:
2101
+ return f"vault:{parts[0]}/{parts[1]}/***"
2102
+ return "vault:***"
2103
+ return "***"
2104
+
2105
+
2106
+ __all__: list[str] = [
2107
+ "ProtocolSecretResolverMetrics",
2108
+ "SecretResolver",
2109
+ "SecretSourceType",
2110
+ ]