omnibase_infra 0.2.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- omnibase_infra/__init__.py +101 -0
- omnibase_infra/cli/__init__.py +1 -0
- omnibase_infra/cli/commands.py +216 -0
- omnibase_infra/clients/__init__.py +0 -0
- omnibase_infra/contracts/handlers/filesystem/handler_contract.yaml +261 -0
- omnibase_infra/contracts/handlers/mcp/handler_contract.yaml +138 -0
- omnibase_infra/decorators/__init__.py +29 -0
- omnibase_infra/decorators/allow_any.py +109 -0
- omnibase_infra/dlq/__init__.py +90 -0
- omnibase_infra/dlq/constants_dlq.py +57 -0
- omnibase_infra/dlq/models/__init__.py +26 -0
- omnibase_infra/dlq/models/enum_replay_status.py +37 -0
- omnibase_infra/dlq/models/model_dlq_replay_record.py +135 -0
- omnibase_infra/dlq/models/model_dlq_tracking_config.py +184 -0
- omnibase_infra/dlq/service_dlq_tracking.py +611 -0
- omnibase_infra/enums/__init__.py +123 -0
- omnibase_infra/enums/enum_any_type_violation.py +104 -0
- omnibase_infra/enums/enum_backend_type.py +27 -0
- omnibase_infra/enums/enum_capture_outcome.py +42 -0
- omnibase_infra/enums/enum_capture_state.py +88 -0
- omnibase_infra/enums/enum_chain_violation_type.py +119 -0
- omnibase_infra/enums/enum_circuit_state.py +51 -0
- omnibase_infra/enums/enum_confirmation_event_type.py +27 -0
- omnibase_infra/enums/enum_contract_type.py +84 -0
- omnibase_infra/enums/enum_dedupe_strategy.py +46 -0
- omnibase_infra/enums/enum_dispatch_status.py +191 -0
- omnibase_infra/enums/enum_environment.py +46 -0
- omnibase_infra/enums/enum_execution_shape_violation.py +103 -0
- omnibase_infra/enums/enum_handler_error_type.py +101 -0
- omnibase_infra/enums/enum_handler_loader_error.py +178 -0
- omnibase_infra/enums/enum_handler_source_type.py +87 -0
- omnibase_infra/enums/enum_handler_type.py +77 -0
- omnibase_infra/enums/enum_handler_type_category.py +61 -0
- omnibase_infra/enums/enum_infra_transport_type.py +73 -0
- omnibase_infra/enums/enum_introspection_reason.py +154 -0
- omnibase_infra/enums/enum_message_category.py +213 -0
- omnibase_infra/enums/enum_node_archetype.py +74 -0
- omnibase_infra/enums/enum_node_output_type.py +185 -0
- omnibase_infra/enums/enum_non_retryable_error_category.py +224 -0
- omnibase_infra/enums/enum_policy_type.py +32 -0
- omnibase_infra/enums/enum_registration_state.py +261 -0
- omnibase_infra/enums/enum_registration_status.py +33 -0
- omnibase_infra/enums/enum_registry_response_status.py +28 -0
- omnibase_infra/enums/enum_response_status.py +26 -0
- omnibase_infra/enums/enum_retry_error_category.py +98 -0
- omnibase_infra/enums/enum_security_rule_id.py +103 -0
- omnibase_infra/enums/enum_selection_strategy.py +91 -0
- omnibase_infra/enums/enum_topic_standard.py +42 -0
- omnibase_infra/enums/enum_validation_severity.py +78 -0
- omnibase_infra/errors/__init__.py +156 -0
- omnibase_infra/errors/error_architecture_violation.py +152 -0
- omnibase_infra/errors/error_chain_propagation.py +188 -0
- omnibase_infra/errors/error_compute_registry.py +92 -0
- omnibase_infra/errors/error_consul.py +132 -0
- omnibase_infra/errors/error_container_wiring.py +243 -0
- omnibase_infra/errors/error_event_bus_registry.py +102 -0
- omnibase_infra/errors/error_infra.py +608 -0
- omnibase_infra/errors/error_message_type_registry.py +101 -0
- omnibase_infra/errors/error_policy_registry.py +112 -0
- omnibase_infra/errors/error_vault.py +123 -0
- omnibase_infra/event_bus/__init__.py +72 -0
- omnibase_infra/event_bus/configs/kafka_event_bus_config.yaml +86 -0
- omnibase_infra/event_bus/event_bus_inmemory.py +743 -0
- omnibase_infra/event_bus/event_bus_kafka.py +1658 -0
- omnibase_infra/event_bus/mixin_kafka_broadcast.py +184 -0
- omnibase_infra/event_bus/mixin_kafka_dlq.py +765 -0
- omnibase_infra/event_bus/models/__init__.py +29 -0
- omnibase_infra/event_bus/models/config/__init__.py +20 -0
- omnibase_infra/event_bus/models/config/model_kafka_event_bus_config.py +725 -0
- omnibase_infra/event_bus/models/model_dlq_event.py +206 -0
- omnibase_infra/event_bus/models/model_dlq_metrics.py +304 -0
- omnibase_infra/event_bus/models/model_event_headers.py +115 -0
- omnibase_infra/event_bus/models/model_event_message.py +60 -0
- omnibase_infra/event_bus/topic_constants.py +376 -0
- omnibase_infra/handlers/__init__.py +75 -0
- omnibase_infra/handlers/filesystem/__init__.py +48 -0
- omnibase_infra/handlers/filesystem/enum_file_system_operation.py +35 -0
- omnibase_infra/handlers/filesystem/model_file_system_request.py +298 -0
- omnibase_infra/handlers/filesystem/model_file_system_result.py +166 -0
- omnibase_infra/handlers/handler_consul.py +787 -0
- omnibase_infra/handlers/handler_db.py +1039 -0
- omnibase_infra/handlers/handler_filesystem.py +1478 -0
- omnibase_infra/handlers/handler_graph.py +1154 -0
- omnibase_infra/handlers/handler_http.py +920 -0
- omnibase_infra/handlers/handler_manifest_persistence.contract.yaml +184 -0
- omnibase_infra/handlers/handler_manifest_persistence.py +1539 -0
- omnibase_infra/handlers/handler_mcp.py +748 -0
- omnibase_infra/handlers/handler_qdrant.py +1076 -0
- omnibase_infra/handlers/handler_vault.py +422 -0
- omnibase_infra/handlers/mcp/__init__.py +19 -0
- omnibase_infra/handlers/mcp/adapter_onex_to_mcp.py +446 -0
- omnibase_infra/handlers/mcp/protocols.py +178 -0
- omnibase_infra/handlers/mcp/transport_streamable_http.py +352 -0
- omnibase_infra/handlers/mixins/__init__.py +42 -0
- omnibase_infra/handlers/mixins/mixin_consul_initialization.py +349 -0
- omnibase_infra/handlers/mixins/mixin_consul_kv.py +337 -0
- omnibase_infra/handlers/mixins/mixin_consul_service.py +277 -0
- omnibase_infra/handlers/mixins/mixin_vault_initialization.py +338 -0
- omnibase_infra/handlers/mixins/mixin_vault_retry.py +412 -0
- omnibase_infra/handlers/mixins/mixin_vault_secrets.py +450 -0
- omnibase_infra/handlers/mixins/mixin_vault_token.py +365 -0
- omnibase_infra/handlers/models/__init__.py +286 -0
- omnibase_infra/handlers/models/consul/__init__.py +81 -0
- omnibase_infra/handlers/models/consul/enum_consul_operation_type.py +57 -0
- omnibase_infra/handlers/models/consul/model_consul_deregister_payload.py +51 -0
- omnibase_infra/handlers/models/consul/model_consul_handler_config.py +153 -0
- omnibase_infra/handlers/models/consul/model_consul_handler_payload.py +89 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_found_payload.py +55 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_not_found_payload.py +49 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_get_recurse_payload.py +50 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_item.py +33 -0
- omnibase_infra/handlers/models/consul/model_consul_kv_put_payload.py +41 -0
- omnibase_infra/handlers/models/consul/model_consul_register_payload.py +53 -0
- omnibase_infra/handlers/models/consul/model_consul_retry_config.py +66 -0
- omnibase_infra/handlers/models/consul/model_payload_consul.py +66 -0
- omnibase_infra/handlers/models/consul/registry_payload_consul.py +214 -0
- omnibase_infra/handlers/models/graph/__init__.py +35 -0
- omnibase_infra/handlers/models/graph/enum_graph_operation_type.py +20 -0
- omnibase_infra/handlers/models/graph/model_graph_execute_payload.py +38 -0
- omnibase_infra/handlers/models/graph/model_graph_handler_config.py +54 -0
- omnibase_infra/handlers/models/graph/model_graph_handler_payload.py +44 -0
- omnibase_infra/handlers/models/graph/model_graph_query_payload.py +40 -0
- omnibase_infra/handlers/models/graph/model_graph_record.py +22 -0
- omnibase_infra/handlers/models/http/__init__.py +50 -0
- omnibase_infra/handlers/models/http/enum_http_operation_type.py +29 -0
- omnibase_infra/handlers/models/http/model_http_body_content.py +45 -0
- omnibase_infra/handlers/models/http/model_http_get_payload.py +88 -0
- omnibase_infra/handlers/models/http/model_http_handler_payload.py +90 -0
- omnibase_infra/handlers/models/http/model_http_post_payload.py +88 -0
- omnibase_infra/handlers/models/http/model_payload_http.py +66 -0
- omnibase_infra/handlers/models/http/registry_payload_http.py +212 -0
- omnibase_infra/handlers/models/mcp/__init__.py +23 -0
- omnibase_infra/handlers/models/mcp/enum_mcp_operation_type.py +24 -0
- omnibase_infra/handlers/models/mcp/model_mcp_handler_config.py +40 -0
- omnibase_infra/handlers/models/mcp/model_mcp_tool_call.py +32 -0
- omnibase_infra/handlers/models/mcp/model_mcp_tool_result.py +45 -0
- omnibase_infra/handlers/models/model_consul_handler_response.py +96 -0
- omnibase_infra/handlers/models/model_db_describe_response.py +83 -0
- omnibase_infra/handlers/models/model_db_query_payload.py +95 -0
- omnibase_infra/handlers/models/model_db_query_response.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_config.py +98 -0
- omnibase_infra/handlers/models/model_filesystem_delete_payload.py +54 -0
- omnibase_infra/handlers/models/model_filesystem_delete_result.py +77 -0
- omnibase_infra/handlers/models/model_filesystem_directory_entry.py +75 -0
- omnibase_infra/handlers/models/model_filesystem_ensure_directory_payload.py +54 -0
- omnibase_infra/handlers/models/model_filesystem_ensure_directory_result.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_list_directory_payload.py +60 -0
- omnibase_infra/handlers/models/model_filesystem_list_directory_result.py +68 -0
- omnibase_infra/handlers/models/model_filesystem_read_payload.py +62 -0
- omnibase_infra/handlers/models/model_filesystem_read_result.py +61 -0
- omnibase_infra/handlers/models/model_filesystem_write_payload.py +70 -0
- omnibase_infra/handlers/models/model_filesystem_write_result.py +55 -0
- omnibase_infra/handlers/models/model_graph_handler_response.py +98 -0
- omnibase_infra/handlers/models/model_handler_response.py +103 -0
- omnibase_infra/handlers/models/model_http_handler_response.py +101 -0
- omnibase_infra/handlers/models/model_manifest_metadata.py +75 -0
- omnibase_infra/handlers/models/model_manifest_persistence_config.py +62 -0
- omnibase_infra/handlers/models/model_manifest_query_payload.py +90 -0
- omnibase_infra/handlers/models/model_manifest_query_result.py +97 -0
- omnibase_infra/handlers/models/model_manifest_retrieve_payload.py +44 -0
- omnibase_infra/handlers/models/model_manifest_retrieve_result.py +98 -0
- omnibase_infra/handlers/models/model_manifest_store_payload.py +47 -0
- omnibase_infra/handlers/models/model_manifest_store_result.py +67 -0
- omnibase_infra/handlers/models/model_operation_context.py +187 -0
- omnibase_infra/handlers/models/model_qdrant_handler_response.py +98 -0
- omnibase_infra/handlers/models/model_retry_state.py +162 -0
- omnibase_infra/handlers/models/model_vault_handler_response.py +98 -0
- omnibase_infra/handlers/models/qdrant/__init__.py +44 -0
- omnibase_infra/handlers/models/qdrant/enum_qdrant_operation_type.py +26 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_collection_payload.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_delete_payload.py +36 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_handler_config.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_handler_payload.py +54 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_search_payload.py +42 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_search_result.py +30 -0
- omnibase_infra/handlers/models/qdrant/model_qdrant_upsert_payload.py +36 -0
- omnibase_infra/handlers/models/vault/__init__.py +69 -0
- omnibase_infra/handlers/models/vault/enum_vault_operation_type.py +35 -0
- omnibase_infra/handlers/models/vault/model_payload_vault.py +66 -0
- omnibase_infra/handlers/models/vault/model_vault_delete_payload.py +57 -0
- omnibase_infra/handlers/models/vault/model_vault_handler_config.py +148 -0
- omnibase_infra/handlers/models/vault/model_vault_handler_payload.py +101 -0
- omnibase_infra/handlers/models/vault/model_vault_list_payload.py +58 -0
- omnibase_infra/handlers/models/vault/model_vault_renew_token_payload.py +67 -0
- omnibase_infra/handlers/models/vault/model_vault_retry_config.py +66 -0
- omnibase_infra/handlers/models/vault/model_vault_secret_payload.py +106 -0
- omnibase_infra/handlers/models/vault/model_vault_write_payload.py +66 -0
- omnibase_infra/handlers/models/vault/registry_payload_vault.py +213 -0
- omnibase_infra/handlers/registration_storage/__init__.py +43 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_mock.py +392 -0
- omnibase_infra/handlers/registration_storage/handler_registration_storage_postgres.py +915 -0
- omnibase_infra/handlers/registration_storage/models/__init__.py +23 -0
- omnibase_infra/handlers/registration_storage/models/model_delete_registration_request.py +58 -0
- omnibase_infra/handlers/registration_storage/models/model_update_registration_request.py +73 -0
- omnibase_infra/handlers/registration_storage/protocol_registration_persistence.py +191 -0
- omnibase_infra/handlers/service_discovery/__init__.py +43 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_consul.py +747 -0
- omnibase_infra/handlers/service_discovery/handler_service_discovery_mock.py +258 -0
- omnibase_infra/handlers/service_discovery/models/__init__.py +22 -0
- omnibase_infra/handlers/service_discovery/models/model_discovery_result.py +64 -0
- omnibase_infra/handlers/service_discovery/models/model_registration_result.py +138 -0
- omnibase_infra/handlers/service_discovery/models/model_service_info.py +99 -0
- omnibase_infra/handlers/service_discovery/protocol_discovery_operations.py +170 -0
- omnibase_infra/idempotency/__init__.py +94 -0
- omnibase_infra/idempotency/models/__init__.py +43 -0
- omnibase_infra/idempotency/models/model_idempotency_check_result.py +85 -0
- omnibase_infra/idempotency/models/model_idempotency_guard_config.py +130 -0
- omnibase_infra/idempotency/models/model_idempotency_record.py +86 -0
- omnibase_infra/idempotency/models/model_idempotency_store_health_check_result.py +81 -0
- omnibase_infra/idempotency/models/model_idempotency_store_metrics.py +140 -0
- omnibase_infra/idempotency/models/model_postgres_idempotency_store_config.py +299 -0
- omnibase_infra/idempotency/protocol_idempotency_store.py +184 -0
- omnibase_infra/idempotency/store_inmemory.py +265 -0
- omnibase_infra/idempotency/store_postgres.py +923 -0
- omnibase_infra/infrastructure/__init__.py +0 -0
- omnibase_infra/mixins/__init__.py +71 -0
- omnibase_infra/mixins/mixin_async_circuit_breaker.py +655 -0
- omnibase_infra/mixins/mixin_dict_like_accessors.py +146 -0
- omnibase_infra/mixins/mixin_envelope_extraction.py +119 -0
- omnibase_infra/mixins/mixin_node_introspection.py +2465 -0
- omnibase_infra/mixins/mixin_retry_execution.py +386 -0
- omnibase_infra/mixins/protocol_circuit_breaker_aware.py +133 -0
- omnibase_infra/models/__init__.py +136 -0
- omnibase_infra/models/corpus/__init__.py +17 -0
- omnibase_infra/models/corpus/model_capture_config.py +133 -0
- omnibase_infra/models/corpus/model_capture_result.py +86 -0
- omnibase_infra/models/discovery/__init__.py +42 -0
- omnibase_infra/models/discovery/model_dependency_spec.py +319 -0
- omnibase_infra/models/discovery/model_discovered_capabilities.py +50 -0
- omnibase_infra/models/discovery/model_introspection_config.py +311 -0
- omnibase_infra/models/discovery/model_introspection_performance_metrics.py +169 -0
- omnibase_infra/models/discovery/model_introspection_task_config.py +116 -0
- omnibase_infra/models/dispatch/__init__.py +147 -0
- omnibase_infra/models/dispatch/model_dispatch_context.py +439 -0
- omnibase_infra/models/dispatch/model_dispatch_error.py +336 -0
- omnibase_infra/models/dispatch/model_dispatch_log_context.py +400 -0
- omnibase_infra/models/dispatch/model_dispatch_metadata.py +228 -0
- omnibase_infra/models/dispatch/model_dispatch_metrics.py +496 -0
- omnibase_infra/models/dispatch/model_dispatch_outcome.py +317 -0
- omnibase_infra/models/dispatch/model_dispatch_outputs.py +231 -0
- omnibase_infra/models/dispatch/model_dispatch_result.py +436 -0
- omnibase_infra/models/dispatch/model_dispatch_route.py +279 -0
- omnibase_infra/models/dispatch/model_dispatcher_metrics.py +275 -0
- omnibase_infra/models/dispatch/model_dispatcher_registration.py +352 -0
- omnibase_infra/models/dispatch/model_parsed_topic.py +135 -0
- omnibase_infra/models/dispatch/model_topic_parser.py +725 -0
- omnibase_infra/models/dispatch/model_tracing_context.py +285 -0
- omnibase_infra/models/errors/__init__.py +45 -0
- omnibase_infra/models/errors/model_handler_validation_error.py +594 -0
- omnibase_infra/models/errors/model_infra_error_context.py +99 -0
- omnibase_infra/models/errors/model_message_type_registry_error_context.py +71 -0
- omnibase_infra/models/errors/model_timeout_error_context.py +110 -0
- omnibase_infra/models/handlers/__init__.py +37 -0
- omnibase_infra/models/handlers/model_contract_discovery_result.py +80 -0
- omnibase_infra/models/handlers/model_handler_descriptor.py +185 -0
- omnibase_infra/models/handlers/model_handler_identifier.py +215 -0
- omnibase_infra/models/health/__init__.py +9 -0
- omnibase_infra/models/health/model_health_check_result.py +40 -0
- omnibase_infra/models/lifecycle/__init__.py +39 -0
- omnibase_infra/models/logging/__init__.py +51 -0
- omnibase_infra/models/logging/model_log_context.py +756 -0
- omnibase_infra/models/model_retry_error_classification.py +78 -0
- omnibase_infra/models/projection/__init__.py +43 -0
- omnibase_infra/models/projection/model_capability_fields.py +112 -0
- omnibase_infra/models/projection/model_registration_projection.py +434 -0
- omnibase_infra/models/projection/model_registration_snapshot.py +322 -0
- omnibase_infra/models/projection/model_sequence_info.py +182 -0
- omnibase_infra/models/projection/model_snapshot_topic_config.py +590 -0
- omnibase_infra/models/projectors/__init__.py +41 -0
- omnibase_infra/models/projectors/model_projector_column.py +289 -0
- omnibase_infra/models/projectors/model_projector_discovery_result.py +65 -0
- omnibase_infra/models/projectors/model_projector_index.py +270 -0
- omnibase_infra/models/projectors/model_projector_schema.py +415 -0
- omnibase_infra/models/projectors/model_projector_validation_error.py +63 -0
- omnibase_infra/models/projectors/util_sql_identifiers.py +115 -0
- omnibase_infra/models/registration/__init__.py +59 -0
- omnibase_infra/models/registration/commands/__init__.py +15 -0
- omnibase_infra/models/registration/commands/model_node_registration_acked.py +108 -0
- omnibase_infra/models/registration/events/__init__.py +56 -0
- omnibase_infra/models/registration/events/model_node_became_active.py +103 -0
- omnibase_infra/models/registration/events/model_node_liveness_expired.py +103 -0
- omnibase_infra/models/registration/events/model_node_registration_accepted.py +98 -0
- omnibase_infra/models/registration/events/model_node_registration_ack_received.py +98 -0
- omnibase_infra/models/registration/events/model_node_registration_ack_timed_out.py +112 -0
- omnibase_infra/models/registration/events/model_node_registration_initiated.py +107 -0
- omnibase_infra/models/registration/events/model_node_registration_rejected.py +104 -0
- omnibase_infra/models/registration/model_introspection_metrics.py +253 -0
- omnibase_infra/models/registration/model_node_capabilities.py +179 -0
- omnibase_infra/models/registration/model_node_heartbeat_event.py +126 -0
- omnibase_infra/models/registration/model_node_introspection_event.py +175 -0
- omnibase_infra/models/registration/model_node_metadata.py +79 -0
- omnibase_infra/models/registration/model_node_registration.py +162 -0
- omnibase_infra/models/registration/model_node_registration_record.py +162 -0
- omnibase_infra/models/registry/__init__.py +29 -0
- omnibase_infra/models/registry/model_domain_constraint.py +202 -0
- omnibase_infra/models/registry/model_message_type_entry.py +271 -0
- omnibase_infra/models/resilience/__init__.py +9 -0
- omnibase_infra/models/resilience/model_circuit_breaker_config.py +227 -0
- omnibase_infra/models/routing/__init__.py +25 -0
- omnibase_infra/models/routing/model_routing_entry.py +52 -0
- omnibase_infra/models/routing/model_routing_subcontract.py +70 -0
- omnibase_infra/models/runtime/__init__.py +40 -0
- omnibase_infra/models/runtime/model_contract_security_config.py +41 -0
- omnibase_infra/models/runtime/model_discovery_error.py +81 -0
- omnibase_infra/models/runtime/model_discovery_result.py +162 -0
- omnibase_infra/models/runtime/model_discovery_warning.py +74 -0
- omnibase_infra/models/runtime/model_failed_plugin_load.py +63 -0
- omnibase_infra/models/runtime/model_handler_contract.py +280 -0
- omnibase_infra/models/runtime/model_loaded_handler.py +120 -0
- omnibase_infra/models/runtime/model_plugin_load_context.py +93 -0
- omnibase_infra/models/runtime/model_plugin_load_summary.py +124 -0
- omnibase_infra/models/security/__init__.py +50 -0
- omnibase_infra/models/security/classification_levels.py +99 -0
- omnibase_infra/models/security/model_environment_policy.py +145 -0
- omnibase_infra/models/security/model_handler_security_policy.py +107 -0
- omnibase_infra/models/security/model_security_error.py +81 -0
- omnibase_infra/models/security/model_security_validation_result.py +328 -0
- omnibase_infra/models/security/model_security_warning.py +67 -0
- omnibase_infra/models/snapshot/__init__.py +27 -0
- omnibase_infra/models/snapshot/model_field_change.py +65 -0
- omnibase_infra/models/snapshot/model_snapshot.py +270 -0
- omnibase_infra/models/snapshot/model_snapshot_diff.py +203 -0
- omnibase_infra/models/snapshot/model_subject_ref.py +81 -0
- omnibase_infra/models/types/__init__.py +71 -0
- omnibase_infra/models/validation/__init__.py +89 -0
- omnibase_infra/models/validation/model_any_type_validation_result.py +118 -0
- omnibase_infra/models/validation/model_any_type_violation.py +141 -0
- omnibase_infra/models/validation/model_category_match_result.py +345 -0
- omnibase_infra/models/validation/model_chain_violation.py +166 -0
- omnibase_infra/models/validation/model_coverage_metrics.py +316 -0
- omnibase_infra/models/validation/model_execution_shape_rule.py +159 -0
- omnibase_infra/models/validation/model_execution_shape_validation.py +208 -0
- omnibase_infra/models/validation/model_execution_shape_validation_result.py +294 -0
- omnibase_infra/models/validation/model_execution_shape_violation.py +122 -0
- omnibase_infra/models/validation/model_localhandler_validation_result.py +139 -0
- omnibase_infra/models/validation/model_localhandler_violation.py +100 -0
- omnibase_infra/models/validation/model_output_validation_params.py +74 -0
- omnibase_infra/models/validation/model_validate_and_raise_params.py +84 -0
- omnibase_infra/models/validation/model_validation_error_params.py +84 -0
- omnibase_infra/models/validation/model_validation_outcome.py +287 -0
- omnibase_infra/nodes/__init__.py +48 -0
- omnibase_infra/nodes/architecture_validator/__init__.py +79 -0
- omnibase_infra/nodes/architecture_validator/contract.yaml +252 -0
- omnibase_infra/nodes/architecture_validator/contract_architecture_validator.yaml +208 -0
- omnibase_infra/nodes/architecture_validator/mixins/__init__.py +16 -0
- omnibase_infra/nodes/architecture_validator/mixins/mixin_file_path_rule.py +92 -0
- omnibase_infra/nodes/architecture_validator/models/__init__.py +36 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_request.py +56 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_validation_result.py +311 -0
- omnibase_infra/nodes/architecture_validator/models/model_architecture_violation.py +163 -0
- omnibase_infra/nodes/architecture_validator/models/model_rule_check_result.py +265 -0
- omnibase_infra/nodes/architecture_validator/models/model_validation_request.py +105 -0
- omnibase_infra/nodes/architecture_validator/models/model_validation_result.py +314 -0
- omnibase_infra/nodes/architecture_validator/node.py +262 -0
- omnibase_infra/nodes/architecture_validator/node_architecture_validator.py +383 -0
- omnibase_infra/nodes/architecture_validator/protocols/__init__.py +9 -0
- omnibase_infra/nodes/architecture_validator/protocols/protocol_architecture_rule.py +225 -0
- omnibase_infra/nodes/architecture_validator/registry/__init__.py +28 -0
- omnibase_infra/nodes/architecture_validator/registry/registry_infra_architecture_validator.py +99 -0
- omnibase_infra/nodes/architecture_validator/validators/__init__.py +104 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_direct_dispatch.py +422 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_handler_publishing.py +481 -0
- omnibase_infra/nodes/architecture_validator/validators/validator_no_orchestrator_fsm.py +491 -0
- omnibase_infra/nodes/effects/README.md +358 -0
- omnibase_infra/nodes/effects/__init__.py +26 -0
- omnibase_infra/nodes/effects/contract.yaml +172 -0
- omnibase_infra/nodes/effects/models/__init__.py +32 -0
- omnibase_infra/nodes/effects/models/model_backend_result.py +190 -0
- omnibase_infra/nodes/effects/models/model_effect_idempotency_config.py +92 -0
- omnibase_infra/nodes/effects/models/model_registry_request.py +132 -0
- omnibase_infra/nodes/effects/models/model_registry_response.py +263 -0
- omnibase_infra/nodes/effects/protocol_consul_client.py +89 -0
- omnibase_infra/nodes/effects/protocol_effect_idempotency_store.py +143 -0
- omnibase_infra/nodes/effects/protocol_postgres_adapter.py +96 -0
- omnibase_infra/nodes/effects/registry_effect.py +525 -0
- omnibase_infra/nodes/effects/store_effect_idempotency_inmemory.py +425 -0
- omnibase_infra/nodes/node_registration_orchestrator/README.md +542 -0
- omnibase_infra/nodes/node_registration_orchestrator/__init__.py +120 -0
- omnibase_infra/nodes/node_registration_orchestrator/contract.yaml +475 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/__init__.py +53 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_introspected.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_node_registration_acked.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/dispatchers/dispatcher_runtime_tick.py +373 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/__init__.py +62 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_heartbeat.py +376 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_introspected.py +609 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_node_registration_acked.py +458 -0
- omnibase_infra/nodes/node_registration_orchestrator/handlers/handler_runtime_tick.py +364 -0
- omnibase_infra/nodes/node_registration_orchestrator/introspection_event_router.py +544 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/__init__.py +75 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_intent_payload.py +194 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_consul_registration_intent.py +67 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_intent_execution_result.py +50 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_node_liveness_expired.py +107 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_config.py +67 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_input.py +41 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_orchestrator_output.py +166 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_intent_payload.py +235 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_postgres_upsert_intent.py +68 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_execution_result.py +384 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_reducer_state.py +60 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_registration_intent.py +177 -0
- omnibase_infra/nodes/node_registration_orchestrator/models/model_registry_intent.py +247 -0
- omnibase_infra/nodes/node_registration_orchestrator/node.py +195 -0
- omnibase_infra/nodes/node_registration_orchestrator/plugin.py +909 -0
- omnibase_infra/nodes/node_registration_orchestrator/protocols.py +439 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/__init__.py +41 -0
- omnibase_infra/nodes/node_registration_orchestrator/registry/registry_infra_node_registration_orchestrator.py +525 -0
- omnibase_infra/nodes/node_registration_orchestrator/timeout_coordinator.py +392 -0
- omnibase_infra/nodes/node_registration_orchestrator/wiring.py +742 -0
- omnibase_infra/nodes/node_registration_reducer/__init__.py +15 -0
- omnibase_infra/nodes/node_registration_reducer/contract.yaml +301 -0
- omnibase_infra/nodes/node_registration_reducer/models/__init__.py +38 -0
- omnibase_infra/nodes/node_registration_reducer/models/model_validation_result.py +113 -0
- omnibase_infra/nodes/node_registration_reducer/node.py +139 -0
- omnibase_infra/nodes/node_registration_reducer/registry/__init__.py +9 -0
- omnibase_infra/nodes/node_registration_reducer/registry/registry_infra_node_registration_reducer.py +79 -0
- omnibase_infra/nodes/node_registration_storage_effect/__init__.py +41 -0
- omnibase_infra/nodes/node_registration_storage_effect/contract.yaml +225 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/__init__.py +44 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_delete_result.py +132 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_record.py +199 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_registration_update.py +155 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_details.py +123 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_health_check_result.py +117 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_query.py +100 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_storage_result.py +136 -0
- omnibase_infra/nodes/node_registration_storage_effect/models/model_upsert_result.py +127 -0
- omnibase_infra/nodes/node_registration_storage_effect/node.py +109 -0
- omnibase_infra/nodes/node_registration_storage_effect/protocols/__init__.py +22 -0
- omnibase_infra/nodes/node_registration_storage_effect/protocols/protocol_registration_persistence.py +333 -0
- omnibase_infra/nodes/node_registration_storage_effect/registry/__init__.py +23 -0
- omnibase_infra/nodes/node_registration_storage_effect/registry/registry_infra_registration_storage.py +194 -0
- omnibase_infra/nodes/node_registry_effect/__init__.py +85 -0
- omnibase_infra/nodes/node_registry_effect/contract.yaml +682 -0
- omnibase_infra/nodes/node_registry_effect/handlers/__init__.py +70 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_deregister.py +211 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_consul_register.py +212 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_partial_retry.py +416 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_deactivate.py +215 -0
- omnibase_infra/nodes/node_registry_effect/handlers/handler_postgres_upsert.py +208 -0
- omnibase_infra/nodes/node_registry_effect/models/__init__.py +43 -0
- omnibase_infra/nodes/node_registry_effect/models/model_partial_retry_request.py +92 -0
- omnibase_infra/nodes/node_registry_effect/node.py +165 -0
- omnibase_infra/nodes/node_registry_effect/registry/__init__.py +27 -0
- omnibase_infra/nodes/node_registry_effect/registry/registry_infra_registry_effect.py +196 -0
- omnibase_infra/nodes/node_service_discovery_effect/__init__.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/contract.yaml +246 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/__init__.py +67 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/enum_health_status.py +72 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/enum_service_discovery_operation.py +58 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_query.py +99 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_discovery_result.py +98 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_health_check_config.py +121 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_query_metadata.py +63 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_registration_result.py +130 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_details.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_discovery_health_check_result.py +119 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_info.py +106 -0
- omnibase_infra/nodes/node_service_discovery_effect/models/model_service_registration.py +121 -0
- omnibase_infra/nodes/node_service_discovery_effect/node.py +111 -0
- omnibase_infra/nodes/node_service_discovery_effect/protocols/__init__.py +14 -0
- omnibase_infra/nodes/node_service_discovery_effect/protocols/protocol_discovery_operations.py +279 -0
- omnibase_infra/nodes/node_service_discovery_effect/registry/__init__.py +13 -0
- omnibase_infra/nodes/node_service_discovery_effect/registry/registry_infra_service_discovery.py +214 -0
- omnibase_infra/nodes/reducers/__init__.py +30 -0
- omnibase_infra/nodes/reducers/models/__init__.py +32 -0
- omnibase_infra/nodes/reducers/models/model_payload_consul_register.py +76 -0
- omnibase_infra/nodes/reducers/models/model_payload_postgres_upsert_registration.py +60 -0
- omnibase_infra/nodes/reducers/models/model_registration_confirmation.py +166 -0
- omnibase_infra/nodes/reducers/models/model_registration_state.py +433 -0
- omnibase_infra/nodes/reducers/registration_reducer.py +1137 -0
- omnibase_infra/observability/__init__.py +143 -0
- omnibase_infra/observability/constants_metrics.py +91 -0
- omnibase_infra/observability/factory_observability_sink.py +525 -0
- omnibase_infra/observability/handlers/__init__.py +118 -0
- omnibase_infra/observability/handlers/handler_logging_structured.py +967 -0
- omnibase_infra/observability/handlers/handler_metrics_prometheus.py +1120 -0
- omnibase_infra/observability/handlers/model_logging_handler_config.py +71 -0
- omnibase_infra/observability/handlers/model_logging_handler_response.py +77 -0
- omnibase_infra/observability/handlers/model_metrics_handler_config.py +172 -0
- omnibase_infra/observability/handlers/model_metrics_handler_payload.py +135 -0
- omnibase_infra/observability/handlers/model_metrics_handler_response.py +101 -0
- omnibase_infra/observability/hooks/__init__.py +74 -0
- omnibase_infra/observability/hooks/hook_observability.py +1223 -0
- omnibase_infra/observability/models/__init__.py +30 -0
- omnibase_infra/observability/models/enum_required_log_context_key.py +77 -0
- omnibase_infra/observability/models/model_buffered_log_entry.py +117 -0
- omnibase_infra/observability/models/model_logging_sink_config.py +73 -0
- omnibase_infra/observability/models/model_metrics_sink_config.py +156 -0
- omnibase_infra/observability/sinks/__init__.py +69 -0
- omnibase_infra/observability/sinks/sink_logging_structured.py +809 -0
- omnibase_infra/observability/sinks/sink_metrics_prometheus.py +710 -0
- omnibase_infra/plugins/__init__.py +27 -0
- omnibase_infra/plugins/examples/__init__.py +28 -0
- omnibase_infra/plugins/examples/plugin_json_normalizer.py +271 -0
- omnibase_infra/plugins/examples/plugin_json_normalizer_error_handling.py +210 -0
- omnibase_infra/plugins/models/__init__.py +21 -0
- omnibase_infra/plugins/models/model_plugin_context.py +76 -0
- omnibase_infra/plugins/models/model_plugin_input_data.py +58 -0
- omnibase_infra/plugins/models/model_plugin_output_data.py +62 -0
- omnibase_infra/plugins/plugin_compute_base.py +435 -0
- omnibase_infra/projectors/__init__.py +30 -0
- omnibase_infra/projectors/contracts/__init__.py +63 -0
- omnibase_infra/projectors/contracts/registration_projector.yaml +370 -0
- omnibase_infra/projectors/projection_reader_registration.py +1559 -0
- omnibase_infra/projectors/snapshot_publisher_registration.py +1329 -0
- omnibase_infra/protocols/__init__.py +99 -0
- omnibase_infra/protocols/protocol_capability_projection.py +253 -0
- omnibase_infra/protocols/protocol_capability_query.py +251 -0
- omnibase_infra/protocols/protocol_event_bus_like.py +127 -0
- omnibase_infra/protocols/protocol_event_projector.py +96 -0
- omnibase_infra/protocols/protocol_idempotency_store.py +142 -0
- omnibase_infra/protocols/protocol_message_dispatcher.py +247 -0
- omnibase_infra/protocols/protocol_message_type_registry.py +306 -0
- omnibase_infra/protocols/protocol_plugin_compute.py +368 -0
- omnibase_infra/protocols/protocol_projector_schema_validator.py +82 -0
- omnibase_infra/protocols/protocol_registry_metrics.py +215 -0
- omnibase_infra/protocols/protocol_snapshot_publisher.py +396 -0
- omnibase_infra/protocols/protocol_snapshot_store.py +567 -0
- omnibase_infra/runtime/__init__.py +296 -0
- omnibase_infra/runtime/binding_config_resolver.py +2706 -0
- omnibase_infra/runtime/chain_aware_dispatch.py +467 -0
- omnibase_infra/runtime/contract_handler_discovery.py +582 -0
- omnibase_infra/runtime/contract_loaders/__init__.py +42 -0
- omnibase_infra/runtime/contract_loaders/handler_routing_loader.py +464 -0
- omnibase_infra/runtime/dispatch_context_enforcer.py +427 -0
- omnibase_infra/runtime/enums/__init__.py +18 -0
- omnibase_infra/runtime/enums/enum_config_ref_scheme.py +33 -0
- omnibase_infra/runtime/enums/enum_scheduler_status.py +170 -0
- omnibase_infra/runtime/envelope_validator.py +179 -0
- omnibase_infra/runtime/handler_contract_source.py +669 -0
- omnibase_infra/runtime/handler_plugin_loader.py +2029 -0
- omnibase_infra/runtime/handler_registry.py +321 -0
- omnibase_infra/runtime/invocation_security_enforcer.py +427 -0
- omnibase_infra/runtime/kernel.py +40 -0
- omnibase_infra/runtime/mixin_policy_validation.py +522 -0
- omnibase_infra/runtime/mixin_semver_cache.py +378 -0
- omnibase_infra/runtime/mixins/__init__.py +17 -0
- omnibase_infra/runtime/mixins/mixin_projector_sql_operations.py +757 -0
- omnibase_infra/runtime/models/__init__.py +192 -0
- omnibase_infra/runtime/models/model_batch_lifecycle_result.py +217 -0
- omnibase_infra/runtime/models/model_binding_config.py +168 -0
- omnibase_infra/runtime/models/model_binding_config_cache_stats.py +135 -0
- omnibase_infra/runtime/models/model_binding_config_resolver_config.py +329 -0
- omnibase_infra/runtime/models/model_cached_secret.py +138 -0
- omnibase_infra/runtime/models/model_compute_key.py +138 -0
- omnibase_infra/runtime/models/model_compute_registration.py +97 -0
- omnibase_infra/runtime/models/model_config_cache_entry.py +61 -0
- omnibase_infra/runtime/models/model_config_ref.py +331 -0
- omnibase_infra/runtime/models/model_config_ref_parse_result.py +125 -0
- omnibase_infra/runtime/models/model_domain_plugin_config.py +92 -0
- omnibase_infra/runtime/models/model_domain_plugin_result.py +270 -0
- omnibase_infra/runtime/models/model_duplicate_response.py +54 -0
- omnibase_infra/runtime/models/model_enabled_protocols_config.py +61 -0
- omnibase_infra/runtime/models/model_event_bus_config.py +54 -0
- omnibase_infra/runtime/models/model_failed_component.py +55 -0
- omnibase_infra/runtime/models/model_health_check_response.py +168 -0
- omnibase_infra/runtime/models/model_health_check_result.py +228 -0
- omnibase_infra/runtime/models/model_lifecycle_result.py +245 -0
- omnibase_infra/runtime/models/model_logging_config.py +42 -0
- omnibase_infra/runtime/models/model_optional_correlation_id.py +167 -0
- omnibase_infra/runtime/models/model_optional_string.py +94 -0
- omnibase_infra/runtime/models/model_optional_uuid.py +110 -0
- omnibase_infra/runtime/models/model_policy_context.py +100 -0
- omnibase_infra/runtime/models/model_policy_key.py +138 -0
- omnibase_infra/runtime/models/model_policy_registration.py +139 -0
- omnibase_infra/runtime/models/model_policy_result.py +103 -0
- omnibase_infra/runtime/models/model_policy_type_filter.py +157 -0
- omnibase_infra/runtime/models/model_projector_plugin_loader_config.py +47 -0
- omnibase_infra/runtime/models/model_protocol_registration_config.py +65 -0
- omnibase_infra/runtime/models/model_retry_policy.py +105 -0
- omnibase_infra/runtime/models/model_runtime_config.py +150 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_config.py +624 -0
- omnibase_infra/runtime/models/model_runtime_scheduler_metrics.py +233 -0
- omnibase_infra/runtime/models/model_runtime_tick.py +193 -0
- omnibase_infra/runtime/models/model_secret_cache_stats.py +82 -0
- omnibase_infra/runtime/models/model_secret_mapping.py +63 -0
- omnibase_infra/runtime/models/model_secret_resolver_config.py +107 -0
- omnibase_infra/runtime/models/model_secret_resolver_metrics.py +111 -0
- omnibase_infra/runtime/models/model_secret_source_info.py +72 -0
- omnibase_infra/runtime/models/model_secret_source_spec.py +66 -0
- omnibase_infra/runtime/models/model_shutdown_batch_result.py +75 -0
- omnibase_infra/runtime/models/model_shutdown_config.py +94 -0
- omnibase_infra/runtime/projector_plugin_loader.py +1462 -0
- omnibase_infra/runtime/projector_schema_manager.py +565 -0
- omnibase_infra/runtime/projector_shell.py +1102 -0
- omnibase_infra/runtime/protocol_contract_descriptor.py +92 -0
- omnibase_infra/runtime/protocol_contract_source.py +92 -0
- omnibase_infra/runtime/protocol_domain_plugin.py +474 -0
- omnibase_infra/runtime/protocol_handler_discovery.py +221 -0
- omnibase_infra/runtime/protocol_handler_plugin_loader.py +327 -0
- omnibase_infra/runtime/protocol_lifecycle_executor.py +435 -0
- omnibase_infra/runtime/protocol_policy.py +366 -0
- omnibase_infra/runtime/protocols/__init__.py +27 -0
- omnibase_infra/runtime/protocols/protocol_runtime_scheduler.py +468 -0
- omnibase_infra/runtime/registry/__init__.py +93 -0
- omnibase_infra/runtime/registry/mixin_message_type_query.py +326 -0
- omnibase_infra/runtime/registry/mixin_message_type_registration.py +354 -0
- omnibase_infra/runtime/registry/registry_event_bus_binding.py +268 -0
- omnibase_infra/runtime/registry/registry_message_type.py +542 -0
- omnibase_infra/runtime/registry/registry_protocol_binding.py +444 -0
- omnibase_infra/runtime/registry_compute.py +1143 -0
- omnibase_infra/runtime/registry_dispatcher.py +678 -0
- omnibase_infra/runtime/registry_policy.py +1502 -0
- omnibase_infra/runtime/runtime_scheduler.py +1070 -0
- omnibase_infra/runtime/secret_resolver.py +2110 -0
- omnibase_infra/runtime/security_metadata_validator.py +776 -0
- omnibase_infra/runtime/service_kernel.py +1573 -0
- omnibase_infra/runtime/service_message_dispatch_engine.py +1805 -0
- omnibase_infra/runtime/service_runtime_host_process.py +2260 -0
- omnibase_infra/runtime/util_container_wiring.py +1123 -0
- omnibase_infra/runtime/util_validation.py +314 -0
- omnibase_infra/runtime/util_version.py +98 -0
- omnibase_infra/runtime/util_wiring.py +566 -0
- omnibase_infra/schemas/schema_registration_projection.sql +320 -0
- omnibase_infra/services/__init__.py +68 -0
- omnibase_infra/services/corpus_capture.py +678 -0
- omnibase_infra/services/service_capability_query.py +945 -0
- omnibase_infra/services/service_health.py +897 -0
- omnibase_infra/services/service_node_selector.py +530 -0
- omnibase_infra/services/service_timeout_emitter.py +682 -0
- omnibase_infra/services/service_timeout_scanner.py +390 -0
- omnibase_infra/services/snapshot/__init__.py +31 -0
- omnibase_infra/services/snapshot/service_snapshot.py +647 -0
- omnibase_infra/services/snapshot/store_inmemory.py +637 -0
- omnibase_infra/services/snapshot/store_postgres.py +1279 -0
- omnibase_infra/shared/__init__.py +8 -0
- omnibase_infra/testing/__init__.py +10 -0
- omnibase_infra/testing/utils.py +23 -0
- omnibase_infra/types/__init__.py +48 -0
- omnibase_infra/types/type_cache_info.py +49 -0
- omnibase_infra/types/type_dsn.py +173 -0
- omnibase_infra/types/type_infra_aliases.py +60 -0
- omnibase_infra/types/typed_dict/__init__.py +21 -0
- omnibase_infra/types/typed_dict/typed_dict_introspection_cache.py +128 -0
- omnibase_infra/types/typed_dict/typed_dict_performance_metrics_cache.py +140 -0
- omnibase_infra/types/typed_dict_capabilities.py +64 -0
- omnibase_infra/utils/__init__.py +89 -0
- omnibase_infra/utils/correlation.py +208 -0
- omnibase_infra/utils/util_datetime.py +372 -0
- omnibase_infra/utils/util_dsn_validation.py +333 -0
- omnibase_infra/utils/util_env_parsing.py +264 -0
- omnibase_infra/utils/util_error_sanitization.py +457 -0
- omnibase_infra/utils/util_pydantic_validators.py +477 -0
- omnibase_infra/utils/util_semver.py +233 -0
- omnibase_infra/validation/__init__.py +307 -0
- omnibase_infra/validation/enums/__init__.py +11 -0
- omnibase_infra/validation/enums/enum_contract_violation_severity.py +13 -0
- omnibase_infra/validation/infra_validators.py +1486 -0
- omnibase_infra/validation/linter_contract.py +907 -0
- omnibase_infra/validation/mixin_any_type_classification.py +120 -0
- omnibase_infra/validation/mixin_any_type_exemption.py +580 -0
- omnibase_infra/validation/mixin_any_type_reporting.py +106 -0
- omnibase_infra/validation/mixin_execution_shape_violation_checks.py +596 -0
- omnibase_infra/validation/mixin_node_archetype_detection.py +254 -0
- omnibase_infra/validation/models/__init__.py +15 -0
- omnibase_infra/validation/models/model_contract_lint_result.py +101 -0
- omnibase_infra/validation/models/model_contract_violation.py +41 -0
- omnibase_infra/validation/service_validation_aggregator.py +395 -0
- omnibase_infra/validation/validation_exemptions.yaml +1710 -0
- omnibase_infra/validation/validator_any_type.py +715 -0
- omnibase_infra/validation/validator_chain_propagation.py +839 -0
- omnibase_infra/validation/validator_execution_shape.py +465 -0
- omnibase_infra/validation/validator_localhandler.py +261 -0
- omnibase_infra/validation/validator_registration_security.py +410 -0
- omnibase_infra/validation/validator_routing_coverage.py +1020 -0
- omnibase_infra/validation/validator_runtime_shape.py +915 -0
- omnibase_infra/validation/validator_security.py +410 -0
- omnibase_infra/validation/validator_topic_category.py +1152 -0
- omnibase_infra-0.2.1.dist-info/METADATA +197 -0
- omnibase_infra-0.2.1.dist-info/RECORD +675 -0
- omnibase_infra-0.2.1.dist-info/WHEEL +4 -0
- omnibase_infra-0.2.1.dist-info/entry_points.txt +4 -0
- omnibase_infra-0.2.1.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,1462 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Projector Plugin Loader for Contract-Based Discovery.
|
|
4
|
+
|
|
5
|
+
This module provides ProjectorPluginLoader, which discovers and loads projectors
|
|
6
|
+
from YAML contract definitions. It implements ProtocolProjectorLoader and supports
|
|
7
|
+
two operation modes:
|
|
8
|
+
- Strict mode (default): Raises on first error encountered
|
|
9
|
+
- Graceful mode: Collects errors, continues discovery
|
|
10
|
+
|
|
11
|
+
Part of OMN-1168: ProjectorPluginLoader contract discovery loading.
|
|
12
|
+
|
|
13
|
+
Security Features:
|
|
14
|
+
- File size validation (max 10MB) to prevent memory exhaustion
|
|
15
|
+
- Symlink protection to prevent path traversal attacks
|
|
16
|
+
- Path sanitization for safe logging
|
|
17
|
+
- YAML safe_load to prevent arbitrary code execution
|
|
18
|
+
|
|
19
|
+
See Also:
|
|
20
|
+
- ProtocolProjectorLoader: Protocol definition from omnibase_spi
|
|
21
|
+
- ModelProjectorContract: Contract model from omnibase_core
|
|
22
|
+
- ProtocolEventProjector: Protocol for loaded projectors
|
|
23
|
+
|
|
24
|
+
.. versionadded:: 0.7.0
|
|
25
|
+
Created as part of OMN-1168 projector contract discovery.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import logging
|
|
31
|
+
import time
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
from typing import TYPE_CHECKING
|
|
34
|
+
from uuid import UUID, uuid4
|
|
35
|
+
|
|
36
|
+
import asyncpg
|
|
37
|
+
import yaml
|
|
38
|
+
from pydantic import ValidationError
|
|
39
|
+
|
|
40
|
+
from omnibase_core.container import ModelONEXContainer
|
|
41
|
+
from omnibase_core.enums.enum_core_error_code import EnumCoreErrorCode
|
|
42
|
+
from omnibase_core.models.errors.model_onex_error import ModelOnexError
|
|
43
|
+
from omnibase_core.models.projectors import ModelProjectorContract
|
|
44
|
+
from omnibase_infra.models.projectors import (
|
|
45
|
+
ModelProjectorDiscoveryResult,
|
|
46
|
+
ModelProjectorValidationError,
|
|
47
|
+
)
|
|
48
|
+
from omnibase_infra.protocols import (
|
|
49
|
+
ProtocolEventProjector,
|
|
50
|
+
ProtocolProjectorSchemaValidator,
|
|
51
|
+
)
|
|
52
|
+
from omnibase_infra.runtime.models import ModelProjectorPluginLoaderConfig
|
|
53
|
+
|
|
54
|
+
if TYPE_CHECKING:
|
|
55
|
+
from omnibase_core.models.events import ModelEventEnvelope
|
|
56
|
+
from omnibase_core.models.projectors import ModelProjectionResult
|
|
57
|
+
|
|
58
|
+
logger = logging.getLogger(__name__)
|
|
59
|
+
|
|
60
|
+
# Contract file patterns for projector discovery
|
|
61
|
+
PROJECTOR_CONTRACT_PATTERNS = ("*_projector.yaml", "projector_contract.yaml")
|
|
62
|
+
|
|
63
|
+
# Maximum contract file size (10MB) to prevent memory exhaustion
|
|
64
|
+
MAX_CONTRACT_SIZE = 10 * 1024 * 1024
|
|
65
|
+
|
|
66
|
+
# Maximum files to discover in a single operation to prevent DoS via filesystem-wide glob scans
|
|
67
|
+
MAX_DISCOVERY_FILES = 10000
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# =============================================================================
|
|
71
|
+
# ProjectorShell Import (OMN-1169)
|
|
72
|
+
# =============================================================================
|
|
73
|
+
from omnibase_infra.runtime.projector_shell import ProjectorShell
|
|
74
|
+
|
|
75
|
+
# =============================================================================
|
|
76
|
+
# Placeholder Projector (used when no database pool is provided)
|
|
77
|
+
# =============================================================================
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ProjectorShellPlaceholder:
|
|
81
|
+
"""Placeholder projector shell used when no database pool is provided.
|
|
82
|
+
|
|
83
|
+
This class provides a stub implementation that holds contract metadata
|
|
84
|
+
but cannot actually project events. It is used by the loader for
|
|
85
|
+
discovery-only scenarios where database access is not needed.
|
|
86
|
+
|
|
87
|
+
For full projection functionality, provide a database pool to the
|
|
88
|
+
ProjectorPluginLoader constructor, which will instantiate ProjectorShell.
|
|
89
|
+
|
|
90
|
+
Note:
|
|
91
|
+
The project() and get_state() methods will raise NotImplementedError.
|
|
92
|
+
Use ProjectorShell with a database pool for actual projections.
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(self, contract: ModelProjectorContract) -> None:
|
|
96
|
+
"""Initialize placeholder with contract metadata.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
contract: The parsed and validated projector contract.
|
|
100
|
+
"""
|
|
101
|
+
self._contract = contract
|
|
102
|
+
logger.debug(
|
|
103
|
+
"ProjectorShellPlaceholder loaded for '%s' - "
|
|
104
|
+
"no database pool provided, projection methods will raise NotImplementedError",
|
|
105
|
+
contract.projector_id,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
@property
|
|
109
|
+
def projector_id(self) -> str:
|
|
110
|
+
"""Unique identifier from contract."""
|
|
111
|
+
return str(self._contract.projector_id)
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def aggregate_type(self) -> str:
|
|
115
|
+
"""Aggregate type from contract."""
|
|
116
|
+
return str(self._contract.aggregate_type)
|
|
117
|
+
|
|
118
|
+
@property
|
|
119
|
+
def consumed_events(self) -> list[str]:
|
|
120
|
+
"""Event types from contract."""
|
|
121
|
+
return list(self._contract.consumed_events)
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def contract(self) -> ModelProjectorContract:
|
|
125
|
+
"""Access the underlying contract."""
|
|
126
|
+
return self._contract
|
|
127
|
+
|
|
128
|
+
@property
|
|
129
|
+
def is_placeholder(self) -> bool:
|
|
130
|
+
"""Whether this is a placeholder implementation.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
True, as this is a placeholder that will raise NotImplementedError
|
|
134
|
+
on projection methods until OMN-1169 is implemented.
|
|
135
|
+
"""
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
async def project(
|
|
139
|
+
self,
|
|
140
|
+
event: ModelEventEnvelope,
|
|
141
|
+
correlation_id: UUID,
|
|
142
|
+
) -> ModelProjectionResult:
|
|
143
|
+
"""Placeholder - requires database pool for actual projections.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
event: The event envelope to project.
|
|
147
|
+
correlation_id: Correlation ID for distributed tracing.
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
NotImplementedError: Always, as this is a placeholder without DB access.
|
|
151
|
+
"""
|
|
152
|
+
raise NotImplementedError(
|
|
153
|
+
f"ProjectorShellPlaceholder for '{self.projector_id}' cannot project events. "
|
|
154
|
+
"Provide a database pool to ProjectorPluginLoader to use ProjectorShell."
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
async def get_state(
|
|
158
|
+
self,
|
|
159
|
+
aggregate_id: UUID,
|
|
160
|
+
correlation_id: UUID,
|
|
161
|
+
) -> object | None:
|
|
162
|
+
"""Placeholder - requires database pool for state queries.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
aggregate_id: The unique identifier of the aggregate.
|
|
166
|
+
correlation_id: Correlation ID for distributed tracing.
|
|
167
|
+
|
|
168
|
+
Raises:
|
|
169
|
+
NotImplementedError: Always, as this is a placeholder without DB access.
|
|
170
|
+
"""
|
|
171
|
+
raise NotImplementedError(
|
|
172
|
+
f"ProjectorShellPlaceholder for '{self.projector_id}' cannot query state. "
|
|
173
|
+
"Provide a database pool to ProjectorPluginLoader to use ProjectorShell."
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def __repr__(self) -> str:
|
|
177
|
+
"""Return string representation."""
|
|
178
|
+
return (
|
|
179
|
+
f"ProjectorShellPlaceholder("
|
|
180
|
+
f"id={self.projector_id!r}, "
|
|
181
|
+
f"aggregate_type={self.aggregate_type!r}, "
|
|
182
|
+
f"events={len(self.consumed_events)})"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# =============================================================================
|
|
187
|
+
# ProjectorPluginLoader Implementation
|
|
188
|
+
# Note: ONEX-PATTERN-001 exemption - loader requires multiple discovery methods
|
|
189
|
+
# =============================================================================
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class ProjectorPluginLoader:
|
|
193
|
+
"""Projector loader that discovers contracts from the filesystem.
|
|
194
|
+
|
|
195
|
+
This class implements the projector loader protocol by recursively scanning
|
|
196
|
+
configured paths for projector contract files, parsing them with YAML and
|
|
197
|
+
validating against ModelProjectorContract from omnibase_core.
|
|
198
|
+
|
|
199
|
+
Protocol Compliance:
|
|
200
|
+
This class implements the ProtocolProjectorLoader interface with
|
|
201
|
+
all required methods: load_from_contract(), load_from_directory(),
|
|
202
|
+
and discover_and_load().
|
|
203
|
+
|
|
204
|
+
The loader supports two operation modes:
|
|
205
|
+
- Strict mode (default): Raises ModelOnexError on first error
|
|
206
|
+
- Graceful mode: Collects all errors, continues discovery
|
|
207
|
+
|
|
208
|
+
Both modes return results for a consistent interface. In strict mode,
|
|
209
|
+
errors raise exceptions instead of being collected.
|
|
210
|
+
|
|
211
|
+
Security Features:
|
|
212
|
+
- File size validation (max 10MB) to prevent memory exhaustion
|
|
213
|
+
- Symlink protection to prevent path traversal attacks
|
|
214
|
+
- Path sanitization for safe logging (no full paths exposed)
|
|
215
|
+
- YAML safe_load to prevent arbitrary code execution
|
|
216
|
+
|
|
217
|
+
Example:
|
|
218
|
+
>>> # Strict mode loading (raises on error)
|
|
219
|
+
>>> loader = ProjectorPluginLoader(schema_manager=schema_mgr)
|
|
220
|
+
>>> projector = await loader.load_from_contract(Path("./orders_projector.yaml"))
|
|
221
|
+
>>> print(f"Loaded projector: {projector.projector_id}")
|
|
222
|
+
|
|
223
|
+
>>> # Graceful mode with error collection
|
|
224
|
+
>>> config = ModelProjectorPluginLoaderConfig(graceful_mode=True)
|
|
225
|
+
>>> loader = ProjectorPluginLoader(config=config, schema_manager=schema_mgr)
|
|
226
|
+
>>> result = await loader.discover_with_errors(Path("./projectors"))
|
|
227
|
+
>>> print(f"Found {result.success_count} projectors")
|
|
228
|
+
>>> print(f"Encountered {result.error_count} errors")
|
|
229
|
+
|
|
230
|
+
Performance Characteristics:
|
|
231
|
+
- File system scanning is O(n) where n is total files in paths
|
|
232
|
+
- YAML parsing is synchronous (consider aiofiles for high-throughput)
|
|
233
|
+
- Typical performance: 100-500 contracts/second on SSD
|
|
234
|
+
- Memory: ~1KB per contract retained
|
|
235
|
+
|
|
236
|
+
.. versionadded:: 0.7.0
|
|
237
|
+
Created as part of OMN-1168 projector contract discovery.
|
|
238
|
+
"""
|
|
239
|
+
|
|
240
|
+
def __init__(
|
|
241
|
+
self,
|
|
242
|
+
config: ModelProjectorPluginLoaderConfig | None = None,
|
|
243
|
+
container: ModelONEXContainer | None = None,
|
|
244
|
+
schema_manager: ProtocolProjectorSchemaValidator | None = None,
|
|
245
|
+
pool: asyncpg.Pool | None = None,
|
|
246
|
+
) -> None:
|
|
247
|
+
"""Initialize the projector plugin loader.
|
|
248
|
+
|
|
249
|
+
Args:
|
|
250
|
+
config: Configuration for the loader. If None, uses default settings.
|
|
251
|
+
container: ONEX container for dependency injection. If provided,
|
|
252
|
+
can be used to resolve dependencies like schema_manager.
|
|
253
|
+
schema_manager: Schema validator for validating database schemas.
|
|
254
|
+
If None, schema validation is skipped during loading.
|
|
255
|
+
pool: Optional asyncpg connection pool for database access.
|
|
256
|
+
If provided, loaded projectors will be full ProjectorShell
|
|
257
|
+
instances capable of actual projections. If None (default),
|
|
258
|
+
loaded projectors will be ProjectorShellPlaceholder instances
|
|
259
|
+
that hold contract metadata but cannot perform projections.
|
|
260
|
+
"""
|
|
261
|
+
self._config = config or ModelProjectorPluginLoaderConfig()
|
|
262
|
+
self._container = container
|
|
263
|
+
self._schema_manager = schema_manager
|
|
264
|
+
if schema_manager is not None:
|
|
265
|
+
logger.debug("Schema manager provided - will be used for schema validation")
|
|
266
|
+
self._pool = pool
|
|
267
|
+
if pool is not None:
|
|
268
|
+
logger.debug(
|
|
269
|
+
"Database pool provided - will create ProjectorShell instances"
|
|
270
|
+
)
|
|
271
|
+
else:
|
|
272
|
+
logger.debug(
|
|
273
|
+
"No database pool provided - will create placeholder instances"
|
|
274
|
+
)
|
|
275
|
+
self._graceful_mode = self._config.graceful_mode
|
|
276
|
+
|
|
277
|
+
# Security: Validate base_paths don't contain filesystem root
|
|
278
|
+
# to prevent DoS via filesystem-wide glob scanning
|
|
279
|
+
base_paths = self._config.base_paths
|
|
280
|
+
if base_paths:
|
|
281
|
+
for base_path in base_paths:
|
|
282
|
+
resolved = base_path.resolve()
|
|
283
|
+
# Reject root paths (/, /root, C:\, etc.)
|
|
284
|
+
if resolved == Path("/").resolve() or len(resolved.parts) <= 1:
|
|
285
|
+
raise ModelOnexError(
|
|
286
|
+
"Root or near-root path not allowed as base_path - "
|
|
287
|
+
"would allow filesystem-wide scanning",
|
|
288
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
self._base_paths = base_paths or []
|
|
292
|
+
|
|
293
|
+
def _create_projector(
|
|
294
|
+
self,
|
|
295
|
+
contract: ModelProjectorContract,
|
|
296
|
+
) -> ProtocolEventProjector:
|
|
297
|
+
"""Create a projector instance from a contract.
|
|
298
|
+
|
|
299
|
+
If a database pool was provided to the loader, creates a full
|
|
300
|
+
ProjectorShell instance. Otherwise, creates a placeholder.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
contract: The validated projector contract.
|
|
304
|
+
|
|
305
|
+
Returns:
|
|
306
|
+
Either ProjectorShell (if pool available) or ProjectorShellPlaceholder.
|
|
307
|
+
"""
|
|
308
|
+
if self._pool is not None:
|
|
309
|
+
return ProjectorShell(contract, self._pool)
|
|
310
|
+
return ProjectorShellPlaceholder(contract)
|
|
311
|
+
|
|
312
|
+
def _sanitize_path_for_logging(self, path: Path) -> str:
|
|
313
|
+
"""Sanitize a file path for safe inclusion in logs and error messages.
|
|
314
|
+
|
|
315
|
+
In production environments, full paths may leak sensitive information
|
|
316
|
+
about directory structure. This method returns only the filename and
|
|
317
|
+
parent directory to provide context without exposing full paths.
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
path: The full path to sanitize.
|
|
321
|
+
|
|
322
|
+
Returns:
|
|
323
|
+
Sanitized path string showing only parent/filename.
|
|
324
|
+
For example: "/home/user/code/projectors/orders_projector.yaml"
|
|
325
|
+
becomes "projectors/orders_projector.yaml".
|
|
326
|
+
"""
|
|
327
|
+
try:
|
|
328
|
+
return str(Path(path.parent.name) / path.name)
|
|
329
|
+
except (ValueError, AttributeError):
|
|
330
|
+
return path.name
|
|
331
|
+
|
|
332
|
+
def _format_file_size(self, size_bytes: int) -> str:
|
|
333
|
+
"""Format file size with KB/MB granularity for safe logging.
|
|
334
|
+
|
|
335
|
+
Avoids exposing exact byte counts in error messages which could
|
|
336
|
+
leak storage implementation details.
|
|
337
|
+
|
|
338
|
+
Args:
|
|
339
|
+
size_bytes: The file size in bytes.
|
|
340
|
+
|
|
341
|
+
Returns:
|
|
342
|
+
Human-readable size string with KB/MB granularity.
|
|
343
|
+
"""
|
|
344
|
+
if size_bytes < 1024:
|
|
345
|
+
return "less than 1 KB"
|
|
346
|
+
elif size_bytes < 1024 * 1024:
|
|
347
|
+
return f"{size_bytes // 1024} KB"
|
|
348
|
+
else:
|
|
349
|
+
return f"{size_bytes / (1024 * 1024):.1f} MB"
|
|
350
|
+
|
|
351
|
+
def _is_projector_contract(self, filename: str) -> bool:
|
|
352
|
+
"""Check if a filename matches projector contract patterns.
|
|
353
|
+
|
|
354
|
+
Args:
|
|
355
|
+
filename: The filename to check.
|
|
356
|
+
|
|
357
|
+
Returns:
|
|
358
|
+
True if filename matches any projector contract pattern.
|
|
359
|
+
"""
|
|
360
|
+
for pattern in PROJECTOR_CONTRACT_PATTERNS:
|
|
361
|
+
if pattern.startswith("*"):
|
|
362
|
+
suffix = pattern[1:]
|
|
363
|
+
if filename.endswith(suffix):
|
|
364
|
+
return True
|
|
365
|
+
elif filename == pattern:
|
|
366
|
+
return True
|
|
367
|
+
return False
|
|
368
|
+
|
|
369
|
+
def _validate_file_security(
|
|
370
|
+
self,
|
|
371
|
+
contract_path: Path,
|
|
372
|
+
allowed_bases: list[Path],
|
|
373
|
+
) -> tuple[bool, str | None]:
|
|
374
|
+
"""Validate file security constraints.
|
|
375
|
+
|
|
376
|
+
Checks file size limits and symlink containment.
|
|
377
|
+
|
|
378
|
+
Args:
|
|
379
|
+
contract_path: Path to the contract file.
|
|
380
|
+
allowed_bases: List of allowed base directories.
|
|
381
|
+
|
|
382
|
+
Returns:
|
|
383
|
+
Tuple of (is_valid, error_message).
|
|
384
|
+
If valid, error_message is None.
|
|
385
|
+
"""
|
|
386
|
+
resolved_path = contract_path.resolve()
|
|
387
|
+
|
|
388
|
+
# Symlink protection: verify resolved path is within allowed paths
|
|
389
|
+
if allowed_bases:
|
|
390
|
+
is_within_allowed = any(
|
|
391
|
+
resolved_path.is_relative_to(base.resolve()) for base in allowed_bases
|
|
392
|
+
)
|
|
393
|
+
if not is_within_allowed:
|
|
394
|
+
return (
|
|
395
|
+
False,
|
|
396
|
+
f"Contract file resolves outside allowed paths: {self._sanitize_path_for_logging(contract_path)}",
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# File size check
|
|
400
|
+
try:
|
|
401
|
+
file_size = contract_path.stat().st_size
|
|
402
|
+
if file_size > MAX_CONTRACT_SIZE:
|
|
403
|
+
return (
|
|
404
|
+
False,
|
|
405
|
+
f"Contract file exceeds size limit: {self._format_file_size(file_size)} (max: {MAX_CONTRACT_SIZE // (1024 * 1024)} MB)",
|
|
406
|
+
)
|
|
407
|
+
except OSError as e:
|
|
408
|
+
# Use strerror to avoid leaking full path in error message
|
|
409
|
+
error_msg = e.strerror or "unknown error"
|
|
410
|
+
return (False, f"Failed to stat file: {error_msg}")
|
|
411
|
+
|
|
412
|
+
return (True, None)
|
|
413
|
+
|
|
414
|
+
def _load_contract(self, contract_path: Path) -> ModelProjectorContract:
|
|
415
|
+
"""Parse and validate a contract file.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
contract_path: Path to the projector contract YAML file.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
Validated ModelProjectorContract instance.
|
|
422
|
+
|
|
423
|
+
Raises:
|
|
424
|
+
ModelOnexError: If file exceeds size limit.
|
|
425
|
+
yaml.YAMLError: If YAML parsing fails.
|
|
426
|
+
ValidationError: If contract validation fails.
|
|
427
|
+
OSError: If file I/O fails.
|
|
428
|
+
"""
|
|
429
|
+
# Validate file size before reading
|
|
430
|
+
file_size = contract_path.stat().st_size
|
|
431
|
+
if file_size > MAX_CONTRACT_SIZE:
|
|
432
|
+
raise ModelOnexError(
|
|
433
|
+
f"Contract file exceeds size limit: {self._format_file_size(file_size)} "
|
|
434
|
+
f"(max: {MAX_CONTRACT_SIZE // (1024 * 1024)} MB)",
|
|
435
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
with contract_path.open("r", encoding="utf-8") as f:
|
|
439
|
+
raw_data = yaml.safe_load(f)
|
|
440
|
+
|
|
441
|
+
# Strip extension fields not in base ModelProjectorContract.
|
|
442
|
+
# partial_updates is an extension field for OMN-1170 that defines
|
|
443
|
+
# partial update operations. It is used by the runtime for optimized
|
|
444
|
+
# UPDATE statements but is not part of the core contract schema.
|
|
445
|
+
if isinstance(raw_data, dict):
|
|
446
|
+
raw_data.pop("partial_updates", None)
|
|
447
|
+
|
|
448
|
+
# Handle composite key fields: ModelProjectorContract expects strings,
|
|
449
|
+
# but the contract YAML uses lists for composite primary/upsert keys.
|
|
450
|
+
# Convert first element of list to string for model validation.
|
|
451
|
+
# The full composite key information is preserved in the SQL schema.
|
|
452
|
+
if isinstance(
|
|
453
|
+
raw_data.get("projection_schema", {}).get("primary_key"), list
|
|
454
|
+
):
|
|
455
|
+
pk_list = raw_data["projection_schema"]["primary_key"]
|
|
456
|
+
raw_data["projection_schema"]["primary_key"] = (
|
|
457
|
+
pk_list[0] if pk_list else "entity_id"
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
if isinstance(raw_data.get("behavior", {}).get("upsert_key"), list):
|
|
461
|
+
upsert_list = raw_data["behavior"]["upsert_key"]
|
|
462
|
+
raw_data["behavior"]["upsert_key"] = (
|
|
463
|
+
upsert_list[0] if upsert_list else None
|
|
464
|
+
)
|
|
465
|
+
|
|
466
|
+
# Validate against ModelProjectorContract
|
|
467
|
+
contract = ModelProjectorContract.model_validate(raw_data)
|
|
468
|
+
return contract
|
|
469
|
+
|
|
470
|
+
def _create_parse_error(
|
|
471
|
+
self,
|
|
472
|
+
contract_path: Path,
|
|
473
|
+
error: yaml.YAMLError,
|
|
474
|
+
correlation_id: UUID | None = None,
|
|
475
|
+
) -> ModelProjectorValidationError:
|
|
476
|
+
"""Create a validation error for YAML parse failures.
|
|
477
|
+
|
|
478
|
+
Args:
|
|
479
|
+
contract_path: Path to the failing contract file.
|
|
480
|
+
error: The YAML parsing error.
|
|
481
|
+
correlation_id: Correlation ID for distributed tracing.
|
|
482
|
+
|
|
483
|
+
Returns:
|
|
484
|
+
ModelProjectorValidationError with parse error details.
|
|
485
|
+
"""
|
|
486
|
+
return ModelProjectorValidationError(
|
|
487
|
+
error_type="CONTRACT_PARSE_ERROR",
|
|
488
|
+
contract_path=self._sanitize_path_for_logging(contract_path),
|
|
489
|
+
message=f"Failed to parse YAML in {self._sanitize_path_for_logging(contract_path)}: {error}",
|
|
490
|
+
remediation_hint="Check YAML syntax and ensure proper indentation",
|
|
491
|
+
correlation_id=correlation_id,
|
|
492
|
+
)
|
|
493
|
+
|
|
494
|
+
def _create_validation_error(
|
|
495
|
+
self,
|
|
496
|
+
contract_path: Path,
|
|
497
|
+
error: ValidationError,
|
|
498
|
+
correlation_id: UUID | None = None,
|
|
499
|
+
) -> ModelProjectorValidationError:
|
|
500
|
+
"""Create a validation error for contract validation failures.
|
|
501
|
+
|
|
502
|
+
Args:
|
|
503
|
+
contract_path: Path to the failing contract file.
|
|
504
|
+
error: The Pydantic validation error.
|
|
505
|
+
correlation_id: Correlation ID for distributed tracing.
|
|
506
|
+
|
|
507
|
+
Returns:
|
|
508
|
+
ModelProjectorValidationError with validation details.
|
|
509
|
+
"""
|
|
510
|
+
error_details = error.errors()
|
|
511
|
+
if error_details:
|
|
512
|
+
first_error = error_details[0]
|
|
513
|
+
field_loc = " -> ".join(str(x) for x in first_error.get("loc", ()))
|
|
514
|
+
error_msg = str(first_error.get("msg", "validation failed"))
|
|
515
|
+
else:
|
|
516
|
+
field_loc = "unknown"
|
|
517
|
+
error_msg = "validation failed"
|
|
518
|
+
|
|
519
|
+
return ModelProjectorValidationError(
|
|
520
|
+
error_type="CONTRACT_VALIDATION_ERROR",
|
|
521
|
+
contract_path=self._sanitize_path_for_logging(contract_path),
|
|
522
|
+
message=f"Contract validation failed in {self._sanitize_path_for_logging(contract_path)}: {error_msg} at {field_loc}",
|
|
523
|
+
remediation_hint=f"Check the '{field_loc}' field in the contract",
|
|
524
|
+
correlation_id=correlation_id,
|
|
525
|
+
)
|
|
526
|
+
|
|
527
|
+
def _create_size_limit_error(
|
|
528
|
+
self,
|
|
529
|
+
contract_path: Path,
|
|
530
|
+
file_size: int,
|
|
531
|
+
correlation_id: UUID | None = None,
|
|
532
|
+
) -> ModelProjectorValidationError:
|
|
533
|
+
"""Create a validation error for file size limit violations.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
contract_path: Path to the oversized contract file.
|
|
537
|
+
file_size: The actual file size in bytes.
|
|
538
|
+
correlation_id: Correlation ID for distributed tracing.
|
|
539
|
+
|
|
540
|
+
Returns:
|
|
541
|
+
ModelProjectorValidationError with size limit details.
|
|
542
|
+
"""
|
|
543
|
+
return ModelProjectorValidationError(
|
|
544
|
+
error_type="SIZE_LIMIT_ERROR",
|
|
545
|
+
contract_path=self._sanitize_path_for_logging(contract_path),
|
|
546
|
+
message=(
|
|
547
|
+
f"Contract file {self._sanitize_path_for_logging(contract_path)} exceeds size limit: "
|
|
548
|
+
f"{self._format_file_size(file_size)} (max: {MAX_CONTRACT_SIZE // (1024 * 1024)} MB)"
|
|
549
|
+
),
|
|
550
|
+
remediation_hint=(
|
|
551
|
+
f"Reduce contract file size to under {MAX_CONTRACT_SIZE // (1024 * 1024)} MB. "
|
|
552
|
+
"Consider splitting into multiple contracts if needed."
|
|
553
|
+
),
|
|
554
|
+
correlation_id=correlation_id,
|
|
555
|
+
)
|
|
556
|
+
|
|
557
|
+
def _create_io_error(
|
|
558
|
+
self,
|
|
559
|
+
contract_path: Path,
|
|
560
|
+
error: OSError,
|
|
561
|
+
correlation_id: UUID | None = None,
|
|
562
|
+
) -> ModelProjectorValidationError:
|
|
563
|
+
"""Create a validation error for I/O failures.
|
|
564
|
+
|
|
565
|
+
Args:
|
|
566
|
+
contract_path: Path to the contract file that failed to read.
|
|
567
|
+
error: The I/O error encountered.
|
|
568
|
+
correlation_id: Correlation ID for distributed tracing.
|
|
569
|
+
|
|
570
|
+
Returns:
|
|
571
|
+
ModelProjectorValidationError with I/O error details.
|
|
572
|
+
"""
|
|
573
|
+
# Use strerror to avoid leaking full paths, fallback to generic message
|
|
574
|
+
error_message = error.strerror or "I/O error occurred"
|
|
575
|
+
|
|
576
|
+
return ModelProjectorValidationError(
|
|
577
|
+
error_type="IO_ERROR",
|
|
578
|
+
contract_path=self._sanitize_path_for_logging(contract_path),
|
|
579
|
+
message=f"Failed to read contract file {self._sanitize_path_for_logging(contract_path)}: {error_message}",
|
|
580
|
+
remediation_hint="Check file permissions and ensure the file exists",
|
|
581
|
+
correlation_id=correlation_id,
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
def _create_security_error(
|
|
585
|
+
self,
|
|
586
|
+
contract_path: Path,
|
|
587
|
+
message: str,
|
|
588
|
+
correlation_id: UUID | None = None,
|
|
589
|
+
) -> ModelProjectorValidationError:
|
|
590
|
+
"""Create a validation error for security violations.
|
|
591
|
+
|
|
592
|
+
Args:
|
|
593
|
+
contract_path: Path to the contract file.
|
|
594
|
+
message: Security violation message.
|
|
595
|
+
correlation_id: Correlation ID for distributed tracing.
|
|
596
|
+
|
|
597
|
+
Returns:
|
|
598
|
+
ModelProjectorValidationError with security error details.
|
|
599
|
+
"""
|
|
600
|
+
return ModelProjectorValidationError(
|
|
601
|
+
error_type="SECURITY_ERROR",
|
|
602
|
+
contract_path=self._sanitize_path_for_logging(contract_path),
|
|
603
|
+
message=message,
|
|
604
|
+
remediation_hint="Ensure contract files are within allowed directories and not symlinks to external locations",
|
|
605
|
+
correlation_id=correlation_id,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
def _find_contract_files(self, base_path: Path) -> list[Path]:
|
|
609
|
+
"""Find all projector contract files under a base path.
|
|
610
|
+
|
|
611
|
+
Uses specific glob patterns (e.g., "*_projector.yaml") to directly
|
|
612
|
+
discover contract files, avoiding post-filtering overhead. Patterns
|
|
613
|
+
are already specific enough that additional name validation is
|
|
614
|
+
redundant after rglob matching.
|
|
615
|
+
|
|
616
|
+
Args:
|
|
617
|
+
base_path: Directory to scan recursively.
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
List of paths to projector contract files, deduplicated.
|
|
621
|
+
"""
|
|
622
|
+
if base_path.is_file():
|
|
623
|
+
if self._is_projector_contract(base_path.name):
|
|
624
|
+
return [base_path]
|
|
625
|
+
return []
|
|
626
|
+
|
|
627
|
+
# Use a set to deduplicate files matched by multiple patterns
|
|
628
|
+
# (e.g., symlinks or overlapping patterns)
|
|
629
|
+
discovered: set[Path] = set()
|
|
630
|
+
for pattern in PROJECTOR_CONTRACT_PATTERNS:
|
|
631
|
+
# rglob with specific patterns like "*_projector.yaml" already
|
|
632
|
+
# filters to matching files - no need for redundant name filtering
|
|
633
|
+
discovered.update(base_path.rglob(pattern))
|
|
634
|
+
|
|
635
|
+
return list(discovered)
|
|
636
|
+
|
|
637
|
+
def _log_discovery_results(
|
|
638
|
+
self,
|
|
639
|
+
discovered_count: int,
|
|
640
|
+
failure_count: int,
|
|
641
|
+
duration_seconds: float,
|
|
642
|
+
) -> None:
|
|
643
|
+
"""Log discovery results with structured telemetry.
|
|
644
|
+
|
|
645
|
+
Args:
|
|
646
|
+
discovered_count: Number of successfully discovered contracts.
|
|
647
|
+
failure_count: Number of validation failures.
|
|
648
|
+
duration_seconds: Total discovery duration in seconds.
|
|
649
|
+
"""
|
|
650
|
+
contracts_per_sec = (
|
|
651
|
+
discovered_count / duration_seconds if duration_seconds > 0 else 0.0
|
|
652
|
+
)
|
|
653
|
+
|
|
654
|
+
logger.info(
|
|
655
|
+
"Projector contract discovery completed: "
|
|
656
|
+
"discovered_count=%d, failure_count=%d, "
|
|
657
|
+
"graceful_mode=%s, duration_seconds=%.3f, contracts_per_second=%.1f",
|
|
658
|
+
discovered_count,
|
|
659
|
+
failure_count,
|
|
660
|
+
self._graceful_mode,
|
|
661
|
+
duration_seconds,
|
|
662
|
+
contracts_per_sec,
|
|
663
|
+
extra={
|
|
664
|
+
"discovered_count": discovered_count,
|
|
665
|
+
"failure_count": failure_count,
|
|
666
|
+
"graceful_mode": self._graceful_mode,
|
|
667
|
+
"duration_seconds": duration_seconds,
|
|
668
|
+
"contracts_per_second": contracts_per_sec,
|
|
669
|
+
},
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
async def load_from_contract(
|
|
673
|
+
self,
|
|
674
|
+
contract_path: Path,
|
|
675
|
+
) -> ProtocolEventProjector:
|
|
676
|
+
"""Load a projector from a YAML contract file.
|
|
677
|
+
|
|
678
|
+
Parses the contract, validates its structure and semantics,
|
|
679
|
+
and returns a configured projector instance.
|
|
680
|
+
|
|
681
|
+
Args:
|
|
682
|
+
contract_path: Path to the YAML contract file. Must exist
|
|
683
|
+
and be readable.
|
|
684
|
+
|
|
685
|
+
Returns:
|
|
686
|
+
A configured ProtocolEventProjector instance.
|
|
687
|
+
Note: Currently returns ProjectorShellPlaceholder until
|
|
688
|
+
OMN-1169 implements the full ProjectorShell.
|
|
689
|
+
|
|
690
|
+
Raises:
|
|
691
|
+
ModelOnexError: If contract parsing or validation fails.
|
|
692
|
+
FileNotFoundError: If contract_path does not exist.
|
|
693
|
+
PermissionError: If contract_path is not readable.
|
|
694
|
+
"""
|
|
695
|
+
if not contract_path.exists():
|
|
696
|
+
raise FileNotFoundError(
|
|
697
|
+
f"Contract file does not exist: {self._sanitize_path_for_logging(contract_path)}"
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
# Validate security constraints
|
|
701
|
+
allowed_bases = self._base_paths if self._base_paths else [contract_path.parent]
|
|
702
|
+
is_valid, error_msg = self._validate_file_security(contract_path, allowed_bases)
|
|
703
|
+
if not is_valid:
|
|
704
|
+
raise ModelOnexError(
|
|
705
|
+
error_msg or "Security validation failed",
|
|
706
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
707
|
+
)
|
|
708
|
+
|
|
709
|
+
try:
|
|
710
|
+
contract = self._load_contract(contract_path)
|
|
711
|
+
except yaml.YAMLError as e:
|
|
712
|
+
raise ModelOnexError(
|
|
713
|
+
f"Failed to parse YAML contract at {self._sanitize_path_for_logging(contract_path)}: {e}",
|
|
714
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
715
|
+
) from e
|
|
716
|
+
except ValidationError as e:
|
|
717
|
+
raise ModelOnexError(
|
|
718
|
+
f"Contract validation failed at {self._sanitize_path_for_logging(contract_path)}: {e}",
|
|
719
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
720
|
+
) from e
|
|
721
|
+
except OSError as e:
|
|
722
|
+
raise ModelOnexError(
|
|
723
|
+
f"Failed to read contract file at {self._sanitize_path_for_logging(contract_path)}: {e.strerror or e}",
|
|
724
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
725
|
+
) from e
|
|
726
|
+
|
|
727
|
+
logger.debug(
|
|
728
|
+
"Successfully loaded projector contract: %s",
|
|
729
|
+
self._sanitize_path_for_logging(contract_path),
|
|
730
|
+
extra={
|
|
731
|
+
"contract_path": self._sanitize_path_for_logging(contract_path),
|
|
732
|
+
"projector_id": contract.projector_id,
|
|
733
|
+
"aggregate_type": contract.aggregate_type,
|
|
734
|
+
"consumed_events": contract.consumed_events,
|
|
735
|
+
},
|
|
736
|
+
)
|
|
737
|
+
|
|
738
|
+
# Return placeholder until OMN-1169 implements ProjectorShell
|
|
739
|
+
return self._create_projector(contract)
|
|
740
|
+
|
|
741
|
+
async def load_from_directory(
|
|
742
|
+
self,
|
|
743
|
+
directory: Path,
|
|
744
|
+
) -> list[ProtocolEventProjector]:
|
|
745
|
+
"""Load all projectors from contracts in a directory.
|
|
746
|
+
|
|
747
|
+
Discovers all projector contract files in the specified directory
|
|
748
|
+
(recursively) and loads each as a projector.
|
|
749
|
+
|
|
750
|
+
Args:
|
|
751
|
+
directory: Directory containing contract files. Must exist
|
|
752
|
+
and be a directory.
|
|
753
|
+
|
|
754
|
+
Returns:
|
|
755
|
+
List of configured ProtocolEventProjector instances, one for
|
|
756
|
+
each valid contract file found.
|
|
757
|
+
|
|
758
|
+
Raises:
|
|
759
|
+
FileNotFoundError: If directory does not exist.
|
|
760
|
+
NotADirectoryError: If directory is not a directory.
|
|
761
|
+
ModelOnexError: If any contract file is invalid (in strict mode).
|
|
762
|
+
"""
|
|
763
|
+
if not directory.exists():
|
|
764
|
+
raise FileNotFoundError(
|
|
765
|
+
f"Directory does not exist: {self._sanitize_path_for_logging(directory)}"
|
|
766
|
+
)
|
|
767
|
+
|
|
768
|
+
if not directory.is_dir():
|
|
769
|
+
raise NotADirectoryError(
|
|
770
|
+
f"Path is not a directory: {self._sanitize_path_for_logging(directory)}"
|
|
771
|
+
)
|
|
772
|
+
|
|
773
|
+
# Generate correlation_id for this discovery operation
|
|
774
|
+
discovery_correlation_id = uuid4()
|
|
775
|
+
|
|
776
|
+
start_time = time.perf_counter()
|
|
777
|
+
projectors: list[ProtocolEventProjector] = []
|
|
778
|
+
validation_errors: list[ModelProjectorValidationError] = []
|
|
779
|
+
discovered_paths: set[Path] = set()
|
|
780
|
+
|
|
781
|
+
allowed_bases = self._base_paths if self._base_paths else [directory]
|
|
782
|
+
contract_files = self._find_contract_files(directory)
|
|
783
|
+
|
|
784
|
+
logger.debug(
|
|
785
|
+
"Scanning directory for projector contracts: %s",
|
|
786
|
+
self._sanitize_path_for_logging(directory),
|
|
787
|
+
extra={
|
|
788
|
+
"directory": self._sanitize_path_for_logging(directory),
|
|
789
|
+
"contracts_found": len(contract_files),
|
|
790
|
+
"graceful_mode": self._graceful_mode,
|
|
791
|
+
"correlation_id": str(discovery_correlation_id),
|
|
792
|
+
},
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
for contract_file in contract_files:
|
|
796
|
+
resolved_path = contract_file.resolve()
|
|
797
|
+
if resolved_path in discovered_paths:
|
|
798
|
+
continue
|
|
799
|
+
|
|
800
|
+
discovered_paths.add(resolved_path)
|
|
801
|
+
|
|
802
|
+
# Security validation
|
|
803
|
+
is_valid, error_msg = self._validate_file_security(
|
|
804
|
+
contract_file, allowed_bases
|
|
805
|
+
)
|
|
806
|
+
if not is_valid:
|
|
807
|
+
if not self._graceful_mode:
|
|
808
|
+
raise ModelOnexError(
|
|
809
|
+
error_msg or "Security validation failed",
|
|
810
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
811
|
+
)
|
|
812
|
+
logger.warning(
|
|
813
|
+
"Security validation failed for %s, skipping",
|
|
814
|
+
self._sanitize_path_for_logging(contract_file),
|
|
815
|
+
extra={
|
|
816
|
+
"contract_file": self._sanitize_path_for_logging(contract_file),
|
|
817
|
+
"graceful_mode": self._graceful_mode,
|
|
818
|
+
"correlation_id": str(discovery_correlation_id),
|
|
819
|
+
},
|
|
820
|
+
)
|
|
821
|
+
validation_errors.append(
|
|
822
|
+
self._create_security_error(
|
|
823
|
+
contract_file, error_msg or "", discovery_correlation_id
|
|
824
|
+
)
|
|
825
|
+
)
|
|
826
|
+
continue
|
|
827
|
+
|
|
828
|
+
try:
|
|
829
|
+
contract = self._load_contract(contract_file)
|
|
830
|
+
projector = self._create_projector(contract)
|
|
831
|
+
projectors.append(projector)
|
|
832
|
+
logger.debug(
|
|
833
|
+
"Successfully parsed contract: %s",
|
|
834
|
+
self._sanitize_path_for_logging(contract_file),
|
|
835
|
+
extra={
|
|
836
|
+
"contract_file": self._sanitize_path_for_logging(contract_file),
|
|
837
|
+
"projector_id": contract.projector_id,
|
|
838
|
+
"aggregate_type": contract.aggregate_type,
|
|
839
|
+
},
|
|
840
|
+
)
|
|
841
|
+
except yaml.YAMLError as e:
|
|
842
|
+
error = self._create_parse_error(
|
|
843
|
+
contract_file, e, discovery_correlation_id
|
|
844
|
+
)
|
|
845
|
+
if not self._graceful_mode:
|
|
846
|
+
raise ModelOnexError(
|
|
847
|
+
f"Failed to parse YAML contract at {self._sanitize_path_for_logging(contract_file)}: {e}",
|
|
848
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
849
|
+
) from e
|
|
850
|
+
logger.warning(
|
|
851
|
+
"Failed to parse YAML contract in %s, continuing in graceful mode",
|
|
852
|
+
self._sanitize_path_for_logging(contract_file),
|
|
853
|
+
extra={
|
|
854
|
+
"contract_file": self._sanitize_path_for_logging(contract_file),
|
|
855
|
+
"error_type": "yaml_parse_error",
|
|
856
|
+
"graceful_mode": self._graceful_mode,
|
|
857
|
+
"correlation_id": str(discovery_correlation_id),
|
|
858
|
+
},
|
|
859
|
+
)
|
|
860
|
+
validation_errors.append(error)
|
|
861
|
+
except ValidationError as e:
|
|
862
|
+
error = self._create_validation_error(
|
|
863
|
+
contract_file, e, discovery_correlation_id
|
|
864
|
+
)
|
|
865
|
+
if not self._graceful_mode:
|
|
866
|
+
raise ModelOnexError(
|
|
867
|
+
f"Contract validation failed at {self._sanitize_path_for_logging(contract_file)}: {e}",
|
|
868
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
869
|
+
) from e
|
|
870
|
+
logger.warning(
|
|
871
|
+
"Contract validation failed in %s, continuing in graceful mode",
|
|
872
|
+
self._sanitize_path_for_logging(contract_file),
|
|
873
|
+
extra={
|
|
874
|
+
"contract_file": self._sanitize_path_for_logging(contract_file),
|
|
875
|
+
"error_type": "validation_error",
|
|
876
|
+
"error_count": len(e.errors()),
|
|
877
|
+
"graceful_mode": self._graceful_mode,
|
|
878
|
+
"correlation_id": str(discovery_correlation_id),
|
|
879
|
+
},
|
|
880
|
+
)
|
|
881
|
+
validation_errors.append(error)
|
|
882
|
+
except ModelOnexError as e:
|
|
883
|
+
# In strict mode, always re-raise
|
|
884
|
+
if not self._graceful_mode:
|
|
885
|
+
raise
|
|
886
|
+
|
|
887
|
+
# Graceful mode: collect all ONEX errors
|
|
888
|
+
error_code = getattr(e, "error_code", None)
|
|
889
|
+
error_message = str(e)
|
|
890
|
+
# Check specifically for size limit errors by both error_code AND message
|
|
891
|
+
is_size_limit_error = (
|
|
892
|
+
error_code == EnumCoreErrorCode.VALIDATION_FAILED
|
|
893
|
+
and "size limit" in error_message.lower()
|
|
894
|
+
)
|
|
895
|
+
if is_size_limit_error:
|
|
896
|
+
# File size limit error
|
|
897
|
+
try:
|
|
898
|
+
file_size = contract_file.stat().st_size
|
|
899
|
+
except OSError:
|
|
900
|
+
file_size = 0
|
|
901
|
+
error = self._create_size_limit_error(
|
|
902
|
+
contract_file, file_size, discovery_correlation_id
|
|
903
|
+
)
|
|
904
|
+
logger.warning(
|
|
905
|
+
"Contract file %s exceeds size limit, continuing in graceful mode",
|
|
906
|
+
self._sanitize_path_for_logging(contract_file),
|
|
907
|
+
extra={
|
|
908
|
+
"contract_file": self._sanitize_path_for_logging(
|
|
909
|
+
contract_file
|
|
910
|
+
),
|
|
911
|
+
"error_type": "size_limit_error",
|
|
912
|
+
"graceful_mode": self._graceful_mode,
|
|
913
|
+
"correlation_id": str(discovery_correlation_id),
|
|
914
|
+
},
|
|
915
|
+
)
|
|
916
|
+
validation_errors.append(error)
|
|
917
|
+
else:
|
|
918
|
+
# Other ONEX errors - collect in graceful mode
|
|
919
|
+
error = ModelProjectorValidationError(
|
|
920
|
+
error_type="ONEX_ERROR",
|
|
921
|
+
contract_path=self._sanitize_path_for_logging(contract_file),
|
|
922
|
+
message=error_message,
|
|
923
|
+
remediation_hint="Check the contract file for issues",
|
|
924
|
+
correlation_id=discovery_correlation_id,
|
|
925
|
+
)
|
|
926
|
+
logger.warning(
|
|
927
|
+
"ONEX error processing %s, continuing in graceful mode",
|
|
928
|
+
self._sanitize_path_for_logging(contract_file),
|
|
929
|
+
extra={
|
|
930
|
+
"contract_file": self._sanitize_path_for_logging(
|
|
931
|
+
contract_file
|
|
932
|
+
),
|
|
933
|
+
"error_type": "onex_error",
|
|
934
|
+
"graceful_mode": self._graceful_mode,
|
|
935
|
+
"correlation_id": str(discovery_correlation_id),
|
|
936
|
+
},
|
|
937
|
+
)
|
|
938
|
+
validation_errors.append(error)
|
|
939
|
+
except OSError as e:
|
|
940
|
+
if not self._graceful_mode:
|
|
941
|
+
raise ModelOnexError(
|
|
942
|
+
f"Failed to read contract file at {self._sanitize_path_for_logging(contract_file)}: {e}",
|
|
943
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
944
|
+
) from e
|
|
945
|
+
error = self._create_io_error(
|
|
946
|
+
contract_file, e, discovery_correlation_id
|
|
947
|
+
)
|
|
948
|
+
logger.warning(
|
|
949
|
+
"Failed to read contract file, continuing in graceful mode: %s",
|
|
950
|
+
self._sanitize_path_for_logging(contract_file),
|
|
951
|
+
extra={
|
|
952
|
+
"contract_file": self._sanitize_path_for_logging(contract_file),
|
|
953
|
+
"error_type": "io_error",
|
|
954
|
+
# Use strerror to avoid leaking full paths
|
|
955
|
+
"error_message": e.strerror or "unknown error",
|
|
956
|
+
"graceful_mode": self._graceful_mode,
|
|
957
|
+
"correlation_id": str(discovery_correlation_id),
|
|
958
|
+
},
|
|
959
|
+
)
|
|
960
|
+
validation_errors.append(error)
|
|
961
|
+
|
|
962
|
+
duration_seconds = time.perf_counter() - start_time
|
|
963
|
+
self._log_discovery_results(
|
|
964
|
+
len(projectors), len(validation_errors), duration_seconds
|
|
965
|
+
)
|
|
966
|
+
|
|
967
|
+
return projectors
|
|
968
|
+
|
|
969
|
+
async def discover_and_load(
|
|
970
|
+
self,
|
|
971
|
+
patterns: list[str],
|
|
972
|
+
) -> list[ProtocolEventProjector]:
|
|
973
|
+
"""Discover contracts matching glob patterns and load projectors.
|
|
974
|
+
|
|
975
|
+
Supports flexible contract discovery using glob patterns,
|
|
976
|
+
enabling contracts to be organized in various directory structures.
|
|
977
|
+
|
|
978
|
+
Args:
|
|
979
|
+
patterns: List of glob patterns to match contract files.
|
|
980
|
+
Patterns are relative to the current working directory
|
|
981
|
+
unless absolute. Supports recursive patterns (**).
|
|
982
|
+
|
|
983
|
+
Examples:
|
|
984
|
+
- "contracts/*.yaml" - all YAML in contracts/
|
|
985
|
+
- "**/projectors/*.yaml" - recursive projector discovery
|
|
986
|
+
- "modules/*/projections.yml" - per-module contracts
|
|
987
|
+
|
|
988
|
+
Returns:
|
|
989
|
+
List of configured ProtocolEventProjector instances for all
|
|
990
|
+
contracts matching any of the patterns. Duplicates (same
|
|
991
|
+
file matched by multiple patterns) are deduplicated.
|
|
992
|
+
|
|
993
|
+
Raises:
|
|
994
|
+
ModelOnexError: If any matched contract is invalid (in strict mode).
|
|
995
|
+
"""
|
|
996
|
+
# Generate correlation_id for this discovery operation
|
|
997
|
+
discovery_correlation_id = uuid4()
|
|
998
|
+
|
|
999
|
+
start_time = time.perf_counter()
|
|
1000
|
+
projectors: list[ProtocolEventProjector] = []
|
|
1001
|
+
validation_errors: list[ModelProjectorValidationError] = []
|
|
1002
|
+
discovered_paths: set[Path] = set()
|
|
1003
|
+
|
|
1004
|
+
# Determine base paths from patterns for security validation
|
|
1005
|
+
cwd = Path.cwd()
|
|
1006
|
+
allowed_bases = self._base_paths if self._base_paths else [cwd]
|
|
1007
|
+
|
|
1008
|
+
logger.debug(
|
|
1009
|
+
"Starting projector discovery with patterns",
|
|
1010
|
+
extra={
|
|
1011
|
+
"patterns": patterns,
|
|
1012
|
+
"graceful_mode": self._graceful_mode,
|
|
1013
|
+
"cwd": self._sanitize_path_for_logging(cwd),
|
|
1014
|
+
"correlation_id": str(discovery_correlation_id),
|
|
1015
|
+
},
|
|
1016
|
+
)
|
|
1017
|
+
|
|
1018
|
+
# Phase 1: Collect all matched files from all patterns (for count-based DoS prevention)
|
|
1019
|
+
unique_contract_files: list[Path] = []
|
|
1020
|
+
seen_resolved: set[Path] = set()
|
|
1021
|
+
|
|
1022
|
+
for pattern in patterns:
|
|
1023
|
+
pattern_path = Path(pattern)
|
|
1024
|
+
|
|
1025
|
+
# Use glob from cwd for relative patterns
|
|
1026
|
+
if not pattern_path.is_absolute():
|
|
1027
|
+
# Security: If base_paths is configured, ensure cwd is within allowed paths
|
|
1028
|
+
# to prevent bypassing path restrictions with relative patterns
|
|
1029
|
+
if self._base_paths:
|
|
1030
|
+
cwd_resolved = cwd.resolve()
|
|
1031
|
+
cwd_allowed = any(
|
|
1032
|
+
cwd_resolved.is_relative_to(base.resolve())
|
|
1033
|
+
for base in self._base_paths
|
|
1034
|
+
)
|
|
1035
|
+
if not cwd_allowed:
|
|
1036
|
+
raise ModelOnexError(
|
|
1037
|
+
"Relative patterns require cwd to be within allowed base_paths",
|
|
1038
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
1039
|
+
)
|
|
1040
|
+
matched_files = list(cwd.glob(pattern))
|
|
1041
|
+
else:
|
|
1042
|
+
# Reject absolute glob patterns unless explicit base_paths configured
|
|
1043
|
+
if not self._base_paths:
|
|
1044
|
+
raise ModelOnexError(
|
|
1045
|
+
"Absolute glob patterns are not allowed without explicit base_paths",
|
|
1046
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
1047
|
+
)
|
|
1048
|
+
|
|
1049
|
+
# Security: Explicitly reject patterns starting from root
|
|
1050
|
+
# to prevent DoS via filesystem-wide glob scanning (e.g., "/**/foo.yaml")
|
|
1051
|
+
pattern_resolved = pattern_path.resolve()
|
|
1052
|
+
if (
|
|
1053
|
+
pattern_resolved == Path("/").resolve()
|
|
1054
|
+
or len(pattern_resolved.parts) <= 1
|
|
1055
|
+
):
|
|
1056
|
+
raise ModelOnexError(
|
|
1057
|
+
"Root-level absolute patterns are not allowed - "
|
|
1058
|
+
"would cause filesystem-wide scanning",
|
|
1059
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
1060
|
+
)
|
|
1061
|
+
|
|
1062
|
+
# Security: Validate absolute pattern is under an allowed base path
|
|
1063
|
+
# to prevent DoS via filesystem-wide glob scanning
|
|
1064
|
+
allowed_base = None
|
|
1065
|
+
for base in self._base_paths:
|
|
1066
|
+
try:
|
|
1067
|
+
base_resolved = base.resolve()
|
|
1068
|
+
# Check if pattern starts within this base (for glob patterns,
|
|
1069
|
+
# check the non-glob prefix)
|
|
1070
|
+
pattern_prefix = pattern_resolved
|
|
1071
|
+
# Find the first glob component to get the concrete prefix
|
|
1072
|
+
pattern_parts = pattern_path.parts
|
|
1073
|
+
concrete_parts: list[str] = []
|
|
1074
|
+
for part in pattern_parts:
|
|
1075
|
+
if "*" in part or "?" in part or "[" in part:
|
|
1076
|
+
break
|
|
1077
|
+
concrete_parts.append(part)
|
|
1078
|
+
if concrete_parts:
|
|
1079
|
+
pattern_prefix = Path(*concrete_parts).resolve()
|
|
1080
|
+
|
|
1081
|
+
if pattern_prefix.is_relative_to(base_resolved):
|
|
1082
|
+
allowed_base = base_resolved
|
|
1083
|
+
break
|
|
1084
|
+
except (ValueError, OSError):
|
|
1085
|
+
continue
|
|
1086
|
+
|
|
1087
|
+
if allowed_base is None:
|
|
1088
|
+
raise ModelOnexError(
|
|
1089
|
+
"Absolute pattern not under allowed base_paths",
|
|
1090
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
1091
|
+
)
|
|
1092
|
+
|
|
1093
|
+
# Glob from the allowed base, using relative pattern portion
|
|
1094
|
+
try:
|
|
1095
|
+
# Use the resolved pattern_prefix (non-glob prefix) to compute
|
|
1096
|
+
# the relative path, then reconstruct the pattern
|
|
1097
|
+
relative_prefix = pattern_prefix.relative_to(allowed_base)
|
|
1098
|
+
# Get the glob suffix (parts after the concrete prefix)
|
|
1099
|
+
glob_suffix_parts = pattern_path.parts[len(concrete_parts) :]
|
|
1100
|
+
if glob_suffix_parts:
|
|
1101
|
+
relative_pattern = str(
|
|
1102
|
+
relative_prefix / Path(*glob_suffix_parts)
|
|
1103
|
+
)
|
|
1104
|
+
else:
|
|
1105
|
+
relative_pattern = str(relative_prefix)
|
|
1106
|
+
except ValueError:
|
|
1107
|
+
# If relative_to fails even after is_relative_to succeeded,
|
|
1108
|
+
# this indicates a path resolution issue (e.g., symlinks)
|
|
1109
|
+
# Reject the pattern to prevent potential security bypass
|
|
1110
|
+
raise ModelOnexError(
|
|
1111
|
+
"Absolute pattern path resolution failed - "
|
|
1112
|
+
"possible symlink security issue",
|
|
1113
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
1114
|
+
) from None
|
|
1115
|
+
|
|
1116
|
+
matched_files = list(allowed_base.glob(relative_pattern))
|
|
1117
|
+
|
|
1118
|
+
# Filter to projector contracts
|
|
1119
|
+
matched_files = [
|
|
1120
|
+
f for f in matched_files if self._is_projector_contract(f.name)
|
|
1121
|
+
]
|
|
1122
|
+
|
|
1123
|
+
# Collect unique files (deduplicate by resolved path)
|
|
1124
|
+
for contract_file in matched_files:
|
|
1125
|
+
resolved_path = contract_file.resolve()
|
|
1126
|
+
if resolved_path not in seen_resolved:
|
|
1127
|
+
seen_resolved.add(resolved_path)
|
|
1128
|
+
unique_contract_files.append(contract_file)
|
|
1129
|
+
|
|
1130
|
+
# Phase 2: Check file count to prevent DoS via expensive glob patterns
|
|
1131
|
+
total_matched = len(unique_contract_files)
|
|
1132
|
+
if total_matched > MAX_DISCOVERY_FILES:
|
|
1133
|
+
raise ModelOnexError(
|
|
1134
|
+
f"Discovery aborted: matched {total_matched} files (limit: {MAX_DISCOVERY_FILES}). "
|
|
1135
|
+
"Use more specific patterns to reduce scope.",
|
|
1136
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
1137
|
+
)
|
|
1138
|
+
|
|
1139
|
+
logger.debug(
|
|
1140
|
+
"Discovery matched %d unique contract files",
|
|
1141
|
+
total_matched,
|
|
1142
|
+
extra={
|
|
1143
|
+
"total_matched": total_matched,
|
|
1144
|
+
"max_allowed": MAX_DISCOVERY_FILES,
|
|
1145
|
+
"correlation_id": str(discovery_correlation_id),
|
|
1146
|
+
},
|
|
1147
|
+
)
|
|
1148
|
+
|
|
1149
|
+
# Phase 3: Process each collected file
|
|
1150
|
+
for contract_file in unique_contract_files:
|
|
1151
|
+
resolved_path = contract_file.resolve()
|
|
1152
|
+
discovered_paths.add(resolved_path)
|
|
1153
|
+
|
|
1154
|
+
# Security validation
|
|
1155
|
+
is_valid, error_msg = self._validate_file_security(
|
|
1156
|
+
contract_file, allowed_bases
|
|
1157
|
+
)
|
|
1158
|
+
if not is_valid:
|
|
1159
|
+
if not self._graceful_mode:
|
|
1160
|
+
raise ModelOnexError(
|
|
1161
|
+
error_msg or "Security validation failed",
|
|
1162
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
1163
|
+
)
|
|
1164
|
+
logger.warning(
|
|
1165
|
+
"Security validation failed for %s, skipping",
|
|
1166
|
+
self._sanitize_path_for_logging(contract_file),
|
|
1167
|
+
extra={"correlation_id": str(discovery_correlation_id)},
|
|
1168
|
+
)
|
|
1169
|
+
validation_errors.append(
|
|
1170
|
+
self._create_security_error(
|
|
1171
|
+
contract_file, error_msg or "", discovery_correlation_id
|
|
1172
|
+
)
|
|
1173
|
+
)
|
|
1174
|
+
continue
|
|
1175
|
+
|
|
1176
|
+
try:
|
|
1177
|
+
contract = self._load_contract(contract_file)
|
|
1178
|
+
projector = self._create_projector(contract)
|
|
1179
|
+
projectors.append(projector)
|
|
1180
|
+
logger.debug(
|
|
1181
|
+
"Successfully loaded contract from pattern: %s",
|
|
1182
|
+
self._sanitize_path_for_logging(contract_file),
|
|
1183
|
+
extra={
|
|
1184
|
+
"contract_file": self._sanitize_path_for_logging(contract_file),
|
|
1185
|
+
"projector_id": contract.projector_id,
|
|
1186
|
+
"correlation_id": str(discovery_correlation_id),
|
|
1187
|
+
},
|
|
1188
|
+
)
|
|
1189
|
+
except yaml.YAMLError as e:
|
|
1190
|
+
error = self._create_parse_error(
|
|
1191
|
+
contract_file, e, discovery_correlation_id
|
|
1192
|
+
)
|
|
1193
|
+
if not self._graceful_mode:
|
|
1194
|
+
raise ModelOnexError(
|
|
1195
|
+
f"Failed to parse YAML contract at {self._sanitize_path_for_logging(contract_file)}: {e}",
|
|
1196
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
1197
|
+
) from e
|
|
1198
|
+
validation_errors.append(error)
|
|
1199
|
+
except ValidationError as e:
|
|
1200
|
+
error = self._create_validation_error(
|
|
1201
|
+
contract_file, e, discovery_correlation_id
|
|
1202
|
+
)
|
|
1203
|
+
if not self._graceful_mode:
|
|
1204
|
+
raise ModelOnexError(
|
|
1205
|
+
f"Contract validation failed at {self._sanitize_path_for_logging(contract_file)}: {e}",
|
|
1206
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
1207
|
+
) from e
|
|
1208
|
+
validation_errors.append(error)
|
|
1209
|
+
except ModelOnexError as e:
|
|
1210
|
+
# In strict mode, always re-raise
|
|
1211
|
+
if not self._graceful_mode:
|
|
1212
|
+
raise
|
|
1213
|
+
|
|
1214
|
+
# Graceful mode: collect all ONEX errors
|
|
1215
|
+
error_code = getattr(e, "error_code", None)
|
|
1216
|
+
error_message = str(e)
|
|
1217
|
+
# Check specifically for size limit errors by both error_code AND message
|
|
1218
|
+
is_size_limit_error = (
|
|
1219
|
+
error_code == EnumCoreErrorCode.VALIDATION_FAILED
|
|
1220
|
+
and "size limit" in error_message.lower()
|
|
1221
|
+
)
|
|
1222
|
+
if is_size_limit_error:
|
|
1223
|
+
try:
|
|
1224
|
+
file_size = contract_file.stat().st_size
|
|
1225
|
+
except OSError:
|
|
1226
|
+
file_size = 0
|
|
1227
|
+
validation_errors.append(
|
|
1228
|
+
self._create_size_limit_error(
|
|
1229
|
+
contract_file, file_size, discovery_correlation_id
|
|
1230
|
+
)
|
|
1231
|
+
)
|
|
1232
|
+
else:
|
|
1233
|
+
# Other ONEX errors - collect in graceful mode
|
|
1234
|
+
validation_errors.append(
|
|
1235
|
+
ModelProjectorValidationError(
|
|
1236
|
+
error_type="ONEX_ERROR",
|
|
1237
|
+
contract_path=self._sanitize_path_for_logging(
|
|
1238
|
+
contract_file
|
|
1239
|
+
),
|
|
1240
|
+
message=error_message,
|
|
1241
|
+
remediation_hint="Check the contract file for issues",
|
|
1242
|
+
correlation_id=discovery_correlation_id,
|
|
1243
|
+
)
|
|
1244
|
+
)
|
|
1245
|
+
except OSError as e:
|
|
1246
|
+
if not self._graceful_mode:
|
|
1247
|
+
raise ModelOnexError(
|
|
1248
|
+
f"Failed to read contract file at {self._sanitize_path_for_logging(contract_file)}: {e}",
|
|
1249
|
+
error_code=EnumCoreErrorCode.VALIDATION_FAILED,
|
|
1250
|
+
) from e
|
|
1251
|
+
validation_errors.append(
|
|
1252
|
+
self._create_io_error(contract_file, e, discovery_correlation_id)
|
|
1253
|
+
)
|
|
1254
|
+
|
|
1255
|
+
duration_seconds = time.perf_counter() - start_time
|
|
1256
|
+
self._log_discovery_results(
|
|
1257
|
+
len(projectors), len(validation_errors), duration_seconds
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1260
|
+
return projectors
|
|
1261
|
+
|
|
1262
|
+
async def discover_with_errors(
|
|
1263
|
+
self,
|
|
1264
|
+
directory: Path,
|
|
1265
|
+
) -> ModelProjectorDiscoveryResult:
|
|
1266
|
+
"""Discover projectors and return both successes and errors.
|
|
1267
|
+
|
|
1268
|
+
This method always operates in graceful mode, collecting all
|
|
1269
|
+
errors rather than raising on the first failure.
|
|
1270
|
+
|
|
1271
|
+
Args:
|
|
1272
|
+
directory: Directory to scan for contract files.
|
|
1273
|
+
|
|
1274
|
+
Returns:
|
|
1275
|
+
ModelProjectorDiscoveryResult containing both loaded projectors
|
|
1276
|
+
and validation errors.
|
|
1277
|
+
|
|
1278
|
+
Raises:
|
|
1279
|
+
FileNotFoundError: If directory does not exist.
|
|
1280
|
+
NotADirectoryError: If directory is not a directory.
|
|
1281
|
+
"""
|
|
1282
|
+
if not directory.exists():
|
|
1283
|
+
raise FileNotFoundError(
|
|
1284
|
+
f"Directory does not exist: {self._sanitize_path_for_logging(directory)}"
|
|
1285
|
+
)
|
|
1286
|
+
|
|
1287
|
+
if not directory.is_dir():
|
|
1288
|
+
raise NotADirectoryError(
|
|
1289
|
+
f"Path is not a directory: {self._sanitize_path_for_logging(directory)}"
|
|
1290
|
+
)
|
|
1291
|
+
|
|
1292
|
+
# Generate correlation_id for this discovery operation
|
|
1293
|
+
discovery_correlation_id = uuid4()
|
|
1294
|
+
|
|
1295
|
+
start_time = time.perf_counter()
|
|
1296
|
+
projectors: list[ProtocolEventProjector] = []
|
|
1297
|
+
validation_errors: list[ModelProjectorValidationError] = []
|
|
1298
|
+
discovered_paths: set[Path] = set()
|
|
1299
|
+
|
|
1300
|
+
allowed_bases = self._base_paths if self._base_paths else [directory]
|
|
1301
|
+
contract_files = self._find_contract_files(directory)
|
|
1302
|
+
|
|
1303
|
+
logger.debug(
|
|
1304
|
+
"Scanning directory for projector contracts with error collection: %s",
|
|
1305
|
+
self._sanitize_path_for_logging(directory),
|
|
1306
|
+
extra={
|
|
1307
|
+
"directory": self._sanitize_path_for_logging(directory),
|
|
1308
|
+
"contracts_found": len(contract_files),
|
|
1309
|
+
"correlation_id": str(discovery_correlation_id),
|
|
1310
|
+
},
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1313
|
+
for contract_file in contract_files:
|
|
1314
|
+
resolved_path = contract_file.resolve()
|
|
1315
|
+
if resolved_path in discovered_paths:
|
|
1316
|
+
continue
|
|
1317
|
+
|
|
1318
|
+
discovered_paths.add(resolved_path)
|
|
1319
|
+
|
|
1320
|
+
# Security validation
|
|
1321
|
+
is_valid, error_msg = self._validate_file_security(
|
|
1322
|
+
contract_file, allowed_bases
|
|
1323
|
+
)
|
|
1324
|
+
if not is_valid:
|
|
1325
|
+
logger.warning(
|
|
1326
|
+
"Security validation failed for %s, collecting error",
|
|
1327
|
+
self._sanitize_path_for_logging(contract_file),
|
|
1328
|
+
extra={
|
|
1329
|
+
"contract_file": self._sanitize_path_for_logging(contract_file),
|
|
1330
|
+
"correlation_id": str(discovery_correlation_id),
|
|
1331
|
+
},
|
|
1332
|
+
)
|
|
1333
|
+
validation_errors.append(
|
|
1334
|
+
self._create_security_error(
|
|
1335
|
+
contract_file, error_msg or "", discovery_correlation_id
|
|
1336
|
+
)
|
|
1337
|
+
)
|
|
1338
|
+
continue
|
|
1339
|
+
|
|
1340
|
+
try:
|
|
1341
|
+
contract = self._load_contract(contract_file)
|
|
1342
|
+
projector = self._create_projector(contract)
|
|
1343
|
+
projectors.append(projector)
|
|
1344
|
+
logger.debug(
|
|
1345
|
+
"Successfully parsed contract: %s",
|
|
1346
|
+
self._sanitize_path_for_logging(contract_file),
|
|
1347
|
+
extra={
|
|
1348
|
+
"contract_file": self._sanitize_path_for_logging(contract_file),
|
|
1349
|
+
"projector_id": contract.projector_id,
|
|
1350
|
+
"aggregate_type": contract.aggregate_type,
|
|
1351
|
+
"correlation_id": str(discovery_correlation_id),
|
|
1352
|
+
},
|
|
1353
|
+
)
|
|
1354
|
+
except yaml.YAMLError as e:
|
|
1355
|
+
error = self._create_parse_error(
|
|
1356
|
+
contract_file, e, discovery_correlation_id
|
|
1357
|
+
)
|
|
1358
|
+
logger.warning(
|
|
1359
|
+
"Failed to parse YAML contract in %s, collecting error",
|
|
1360
|
+
self._sanitize_path_for_logging(contract_file),
|
|
1361
|
+
extra={
|
|
1362
|
+
"contract_file": self._sanitize_path_for_logging(contract_file),
|
|
1363
|
+
"error_type": "yaml_parse_error",
|
|
1364
|
+
"correlation_id": str(discovery_correlation_id),
|
|
1365
|
+
},
|
|
1366
|
+
)
|
|
1367
|
+
validation_errors.append(error)
|
|
1368
|
+
except ValidationError as e:
|
|
1369
|
+
error = self._create_validation_error(
|
|
1370
|
+
contract_file, e, discovery_correlation_id
|
|
1371
|
+
)
|
|
1372
|
+
logger.warning(
|
|
1373
|
+
"Contract validation failed in %s, collecting error",
|
|
1374
|
+
self._sanitize_path_for_logging(contract_file),
|
|
1375
|
+
extra={
|
|
1376
|
+
"contract_file": self._sanitize_path_for_logging(contract_file),
|
|
1377
|
+
"error_type": "validation_error",
|
|
1378
|
+
"error_count": len(e.errors()),
|
|
1379
|
+
"correlation_id": str(discovery_correlation_id),
|
|
1380
|
+
},
|
|
1381
|
+
)
|
|
1382
|
+
validation_errors.append(error)
|
|
1383
|
+
except ModelOnexError as e:
|
|
1384
|
+
error_code = getattr(e, "error_code", None)
|
|
1385
|
+
error_message = str(e)
|
|
1386
|
+
# Check specifically for size limit errors by both error_code AND message
|
|
1387
|
+
is_size_limit_error = (
|
|
1388
|
+
error_code == EnumCoreErrorCode.VALIDATION_FAILED
|
|
1389
|
+
and "size limit" in error_message.lower()
|
|
1390
|
+
)
|
|
1391
|
+
if is_size_limit_error:
|
|
1392
|
+
# File size limit error
|
|
1393
|
+
try:
|
|
1394
|
+
file_size = contract_file.stat().st_size
|
|
1395
|
+
except OSError:
|
|
1396
|
+
file_size = 0
|
|
1397
|
+
error = self._create_size_limit_error(
|
|
1398
|
+
contract_file, file_size, discovery_correlation_id
|
|
1399
|
+
)
|
|
1400
|
+
logger.warning(
|
|
1401
|
+
"Contract file %s exceeds size limit, collecting error",
|
|
1402
|
+
self._sanitize_path_for_logging(contract_file),
|
|
1403
|
+
extra={
|
|
1404
|
+
"contract_file": self._sanitize_path_for_logging(
|
|
1405
|
+
contract_file
|
|
1406
|
+
),
|
|
1407
|
+
"error_type": "size_limit_error",
|
|
1408
|
+
"correlation_id": str(discovery_correlation_id),
|
|
1409
|
+
},
|
|
1410
|
+
)
|
|
1411
|
+
validation_errors.append(error)
|
|
1412
|
+
else:
|
|
1413
|
+
# For other ModelOnexError types, create a generic validation error
|
|
1414
|
+
error = ModelProjectorValidationError(
|
|
1415
|
+
error_type="ONEX_ERROR",
|
|
1416
|
+
contract_path=self._sanitize_path_for_logging(contract_file),
|
|
1417
|
+
message=error_message,
|
|
1418
|
+
remediation_hint="Check the contract file for issues",
|
|
1419
|
+
correlation_id=discovery_correlation_id,
|
|
1420
|
+
)
|
|
1421
|
+
validation_errors.append(error)
|
|
1422
|
+
except OSError as e:
|
|
1423
|
+
error = self._create_io_error(
|
|
1424
|
+
contract_file, e, discovery_correlation_id
|
|
1425
|
+
)
|
|
1426
|
+
logger.warning(
|
|
1427
|
+
"Failed to read contract file, collecting error: %s",
|
|
1428
|
+
self._sanitize_path_for_logging(contract_file),
|
|
1429
|
+
extra={
|
|
1430
|
+
"contract_file": self._sanitize_path_for_logging(contract_file),
|
|
1431
|
+
"error_type": "io_error",
|
|
1432
|
+
# Use strerror to avoid leaking full paths
|
|
1433
|
+
"error_message": e.strerror or "unknown error",
|
|
1434
|
+
"correlation_id": str(discovery_correlation_id),
|
|
1435
|
+
},
|
|
1436
|
+
)
|
|
1437
|
+
validation_errors.append(error)
|
|
1438
|
+
|
|
1439
|
+
duration_seconds = time.perf_counter() - start_time
|
|
1440
|
+
self._log_discovery_results(
|
|
1441
|
+
len(projectors), len(validation_errors), duration_seconds
|
|
1442
|
+
)
|
|
1443
|
+
|
|
1444
|
+
return ModelProjectorDiscoveryResult(
|
|
1445
|
+
projectors=projectors,
|
|
1446
|
+
validation_errors=validation_errors,
|
|
1447
|
+
discovery_correlation_id=discovery_correlation_id,
|
|
1448
|
+
)
|
|
1449
|
+
|
|
1450
|
+
|
|
1451
|
+
__all__ = [
|
|
1452
|
+
"MAX_CONTRACT_SIZE",
|
|
1453
|
+
"MAX_DISCOVERY_FILES",
|
|
1454
|
+
"ModelProjectorDiscoveryResult", # Re-exported from omnibase_infra.models.projectors
|
|
1455
|
+
"ModelProjectorValidationError", # Re-exported from omnibase_infra.models.projectors
|
|
1456
|
+
"PROJECTOR_CONTRACT_PATTERNS",
|
|
1457
|
+
"ProjectorPluginLoader",
|
|
1458
|
+
"ProjectorShell", # Full implementation with database access (OMN-1169)
|
|
1459
|
+
"ProjectorShellPlaceholder", # Placeholder when no pool is provided
|
|
1460
|
+
"ProtocolEventProjector", # Re-exported from omnibase_infra.protocols
|
|
1461
|
+
"ProtocolProjectorSchemaValidator", # Re-exported from omnibase_infra.protocols
|
|
1462
|
+
]
|