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