omnibase_infra 0.2.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnibase_infra/__init__.py +101 -0
- omnibase_infra/adapters/adapter_onex_tool_execution.py +451 -0
- omnibase_infra/capabilities/__init__.py +15 -0
- omnibase_infra/capabilities/capability_inference_rules.py +211 -0
- omnibase_infra/capabilities/contract_capability_extractor.py +221 -0
- omnibase_infra/capabilities/intent_type_extractor.py +160 -0
- omnibase_infra/cli/__init__.py +1 -0
- omnibase_infra/cli/commands.py +216 -0
- omnibase_infra/clients/__init__.py +0 -0
- omnibase_infra/configs/widget_mapping.yaml +176 -0
- omnibase_infra/constants_topic_patterns.py +26 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +264 -0
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +141 -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 +132 -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_consumer_group_purpose.py +92 -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 +111 -0
- omnibase_infra/enums/enum_handler_loader_error.py +178 -0
- omnibase_infra/enums/enum_handler_source_mode.py +86 -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_kafka_acks.py +99 -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 +160 -0
- omnibase_infra/errors/error_architecture_violation.py +152 -0
- omnibase_infra/errors/error_binding_resolution.py +128 -0
- omnibase_infra/errors/error_chain_propagation.py +188 -0
- omnibase_infra/errors/error_compute_registry.py +95 -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 +105 -0
- omnibase_infra/errors/error_infra.py +610 -0
- omnibase_infra/errors/error_message_type_registry.py +101 -0
- omnibase_infra/errors/error_policy_registry.py +115 -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 +84 -0
- omnibase_infra/event_bus/event_bus_inmemory.py +797 -0
- omnibase_infra/event_bus/event_bus_kafka.py +1716 -0
- omnibase_infra/event_bus/mixin_kafka_broadcast.py +180 -0
- omnibase_infra/event_bus/mixin_kafka_dlq.py +771 -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 +693 -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/testing/__init__.py +26 -0
- omnibase_infra/event_bus/testing/adapter_protocol_event_publisher_inmemory.py +418 -0
- omnibase_infra/event_bus/testing/model_publisher_metrics.py +64 -0
- omnibase_infra/event_bus/topic_constants.py +376 -0
- omnibase_infra/handlers/__init__.py +82 -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 +795 -0
- omnibase_infra/handlers/handler_db.py +1046 -0
- omnibase_infra/handlers/handler_filesystem.py +1478 -0
- omnibase_infra/handlers/handler_graph.py +2015 -0
- omnibase_infra/handlers/handler_http.py +926 -0
- omnibase_infra/handlers/handler_intent.py +387 -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 +1430 -0
- omnibase_infra/handlers/handler_qdrant.py +1076 -0
- omnibase_infra/handlers/handler_vault.py +428 -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 +47 -0
- omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +338 -0
- omnibase_infra/handlers/mixins/mixin_consul_service.py +542 -0
- omnibase_infra/handlers/mixins/mixin_consul_topic_index.py +585 -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 +922 -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 +1051 -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 +109 -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/migrations/001_create_event_ledger.sql +166 -0
- omnibase_infra/migrations/001_drop_event_ledger.sql +18 -0
- omnibase_infra/mixins/__init__.py +71 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +656 -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 +2670 -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 +144 -0
- omnibase_infra/models/bindings/__init__.py +59 -0
- omnibase_infra/models/bindings/constants.py +144 -0
- omnibase_infra/models/bindings/model_binding_resolution_result.py +103 -0
- omnibase_infra/models/bindings/model_operation_binding.py +44 -0
- omnibase_infra/models/bindings/model_operation_bindings_subcontract.py +152 -0
- omnibase_infra/models/bindings/model_parsed_binding.py +52 -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 +330 -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 +155 -0
- omnibase_infra/models/dispatch/model_debug_trace_snapshot.py +114 -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_materialized_dispatch.py +141 -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 +80 -0
- omnibase_infra/models/handlers/model_bootstrap_handler_descriptor.py +162 -0
- omnibase_infra/models/handlers/model_contract_discovery_result.py +82 -0
- omnibase_infra/models/handlers/model_handler_descriptor.py +200 -0
- omnibase_infra/models/handlers/model_handler_identifier.py +215 -0
- omnibase_infra/models/handlers/model_handler_source_config.py +220 -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/mcp/__init__.py +15 -0
- omnibase_infra/models/mcp/model_mcp_contract_config.py +80 -0
- omnibase_infra/models/mcp/model_mcp_server_config.py +67 -0
- omnibase_infra/models/mcp/model_mcp_tool_definition.py +73 -0
- omnibase_infra/models/mcp/model_mcp_tool_parameter.py +35 -0
- omnibase_infra/models/model_node_identity.py +126 -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 +591 -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 +68 -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_event_bus_topic_entry.py +59 -0
- omnibase_infra/models/registration/model_introspection_metrics.py +253 -0
- omnibase_infra/models/registration/model_node_capabilities.py +190 -0
- omnibase_infra/models/registration/model_node_event_bus_config.py +99 -0
- omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +195 -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 +49 -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 +296 -0
- omnibase_infra/models/runtime/model_loaded_handler.py +129 -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 +57 -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 +203 -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 +106 -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/contract_registry_reducer/__init__.py +29 -0
- omnibase_infra/nodes/contract_registry_reducer/contract.yaml +255 -0
- omnibase_infra/nodes/contract_registry_reducer/models/__init__.py +38 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_contract_registry_state.py +266 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_cleanup_topic_references.py +55 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_deactivate_contract.py +58 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_mark_stale.py +49 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_heartbeat.py +71 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_update_topic.py +66 -0
- omnibase_infra/nodes/contract_registry_reducer/models/model_payload_upsert_contract.py +92 -0
- omnibase_infra/nodes/contract_registry_reducer/node.py +121 -0
- omnibase_infra/nodes/contract_registry_reducer/reducer.py +784 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/__init__.py +9 -0
- omnibase_infra/nodes/contract_registry_reducer/registry/registry_infra_contract_registry_reducer.py +101 -0
- omnibase_infra/nodes/effects/README.md +358 -0
- omnibase_infra/nodes/effects/__init__.py +26 -0
- omnibase_infra/nodes/effects/contract.yaml +167 -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/handlers/consul/contract.yaml +85 -0
- omnibase_infra/nodes/handlers/db/contract.yaml +72 -0
- omnibase_infra/nodes/handlers/graph/contract.yaml +127 -0
- omnibase_infra/nodes/handlers/http/contract.yaml +74 -0
- omnibase_infra/nodes/handlers/intent/contract.yaml +66 -0
- omnibase_infra/nodes/handlers/mcp/contract.yaml +69 -0
- omnibase_infra/nodes/handlers/vault/contract.yaml +91 -0
- omnibase_infra/nodes/node_intent_storage_effect/__init__.py +50 -0
- omnibase_infra/nodes/node_intent_storage_effect/contract.yaml +194 -0
- omnibase_infra/nodes/node_intent_storage_effect/models/__init__.py +24 -0
- omnibase_infra/nodes/node_intent_storage_effect/models/model_intent_storage_input.py +141 -0
- omnibase_infra/nodes/node_intent_storage_effect/models/model_intent_storage_output.py +130 -0
- omnibase_infra/nodes/node_intent_storage_effect/node.py +94 -0
- omnibase_infra/nodes/node_intent_storage_effect/registry/__init__.py +35 -0
- omnibase_infra/nodes/node_intent_storage_effect/registry/registry_infra_intent_storage.py +294 -0
- omnibase_infra/nodes/node_ledger_projection_compute/__init__.py +50 -0
- omnibase_infra/nodes/node_ledger_projection_compute/contract.yaml +104 -0
- omnibase_infra/nodes/node_ledger_projection_compute/node.py +284 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/__init__.py +29 -0
- omnibase_infra/nodes/node_ledger_projection_compute/registry/registry_infra_ledger_projection.py +118 -0
- omnibase_infra/nodes/node_ledger_write_effect/__init__.py +82 -0
- omnibase_infra/nodes/node_ledger_write_effect/contract.yaml +200 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/__init__.py +22 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_append.py +372 -0
- omnibase_infra/nodes/node_ledger_write_effect/handlers/handler_ledger_query.py +597 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/__init__.py +31 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_append_result.py +54 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_entry.py +92 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query.py +53 -0
- omnibase_infra/nodes/node_ledger_write_effect/models/model_ledger_query_result.py +41 -0
- omnibase_infra/nodes/node_ledger_write_effect/node.py +89 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/__init__.py +13 -0
- omnibase_infra/nodes/node_ledger_write_effect/protocols/protocol_ledger_persistence.py +127 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/__init__.py +9 -0
- omnibase_infra/nodes/node_ledger_write_effect/registry/registry_infra_ledger_write.py +121 -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 +482 -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 +694 -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 +528 -0
- omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +393 -0
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +743 -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 +220 -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 +112 -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 +215 -0
- omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
- omnibase_infra/nodes/node_registry_effect/contract.yaml +677 -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 +417 -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 +222 -0
- omnibase_infra/nodes/reducers/__init__.py +30 -0
- omnibase_infra/nodes/reducers/models/__init__.py +37 -0
- omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +87 -0
- omnibase_infra/nodes/reducers/models/model_payload_ledger_append.py +133 -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 +1138 -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 +449 -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 +104 -0
- omnibase_infra/protocols/protocol_capability_projection.py +253 -0
- omnibase_infra/protocols/protocol_capability_query.py +251 -0
- omnibase_infra/protocols/protocol_container_aware.py +200 -0
- omnibase_infra/protocols/protocol_dispatch_engine.py +152 -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 +445 -0
- omnibase_infra/runtime/binding_config_resolver.py +2771 -0
- omnibase_infra/runtime/binding_resolver.py +753 -0
- omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
- omnibase_infra/runtime/constants_notification.py +75 -0
- omnibase_infra/runtime/constants_security.py +70 -0
- omnibase_infra/runtime/contract_handler_discovery.py +587 -0
- omnibase_infra/runtime/contract_loaders/__init__.py +51 -0
- omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
- omnibase_infra/runtime/contract_loaders/operation_bindings_loader.py +789 -0
- omnibase_infra/runtime/dispatch_context_enforcer.py +427 -0
- omnibase_infra/runtime/emit_daemon/__init__.py +97 -0
- omnibase_infra/runtime/emit_daemon/cli.py +844 -0
- omnibase_infra/runtime/emit_daemon/client.py +811 -0
- omnibase_infra/runtime/emit_daemon/config.py +535 -0
- omnibase_infra/runtime/emit_daemon/daemon.py +812 -0
- omnibase_infra/runtime/emit_daemon/event_registry.py +477 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_request.py +139 -0
- omnibase_infra/runtime/emit_daemon/model_daemon_response.py +191 -0
- omnibase_infra/runtime/emit_daemon/queue.py +618 -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/event_bus_subcontract_wiring.py +466 -0
- omnibase_infra/runtime/handler_bootstrap_source.py +507 -0
- omnibase_infra/runtime/handler_contract_config_loader.py +603 -0
- omnibase_infra/runtime/handler_contract_source.py +750 -0
- omnibase_infra/runtime/handler_identity.py +81 -0
- omnibase_infra/runtime/handler_plugin_loader.py +2046 -0
- omnibase_infra/runtime/handler_registry.py +329 -0
- omnibase_infra/runtime/handler_source_resolver.py +367 -0
- omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
- omnibase_infra/runtime/kafka_contract_source.py +984 -0
- omnibase_infra/runtime/kernel.py +40 -0
- omnibase_infra/runtime/mixin_policy_validation.py +522 -0
- omnibase_infra/runtime/mixin_semver_cache.py +402 -0
- omnibase_infra/runtime/mixins/__init__.py +24 -0
- omnibase_infra/runtime/mixins/mixin_projector_notification_publishing.py +566 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +778 -0
- omnibase_infra/runtime/models/__init__.py +229 -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_contract_load_result.py +224 -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 +229 -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_notification_config.py +171 -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_contract_config.py +268 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_config.py +625 -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_security_config.py +109 -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/models/model_transition_notification_outbox_config.py +112 -0
- omnibase_infra/runtime/models/model_transition_notification_outbox_metrics.py +140 -0
- omnibase_infra/runtime/models/model_transition_notification_publisher_metrics.py +357 -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 +1330 -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 +37 -0
- omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -0
- omnibase_infra/runtime/publisher_topic_scoped.py +294 -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 +445 -0
- omnibase_infra/runtime/registry_compute.py +1143 -0
- omnibase_infra/runtime/registry_contract_source.py +693 -0
- omnibase_infra/runtime/registry_dispatcher.py +678 -0
- omnibase_infra/runtime/registry_policy.py +1185 -0
- omnibase_infra/runtime/runtime_contract_config_loader.py +406 -0
- omnibase_infra/runtime/runtime_scheduler.py +1070 -0
- omnibase_infra/runtime/secret_resolver.py +2112 -0
- omnibase_infra/runtime/security_metadata_validator.py +776 -0
- omnibase_infra/runtime/service_kernel.py +1651 -0
- omnibase_infra/runtime/service_message_dispatch_engine.py +2350 -0
- omnibase_infra/runtime/service_runtime_host_process.py +3493 -0
- omnibase_infra/runtime/transition_notification_outbox.py +1190 -0
- omnibase_infra/runtime/transition_notification_publisher.py +765 -0
- omnibase_infra/runtime/util_container_wiring.py +1124 -0
- omnibase_infra/runtime/util_validation.py +314 -0
- omnibase_infra/runtime/util_version.py +98 -0
- omnibase_infra/runtime/util_wiring.py +723 -0
- omnibase_infra/schemas/schema_registration_projection.sql +320 -0
- omnibase_infra/schemas/schema_transition_notification_outbox.sql +245 -0
- omnibase_infra/services/__init__.py +89 -0
- omnibase_infra/services/corpus_capture.py +684 -0
- omnibase_infra/services/mcp/__init__.py +31 -0
- omnibase_infra/services/mcp/mcp_server_lifecycle.py +449 -0
- omnibase_infra/services/mcp/service_mcp_tool_discovery.py +411 -0
- omnibase_infra/services/mcp/service_mcp_tool_registry.py +329 -0
- omnibase_infra/services/mcp/service_mcp_tool_sync.py +565 -0
- omnibase_infra/services/registry_api/__init__.py +40 -0
- omnibase_infra/services/registry_api/main.py +261 -0
- omnibase_infra/services/registry_api/models/__init__.py +66 -0
- omnibase_infra/services/registry_api/models/model_capability_widget_mapping.py +38 -0
- omnibase_infra/services/registry_api/models/model_pagination_info.py +48 -0
- omnibase_infra/services/registry_api/models/model_registry_discovery_response.py +73 -0
- omnibase_infra/services/registry_api/models/model_registry_health_response.py +49 -0
- omnibase_infra/services/registry_api/models/model_registry_instance_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_node_view.py +88 -0
- omnibase_infra/services/registry_api/models/model_registry_summary.py +60 -0
- omnibase_infra/services/registry_api/models/model_response_list_instances.py +43 -0
- omnibase_infra/services/registry_api/models/model_response_list_nodes.py +51 -0
- omnibase_infra/services/registry_api/models/model_warning.py +49 -0
- omnibase_infra/services/registry_api/models/model_widget_defaults.py +28 -0
- omnibase_infra/services/registry_api/models/model_widget_mapping.py +51 -0
- omnibase_infra/services/registry_api/routes.py +371 -0
- omnibase_infra/services/registry_api/service.py +837 -0
- omnibase_infra/services/service_capability_query.py +945 -0
- omnibase_infra/services/service_health.py +898 -0
- omnibase_infra/services/service_node_selector.py +530 -0
- omnibase_infra/services/service_timeout_emitter.py +699 -0
- omnibase_infra/services/service_timeout_scanner.py +394 -0
- omnibase_infra/services/session/__init__.py +56 -0
- omnibase_infra/services/session/config_consumer.py +137 -0
- omnibase_infra/services/session/config_store.py +139 -0
- omnibase_infra/services/session/consumer.py +1007 -0
- omnibase_infra/services/session/protocol_session_aggregator.py +117 -0
- omnibase_infra/services/session/store.py +997 -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/topics/__init__.py +45 -0
- omnibase_infra/topics/platform_topic_suffixes.py +140 -0
- omnibase_infra/topics/util_topic_composition.py +95 -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 +29 -0
- omnibase_infra/types/typed_dict/typed_dict_envelope_build_params.py +115 -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 +117 -0
- omnibase_infra/utils/correlation.py +208 -0
- omnibase_infra/utils/util_atomic_file.py +261 -0
- omnibase_infra/utils/util_consumer_group.py +232 -0
- omnibase_infra/utils/util_datetime.py +372 -0
- omnibase_infra/utils/util_db_transaction.py +239 -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_retry_optimistic.py +281 -0
- omnibase_infra/utils/util_semver.py +233 -0
- omnibase_infra/validation/__init__.py +307 -0
- omnibase_infra/validation/contracts/security.validation.yaml +114 -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 +1514 -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 +2033 -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 +513 -0
- omnibase_infra/validation/validator_topic_category.py +1152 -0
- omnibase_infra-0.2.6.dist-info/METADATA +197 -0
- omnibase_infra-0.2.6.dist-info/RECORD +833 -0
- omnibase_infra-0.2.6.dist-info/WHEEL +4 -0
- omnibase_infra-0.2.6.dist-info/entry_points.txt +5 -0
- omnibase_infra-0.2.6.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,2046 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Handler Plugin Loader for Contract-Driven Discovery.
|
|
4
|
+
|
|
5
|
+
This module provides HandlerPluginLoader, which discovers handler contracts
|
|
6
|
+
from the filesystem, validates handlers against protocols, and creates
|
|
7
|
+
ModelLoadedHandler instances for runtime registration.
|
|
8
|
+
|
|
9
|
+
Part of OMN-1132: Handler Plugin Loader implementation.
|
|
10
|
+
|
|
11
|
+
The loader implements ProtocolHandlerPluginLoader and supports:
|
|
12
|
+
- Single contract loading from a specific path
|
|
13
|
+
- Directory-based discovery with recursive scanning
|
|
14
|
+
- Glob pattern-based discovery for flexible matching
|
|
15
|
+
|
|
16
|
+
Error Codes:
|
|
17
|
+
This module uses structured error codes from ``EnumHandlerLoaderError`` for
|
|
18
|
+
precise error classification. Error codes are organized by category:
|
|
19
|
+
|
|
20
|
+
**File-Level Errors (HANDLER_LOADER_001 - HANDLER_LOADER_009)**:
|
|
21
|
+
- ``HANDLER_LOADER_001`` (FILE_NOT_FOUND): Contract file path does not exist
|
|
22
|
+
- ``HANDLER_LOADER_002`` (INVALID_YAML_SYNTAX): Contract file has invalid YAML
|
|
23
|
+
- ``HANDLER_LOADER_003`` (SCHEMA_VALIDATION_FAILED): Contract fails Pydantic validation
|
|
24
|
+
- ``HANDLER_LOADER_004`` (MISSING_REQUIRED_FIELDS): Required contract fields missing
|
|
25
|
+
- ``HANDLER_LOADER_005`` (FILE_SIZE_EXCEEDED): Contract exceeds 10MB size limit
|
|
26
|
+
- ``HANDLER_LOADER_006`` (PROTOCOL_NOT_IMPLEMENTED): Handler missing protocol methods
|
|
27
|
+
- ``HANDLER_LOADER_007`` (NOT_A_FILE): Path exists but is not a regular file
|
|
28
|
+
- ``HANDLER_LOADER_008`` (FILE_READ_ERROR): Failed to read contract file (I/O)
|
|
29
|
+
- ``HANDLER_LOADER_009`` (FILE_STAT_ERROR): Failed to stat contract file (I/O)
|
|
30
|
+
|
|
31
|
+
**Import Errors (HANDLER_LOADER_010 - HANDLER_LOADER_013)**:
|
|
32
|
+
- ``HANDLER_LOADER_010`` (MODULE_NOT_FOUND): Handler module not found
|
|
33
|
+
- ``HANDLER_LOADER_011`` (CLASS_NOT_FOUND): Handler class not found in module
|
|
34
|
+
- ``HANDLER_LOADER_012`` (IMPORT_ERROR): Import error (syntax/dependency)
|
|
35
|
+
- ``HANDLER_LOADER_013`` (NAMESPACE_NOT_ALLOWED): Handler module namespace not allowed
|
|
36
|
+
|
|
37
|
+
**Directory Errors (HANDLER_LOADER_020 - HANDLER_LOADER_022)**:
|
|
38
|
+
- ``HANDLER_LOADER_020`` (DIRECTORY_NOT_FOUND): Directory does not exist
|
|
39
|
+
- ``HANDLER_LOADER_021`` (PERMISSION_DENIED): Permission denied accessing directory
|
|
40
|
+
- ``HANDLER_LOADER_022`` (NOT_A_DIRECTORY): Path exists but is not a directory
|
|
41
|
+
|
|
42
|
+
**Pattern Errors (HANDLER_LOADER_030 - HANDLER_LOADER_031)**:
|
|
43
|
+
- ``HANDLER_LOADER_030`` (EMPTY_PATTERNS_LIST): Patterns list cannot be empty
|
|
44
|
+
- ``HANDLER_LOADER_031`` (INVALID_GLOB_PATTERN): Invalid glob pattern syntax
|
|
45
|
+
(logged only, not raised - pattern is skipped and discovery continues)
|
|
46
|
+
|
|
47
|
+
**Configuration Errors (HANDLER_LOADER_040)**:
|
|
48
|
+
- ``HANDLER_LOADER_040`` (AMBIGUOUS_CONTRACT_CONFIGURATION): Both contract types
|
|
49
|
+
exist in the same directory
|
|
50
|
+
|
|
51
|
+
Error codes are accessible via ``error.model.context.get("loader_error")`` on
|
|
52
|
+
raised exceptions. Note: HANDLER_LOADER_031 is logged but not raised as an
|
|
53
|
+
exception to allow graceful continuation during discovery operations.
|
|
54
|
+
|
|
55
|
+
Concurrency Notes:
|
|
56
|
+
The loader is stateless and reentrant - each load operation is independent:
|
|
57
|
+
|
|
58
|
+
- No instance state is stored after ``__init__`` (empty constructor)
|
|
59
|
+
- All method variables are local to each call
|
|
60
|
+
- ``importlib.import_module()`` is thread-safe in CPython (uses GIL and import lock)
|
|
61
|
+
- File operations use independent file handles per call
|
|
62
|
+
|
|
63
|
+
**Thread Safety Guarantees**:
|
|
64
|
+
- Multiple threads can safely call any loader method concurrently
|
|
65
|
+
- Concurrent imports of the SAME module are serialized by Python's import lock
|
|
66
|
+
- Repeated loads of the same handler are idempotent (cached by Python)
|
|
67
|
+
|
|
68
|
+
**Thread Safety Limitations**:
|
|
69
|
+
- The loader does NOT provide transactional semantics across multiple calls
|
|
70
|
+
- If contracts change on disk during concurrent loading, results may be inconsistent
|
|
71
|
+
- The ``discover_and_load()`` method's default ``Path.cwd()`` behavior is
|
|
72
|
+
process-global; if cwd changes between calls, results will differ
|
|
73
|
+
|
|
74
|
+
Working Directory Dependency:
|
|
75
|
+
The ``discover_and_load()`` method uses ``Path.cwd()`` by default,
|
|
76
|
+
which reads process-level state. For deterministic behavior when cwd
|
|
77
|
+
may change between calls, provide an explicit ``base_path`` parameter.
|
|
78
|
+
|
|
79
|
+
See Also:
|
|
80
|
+
- ProtocolHandlerPluginLoader: Protocol definition for plugin loaders
|
|
81
|
+
- HandlerContractSource: Contract discovery and parsing
|
|
82
|
+
- ModelLoadedHandler: Model representing loaded handler metadata
|
|
83
|
+
- EnumHandlerLoaderError: Structured error codes for loader operations
|
|
84
|
+
|
|
85
|
+
Security Considerations:
|
|
86
|
+
This loader dynamically imports Python classes specified in YAML contracts.
|
|
87
|
+
Contract files should be treated as code and protected accordingly:
|
|
88
|
+
- Only load contracts from trusted sources
|
|
89
|
+
- Validate contract file permissions in production environments
|
|
90
|
+
- Be aware that module side effects execute during import
|
|
91
|
+
- Use the ``allowed_namespaces`` parameter to restrict imports to trusted packages
|
|
92
|
+
|
|
93
|
+
Namespace Allowlisting:
|
|
94
|
+
The loader supports namespace-based import restrictions via the
|
|
95
|
+
``allowed_namespaces`` parameter. When configured, only handler modules
|
|
96
|
+
whose fully-qualified path starts with one of the allowed namespace
|
|
97
|
+
prefixes will be imported. This provides defense-in-depth against
|
|
98
|
+
malicious contract files attempting to load untrusted code.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
>>> loader = HandlerPluginLoader(
|
|
102
|
+
... allowed_namespaces=["omnibase_infra.", "omnibase_core.", "mycompany."]
|
|
103
|
+
... )
|
|
104
|
+
>>> # This would succeed:
|
|
105
|
+
>>> loader.load_from_contract(Path("contract.yaml")) # handler_class: omnibase_infra.handlers.HandlerAuth
|
|
106
|
+
>>> # This would fail with NAMESPACE_NOT_ALLOWED:
|
|
107
|
+
>>> loader.load_from_contract(Path("malicious.yaml")) # handler_class: malicious_pkg.EvilHandler
|
|
108
|
+
|
|
109
|
+
**Error Message Sanitization**:
|
|
110
|
+
Error messages are designed to be safe for end users and prevent
|
|
111
|
+
information disclosure:
|
|
112
|
+
|
|
113
|
+
- User-facing error messages use filename only (not full filesystem paths)
|
|
114
|
+
- System exception details are sanitized to prevent path disclosure
|
|
115
|
+
- Full paths are stored in error context for internal debugging only
|
|
116
|
+
- Correlation IDs enable tracing without exposing sensitive information
|
|
117
|
+
- Exception messages from underlying libraries are sanitized before inclusion
|
|
118
|
+
|
|
119
|
+
The ``_sanitize_exception_message()`` helper strips filesystem paths from
|
|
120
|
+
exception messages while preserving useful diagnostic information like
|
|
121
|
+
line numbers and error types.
|
|
122
|
+
|
|
123
|
+
.. versionadded:: 0.7.0
|
|
124
|
+
Created as part of OMN-1132 handler plugin loader implementation.
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
from __future__ import annotations
|
|
128
|
+
|
|
129
|
+
import importlib
|
|
130
|
+
import logging
|
|
131
|
+
import re
|
|
132
|
+
import time
|
|
133
|
+
from datetime import UTC, datetime
|
|
134
|
+
from pathlib import Path
|
|
135
|
+
from uuid import UUID, uuid4
|
|
136
|
+
|
|
137
|
+
import yaml
|
|
138
|
+
from pydantic import ValidationError
|
|
139
|
+
|
|
140
|
+
from omnibase_infra.enums import EnumHandlerLoaderError, EnumInfraTransportType
|
|
141
|
+
from omnibase_infra.errors import InfraConnectionError, ProtocolConfigurationError
|
|
142
|
+
from omnibase_infra.models.errors import ModelInfraErrorContext
|
|
143
|
+
from omnibase_infra.models.runtime import (
|
|
144
|
+
ModelFailedPluginLoad,
|
|
145
|
+
ModelHandlerContract,
|
|
146
|
+
ModelLoadedHandler,
|
|
147
|
+
ModelPluginLoadContext,
|
|
148
|
+
ModelPluginLoadSummary,
|
|
149
|
+
)
|
|
150
|
+
from omnibase_infra.runtime.protocol_handler_plugin_loader import (
|
|
151
|
+
ProtocolHandlerPluginLoader,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
logger = logging.getLogger(__name__)
|
|
155
|
+
|
|
156
|
+
# Regex pattern for detecting filesystem paths in error messages
|
|
157
|
+
# Matches Unix paths (/path/to/file) and Windows paths (C:\path\to\file)
|
|
158
|
+
_PATH_PATTERN = re.compile(
|
|
159
|
+
r"""
|
|
160
|
+
(?: # Non-capturing group for path types
|
|
161
|
+
/(?:[\w.-]+/)+[\w.-]+ # Unix absolute path: /path/to/file
|
|
162
|
+
|
|
|
163
|
+
[A-Za-z]:\\(?:[\w.-]+\\)*[\w.-]+ # Windows path: C:\path\to\file
|
|
164
|
+
|
|
|
165
|
+
\.\.?/(?:[\w.-]+/)*[\w.-]* # Relative path: ./path or ../path
|
|
166
|
+
)
|
|
167
|
+
""",
|
|
168
|
+
re.VERBOSE,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _sanitize_exception_message(exception: BaseException) -> str:
|
|
173
|
+
"""Sanitize exception message to prevent information disclosure.
|
|
174
|
+
|
|
175
|
+
Removes or masks filesystem paths from exception messages to prevent
|
|
176
|
+
exposing internal directory structures in user-facing error messages.
|
|
177
|
+
Preserves useful diagnostic information like line numbers and error types.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
exception: The exception whose message should be sanitized.
|
|
181
|
+
|
|
182
|
+
Returns:
|
|
183
|
+
A sanitized version of the exception message with paths removed.
|
|
184
|
+
|
|
185
|
+
Example:
|
|
186
|
+
>>> e = OSError("[Errno 13] Permission denied: '/etc/secrets/key.pem'")
|
|
187
|
+
>>> _sanitize_exception_message(e)
|
|
188
|
+
"[Errno 13] Permission denied: '<path>'"
|
|
189
|
+
|
|
190
|
+
>>> e = yaml.YAMLError("expected ... in '/home/user/config.yaml', line 10")
|
|
191
|
+
>>> _sanitize_exception_message(e)
|
|
192
|
+
"expected ... in '<path>', line 10"
|
|
193
|
+
"""
|
|
194
|
+
message = str(exception)
|
|
195
|
+
|
|
196
|
+
# Replace filesystem paths with <path> placeholder
|
|
197
|
+
sanitized = _PATH_PATTERN.sub("<path>", message)
|
|
198
|
+
|
|
199
|
+
# Also handle quoted paths that might have been missed
|
|
200
|
+
# Pattern: 'path/to/file' or "path/to/file"
|
|
201
|
+
sanitized = re.sub(r"['\"](?:[^'\"]*[/\\][^'\"]+)['\"]", "'<path>'", sanitized)
|
|
202
|
+
|
|
203
|
+
return sanitized
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
# File pattern for handler contracts
|
|
207
|
+
HANDLER_CONTRACT_FILENAME = "handler_contract.yaml"
|
|
208
|
+
CONTRACT_YAML_FILENAME = "contract.yaml"
|
|
209
|
+
|
|
210
|
+
# Maximum contract file size (10MB) to prevent memory exhaustion
|
|
211
|
+
MAX_CONTRACT_SIZE = 10 * 1024 * 1024
|
|
212
|
+
|
|
213
|
+
# ---------------------------------------------------------------------------
|
|
214
|
+
# Correlation ID Design Decision: UUID Type
|
|
215
|
+
# ---------------------------------------------------------------------------
|
|
216
|
+
# The correlation_id parameter is typed as `UUID | None` to comply with ONEX
|
|
217
|
+
# standards requiring typed models rather than primitives. This aligns with:
|
|
218
|
+
#
|
|
219
|
+
# 1. **ONEX Protocol Conventions**: All other protocols in the codebase use
|
|
220
|
+
# `correlation_id: UUID | None` (see ProtocolIdempotencyStore,
|
|
221
|
+
# ProtocolServiceDiscoveryHandler, etc.).
|
|
222
|
+
#
|
|
223
|
+
# 2. **Type Safety**: UUID type ensures valid correlation IDs at compile time.
|
|
224
|
+
#
|
|
225
|
+
# 3. **Auto-Generation Pattern**: Methods auto-generate correlation IDs via
|
|
226
|
+
# `uuid4()` when not provided, ensuring all operations are traceable.
|
|
227
|
+
#
|
|
228
|
+
# For external system compatibility (OpenTelemetry, Zipkin, etc.), convert
|
|
229
|
+
# string-based correlation IDs to UUID at the call boundary, or use
|
|
230
|
+
# uuid.UUID(external_id) if the external ID is UUID-compatible.
|
|
231
|
+
#
|
|
232
|
+
# See: ONEX correlation ID conventions in omnibase_core.
|
|
233
|
+
# ---------------------------------------------------------------------------
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
class HandlerPluginLoader(ProtocolHandlerPluginLoader):
|
|
237
|
+
"""Load handlers as plugins from contracts.
|
|
238
|
+
|
|
239
|
+
Discovers handler contracts, validates handlers against protocols,
|
|
240
|
+
and registers them with the handler registry.
|
|
241
|
+
|
|
242
|
+
This class implements ProtocolHandlerPluginLoader by scanning filesystem
|
|
243
|
+
paths for handler_contract.yaml or contract.yaml files, parsing them,
|
|
244
|
+
dynamically importing the handler classes, and creating ModelLoadedHandler
|
|
245
|
+
instances.
|
|
246
|
+
|
|
247
|
+
Protocol Compliance:
|
|
248
|
+
This class explicitly implements ProtocolHandlerPluginLoader and provides
|
|
249
|
+
all required methods: load_from_contract(), load_from_directory(), and
|
|
250
|
+
discover_and_load(). Protocol compliance is verified via duck typing.
|
|
251
|
+
|
|
252
|
+
Example:
|
|
253
|
+
>>> # Load a single handler from contract
|
|
254
|
+
>>> loader = HandlerPluginLoader()
|
|
255
|
+
>>> handler = loader.load_from_contract(
|
|
256
|
+
... Path("src/handlers/auth/handler_contract.yaml")
|
|
257
|
+
... )
|
|
258
|
+
>>> print(f"Loaded: {handler.handler_name}")
|
|
259
|
+
|
|
260
|
+
>>> # Load all handlers from a directory
|
|
261
|
+
>>> handlers = loader.load_from_directory(Path("src/handlers"))
|
|
262
|
+
>>> print(f"Loaded {len(handlers)} handlers")
|
|
263
|
+
|
|
264
|
+
>>> # Discover with glob patterns
|
|
265
|
+
>>> handlers = loader.discover_and_load([
|
|
266
|
+
... "src/**/handler_contract.yaml",
|
|
267
|
+
... "plugins/**/contract.yaml",
|
|
268
|
+
... ])
|
|
269
|
+
|
|
270
|
+
.. versionadded:: 0.7.0
|
|
271
|
+
Created as part of OMN-1132 handler plugin loader implementation.
|
|
272
|
+
"""
|
|
273
|
+
|
|
274
|
+
def __init__(self, allowed_namespaces: list[str] | None = None) -> None:
|
|
275
|
+
"""Initialize the handler plugin loader.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
allowed_namespaces: Optional list of allowed namespace prefixes for
|
|
279
|
+
handler module imports. When provided, only handler modules whose
|
|
280
|
+
fully-qualified class path starts with one of these prefixes will
|
|
281
|
+
be loaded. This provides defense-in-depth security by restricting
|
|
282
|
+
which packages can be dynamically imported.
|
|
283
|
+
|
|
284
|
+
Each prefix should end with a period for explicit package boundary
|
|
285
|
+
matching (e.g., "omnibase_infra." not "omnibase_infra"). Prefixes
|
|
286
|
+
without a trailing period are validated at package boundaries to
|
|
287
|
+
prevent unintended matches (e.g., "omnibase" matches "omnibase" or
|
|
288
|
+
"omnibase.handlers" but NOT "omnibase_other").
|
|
289
|
+
|
|
290
|
+
If None (default), no namespace restriction is applied and any
|
|
291
|
+
importable module can be loaded.
|
|
292
|
+
|
|
293
|
+
If an empty list is provided, NO namespaces are allowed, effectively
|
|
294
|
+
blocking all handler imports.
|
|
295
|
+
|
|
296
|
+
Example:
|
|
297
|
+
>>> # Restrict to trusted packages
|
|
298
|
+
>>> loader = HandlerPluginLoader(
|
|
299
|
+
... allowed_namespaces=["omnibase_infra.", "omnibase_core.", "mycompany.handlers."]
|
|
300
|
+
... )
|
|
301
|
+
>>>
|
|
302
|
+
>>> # No restriction (default)
|
|
303
|
+
>>> loader = HandlerPluginLoader()
|
|
304
|
+
>>>
|
|
305
|
+
>>> # Block all imports (empty list)
|
|
306
|
+
>>> loader = HandlerPluginLoader(allowed_namespaces=[])
|
|
307
|
+
|
|
308
|
+
Security Note:
|
|
309
|
+
Namespace validation occurs BEFORE ``importlib.import_module()`` is
|
|
310
|
+
called, preventing any module-level side effects from untrusted packages.
|
|
311
|
+
"""
|
|
312
|
+
self._allowed_namespaces: list[str] | None = allowed_namespaces
|
|
313
|
+
|
|
314
|
+
# Security best practice: warn if no namespace restriction is configured
|
|
315
|
+
if allowed_namespaces is None:
|
|
316
|
+
logger.info(
|
|
317
|
+
"HandlerPluginLoader initialized without namespace restrictions. "
|
|
318
|
+
"For production environments, consider setting allowed_namespaces to "
|
|
319
|
+
"restrict handler imports to trusted packages (e.g., "
|
|
320
|
+
"allowed_namespaces=['omnibase_infra.', 'omnibase_core.', 'mycompany.']).",
|
|
321
|
+
)
|
|
322
|
+
# Warn if empty list is provided - this blocks ALL handler imports
|
|
323
|
+
elif len(allowed_namespaces) == 0:
|
|
324
|
+
logger.warning(
|
|
325
|
+
"HandlerPluginLoader initialized with empty allowed_namespaces list. "
|
|
326
|
+
"This will block ALL handler imports. If this is intentional, ignore "
|
|
327
|
+
"this warning. Otherwise, set allowed_namespaces=None to allow all "
|
|
328
|
+
"namespaces or provide a list of allowed namespace prefixes.",
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
def _validate_correlation_id(
|
|
332
|
+
self,
|
|
333
|
+
correlation_id: UUID | None,
|
|
334
|
+
operation: str,
|
|
335
|
+
) -> None:
|
|
336
|
+
"""Validate correlation_id is a UUID or None at runtime.
|
|
337
|
+
|
|
338
|
+
Provides runtime type validation for correlation_id parameters at public
|
|
339
|
+
API entry points. While type hints provide static checking, runtime
|
|
340
|
+
validation catches cases where callers bypass type checking (e.g.,
|
|
341
|
+
dynamically constructed calls, JSON deserialization without validation).
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
correlation_id: The correlation ID to validate. Must be UUID or None.
|
|
345
|
+
operation: Name of the calling operation (for error context).
|
|
346
|
+
|
|
347
|
+
Raises:
|
|
348
|
+
ProtocolConfigurationError: If correlation_id is not a UUID instance
|
|
349
|
+
or None. The error message includes the actual type received and
|
|
350
|
+
the operation name for debugging, along with guidance on how to
|
|
351
|
+
convert string IDs to UUID.
|
|
352
|
+
|
|
353
|
+
Example:
|
|
354
|
+
>>> loader = HandlerPluginLoader()
|
|
355
|
+
>>> loader._validate_correlation_id(UUID("..."), "load_from_contract") # OK
|
|
356
|
+
>>> loader._validate_correlation_id(None, "load_from_contract") # OK
|
|
357
|
+
>>> loader._validate_correlation_id("not-a-uuid", "load_from_contract")
|
|
358
|
+
ProtocolConfigurationError: correlation_id must be UUID or None...
|
|
359
|
+
"""
|
|
360
|
+
if correlation_id is not None and not isinstance(correlation_id, UUID):
|
|
361
|
+
context = ModelInfraErrorContext(
|
|
362
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
363
|
+
operation=operation,
|
|
364
|
+
correlation_id=None, # Cannot use invalid correlation_id in context
|
|
365
|
+
)
|
|
366
|
+
raise ProtocolConfigurationError(
|
|
367
|
+
f"correlation_id must be UUID or None, got {type(correlation_id).__name__} "
|
|
368
|
+
f"in {operation}(). Convert string IDs using uuid.UUID(your_string).",
|
|
369
|
+
context=context,
|
|
370
|
+
loader_error="INVALID_CORRELATION_ID_TYPE",
|
|
371
|
+
received_type=type(correlation_id).__name__,
|
|
372
|
+
)
|
|
373
|
+
|
|
374
|
+
def load_from_contract(
|
|
375
|
+
self,
|
|
376
|
+
contract_path: Path,
|
|
377
|
+
correlation_id: UUID | None = None,
|
|
378
|
+
) -> ModelLoadedHandler:
|
|
379
|
+
"""Load a single handler from a contract file.
|
|
380
|
+
|
|
381
|
+
Parses the contract YAML file at the given path, validates it,
|
|
382
|
+
imports the handler class, validates protocol compliance, and
|
|
383
|
+
returns a ModelLoadedHandler with the loaded metadata.
|
|
384
|
+
|
|
385
|
+
Args:
|
|
386
|
+
contract_path: Path to the handler contract YAML file.
|
|
387
|
+
Must be an absolute or relative path to an existing file.
|
|
388
|
+
correlation_id: Optional correlation ID for tracing and error context.
|
|
389
|
+
If not provided, a new UUID4 is auto-generated to ensure all
|
|
390
|
+
operations have traceable correlation IDs.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
ModelLoadedHandler containing the loaded handler metadata
|
|
394
|
+
including handler class, version, and contract information.
|
|
395
|
+
|
|
396
|
+
Raises:
|
|
397
|
+
ProtocolConfigurationError: If the contract file is invalid,
|
|
398
|
+
missing required fields, or fails validation. Error codes:
|
|
399
|
+
- HANDLER_LOADER_001: Contract file not found (path doesn't exist)
|
|
400
|
+
- HANDLER_LOADER_002: Invalid YAML syntax
|
|
401
|
+
- HANDLER_LOADER_003: Schema validation failed
|
|
402
|
+
- HANDLER_LOADER_004: Missing required fields
|
|
403
|
+
- HANDLER_LOADER_005: Contract file exceeds size limit
|
|
404
|
+
- HANDLER_LOADER_006: Handler does not implement protocol
|
|
405
|
+
- HANDLER_LOADER_007: Path exists but is not a file (e.g., directory)
|
|
406
|
+
- HANDLER_LOADER_008: Failed to read contract file (I/O error)
|
|
407
|
+
- HANDLER_LOADER_009: Failed to stat contract file (I/O error)
|
|
408
|
+
ProtocolConfigurationError: If namespace validation fails (when
|
|
409
|
+
allowed_namespaces is configured).
|
|
410
|
+
- HANDLER_LOADER_013: Namespace not allowed
|
|
411
|
+
InfraConnectionError: If the handler class cannot be imported.
|
|
412
|
+
Error codes:
|
|
413
|
+
- HANDLER_LOADER_010: Module not found
|
|
414
|
+
- HANDLER_LOADER_011: Class not found in module
|
|
415
|
+
- HANDLER_LOADER_012: Import error (syntax/dependency)
|
|
416
|
+
ProtocolConfigurationError: If correlation_id is not a UUID or None.
|
|
417
|
+
Error code: INVALID_CORRELATION_ID_TYPE
|
|
418
|
+
"""
|
|
419
|
+
# Validate correlation_id type at entry point (runtime type check)
|
|
420
|
+
self._validate_correlation_id(correlation_id, "load_from_contract")
|
|
421
|
+
|
|
422
|
+
# Auto-generate correlation_id if not provided (per ONEX guidelines)
|
|
423
|
+
correlation_id = correlation_id or uuid4()
|
|
424
|
+
|
|
425
|
+
# Convert UUID to string for logging and error context
|
|
426
|
+
correlation_id_str = str(correlation_id)
|
|
427
|
+
|
|
428
|
+
# Start timing for performance observability
|
|
429
|
+
start_time = time.perf_counter()
|
|
430
|
+
|
|
431
|
+
logger.debug(
|
|
432
|
+
"Loading handler from contract: %s",
|
|
433
|
+
contract_path,
|
|
434
|
+
extra={
|
|
435
|
+
"contract_path": str(contract_path),
|
|
436
|
+
"correlation_id": correlation_id_str,
|
|
437
|
+
},
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Validate contract path exists
|
|
441
|
+
# contract_path.exists() and contract_path.is_file() can raise OSError for:
|
|
442
|
+
# - Permission denied when accessing the path
|
|
443
|
+
# - Filesystem errors (unmounted volumes, network failures)
|
|
444
|
+
# - Broken symlinks where the target cannot be resolved
|
|
445
|
+
try:
|
|
446
|
+
path_exists = contract_path.exists()
|
|
447
|
+
except OSError as e:
|
|
448
|
+
context = ModelInfraErrorContext(
|
|
449
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
450
|
+
operation="load_from_contract",
|
|
451
|
+
correlation_id=correlation_id,
|
|
452
|
+
)
|
|
453
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
454
|
+
raise ProtocolConfigurationError(
|
|
455
|
+
f"Failed to access contract path: {sanitized_msg}",
|
|
456
|
+
context=context,
|
|
457
|
+
loader_error=EnumHandlerLoaderError.FILE_STAT_ERROR.value,
|
|
458
|
+
contract_path=str(contract_path),
|
|
459
|
+
) from e
|
|
460
|
+
|
|
461
|
+
if not path_exists:
|
|
462
|
+
context = ModelInfraErrorContext(
|
|
463
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
464
|
+
operation="load_from_contract",
|
|
465
|
+
correlation_id=correlation_id,
|
|
466
|
+
)
|
|
467
|
+
raise ProtocolConfigurationError(
|
|
468
|
+
f"Contract file not found: {contract_path.name}",
|
|
469
|
+
context=context,
|
|
470
|
+
loader_error=EnumHandlerLoaderError.FILE_NOT_FOUND.value,
|
|
471
|
+
contract_path=str(contract_path),
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
try:
|
|
475
|
+
is_file = contract_path.is_file()
|
|
476
|
+
except OSError as e:
|
|
477
|
+
context = ModelInfraErrorContext(
|
|
478
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
479
|
+
operation="load_from_contract",
|
|
480
|
+
correlation_id=correlation_id,
|
|
481
|
+
)
|
|
482
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
483
|
+
raise ProtocolConfigurationError(
|
|
484
|
+
f"Failed to access contract path: {sanitized_msg}",
|
|
485
|
+
context=context,
|
|
486
|
+
loader_error=EnumHandlerLoaderError.FILE_STAT_ERROR.value,
|
|
487
|
+
contract_path=str(contract_path),
|
|
488
|
+
) from e
|
|
489
|
+
|
|
490
|
+
if not is_file:
|
|
491
|
+
context = ModelInfraErrorContext(
|
|
492
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
493
|
+
operation="load_from_contract",
|
|
494
|
+
correlation_id=correlation_id,
|
|
495
|
+
)
|
|
496
|
+
raise ProtocolConfigurationError(
|
|
497
|
+
f"Contract path is not a file: {contract_path.name}",
|
|
498
|
+
context=context,
|
|
499
|
+
loader_error=EnumHandlerLoaderError.NOT_A_FILE.value,
|
|
500
|
+
contract_path=str(contract_path),
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# Validate file size (raises ProtocolConfigurationError on failure)
|
|
504
|
+
self._validate_file_size(
|
|
505
|
+
contract_path,
|
|
506
|
+
correlation_id=correlation_id,
|
|
507
|
+
operation="load_from_contract",
|
|
508
|
+
raise_on_error=True,
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
# Parse YAML contract
|
|
512
|
+
try:
|
|
513
|
+
with contract_path.open("r", encoding="utf-8") as f:
|
|
514
|
+
raw_data = yaml.safe_load(f)
|
|
515
|
+
except yaml.YAMLError as e:
|
|
516
|
+
context = ModelInfraErrorContext(
|
|
517
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
518
|
+
operation="load_from_contract",
|
|
519
|
+
correlation_id=correlation_id,
|
|
520
|
+
)
|
|
521
|
+
# Sanitize exception message to prevent path disclosure
|
|
522
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
523
|
+
raise ProtocolConfigurationError(
|
|
524
|
+
f"Invalid YAML syntax in contract: {sanitized_msg}",
|
|
525
|
+
context=context,
|
|
526
|
+
loader_error=EnumHandlerLoaderError.INVALID_YAML_SYNTAX.value,
|
|
527
|
+
contract_path=str(contract_path),
|
|
528
|
+
) from e
|
|
529
|
+
except OSError as e:
|
|
530
|
+
context = ModelInfraErrorContext(
|
|
531
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
532
|
+
operation="load_from_contract",
|
|
533
|
+
correlation_id=correlation_id,
|
|
534
|
+
)
|
|
535
|
+
# Sanitize exception message to prevent path disclosure
|
|
536
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
537
|
+
raise ProtocolConfigurationError(
|
|
538
|
+
f"Failed to read contract file: {sanitized_msg}",
|
|
539
|
+
context=context,
|
|
540
|
+
loader_error=EnumHandlerLoaderError.FILE_READ_ERROR.value,
|
|
541
|
+
contract_path=str(contract_path),
|
|
542
|
+
) from e
|
|
543
|
+
|
|
544
|
+
if raw_data is None:
|
|
545
|
+
context = ModelInfraErrorContext(
|
|
546
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
547
|
+
operation="load_from_contract",
|
|
548
|
+
correlation_id=correlation_id,
|
|
549
|
+
)
|
|
550
|
+
raise ProtocolConfigurationError(
|
|
551
|
+
"Contract file is empty",
|
|
552
|
+
context=context,
|
|
553
|
+
loader_error=EnumHandlerLoaderError.SCHEMA_VALIDATION_FAILED.value,
|
|
554
|
+
contract_path=str(contract_path),
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
# Validate contract using Pydantic model
|
|
558
|
+
try:
|
|
559
|
+
contract = ModelHandlerContract.model_validate(raw_data)
|
|
560
|
+
except ValidationError as e:
|
|
561
|
+
context = ModelInfraErrorContext(
|
|
562
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
563
|
+
operation="load_from_contract",
|
|
564
|
+
correlation_id=correlation_id,
|
|
565
|
+
)
|
|
566
|
+
# Convert validation errors to readable message
|
|
567
|
+
error_details = "; ".join(
|
|
568
|
+
f"{'.'.join(str(loc) for loc in err['loc'])}: {err['msg']}"
|
|
569
|
+
for err in e.errors()
|
|
570
|
+
)
|
|
571
|
+
raise ProtocolConfigurationError(
|
|
572
|
+
f"Contract validation failed: {error_details}",
|
|
573
|
+
context=context,
|
|
574
|
+
loader_error=EnumHandlerLoaderError.SCHEMA_VALIDATION_FAILED.value,
|
|
575
|
+
contract_path=str(contract_path),
|
|
576
|
+
validation_errors=[
|
|
577
|
+
{"loc": err["loc"], "msg": err["msg"], "type": err["type"]}
|
|
578
|
+
for err in e.errors()
|
|
579
|
+
],
|
|
580
|
+
) from e
|
|
581
|
+
|
|
582
|
+
handler_name = contract.handler_name
|
|
583
|
+
handler_class_path = contract.handler_class
|
|
584
|
+
handler_type = contract.handler_type
|
|
585
|
+
capability_tags = contract.capability_tags
|
|
586
|
+
# protocol_type is the registry key (e.g., "db", "http")
|
|
587
|
+
# The model_validator in ModelHandlerContract sets this from handler_name
|
|
588
|
+
# if not explicitly provided (strips "handler-" prefix)
|
|
589
|
+
protocol_type = contract.protocol_type
|
|
590
|
+
# Should never be None after model_validator, but assert for type safety
|
|
591
|
+
assert protocol_type is not None, (
|
|
592
|
+
"protocol_type should be set by model_validator"
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
# Import and validate handler class
|
|
596
|
+
handler_class = self._import_handler_class(
|
|
597
|
+
handler_class_path, contract_path, correlation_id
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
# Validate handler implements protocol
|
|
601
|
+
is_valid, missing_methods = self._validate_handler_protocol(handler_class)
|
|
602
|
+
if not is_valid:
|
|
603
|
+
context = ModelInfraErrorContext(
|
|
604
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
605
|
+
operation="load_from_contract",
|
|
606
|
+
correlation_id=correlation_id,
|
|
607
|
+
)
|
|
608
|
+
missing_str = ", ".join(missing_methods)
|
|
609
|
+
raise ProtocolConfigurationError(
|
|
610
|
+
f"Handler class {handler_class_path} is missing required "
|
|
611
|
+
f"ProtocolHandler methods: {missing_str}",
|
|
612
|
+
context=context,
|
|
613
|
+
loader_error=EnumHandlerLoaderError.PROTOCOL_NOT_IMPLEMENTED.value,
|
|
614
|
+
contract_path=str(contract_path),
|
|
615
|
+
handler_class=handler_class_path,
|
|
616
|
+
missing_methods=missing_methods,
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
# Resolve the contract path, handling potential filesystem errors
|
|
620
|
+
# path.resolve() can raise OSError for:
|
|
621
|
+
# - Broken symlinks: symlink target no longer exists
|
|
622
|
+
# - Race conditions: file deleted between validation and resolution
|
|
623
|
+
# - Permission issues: lacking read permission on parent directories
|
|
624
|
+
# - Filesystem errors: unmounted volumes, network filesystem failures
|
|
625
|
+
try:
|
|
626
|
+
resolved_contract_path = contract_path.resolve()
|
|
627
|
+
except OSError as e:
|
|
628
|
+
context = ModelInfraErrorContext(
|
|
629
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
630
|
+
operation="load_from_contract",
|
|
631
|
+
correlation_id=correlation_id,
|
|
632
|
+
)
|
|
633
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
634
|
+
# FILE_STAT_ERROR is used here because path resolution involves
|
|
635
|
+
# filesystem metadata access similar to stat operations
|
|
636
|
+
raise ProtocolConfigurationError(
|
|
637
|
+
f"Failed to access contract file during path resolution: {sanitized_msg}",
|
|
638
|
+
context=context,
|
|
639
|
+
loader_error=EnumHandlerLoaderError.FILE_STAT_ERROR.value,
|
|
640
|
+
contract_path=str(contract_path),
|
|
641
|
+
) from e
|
|
642
|
+
|
|
643
|
+
# Calculate load duration for performance observability
|
|
644
|
+
load_duration_ms = (time.perf_counter() - start_time) * 1000
|
|
645
|
+
|
|
646
|
+
logger.info(
|
|
647
|
+
"Successfully loaded handler from contract: %s -> %s (%.2fms)",
|
|
648
|
+
handler_name,
|
|
649
|
+
handler_class_path,
|
|
650
|
+
load_duration_ms,
|
|
651
|
+
extra={
|
|
652
|
+
"handler_name": handler_name,
|
|
653
|
+
"handler_class": handler_class_path,
|
|
654
|
+
"handler_type": handler_type.value,
|
|
655
|
+
"protocol_type": protocol_type,
|
|
656
|
+
"contract_path": str(resolved_contract_path),
|
|
657
|
+
"correlation_id": correlation_id_str,
|
|
658
|
+
"load_duration_ms": load_duration_ms,
|
|
659
|
+
},
|
|
660
|
+
)
|
|
661
|
+
|
|
662
|
+
# contract.handler_version is guaranteed non-None by model_validator
|
|
663
|
+
if contract.handler_version is None:
|
|
664
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
665
|
+
correlation_id=correlation_id,
|
|
666
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
667
|
+
operation="load_from_contract",
|
|
668
|
+
)
|
|
669
|
+
raise ProtocolConfigurationError(
|
|
670
|
+
"handler_version should be set by model_validator",
|
|
671
|
+
context=context,
|
|
672
|
+
loader_error=EnumHandlerLoaderError.MISSING_REQUIRED_FIELDS.value,
|
|
673
|
+
contract_path=str(contract_path),
|
|
674
|
+
)
|
|
675
|
+
|
|
676
|
+
return ModelLoadedHandler(
|
|
677
|
+
handler_name=handler_name,
|
|
678
|
+
protocol_type=protocol_type,
|
|
679
|
+
handler_type=handler_type,
|
|
680
|
+
handler_class=handler_class_path,
|
|
681
|
+
contract_path=resolved_contract_path,
|
|
682
|
+
capability_tags=capability_tags,
|
|
683
|
+
loaded_at=datetime.now(UTC),
|
|
684
|
+
handler_version=contract.handler_version,
|
|
685
|
+
)
|
|
686
|
+
|
|
687
|
+
def load_from_directory(
|
|
688
|
+
self,
|
|
689
|
+
directory: Path,
|
|
690
|
+
correlation_id: UUID | None = None,
|
|
691
|
+
max_handlers: int | None = None,
|
|
692
|
+
) -> list[ModelLoadedHandler]:
|
|
693
|
+
"""Load all handlers from contract files in a directory.
|
|
694
|
+
|
|
695
|
+
Recursively scans the given directory for handler contract files
|
|
696
|
+
(handler_contract.yaml or contract.yaml), loads each handler,
|
|
697
|
+
and returns a list of successfully loaded handlers.
|
|
698
|
+
|
|
699
|
+
Failed loads are logged but do not stop processing of other handlers.
|
|
700
|
+
A summary is logged at the end of the operation for observability.
|
|
701
|
+
|
|
702
|
+
Args:
|
|
703
|
+
directory: Path to the directory to scan for contract files.
|
|
704
|
+
Must be an existing directory.
|
|
705
|
+
correlation_id: Optional correlation ID for tracing and error context.
|
|
706
|
+
If not provided, a new UUID4 is auto-generated to ensure all
|
|
707
|
+
operations have traceable correlation IDs. The same correlation_id
|
|
708
|
+
is propagated to all contract loads within the directory scan.
|
|
709
|
+
max_handlers: Optional maximum number of handlers to discover and load.
|
|
710
|
+
If specified, discovery stops after finding this many contract files.
|
|
711
|
+
A warning is logged when the limit is reached. Set to None (default)
|
|
712
|
+
for unlimited discovery. This prevents runaway resource usage when
|
|
713
|
+
scanning directories with unexpectedly large numbers of handlers.
|
|
714
|
+
|
|
715
|
+
Returns:
|
|
716
|
+
List of successfully loaded handlers. May be empty if no
|
|
717
|
+
contracts are found or all fail validation.
|
|
718
|
+
|
|
719
|
+
Raises:
|
|
720
|
+
ProtocolConfigurationError: If the directory does not exist
|
|
721
|
+
or is not accessible. Error codes:
|
|
722
|
+
- HANDLER_LOADER_020: Directory not found
|
|
723
|
+
- HANDLER_LOADER_021: Permission denied
|
|
724
|
+
- HANDLER_LOADER_022: Not a directory
|
|
725
|
+
ProtocolConfigurationError: If correlation_id is not a UUID or None.
|
|
726
|
+
Error code: INVALID_CORRELATION_ID_TYPE
|
|
727
|
+
"""
|
|
728
|
+
# Validate correlation_id type at entry point (runtime type check)
|
|
729
|
+
self._validate_correlation_id(correlation_id, "load_from_directory")
|
|
730
|
+
|
|
731
|
+
# Auto-generate correlation_id if not provided (per ONEX guidelines)
|
|
732
|
+
correlation_id = correlation_id or uuid4()
|
|
733
|
+
|
|
734
|
+
# Start timing for observability
|
|
735
|
+
start_time = time.perf_counter()
|
|
736
|
+
|
|
737
|
+
logger.debug(
|
|
738
|
+
"Loading handlers from directory: %s",
|
|
739
|
+
directory,
|
|
740
|
+
extra={
|
|
741
|
+
"directory": str(directory),
|
|
742
|
+
"correlation_id": str(correlation_id),
|
|
743
|
+
"max_handlers": max_handlers,
|
|
744
|
+
},
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
# Validate directory exists
|
|
748
|
+
# directory.exists() and directory.is_dir() can raise OSError for:
|
|
749
|
+
# - Permission denied when accessing the path
|
|
750
|
+
# - Filesystem errors (unmounted volumes, network failures)
|
|
751
|
+
# - Broken symlinks where the target cannot be resolved
|
|
752
|
+
try:
|
|
753
|
+
dir_exists = directory.exists()
|
|
754
|
+
except OSError as e:
|
|
755
|
+
context = ModelInfraErrorContext(
|
|
756
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
757
|
+
operation="load_from_directory",
|
|
758
|
+
correlation_id=correlation_id,
|
|
759
|
+
)
|
|
760
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
761
|
+
raise ProtocolConfigurationError(
|
|
762
|
+
f"Failed to access directory: {sanitized_msg}",
|
|
763
|
+
context=context,
|
|
764
|
+
loader_error=EnumHandlerLoaderError.PERMISSION_DENIED.value,
|
|
765
|
+
directory=str(directory),
|
|
766
|
+
) from e
|
|
767
|
+
|
|
768
|
+
if not dir_exists:
|
|
769
|
+
context = ModelInfraErrorContext(
|
|
770
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
771
|
+
operation="load_from_directory",
|
|
772
|
+
correlation_id=correlation_id,
|
|
773
|
+
)
|
|
774
|
+
raise ProtocolConfigurationError(
|
|
775
|
+
f"Directory not found: {directory.name}",
|
|
776
|
+
context=context,
|
|
777
|
+
loader_error=EnumHandlerLoaderError.DIRECTORY_NOT_FOUND.value,
|
|
778
|
+
directory=str(directory),
|
|
779
|
+
)
|
|
780
|
+
|
|
781
|
+
try:
|
|
782
|
+
is_directory = directory.is_dir()
|
|
783
|
+
except OSError as e:
|
|
784
|
+
context = ModelInfraErrorContext(
|
|
785
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
786
|
+
operation="load_from_directory",
|
|
787
|
+
correlation_id=correlation_id,
|
|
788
|
+
)
|
|
789
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
790
|
+
raise ProtocolConfigurationError(
|
|
791
|
+
f"Failed to access directory: {sanitized_msg}",
|
|
792
|
+
context=context,
|
|
793
|
+
loader_error=EnumHandlerLoaderError.PERMISSION_DENIED.value,
|
|
794
|
+
directory=str(directory),
|
|
795
|
+
) from e
|
|
796
|
+
|
|
797
|
+
if not is_directory:
|
|
798
|
+
context = ModelInfraErrorContext(
|
|
799
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
800
|
+
operation="load_from_directory",
|
|
801
|
+
correlation_id=correlation_id,
|
|
802
|
+
)
|
|
803
|
+
raise ProtocolConfigurationError(
|
|
804
|
+
f"Path is not a directory: {directory.name}",
|
|
805
|
+
context=context,
|
|
806
|
+
loader_error=EnumHandlerLoaderError.NOT_A_DIRECTORY.value,
|
|
807
|
+
directory=str(directory),
|
|
808
|
+
)
|
|
809
|
+
|
|
810
|
+
# Find all contract files (with optional limit)
|
|
811
|
+
contract_files = self._find_contract_files(
|
|
812
|
+
directory, correlation_id, max_handlers=max_handlers
|
|
813
|
+
)
|
|
814
|
+
|
|
815
|
+
logger.debug(
|
|
816
|
+
"Found %d contract files in directory: %s",
|
|
817
|
+
len(contract_files),
|
|
818
|
+
directory,
|
|
819
|
+
extra={
|
|
820
|
+
"directory": str(directory),
|
|
821
|
+
"contract_count": len(contract_files),
|
|
822
|
+
"correlation_id": str(correlation_id),
|
|
823
|
+
},
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
# Load each contract (graceful mode - continue on errors)
|
|
827
|
+
handlers: list[ModelLoadedHandler] = []
|
|
828
|
+
failed_handlers: list[ModelFailedPluginLoad] = []
|
|
829
|
+
|
|
830
|
+
for contract_path in contract_files:
|
|
831
|
+
try:
|
|
832
|
+
handler = self.load_from_contract(contract_path, correlation_id)
|
|
833
|
+
handlers.append(handler)
|
|
834
|
+
except (ProtocolConfigurationError, InfraConnectionError) as e:
|
|
835
|
+
# Extract error code if available
|
|
836
|
+
error_code: str | None = None
|
|
837
|
+
if hasattr(e, "model") and hasattr(e.model, "context"):
|
|
838
|
+
loader_error = e.model.context.get("loader_error")
|
|
839
|
+
error_code = str(loader_error) if loader_error is not None else None
|
|
840
|
+
|
|
841
|
+
failed_handlers.append(
|
|
842
|
+
ModelFailedPluginLoad(
|
|
843
|
+
contract_path=contract_path,
|
|
844
|
+
error_message=str(e),
|
|
845
|
+
error_code=error_code,
|
|
846
|
+
)
|
|
847
|
+
)
|
|
848
|
+
|
|
849
|
+
logger.warning(
|
|
850
|
+
"Failed to load handler from %s: %s",
|
|
851
|
+
contract_path,
|
|
852
|
+
str(e),
|
|
853
|
+
extra={
|
|
854
|
+
"contract_path": str(contract_path),
|
|
855
|
+
"error": str(e),
|
|
856
|
+
"error_code": error_code,
|
|
857
|
+
"correlation_id": str(correlation_id),
|
|
858
|
+
},
|
|
859
|
+
)
|
|
860
|
+
continue
|
|
861
|
+
|
|
862
|
+
# Calculate duration and log summary
|
|
863
|
+
duration_seconds = time.perf_counter() - start_time
|
|
864
|
+
|
|
865
|
+
self._log_load_summary(
|
|
866
|
+
ModelPluginLoadContext(
|
|
867
|
+
operation="load_from_directory",
|
|
868
|
+
source=str(directory),
|
|
869
|
+
total_discovered=len(contract_files),
|
|
870
|
+
handlers=handlers,
|
|
871
|
+
failed_plugins=failed_handlers,
|
|
872
|
+
duration_seconds=duration_seconds,
|
|
873
|
+
correlation_id=correlation_id,
|
|
874
|
+
caller_correlation_string=str(correlation_id),
|
|
875
|
+
)
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
return handlers
|
|
879
|
+
|
|
880
|
+
def discover_and_load(
|
|
881
|
+
self,
|
|
882
|
+
patterns: list[str],
|
|
883
|
+
correlation_id: UUID | None = None,
|
|
884
|
+
base_path: Path | None = None,
|
|
885
|
+
max_handlers: int | None = None,
|
|
886
|
+
) -> list[ModelLoadedHandler]:
|
|
887
|
+
"""Discover contracts matching glob patterns and load handlers.
|
|
888
|
+
|
|
889
|
+
Searches for contract files matching the given glob patterns,
|
|
890
|
+
deduplicates matches, loads each handler, and returns a list
|
|
891
|
+
of successfully loaded handlers.
|
|
892
|
+
|
|
893
|
+
A summary is logged at the end of the operation for observability.
|
|
894
|
+
|
|
895
|
+
Working Directory Dependency:
|
|
896
|
+
By default, glob patterns are resolved relative to the current
|
|
897
|
+
working directory (``Path.cwd()``). This means results may vary
|
|
898
|
+
if the working directory changes between calls. For deterministic
|
|
899
|
+
behavior in environments where cwd may change (e.g., tests,
|
|
900
|
+
multi-threaded applications), provide an explicit ``base_path``
|
|
901
|
+
parameter.
|
|
902
|
+
|
|
903
|
+
Args:
|
|
904
|
+
patterns: List of glob patterns to match contract files.
|
|
905
|
+
Supports standard glob syntax including ** for recursive.
|
|
906
|
+
correlation_id: Optional correlation ID for tracing and error context.
|
|
907
|
+
If not provided, a new UUID4 is auto-generated to ensure all
|
|
908
|
+
operations have traceable correlation IDs. The same correlation_id
|
|
909
|
+
is propagated to all discovered contract loads.
|
|
910
|
+
base_path: Optional base path for resolving glob patterns.
|
|
911
|
+
If not provided, defaults to ``Path.cwd()``. Providing an
|
|
912
|
+
explicit base path ensures deterministic behavior regardless
|
|
913
|
+
of the current working directory.
|
|
914
|
+
max_handlers: Optional maximum number of handlers to discover and load.
|
|
915
|
+
If specified, discovery stops after finding this many contract files.
|
|
916
|
+
A warning is logged when the limit is reached. Set to None (default)
|
|
917
|
+
for unlimited discovery. This prevents runaway resource usage when
|
|
918
|
+
scanning directories with unexpectedly large numbers of handlers.
|
|
919
|
+
|
|
920
|
+
Returns:
|
|
921
|
+
List of successfully loaded handlers. May be empty if no
|
|
922
|
+
patterns match or all fail validation.
|
|
923
|
+
|
|
924
|
+
Raises:
|
|
925
|
+
ProtocolConfigurationError: If patterns list is empty.
|
|
926
|
+
Error codes:
|
|
927
|
+
- HANDLER_LOADER_030: Empty patterns list
|
|
928
|
+
ProtocolConfigurationError: If correlation_id is not a UUID or None.
|
|
929
|
+
Error code: INVALID_CORRELATION_ID_TYPE
|
|
930
|
+
|
|
931
|
+
Example:
|
|
932
|
+
>>> # Using default cwd-based resolution
|
|
933
|
+
>>> handlers = loader.discover_and_load(["src/**/handler_contract.yaml"])
|
|
934
|
+
>>>
|
|
935
|
+
>>> # Using explicit base path for deterministic behavior
|
|
936
|
+
>>> handlers = loader.discover_and_load(
|
|
937
|
+
... ["src/**/handler_contract.yaml"],
|
|
938
|
+
... base_path=Path("/app/project"),
|
|
939
|
+
... )
|
|
940
|
+
"""
|
|
941
|
+
# Validate correlation_id type at entry point (runtime type check)
|
|
942
|
+
self._validate_correlation_id(correlation_id, "discover_and_load")
|
|
943
|
+
|
|
944
|
+
# Auto-generate correlation_id if not provided (per ONEX guidelines)
|
|
945
|
+
correlation_id = correlation_id or uuid4()
|
|
946
|
+
|
|
947
|
+
# Start timing for observability
|
|
948
|
+
start_time = time.perf_counter()
|
|
949
|
+
|
|
950
|
+
logger.debug(
|
|
951
|
+
"Discovering handlers with patterns: %s",
|
|
952
|
+
patterns,
|
|
953
|
+
extra={
|
|
954
|
+
"patterns": patterns,
|
|
955
|
+
"correlation_id": str(correlation_id),
|
|
956
|
+
"max_handlers": max_handlers,
|
|
957
|
+
},
|
|
958
|
+
)
|
|
959
|
+
|
|
960
|
+
if not patterns:
|
|
961
|
+
context = ModelInfraErrorContext(
|
|
962
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
963
|
+
operation="discover_and_load",
|
|
964
|
+
correlation_id=correlation_id,
|
|
965
|
+
)
|
|
966
|
+
raise ProtocolConfigurationError(
|
|
967
|
+
"Patterns list cannot be empty",
|
|
968
|
+
context=context,
|
|
969
|
+
loader_error=EnumHandlerLoaderError.EMPTY_PATTERNS_LIST.value,
|
|
970
|
+
)
|
|
971
|
+
|
|
972
|
+
# Collect all matching contract files, deduplicated by resolved path
|
|
973
|
+
discovered_paths: set[Path] = set()
|
|
974
|
+
limit_reached = False
|
|
975
|
+
|
|
976
|
+
# Use explicit base_path if provided, otherwise fall back to cwd
|
|
977
|
+
# Note: Using cwd can produce different results if the working directory
|
|
978
|
+
# changes between calls. For deterministic behavior, provide base_path.
|
|
979
|
+
# Path.cwd() can raise OSError if:
|
|
980
|
+
# - Current working directory has been deleted
|
|
981
|
+
# - Permission denied accessing current directory
|
|
982
|
+
if base_path is not None:
|
|
983
|
+
glob_base = base_path
|
|
984
|
+
else:
|
|
985
|
+
try:
|
|
986
|
+
glob_base = Path.cwd()
|
|
987
|
+
except OSError as e:
|
|
988
|
+
context = ModelInfraErrorContext(
|
|
989
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
990
|
+
operation="discover_and_load",
|
|
991
|
+
correlation_id=correlation_id,
|
|
992
|
+
)
|
|
993
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
994
|
+
raise ProtocolConfigurationError(
|
|
995
|
+
f"Failed to access current working directory: {sanitized_msg}",
|
|
996
|
+
context=context,
|
|
997
|
+
loader_error=EnumHandlerLoaderError.PERMISSION_DENIED.value,
|
|
998
|
+
) from e
|
|
999
|
+
|
|
1000
|
+
for pattern in patterns:
|
|
1001
|
+
if limit_reached:
|
|
1002
|
+
break
|
|
1003
|
+
|
|
1004
|
+
# Path.glob() can raise:
|
|
1005
|
+
# - OSError: Permission denied, filesystem errors, invalid path components
|
|
1006
|
+
# - ValueError: Invalid glob pattern syntax (e.g., ** not at path segment boundary)
|
|
1007
|
+
try:
|
|
1008
|
+
matched_paths = list(glob_base.glob(pattern))
|
|
1009
|
+
except ValueError as e:
|
|
1010
|
+
# ValueError indicates invalid glob pattern syntax
|
|
1011
|
+
# Example: "foo**bar" - ** must be at path segment boundaries
|
|
1012
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
1013
|
+
logger.warning(
|
|
1014
|
+
"Invalid glob pattern syntax '%s': %s",
|
|
1015
|
+
pattern,
|
|
1016
|
+
sanitized_msg,
|
|
1017
|
+
extra={
|
|
1018
|
+
"pattern": pattern,
|
|
1019
|
+
"base_path": str(glob_base),
|
|
1020
|
+
"error": sanitized_msg,
|
|
1021
|
+
"error_code": EnumHandlerLoaderError.INVALID_GLOB_PATTERN.value,
|
|
1022
|
+
"correlation_id": str(correlation_id),
|
|
1023
|
+
},
|
|
1024
|
+
)
|
|
1025
|
+
continue # Skip to next pattern
|
|
1026
|
+
except OSError as e:
|
|
1027
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
1028
|
+
logger.warning(
|
|
1029
|
+
"Failed to evaluate glob pattern '%s': %s",
|
|
1030
|
+
pattern,
|
|
1031
|
+
sanitized_msg,
|
|
1032
|
+
extra={
|
|
1033
|
+
"pattern": pattern,
|
|
1034
|
+
"base_path": str(glob_base),
|
|
1035
|
+
"error": sanitized_msg,
|
|
1036
|
+
"correlation_id": str(correlation_id),
|
|
1037
|
+
},
|
|
1038
|
+
)
|
|
1039
|
+
continue # Skip to next pattern
|
|
1040
|
+
|
|
1041
|
+
for path in matched_paths:
|
|
1042
|
+
# Check if we've reached the limit
|
|
1043
|
+
if max_handlers is not None and len(discovered_paths) >= max_handlers:
|
|
1044
|
+
limit_reached = True
|
|
1045
|
+
logger.warning(
|
|
1046
|
+
"Handler discovery limit reached: stopped after discovering %d "
|
|
1047
|
+
"handlers (max_handlers=%d). Some handlers may not be loaded.",
|
|
1048
|
+
len(discovered_paths),
|
|
1049
|
+
max_handlers,
|
|
1050
|
+
extra={
|
|
1051
|
+
"discovered_count": len(discovered_paths),
|
|
1052
|
+
"max_handlers": max_handlers,
|
|
1053
|
+
"patterns": patterns,
|
|
1054
|
+
"correlation_id": str(correlation_id),
|
|
1055
|
+
},
|
|
1056
|
+
)
|
|
1057
|
+
break
|
|
1058
|
+
|
|
1059
|
+
# path.is_file() can raise OSError for:
|
|
1060
|
+
# - Permission denied when stat'ing the file
|
|
1061
|
+
# - File deleted between glob discovery and is_file() check
|
|
1062
|
+
# - Filesystem errors (unmounted volumes, network failures)
|
|
1063
|
+
try:
|
|
1064
|
+
is_file = path.is_file()
|
|
1065
|
+
except OSError as e:
|
|
1066
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
1067
|
+
logger.warning(
|
|
1068
|
+
"Failed to check if path is file %s: %s",
|
|
1069
|
+
path.name,
|
|
1070
|
+
sanitized_msg,
|
|
1071
|
+
extra={
|
|
1072
|
+
"path": str(path),
|
|
1073
|
+
"error": sanitized_msg,
|
|
1074
|
+
"error_code": EnumHandlerLoaderError.FILE_STAT_ERROR.value,
|
|
1075
|
+
"correlation_id": str(correlation_id),
|
|
1076
|
+
},
|
|
1077
|
+
)
|
|
1078
|
+
continue
|
|
1079
|
+
|
|
1080
|
+
if is_file:
|
|
1081
|
+
# Early size validation to skip oversized files before expensive operations
|
|
1082
|
+
if (
|
|
1083
|
+
self._validate_file_size(
|
|
1084
|
+
path, correlation_id=correlation_id, raise_on_error=False
|
|
1085
|
+
)
|
|
1086
|
+
is None
|
|
1087
|
+
):
|
|
1088
|
+
continue
|
|
1089
|
+
|
|
1090
|
+
# Early YAML syntax validation to fail fast before expensive resolve operations
|
|
1091
|
+
# This catches malformed YAML immediately after discovery rather than
|
|
1092
|
+
# deferring to load_from_contract, which is more efficient for batch discovery
|
|
1093
|
+
if not self._validate_yaml_syntax(
|
|
1094
|
+
path, correlation_id=correlation_id, raise_on_error=False
|
|
1095
|
+
):
|
|
1096
|
+
continue
|
|
1097
|
+
|
|
1098
|
+
# path.resolve() can raise OSError for:
|
|
1099
|
+
# - Broken symlinks: symlink target no longer exists
|
|
1100
|
+
# - Race conditions: file deleted between glob discovery and resolution
|
|
1101
|
+
# - Permission issues: lacking read permission on parent directories
|
|
1102
|
+
# - Filesystem errors: unmounted volumes, network filesystem failures
|
|
1103
|
+
try:
|
|
1104
|
+
resolved = path.resolve()
|
|
1105
|
+
except OSError as e:
|
|
1106
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
1107
|
+
logger.warning(
|
|
1108
|
+
"Failed to resolve path %s: %s",
|
|
1109
|
+
path.name,
|
|
1110
|
+
sanitized_msg,
|
|
1111
|
+
extra={
|
|
1112
|
+
"path": str(path),
|
|
1113
|
+
"error": sanitized_msg,
|
|
1114
|
+
"correlation_id": str(correlation_id),
|
|
1115
|
+
},
|
|
1116
|
+
)
|
|
1117
|
+
continue # Skip to next path
|
|
1118
|
+
|
|
1119
|
+
discovered_paths.add(resolved)
|
|
1120
|
+
|
|
1121
|
+
logger.debug(
|
|
1122
|
+
"Discovered %d unique contract files from %d patterns",
|
|
1123
|
+
len(discovered_paths),
|
|
1124
|
+
len(patterns),
|
|
1125
|
+
extra={
|
|
1126
|
+
"patterns": patterns,
|
|
1127
|
+
"discovered_count": len(discovered_paths),
|
|
1128
|
+
"limit_reached": limit_reached,
|
|
1129
|
+
"correlation_id": str(correlation_id),
|
|
1130
|
+
},
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
# Load each discovered contract (graceful mode)
|
|
1134
|
+
handlers: list[ModelLoadedHandler] = []
|
|
1135
|
+
failed_handlers: list[ModelFailedPluginLoad] = []
|
|
1136
|
+
|
|
1137
|
+
for contract_path in sorted(discovered_paths):
|
|
1138
|
+
try:
|
|
1139
|
+
handler = self.load_from_contract(contract_path, correlation_id)
|
|
1140
|
+
handlers.append(handler)
|
|
1141
|
+
except (ProtocolConfigurationError, InfraConnectionError) as e:
|
|
1142
|
+
# Extract error code if available
|
|
1143
|
+
error_code: str | None = None
|
|
1144
|
+
if hasattr(e, "model") and hasattr(e.model, "context"):
|
|
1145
|
+
loader_error = e.model.context.get("loader_error")
|
|
1146
|
+
error_code = str(loader_error) if loader_error is not None else None
|
|
1147
|
+
|
|
1148
|
+
failed_handlers.append(
|
|
1149
|
+
ModelFailedPluginLoad(
|
|
1150
|
+
contract_path=contract_path,
|
|
1151
|
+
error_message=str(e),
|
|
1152
|
+
error_code=error_code,
|
|
1153
|
+
)
|
|
1154
|
+
)
|
|
1155
|
+
|
|
1156
|
+
logger.warning(
|
|
1157
|
+
"Failed to load handler from %s: %s",
|
|
1158
|
+
contract_path,
|
|
1159
|
+
str(e),
|
|
1160
|
+
extra={
|
|
1161
|
+
"contract_path": str(contract_path),
|
|
1162
|
+
"error": str(e),
|
|
1163
|
+
"error_code": error_code,
|
|
1164
|
+
"correlation_id": str(correlation_id),
|
|
1165
|
+
},
|
|
1166
|
+
)
|
|
1167
|
+
continue
|
|
1168
|
+
|
|
1169
|
+
# Calculate duration and log summary
|
|
1170
|
+
duration_seconds = time.perf_counter() - start_time
|
|
1171
|
+
|
|
1172
|
+
# Format patterns as comma-separated string for source
|
|
1173
|
+
patterns_str = ", ".join(patterns)
|
|
1174
|
+
|
|
1175
|
+
self._log_load_summary(
|
|
1176
|
+
ModelPluginLoadContext(
|
|
1177
|
+
operation="discover_and_load",
|
|
1178
|
+
source=patterns_str,
|
|
1179
|
+
total_discovered=len(discovered_paths),
|
|
1180
|
+
handlers=handlers,
|
|
1181
|
+
failed_plugins=failed_handlers,
|
|
1182
|
+
duration_seconds=duration_seconds,
|
|
1183
|
+
correlation_id=correlation_id,
|
|
1184
|
+
caller_correlation_string=str(correlation_id),
|
|
1185
|
+
)
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
return handlers
|
|
1189
|
+
|
|
1190
|
+
def _log_load_summary(
|
|
1191
|
+
self,
|
|
1192
|
+
context: ModelPluginLoadContext,
|
|
1193
|
+
) -> ModelPluginLoadSummary:
|
|
1194
|
+
"""Log a summary of the handler loading operation for observability.
|
|
1195
|
+
|
|
1196
|
+
Creates a structured summary of the load operation and logs it at
|
|
1197
|
+
an appropriate level (INFO for success, WARNING if there were failures).
|
|
1198
|
+
|
|
1199
|
+
The log message format is designed for easy parsing:
|
|
1200
|
+
- Single line summary with counts and timing
|
|
1201
|
+
- Detailed handler list with class names and modules
|
|
1202
|
+
- Failed handler details with error reasons
|
|
1203
|
+
|
|
1204
|
+
Args:
|
|
1205
|
+
context: The load context containing operation details, handlers,
|
|
1206
|
+
failures, and timing information.
|
|
1207
|
+
|
|
1208
|
+
Returns:
|
|
1209
|
+
ModelPluginLoadSummary containing the structured summary data.
|
|
1210
|
+
|
|
1211
|
+
Example log output:
|
|
1212
|
+
Handler load complete: 5 handlers loaded in 0.23s (source: /app/handlers)
|
|
1213
|
+
- HandlerAuth (myapp.handlers.auth)
|
|
1214
|
+
- HandlerDb (myapp.handlers.db)
|
|
1215
|
+
...
|
|
1216
|
+
"""
|
|
1217
|
+
# Build list of loaded handler details
|
|
1218
|
+
loaded_handler_details = [
|
|
1219
|
+
{
|
|
1220
|
+
"name": h.handler_name,
|
|
1221
|
+
"class": h.handler_class.rsplit(".", 1)[-1],
|
|
1222
|
+
"module": h.handler_class.rsplit(".", 1)[0],
|
|
1223
|
+
}
|
|
1224
|
+
for h in context.handlers
|
|
1225
|
+
]
|
|
1226
|
+
|
|
1227
|
+
# Create summary model
|
|
1228
|
+
summary = ModelPluginLoadSummary(
|
|
1229
|
+
operation=context.operation,
|
|
1230
|
+
source=context.source,
|
|
1231
|
+
total_discovered=context.total_discovered,
|
|
1232
|
+
total_loaded=len(context.handlers),
|
|
1233
|
+
total_failed=len(context.failed_plugins),
|
|
1234
|
+
loaded_plugins=loaded_handler_details,
|
|
1235
|
+
failed_plugins=context.failed_plugins,
|
|
1236
|
+
duration_seconds=context.duration_seconds,
|
|
1237
|
+
correlation_id=context.correlation_id,
|
|
1238
|
+
completed_at=datetime.now(UTC),
|
|
1239
|
+
)
|
|
1240
|
+
|
|
1241
|
+
# Build log message with handler details
|
|
1242
|
+
handler_lines = [
|
|
1243
|
+
f" - {h['class']} ({h['module']})" for h in loaded_handler_details
|
|
1244
|
+
]
|
|
1245
|
+
handler_list_str = "\n".join(handler_lines) if handler_lines else " (none)"
|
|
1246
|
+
|
|
1247
|
+
# Build failed handler message if any
|
|
1248
|
+
failed_lines = []
|
|
1249
|
+
for failed in context.failed_plugins:
|
|
1250
|
+
error_code_str = f" [{failed.error_code}]" if failed.error_code else ""
|
|
1251
|
+
failed_lines.append(f" - {failed.contract_path}{error_code_str}")
|
|
1252
|
+
|
|
1253
|
+
failed_list_str = "\n".join(failed_lines) if failed_lines else ""
|
|
1254
|
+
|
|
1255
|
+
# Choose log level based on whether there were failures
|
|
1256
|
+
if context.failed_plugins:
|
|
1257
|
+
log_level = logging.WARNING
|
|
1258
|
+
status = "with failures"
|
|
1259
|
+
else:
|
|
1260
|
+
log_level = logging.INFO
|
|
1261
|
+
status = "successfully"
|
|
1262
|
+
|
|
1263
|
+
# Format duration for readability
|
|
1264
|
+
if context.duration_seconds < 0.001:
|
|
1265
|
+
duration_str = f"{context.duration_seconds * 1000000:.0f}us"
|
|
1266
|
+
elif context.duration_seconds < 1.0:
|
|
1267
|
+
duration_str = f"{context.duration_seconds * 1000:.2f}ms"
|
|
1268
|
+
else:
|
|
1269
|
+
duration_str = f"{context.duration_seconds:.2f}s"
|
|
1270
|
+
|
|
1271
|
+
# Log the summary
|
|
1272
|
+
summary_msg = (
|
|
1273
|
+
f"Handler load complete {status}: "
|
|
1274
|
+
f"{len(context.handlers)} handlers loaded in {duration_str}"
|
|
1275
|
+
)
|
|
1276
|
+
if context.failed_plugins:
|
|
1277
|
+
summary_msg += f" ({len(context.failed_plugins)} failed)"
|
|
1278
|
+
|
|
1279
|
+
# Build detailed message
|
|
1280
|
+
detailed_msg = f"{summary_msg}\nLoaded handlers:\n{handler_list_str}"
|
|
1281
|
+
if failed_list_str:
|
|
1282
|
+
detailed_msg += f"\nFailed handlers:\n{failed_list_str}"
|
|
1283
|
+
|
|
1284
|
+
logger.log(
|
|
1285
|
+
log_level,
|
|
1286
|
+
detailed_msg,
|
|
1287
|
+
extra={
|
|
1288
|
+
"operation": context.operation,
|
|
1289
|
+
"source": context.source,
|
|
1290
|
+
"total_discovered": context.total_discovered,
|
|
1291
|
+
"total_loaded": len(context.handlers),
|
|
1292
|
+
"total_failed": len(context.failed_plugins),
|
|
1293
|
+
"duration_seconds": context.duration_seconds,
|
|
1294
|
+
"correlation_id": context.caller_correlation_string,
|
|
1295
|
+
"handler_names": [h.handler_name for h in context.handlers],
|
|
1296
|
+
"handler_classes": [h.handler_class for h in context.handlers],
|
|
1297
|
+
"failed_paths": [str(f.contract_path) for f in context.failed_plugins],
|
|
1298
|
+
},
|
|
1299
|
+
)
|
|
1300
|
+
|
|
1301
|
+
return summary
|
|
1302
|
+
|
|
1303
|
+
def _validate_handler_protocol(self, handler_class: type) -> tuple[bool, list[str]]:
|
|
1304
|
+
"""Validate handler implements required protocol (ProtocolHandler).
|
|
1305
|
+
|
|
1306
|
+
Uses duck typing to verify the handler class has the required
|
|
1307
|
+
methods for ProtocolHandler compliance. Per ONEX conventions, protocol
|
|
1308
|
+
compliance is verified via structural typing (duck typing) rather than
|
|
1309
|
+
isinstance checks or explicit inheritance.
|
|
1310
|
+
|
|
1311
|
+
Protocol Requirements (from omnibase_spi.protocols.handlers.protocol_handler):
|
|
1312
|
+
The ProtocolHandler protocol defines the following required members:
|
|
1313
|
+
|
|
1314
|
+
**Required Methods (validated)**:
|
|
1315
|
+
- ``handler_type`` (property): Returns handler type identifier string
|
|
1316
|
+
- ``initialize(config)``: Async method to initialize connections/pools
|
|
1317
|
+
- ``shutdown(timeout_seconds)``: Async method to release resources
|
|
1318
|
+
- ``execute(request, operation_config)``: Async method for operations
|
|
1319
|
+
- ``describe()``: Sync method returning handler metadata/capabilities
|
|
1320
|
+
|
|
1321
|
+
**Optional Methods (not validated)**:
|
|
1322
|
+
- ``health_check()``: Async method for connectivity verification.
|
|
1323
|
+
While part of the ProtocolHandler protocol, this method is not
|
|
1324
|
+
validated because existing handler implementations (HandlerHttp,
|
|
1325
|
+
HandlerDb, HandlerVault, HandlerConsul) do not implement it.
|
|
1326
|
+
Future handler implementations SHOULD include health_check().
|
|
1327
|
+
|
|
1328
|
+
Validation Approach:
|
|
1329
|
+
This method checks for the presence and callability of all 5 required
|
|
1330
|
+
methods. A handler class must have ALL of these methods to pass validation.
|
|
1331
|
+
This prevents false positives where a class might have only ``describe()``
|
|
1332
|
+
but lack other essential handler functionality.
|
|
1333
|
+
|
|
1334
|
+
The validation uses ``callable(getattr(...))`` for methods and
|
|
1335
|
+
``hasattr()`` for the ``handler_type`` property to accommodate both
|
|
1336
|
+
instance properties and class-level descriptors.
|
|
1337
|
+
|
|
1338
|
+
Why Duck Typing:
|
|
1339
|
+
ONEX uses duck typing for protocol validation to:
|
|
1340
|
+
1. Avoid tight coupling to specific base classes
|
|
1341
|
+
2. Enable flexibility in handler implementation strategies
|
|
1342
|
+
3. Support mixin-based handler composition
|
|
1343
|
+
4. Allow testing with mock handlers that satisfy the protocol
|
|
1344
|
+
|
|
1345
|
+
Args:
|
|
1346
|
+
handler_class: The handler class to validate. Must be a class type,
|
|
1347
|
+
not an instance.
|
|
1348
|
+
|
|
1349
|
+
Returns:
|
|
1350
|
+
A tuple of (is_valid, missing_methods) where:
|
|
1351
|
+
- is_valid: True if handler implements all required protocol methods
|
|
1352
|
+
- missing_methods: List of method names that are missing or not callable.
|
|
1353
|
+
Empty list if all methods are present.
|
|
1354
|
+
|
|
1355
|
+
Example:
|
|
1356
|
+
>>> class ValidHandler:
|
|
1357
|
+
... @property
|
|
1358
|
+
... def handler_type(self) -> str: return "test"
|
|
1359
|
+
... async def initialize(self, config): pass
|
|
1360
|
+
... async def shutdown(self, timeout_seconds=30.0): pass
|
|
1361
|
+
... async def execute(self, request, config): pass
|
|
1362
|
+
... def describe(self): return {}
|
|
1363
|
+
...
|
|
1364
|
+
>>> loader = HandlerPluginLoader()
|
|
1365
|
+
>>> loader._validate_handler_protocol(ValidHandler)
|
|
1366
|
+
(True, [])
|
|
1367
|
+
|
|
1368
|
+
>>> class IncompleteHandler:
|
|
1369
|
+
... def describe(self): return {}
|
|
1370
|
+
...
|
|
1371
|
+
>>> loader._validate_handler_protocol(IncompleteHandler)
|
|
1372
|
+
(False, ['handler_type', 'initialize', 'shutdown', 'execute'])
|
|
1373
|
+
|
|
1374
|
+
See Also:
|
|
1375
|
+
- ``omnibase_spi.protocols.handlers.protocol_handler.ProtocolHandler``
|
|
1376
|
+
- ``docs/architecture/RUNTIME_HOST_IMPLEMENTATION_PLAN.md``
|
|
1377
|
+
"""
|
|
1378
|
+
# Check for required ProtocolHandler methods via duck typing
|
|
1379
|
+
# All 5 core methods must be present for protocol compliance
|
|
1380
|
+
missing_methods: list[str] = []
|
|
1381
|
+
|
|
1382
|
+
# 1. handler_type property - can be property or method
|
|
1383
|
+
if not hasattr(handler_class, "handler_type"):
|
|
1384
|
+
missing_methods.append("handler_type")
|
|
1385
|
+
|
|
1386
|
+
# 2. initialize() - async method for connection setup
|
|
1387
|
+
if not callable(getattr(handler_class, "initialize", None)):
|
|
1388
|
+
missing_methods.append("initialize")
|
|
1389
|
+
|
|
1390
|
+
# 3. shutdown() - async method for resource cleanup
|
|
1391
|
+
if not callable(getattr(handler_class, "shutdown", None)):
|
|
1392
|
+
missing_methods.append("shutdown")
|
|
1393
|
+
|
|
1394
|
+
# 4. execute() - async method for operation execution
|
|
1395
|
+
if not callable(getattr(handler_class, "execute", None)):
|
|
1396
|
+
missing_methods.append("execute")
|
|
1397
|
+
|
|
1398
|
+
# 5. describe() - sync method for introspection
|
|
1399
|
+
if not callable(getattr(handler_class, "describe", None)):
|
|
1400
|
+
missing_methods.append("describe")
|
|
1401
|
+
|
|
1402
|
+
# Note: health_check() is part of ProtocolHandler but is NOT validated
|
|
1403
|
+
# because existing handlers (HandlerHttp, HandlerDb, etc.) do not
|
|
1404
|
+
# implement it. Future handlers SHOULD implement health_check().
|
|
1405
|
+
|
|
1406
|
+
return (len(missing_methods) == 0, missing_methods)
|
|
1407
|
+
|
|
1408
|
+
def _import_handler_class(
|
|
1409
|
+
self,
|
|
1410
|
+
class_path: str,
|
|
1411
|
+
contract_path: Path,
|
|
1412
|
+
correlation_id: UUID | None = None,
|
|
1413
|
+
) -> type:
|
|
1414
|
+
"""Dynamically import handler class from fully qualified path.
|
|
1415
|
+
|
|
1416
|
+
This method validates the namespace (if allowed_namespaces is configured)
|
|
1417
|
+
BEFORE calling ``importlib.import_module()``, preventing any module-level
|
|
1418
|
+
side effects from untrusted packages.
|
|
1419
|
+
|
|
1420
|
+
Args:
|
|
1421
|
+
class_path: Fully qualified class path (e.g., 'myapp.handlers.AuthHandler').
|
|
1422
|
+
contract_path: Path to the contract file (for error context).
|
|
1423
|
+
correlation_id: Optional correlation ID for tracing and error context.
|
|
1424
|
+
|
|
1425
|
+
Returns:
|
|
1426
|
+
The imported class type.
|
|
1427
|
+
|
|
1428
|
+
Raises:
|
|
1429
|
+
ProtocolConfigurationError: If namespace validation fails.
|
|
1430
|
+
- HANDLER_LOADER_013 (NAMESPACE_NOT_ALLOWED): When the class path
|
|
1431
|
+
does not start with any of the allowed namespace prefixes.
|
|
1432
|
+
InfraConnectionError: If the module or class cannot be imported.
|
|
1433
|
+
Error codes include correlation_id when provided for traceability.
|
|
1434
|
+
- HANDLER_LOADER_010 (MODULE_NOT_FOUND): Handler module not found
|
|
1435
|
+
- HANDLER_LOADER_011 (CLASS_NOT_FOUND): Handler class not found in module
|
|
1436
|
+
- HANDLER_LOADER_012 (IMPORT_ERROR): Import error (syntax/dependency)
|
|
1437
|
+
"""
|
|
1438
|
+
# Split class path into module and class name
|
|
1439
|
+
if "." not in class_path:
|
|
1440
|
+
context = ModelInfraErrorContext(
|
|
1441
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1442
|
+
operation="import_handler_class",
|
|
1443
|
+
correlation_id=correlation_id,
|
|
1444
|
+
)
|
|
1445
|
+
raise InfraConnectionError(
|
|
1446
|
+
f"Invalid class path '{class_path}': must be fully qualified "
|
|
1447
|
+
"(e.g., 'myapp.handlers.AuthHandler')",
|
|
1448
|
+
context=context,
|
|
1449
|
+
loader_error=EnumHandlerLoaderError.MODULE_NOT_FOUND.value,
|
|
1450
|
+
class_path=class_path,
|
|
1451
|
+
contract_path=str(contract_path),
|
|
1452
|
+
)
|
|
1453
|
+
|
|
1454
|
+
module_path, class_name = class_path.rsplit(".", 1)
|
|
1455
|
+
|
|
1456
|
+
# Validate namespace BEFORE importing (defense-in-depth)
|
|
1457
|
+
# This prevents any module-level side effects from untrusted packages
|
|
1458
|
+
self._validate_namespace(class_path, contract_path, correlation_id)
|
|
1459
|
+
|
|
1460
|
+
# Import the module
|
|
1461
|
+
try:
|
|
1462
|
+
module = importlib.import_module(module_path)
|
|
1463
|
+
except ModuleNotFoundError as e:
|
|
1464
|
+
context = ModelInfraErrorContext(
|
|
1465
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1466
|
+
operation="import_handler_class",
|
|
1467
|
+
correlation_id=correlation_id,
|
|
1468
|
+
)
|
|
1469
|
+
raise InfraConnectionError(
|
|
1470
|
+
f"Module not found: {module_path}",
|
|
1471
|
+
context=context,
|
|
1472
|
+
loader_error=EnumHandlerLoaderError.MODULE_NOT_FOUND.value,
|
|
1473
|
+
module_path=module_path,
|
|
1474
|
+
class_path=class_path,
|
|
1475
|
+
contract_path=str(contract_path),
|
|
1476
|
+
) from e
|
|
1477
|
+
except ImportError as e:
|
|
1478
|
+
context = ModelInfraErrorContext(
|
|
1479
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1480
|
+
operation="import_handler_class",
|
|
1481
|
+
correlation_id=correlation_id,
|
|
1482
|
+
)
|
|
1483
|
+
# Sanitize exception message to prevent path disclosure
|
|
1484
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
1485
|
+
raise InfraConnectionError(
|
|
1486
|
+
f"Import error loading module {module_path}: {sanitized_msg}",
|
|
1487
|
+
context=context,
|
|
1488
|
+
loader_error=EnumHandlerLoaderError.IMPORT_ERROR.value,
|
|
1489
|
+
module_path=module_path,
|
|
1490
|
+
class_path=class_path,
|
|
1491
|
+
contract_path=str(contract_path),
|
|
1492
|
+
) from e
|
|
1493
|
+
except SyntaxError as e:
|
|
1494
|
+
# SyntaxError can occur during import if the handler module has syntax errors.
|
|
1495
|
+
# This is a subclass of Exception, not ImportError, so must be caught separately.
|
|
1496
|
+
context = ModelInfraErrorContext(
|
|
1497
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1498
|
+
operation="import_handler_class",
|
|
1499
|
+
correlation_id=correlation_id,
|
|
1500
|
+
)
|
|
1501
|
+
# Sanitize exception message to prevent path disclosure
|
|
1502
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
1503
|
+
raise InfraConnectionError(
|
|
1504
|
+
f"Syntax error in module {module_path}: {sanitized_msg}",
|
|
1505
|
+
context=context,
|
|
1506
|
+
loader_error=EnumHandlerLoaderError.IMPORT_ERROR.value,
|
|
1507
|
+
module_path=module_path,
|
|
1508
|
+
class_path=class_path,
|
|
1509
|
+
contract_path=str(contract_path),
|
|
1510
|
+
) from e
|
|
1511
|
+
|
|
1512
|
+
# Get the class from the module
|
|
1513
|
+
if not hasattr(module, class_name):
|
|
1514
|
+
context = ModelInfraErrorContext(
|
|
1515
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1516
|
+
operation="import_handler_class",
|
|
1517
|
+
correlation_id=correlation_id,
|
|
1518
|
+
)
|
|
1519
|
+
raise InfraConnectionError(
|
|
1520
|
+
f"Class '{class_name}' not found in module '{module_path}'",
|
|
1521
|
+
context=context,
|
|
1522
|
+
loader_error=EnumHandlerLoaderError.CLASS_NOT_FOUND.value,
|
|
1523
|
+
module_path=module_path,
|
|
1524
|
+
class_name=class_name,
|
|
1525
|
+
class_path=class_path,
|
|
1526
|
+
contract_path=str(contract_path),
|
|
1527
|
+
)
|
|
1528
|
+
|
|
1529
|
+
handler_class = getattr(module, class_name)
|
|
1530
|
+
|
|
1531
|
+
# Verify it's actually a class
|
|
1532
|
+
if not isinstance(handler_class, type):
|
|
1533
|
+
context = ModelInfraErrorContext(
|
|
1534
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1535
|
+
operation="import_handler_class",
|
|
1536
|
+
correlation_id=correlation_id,
|
|
1537
|
+
)
|
|
1538
|
+
raise InfraConnectionError(
|
|
1539
|
+
f"'{class_path}' is not a class",
|
|
1540
|
+
context=context,
|
|
1541
|
+
loader_error=EnumHandlerLoaderError.CLASS_NOT_FOUND.value,
|
|
1542
|
+
class_path=class_path,
|
|
1543
|
+
contract_path=str(contract_path),
|
|
1544
|
+
)
|
|
1545
|
+
|
|
1546
|
+
return handler_class
|
|
1547
|
+
|
|
1548
|
+
def _validate_namespace(
|
|
1549
|
+
self,
|
|
1550
|
+
class_path: str,
|
|
1551
|
+
contract_path: Path,
|
|
1552
|
+
correlation_id: UUID | None = None,
|
|
1553
|
+
) -> None:
|
|
1554
|
+
"""Validate handler class path against allowed namespaces.
|
|
1555
|
+
|
|
1556
|
+
Checks whether the handler's fully-qualified class path starts with one
|
|
1557
|
+
of the allowed namespace prefixes. This validation occurs BEFORE the
|
|
1558
|
+
module is imported, preventing any module-level side effects from
|
|
1559
|
+
untrusted packages.
|
|
1560
|
+
|
|
1561
|
+
Args:
|
|
1562
|
+
class_path: Fully qualified class path (e.g., 'myapp.handlers.AuthHandler').
|
|
1563
|
+
contract_path: Path to the contract file (for error context).
|
|
1564
|
+
correlation_id: Optional correlation ID for tracing and error context.
|
|
1565
|
+
|
|
1566
|
+
Raises:
|
|
1567
|
+
ProtocolConfigurationError: If namespace validation fails.
|
|
1568
|
+
- HANDLER_LOADER_013 (NAMESPACE_NOT_ALLOWED): When the class path
|
|
1569
|
+
does not start with any of the allowed namespace prefixes.
|
|
1570
|
+
|
|
1571
|
+
Note:
|
|
1572
|
+
This method is a no-op when ``allowed_namespaces`` is None, allowing
|
|
1573
|
+
any namespace. When ``allowed_namespaces`` is an empty list, ALL
|
|
1574
|
+
namespaces are blocked.
|
|
1575
|
+
|
|
1576
|
+
Example:
|
|
1577
|
+
>>> loader = HandlerPluginLoader(
|
|
1578
|
+
... allowed_namespaces=["omnibase_infra.", "mycompany."]
|
|
1579
|
+
... )
|
|
1580
|
+
>>> # This passes validation:
|
|
1581
|
+
>>> loader._validate_namespace(
|
|
1582
|
+
... "omnibase_infra.handlers.HandlerAuth",
|
|
1583
|
+
... Path("contract.yaml"),
|
|
1584
|
+
... )
|
|
1585
|
+
>>> # This raises ProtocolConfigurationError:
|
|
1586
|
+
>>> loader._validate_namespace(
|
|
1587
|
+
... "malicious_pkg.EvilHandler",
|
|
1588
|
+
... Path("malicious.yaml"),
|
|
1589
|
+
... )
|
|
1590
|
+
"""
|
|
1591
|
+
# If no namespace restriction is configured, allow all
|
|
1592
|
+
if self._allowed_namespaces is None:
|
|
1593
|
+
return
|
|
1594
|
+
|
|
1595
|
+
# Check if class_path starts with any allowed namespace with proper
|
|
1596
|
+
# package boundary validation. This prevents "foo" from matching "foobar.module".
|
|
1597
|
+
for namespace in self._allowed_namespaces:
|
|
1598
|
+
if class_path.startswith(namespace):
|
|
1599
|
+
# If namespace ends with ".", we've already matched a package boundary
|
|
1600
|
+
if namespace.endswith("."):
|
|
1601
|
+
return
|
|
1602
|
+
# Otherwise, ensure we're at a package boundary (next char is ".")
|
|
1603
|
+
# This prevents "foo" from matching "foobar.module" - only exact
|
|
1604
|
+
# matches or matches followed by "." are valid.
|
|
1605
|
+
remaining = class_path[len(namespace) :]
|
|
1606
|
+
if remaining == "" or remaining.startswith("."):
|
|
1607
|
+
return
|
|
1608
|
+
|
|
1609
|
+
# Namespace not in allowed list - raise error
|
|
1610
|
+
context = ModelInfraErrorContext(
|
|
1611
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1612
|
+
operation="validate_namespace",
|
|
1613
|
+
correlation_id=correlation_id,
|
|
1614
|
+
)
|
|
1615
|
+
|
|
1616
|
+
# Format allowed namespaces for error message
|
|
1617
|
+
if self._allowed_namespaces:
|
|
1618
|
+
allowed_str = ", ".join(repr(ns) for ns in self._allowed_namespaces)
|
|
1619
|
+
else:
|
|
1620
|
+
allowed_str = "(none - empty allowlist)"
|
|
1621
|
+
|
|
1622
|
+
raise ProtocolConfigurationError(
|
|
1623
|
+
f"Handler namespace not allowed: '{class_path}' does not start with "
|
|
1624
|
+
f"any of the allowed namespaces: {allowed_str}",
|
|
1625
|
+
context=context,
|
|
1626
|
+
loader_error=EnumHandlerLoaderError.NAMESPACE_NOT_ALLOWED.value,
|
|
1627
|
+
class_path=class_path,
|
|
1628
|
+
contract_path=str(contract_path),
|
|
1629
|
+
allowed_namespaces=list(self._allowed_namespaces),
|
|
1630
|
+
)
|
|
1631
|
+
|
|
1632
|
+
def _validate_yaml_syntax(
|
|
1633
|
+
self,
|
|
1634
|
+
path: Path,
|
|
1635
|
+
correlation_id: UUID | None = None,
|
|
1636
|
+
raise_on_error: bool = True,
|
|
1637
|
+
) -> bool:
|
|
1638
|
+
"""Validate YAML syntax of a contract file for early fail-fast behavior.
|
|
1639
|
+
|
|
1640
|
+
Performs early YAML syntax validation to fail fast before expensive
|
|
1641
|
+
operations like path resolution and handler class loading. This method
|
|
1642
|
+
only validates that the file contains valid YAML syntax; it does not
|
|
1643
|
+
perform schema validation.
|
|
1644
|
+
|
|
1645
|
+
This enables the discover_and_load method to skip malformed YAML files
|
|
1646
|
+
immediately after discovery, rather than deferring the error to
|
|
1647
|
+
load_from_contract which would be less efficient for large discovery
|
|
1648
|
+
operations.
|
|
1649
|
+
|
|
1650
|
+
Args:
|
|
1651
|
+
path: Path to the YAML file to validate. Must be an existing file.
|
|
1652
|
+
correlation_id: Optional correlation ID for error context.
|
|
1653
|
+
raise_on_error: If True (default), raises ProtocolConfigurationError
|
|
1654
|
+
on YAML syntax errors. If False, logs a warning and returns False,
|
|
1655
|
+
allowing the caller to skip the file.
|
|
1656
|
+
|
|
1657
|
+
Returns:
|
|
1658
|
+
True if YAML syntax is valid.
|
|
1659
|
+
False if raise_on_error is False and YAML syntax is invalid.
|
|
1660
|
+
|
|
1661
|
+
Raises:
|
|
1662
|
+
ProtocolConfigurationError: If raise_on_error is True and:
|
|
1663
|
+
- INVALID_YAML_SYNTAX: File contains invalid YAML syntax
|
|
1664
|
+
- FILE_READ_ERROR: Failed to read file (I/O error)
|
|
1665
|
+
|
|
1666
|
+
Note:
|
|
1667
|
+
The error message includes the YAML parser error details which
|
|
1668
|
+
typically contain line and column information for the syntax error.
|
|
1669
|
+
"""
|
|
1670
|
+
try:
|
|
1671
|
+
with path.open("r", encoding="utf-8") as f:
|
|
1672
|
+
yaml.safe_load(f)
|
|
1673
|
+
except yaml.YAMLError as e:
|
|
1674
|
+
# Sanitize exception message to prevent path disclosure
|
|
1675
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
1676
|
+
if raise_on_error:
|
|
1677
|
+
context = ModelInfraErrorContext(
|
|
1678
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1679
|
+
operation="validate_yaml_syntax",
|
|
1680
|
+
correlation_id=correlation_id,
|
|
1681
|
+
)
|
|
1682
|
+
raise ProtocolConfigurationError(
|
|
1683
|
+
f"Invalid YAML syntax in contract file '{path.name}': {sanitized_msg}",
|
|
1684
|
+
context=context,
|
|
1685
|
+
loader_error=EnumHandlerLoaderError.INVALID_YAML_SYNTAX.value,
|
|
1686
|
+
contract_path=str(path),
|
|
1687
|
+
) from e
|
|
1688
|
+
logger.warning(
|
|
1689
|
+
"Skipping contract file with invalid YAML syntax %s: %s",
|
|
1690
|
+
path.name,
|
|
1691
|
+
sanitized_msg,
|
|
1692
|
+
extra={
|
|
1693
|
+
"path": str(path),
|
|
1694
|
+
"error": sanitized_msg,
|
|
1695
|
+
"correlation_id": str(correlation_id) if correlation_id else None,
|
|
1696
|
+
},
|
|
1697
|
+
)
|
|
1698
|
+
return False
|
|
1699
|
+
except OSError as e:
|
|
1700
|
+
# Sanitize exception message to prevent path disclosure
|
|
1701
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
1702
|
+
if raise_on_error:
|
|
1703
|
+
context = ModelInfraErrorContext(
|
|
1704
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1705
|
+
operation="validate_yaml_syntax",
|
|
1706
|
+
correlation_id=correlation_id,
|
|
1707
|
+
)
|
|
1708
|
+
raise ProtocolConfigurationError(
|
|
1709
|
+
f"Failed to read contract file '{path.name}': {sanitized_msg}",
|
|
1710
|
+
context=context,
|
|
1711
|
+
loader_error=EnumHandlerLoaderError.FILE_READ_ERROR.value,
|
|
1712
|
+
contract_path=str(path),
|
|
1713
|
+
) from e
|
|
1714
|
+
logger.warning(
|
|
1715
|
+
"Failed to read contract file %s: %s",
|
|
1716
|
+
path.name,
|
|
1717
|
+
sanitized_msg,
|
|
1718
|
+
extra={
|
|
1719
|
+
"path": str(path),
|
|
1720
|
+
"error": sanitized_msg,
|
|
1721
|
+
"correlation_id": str(correlation_id) if correlation_id else None,
|
|
1722
|
+
},
|
|
1723
|
+
)
|
|
1724
|
+
return False
|
|
1725
|
+
|
|
1726
|
+
return True
|
|
1727
|
+
|
|
1728
|
+
def _validate_file_size(
|
|
1729
|
+
self,
|
|
1730
|
+
path: Path,
|
|
1731
|
+
correlation_id: UUID | None = None,
|
|
1732
|
+
operation: str = "load_from_contract",
|
|
1733
|
+
raise_on_error: bool = True,
|
|
1734
|
+
) -> int | None:
|
|
1735
|
+
"""Validate file size is within limits.
|
|
1736
|
+
|
|
1737
|
+
Checks that the file at the given path can be stat'd and does not
|
|
1738
|
+
exceed MAX_CONTRACT_SIZE. Supports both strict mode (raising exceptions)
|
|
1739
|
+
and graceful mode (logging warnings and returning None).
|
|
1740
|
+
|
|
1741
|
+
Args:
|
|
1742
|
+
path: Path to the file to validate. Must be an existing file.
|
|
1743
|
+
correlation_id: Optional correlation ID for error context.
|
|
1744
|
+
operation: The operation name for error context in exceptions.
|
|
1745
|
+
raise_on_error: If True (default), raises ProtocolConfigurationError
|
|
1746
|
+
on stat failure or size exceeded. If False, logs a warning
|
|
1747
|
+
and returns None, allowing the caller to skip the file.
|
|
1748
|
+
|
|
1749
|
+
Returns:
|
|
1750
|
+
File size in bytes if validation passes.
|
|
1751
|
+
None if raise_on_error is False and validation fails (stat error
|
|
1752
|
+
or size exceeded).
|
|
1753
|
+
|
|
1754
|
+
Raises:
|
|
1755
|
+
ProtocolConfigurationError: If raise_on_error is True and:
|
|
1756
|
+
- FILE_STAT_ERROR: Failed to stat the file (I/O error)
|
|
1757
|
+
- FILE_SIZE_EXCEEDED: File exceeds MAX_CONTRACT_SIZE
|
|
1758
|
+
"""
|
|
1759
|
+
# Attempt to get file size
|
|
1760
|
+
try:
|
|
1761
|
+
file_size = path.stat().st_size
|
|
1762
|
+
except OSError as e:
|
|
1763
|
+
# Sanitize exception message to prevent path disclosure
|
|
1764
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
1765
|
+
if raise_on_error:
|
|
1766
|
+
context = ModelInfraErrorContext(
|
|
1767
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1768
|
+
operation=operation,
|
|
1769
|
+
correlation_id=correlation_id,
|
|
1770
|
+
)
|
|
1771
|
+
raise ProtocolConfigurationError(
|
|
1772
|
+
f"Failed to stat contract file: {sanitized_msg}",
|
|
1773
|
+
context=context,
|
|
1774
|
+
loader_error=EnumHandlerLoaderError.FILE_STAT_ERROR.value,
|
|
1775
|
+
contract_path=str(path),
|
|
1776
|
+
) from e
|
|
1777
|
+
logger.warning(
|
|
1778
|
+
"Failed to stat contract file %s: %s",
|
|
1779
|
+
path.name,
|
|
1780
|
+
sanitized_msg,
|
|
1781
|
+
extra={
|
|
1782
|
+
"path": str(path),
|
|
1783
|
+
"error": sanitized_msg,
|
|
1784
|
+
"correlation_id": str(correlation_id) if correlation_id else None,
|
|
1785
|
+
},
|
|
1786
|
+
)
|
|
1787
|
+
return None
|
|
1788
|
+
|
|
1789
|
+
# Check size limit
|
|
1790
|
+
if file_size > MAX_CONTRACT_SIZE:
|
|
1791
|
+
if raise_on_error:
|
|
1792
|
+
context = ModelInfraErrorContext(
|
|
1793
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1794
|
+
operation=operation,
|
|
1795
|
+
correlation_id=correlation_id,
|
|
1796
|
+
)
|
|
1797
|
+
raise ProtocolConfigurationError(
|
|
1798
|
+
f"Contract file exceeds size limit: {file_size} bytes "
|
|
1799
|
+
f"(max: {MAX_CONTRACT_SIZE} bytes)",
|
|
1800
|
+
context=context,
|
|
1801
|
+
loader_error=EnumHandlerLoaderError.FILE_SIZE_EXCEEDED.value,
|
|
1802
|
+
contract_path=str(path),
|
|
1803
|
+
file_size=file_size,
|
|
1804
|
+
max_size=MAX_CONTRACT_SIZE,
|
|
1805
|
+
)
|
|
1806
|
+
logger.warning(
|
|
1807
|
+
"Skipping oversized contract file %s: %d bytes exceeds limit of %d bytes",
|
|
1808
|
+
path.name,
|
|
1809
|
+
file_size,
|
|
1810
|
+
MAX_CONTRACT_SIZE,
|
|
1811
|
+
extra={
|
|
1812
|
+
"path": str(path),
|
|
1813
|
+
"file_size": file_size,
|
|
1814
|
+
"max_size": MAX_CONTRACT_SIZE,
|
|
1815
|
+
"correlation_id": str(correlation_id) if correlation_id else None,
|
|
1816
|
+
},
|
|
1817
|
+
)
|
|
1818
|
+
return None
|
|
1819
|
+
|
|
1820
|
+
return file_size
|
|
1821
|
+
|
|
1822
|
+
def _find_contract_files(
|
|
1823
|
+
self,
|
|
1824
|
+
directory: Path,
|
|
1825
|
+
correlation_id: UUID | None = None,
|
|
1826
|
+
max_handlers: int | None = None,
|
|
1827
|
+
) -> list[Path]:
|
|
1828
|
+
"""Find all handler contract files under a directory.
|
|
1829
|
+
|
|
1830
|
+
Searches for both handler_contract.yaml and contract.yaml files.
|
|
1831
|
+
Files exceeding MAX_CONTRACT_SIZE are skipped during discovery
|
|
1832
|
+
to fail fast before expensive path resolution and loading.
|
|
1833
|
+
|
|
1834
|
+
Ambiguous Contract Detection (Fail-Fast):
|
|
1835
|
+
When BOTH ``handler_contract.yaml`` AND ``contract.yaml`` exist in the
|
|
1836
|
+
same directory, this method raises ``ProtocolConfigurationError`` with
|
|
1837
|
+
error code ``AMBIGUOUS_CONTRACT_CONFIGURATION``. This fail-fast behavior
|
|
1838
|
+
prevents:
|
|
1839
|
+
|
|
1840
|
+
- Duplicate handler registrations
|
|
1841
|
+
- Confusion about which contract is authoritative
|
|
1842
|
+
- Unexpected runtime behavior from conflicting configurations
|
|
1843
|
+
|
|
1844
|
+
Best practice: Use only ONE contract file per handler directory.
|
|
1845
|
+
|
|
1846
|
+
See: docs/patterns/handler_plugin_loader.md#contract-file-precedence
|
|
1847
|
+
|
|
1848
|
+
Args:
|
|
1849
|
+
directory: Directory to search recursively.
|
|
1850
|
+
correlation_id: Optional correlation ID for tracing and error context.
|
|
1851
|
+
max_handlers: Optional maximum number of handlers to discover.
|
|
1852
|
+
If specified, discovery stops after finding this many contract files.
|
|
1853
|
+
Propagated to file size validation for consistent traceability.
|
|
1854
|
+
|
|
1855
|
+
Returns:
|
|
1856
|
+
List of paths to contract files that pass size validation.
|
|
1857
|
+
|
|
1858
|
+
Raises:
|
|
1859
|
+
ProtocolConfigurationError: If both handler_contract.yaml and contract.yaml
|
|
1860
|
+
exist in the same directory. Error code: AMBIGUOUS_CONTRACT_CONFIGURATION
|
|
1861
|
+
(HANDLER_LOADER_040).
|
|
1862
|
+
"""
|
|
1863
|
+
contract_files: list[Path] = []
|
|
1864
|
+
# Track if max_handlers limit was reached
|
|
1865
|
+
limit_reached = False
|
|
1866
|
+
|
|
1867
|
+
# Search for valid contract filenames in a single scan
|
|
1868
|
+
# This consolidates two rglob() calls into one for better performance
|
|
1869
|
+
# NOTE: If both handler_contract.yaml and contract.yaml are found in the
|
|
1870
|
+
# same directory, we fail fast with AMBIGUOUS_CONTRACT_CONFIGURATION error
|
|
1871
|
+
# after discovery (see ambiguity check below).
|
|
1872
|
+
valid_filenames = {HANDLER_CONTRACT_FILENAME, CONTRACT_YAML_FILENAME}
|
|
1873
|
+
|
|
1874
|
+
# directory.rglob() can raise OSError for:
|
|
1875
|
+
# - Permission denied when accessing the directory or subdirectories
|
|
1876
|
+
# - Filesystem errors (unmounted volumes, network failures)
|
|
1877
|
+
# - Directory deleted or becomes inaccessible during iteration
|
|
1878
|
+
try:
|
|
1879
|
+
rglob_iterator = directory.rglob("*.yaml")
|
|
1880
|
+
except OSError as e:
|
|
1881
|
+
# If we can't even start iterating, log warning and return empty list
|
|
1882
|
+
# This is graceful degradation - the caller can handle empty results
|
|
1883
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
1884
|
+
logger.warning(
|
|
1885
|
+
"Failed to scan directory %s for contracts: %s",
|
|
1886
|
+
directory.name,
|
|
1887
|
+
sanitized_msg,
|
|
1888
|
+
extra={
|
|
1889
|
+
"directory": str(directory),
|
|
1890
|
+
"error": sanitized_msg,
|
|
1891
|
+
"error_code": EnumHandlerLoaderError.PERMISSION_DENIED.value,
|
|
1892
|
+
"correlation_id": str(correlation_id) if correlation_id else None,
|
|
1893
|
+
},
|
|
1894
|
+
)
|
|
1895
|
+
return []
|
|
1896
|
+
|
|
1897
|
+
# Iterate over discovered paths, handling per-path errors gracefully
|
|
1898
|
+
try:
|
|
1899
|
+
for path in rglob_iterator:
|
|
1900
|
+
# Check if we've reached the max_handlers limit
|
|
1901
|
+
if max_handlers is not None and len(contract_files) >= max_handlers:
|
|
1902
|
+
limit_reached = True
|
|
1903
|
+
break
|
|
1904
|
+
|
|
1905
|
+
# Filter by filename first (cheap string comparison)
|
|
1906
|
+
if path.name not in valid_filenames:
|
|
1907
|
+
continue
|
|
1908
|
+
|
|
1909
|
+
# path.is_file() can raise OSError for:
|
|
1910
|
+
# - Permission denied when stat'ing the file
|
|
1911
|
+
# - File deleted between rglob discovery and is_file() check
|
|
1912
|
+
# - Filesystem errors (unmounted volumes, network failures)
|
|
1913
|
+
try:
|
|
1914
|
+
is_file = path.is_file()
|
|
1915
|
+
except OSError as e:
|
|
1916
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
1917
|
+
logger.warning(
|
|
1918
|
+
"Failed to check if path is file %s: %s",
|
|
1919
|
+
path.name,
|
|
1920
|
+
sanitized_msg,
|
|
1921
|
+
extra={
|
|
1922
|
+
"path": str(path),
|
|
1923
|
+
"error": sanitized_msg,
|
|
1924
|
+
"error_code": EnumHandlerLoaderError.FILE_STAT_ERROR.value,
|
|
1925
|
+
"correlation_id": str(correlation_id)
|
|
1926
|
+
if correlation_id
|
|
1927
|
+
else None,
|
|
1928
|
+
},
|
|
1929
|
+
)
|
|
1930
|
+
continue
|
|
1931
|
+
|
|
1932
|
+
if not is_file:
|
|
1933
|
+
continue
|
|
1934
|
+
|
|
1935
|
+
# Early size validation to skip oversized files before expensive operations
|
|
1936
|
+
if (
|
|
1937
|
+
self._validate_file_size(
|
|
1938
|
+
path, correlation_id=correlation_id, raise_on_error=False
|
|
1939
|
+
)
|
|
1940
|
+
is None
|
|
1941
|
+
):
|
|
1942
|
+
continue
|
|
1943
|
+
|
|
1944
|
+
contract_files.append(path)
|
|
1945
|
+
except OSError as e:
|
|
1946
|
+
# Handle errors that occur during iteration (e.g., directory becomes
|
|
1947
|
+
# inaccessible mid-scan). Return what we've collected so far.
|
|
1948
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
1949
|
+
logger.warning(
|
|
1950
|
+
"Error during directory scan of %s: %s (returning %d files found so far)",
|
|
1951
|
+
directory.name,
|
|
1952
|
+
sanitized_msg,
|
|
1953
|
+
len(contract_files),
|
|
1954
|
+
extra={
|
|
1955
|
+
"directory": str(directory),
|
|
1956
|
+
"error": sanitized_msg,
|
|
1957
|
+
"error_code": EnumHandlerLoaderError.PERMISSION_DENIED.value,
|
|
1958
|
+
"files_found": len(contract_files),
|
|
1959
|
+
"correlation_id": str(correlation_id) if correlation_id else None,
|
|
1960
|
+
},
|
|
1961
|
+
)
|
|
1962
|
+
|
|
1963
|
+
# Log warning if limit was reached
|
|
1964
|
+
if limit_reached:
|
|
1965
|
+
logger.warning(
|
|
1966
|
+
"Handler discovery limit reached: stopped at %d handlers. "
|
|
1967
|
+
"Increase max_handlers to discover more.",
|
|
1968
|
+
max_handlers,
|
|
1969
|
+
extra={
|
|
1970
|
+
"max_handlers": max_handlers,
|
|
1971
|
+
"directory": str(directory),
|
|
1972
|
+
"correlation_id": str(correlation_id) if correlation_id else None,
|
|
1973
|
+
},
|
|
1974
|
+
)
|
|
1975
|
+
|
|
1976
|
+
# Detect directories with both contract types and fail fast on ambiguity
|
|
1977
|
+
# This is an O(n) check after discovery, not during, to avoid overhead
|
|
1978
|
+
# on every file. Build a map of parent_dir -> set of contract filenames.
|
|
1979
|
+
dir_to_contract_types: dict[Path, set[str]] = {}
|
|
1980
|
+
for path in contract_files:
|
|
1981
|
+
parent = path.parent
|
|
1982
|
+
if parent not in dir_to_contract_types:
|
|
1983
|
+
dir_to_contract_types[parent] = set()
|
|
1984
|
+
dir_to_contract_types[parent].add(path.name)
|
|
1985
|
+
|
|
1986
|
+
# Fail fast if any directory has both contract types (ambiguous configuration)
|
|
1987
|
+
for parent_dir, filenames in dir_to_contract_types.items():
|
|
1988
|
+
if len(filenames) > 1:
|
|
1989
|
+
# Use with_correlation() to ensure correlation_id is always present
|
|
1990
|
+
context = ModelInfraErrorContext.with_correlation(
|
|
1991
|
+
correlation_id=correlation_id,
|
|
1992
|
+
transport_type=EnumInfraTransportType.RUNTIME,
|
|
1993
|
+
operation="find_contract_files",
|
|
1994
|
+
)
|
|
1995
|
+
raise ProtocolConfigurationError(
|
|
1996
|
+
f"Ambiguous contract configuration in '{parent_dir.name}': "
|
|
1997
|
+
f"Found both '{HANDLER_CONTRACT_FILENAME}' and '{CONTRACT_YAML_FILENAME}'. "
|
|
1998
|
+
f"Use only ONE contract file per handler directory to avoid conflicts. "
|
|
1999
|
+
f"Total contracts discovered so far: {len(contract_files)}.",
|
|
2000
|
+
context=context,
|
|
2001
|
+
loader_error=EnumHandlerLoaderError.AMBIGUOUS_CONTRACT_CONFIGURATION.value,
|
|
2002
|
+
directory=str(parent_dir),
|
|
2003
|
+
contract_files=sorted(filenames),
|
|
2004
|
+
total_discovered=len(contract_files),
|
|
2005
|
+
)
|
|
2006
|
+
|
|
2007
|
+
# Deduplicate by resolved path
|
|
2008
|
+
seen: set[Path] = set()
|
|
2009
|
+
deduplicated: list[Path] = []
|
|
2010
|
+
for path in contract_files:
|
|
2011
|
+
# path.resolve() can raise OSError in several scenarios:
|
|
2012
|
+
# - Broken symlinks: symlink target no longer exists
|
|
2013
|
+
# - Race conditions: file deleted between glob discovery and resolution
|
|
2014
|
+
# - Permission issues: lacking read permission on parent directories
|
|
2015
|
+
# - Filesystem errors: unmounted volumes, network filesystem failures
|
|
2016
|
+
try:
|
|
2017
|
+
resolved = path.resolve()
|
|
2018
|
+
except OSError as e:
|
|
2019
|
+
# Sanitize exception message to prevent path disclosure
|
|
2020
|
+
sanitized_msg = _sanitize_exception_message(e)
|
|
2021
|
+
logger.warning(
|
|
2022
|
+
"Failed to resolve path %s: %s",
|
|
2023
|
+
path.name,
|
|
2024
|
+
sanitized_msg,
|
|
2025
|
+
extra={
|
|
2026
|
+
"path": str(path),
|
|
2027
|
+
"error": sanitized_msg,
|
|
2028
|
+
"correlation_id": str(correlation_id)
|
|
2029
|
+
if correlation_id
|
|
2030
|
+
else None,
|
|
2031
|
+
},
|
|
2032
|
+
)
|
|
2033
|
+
continue
|
|
2034
|
+
if resolved not in seen:
|
|
2035
|
+
seen.add(resolved)
|
|
2036
|
+
deduplicated.append(path)
|
|
2037
|
+
|
|
2038
|
+
return deduplicated
|
|
2039
|
+
|
|
2040
|
+
|
|
2041
|
+
__all__ = [
|
|
2042
|
+
"CONTRACT_YAML_FILENAME",
|
|
2043
|
+
"HANDLER_CONTRACT_FILENAME",
|
|
2044
|
+
"HandlerPluginLoader",
|
|
2045
|
+
"MAX_CONTRACT_SIZE",
|
|
2046
|
+
]
|