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,1478 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
# Copyright (c) 2025 OmniNode Team
|
|
3
|
+
"""Filesystem Handler - Secure filesystem operations with path whitelisting.
|
|
4
|
+
|
|
5
|
+
Provides secure filesystem operations including read, write, list, ensure directory,
|
|
6
|
+
and delete operations with comprehensive security features.
|
|
7
|
+
|
|
8
|
+
Security Features:
|
|
9
|
+
- Path whitelist validation: Only allowed directories can be accessed
|
|
10
|
+
- File size limits: Configurable max sizes for read/write operations
|
|
11
|
+
- Symlink protection: Symlinks are resolved and validated against allowed paths
|
|
12
|
+
- Path traversal prevention: Prevents escape from allowed directories via ../
|
|
13
|
+
|
|
14
|
+
Supported Operations:
|
|
15
|
+
- filesystem.read_file: Read file contents (text or binary)
|
|
16
|
+
- filesystem.write_file: Write content to file
|
|
17
|
+
- filesystem.list_directory: List directory contents with optional glob filtering
|
|
18
|
+
- filesystem.ensure_directory: Create directory structure
|
|
19
|
+
- filesystem.delete_file: Delete file with safety checks
|
|
20
|
+
|
|
21
|
+
Note:
|
|
22
|
+
Environment variable configuration (ONEX_FS_MAX_READ_SIZE, ONEX_FS_MAX_WRITE_SIZE)
|
|
23
|
+
is parsed at module import time, not at handler instantiation. This means:
|
|
24
|
+
|
|
25
|
+
- Changes to environment variables require application restart to take effect
|
|
26
|
+
- Tests should use ``unittest.mock.patch.dict(os.environ, ...)`` before importing,
|
|
27
|
+
or use ``importlib.reload()`` to re-import the module after patching
|
|
28
|
+
- This is an intentional design choice for startup-time validation
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import base64
|
|
34
|
+
import binascii
|
|
35
|
+
import errno
|
|
36
|
+
import fnmatch
|
|
37
|
+
import logging
|
|
38
|
+
import os
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from uuid import UUID, uuid4
|
|
41
|
+
|
|
42
|
+
from omnibase_core.container import ModelONEXContainer
|
|
43
|
+
from omnibase_core.models.dispatch import ModelHandlerOutput
|
|
44
|
+
from omnibase_infra.enums import (
|
|
45
|
+
EnumHandlerType,
|
|
46
|
+
EnumHandlerTypeCategory,
|
|
47
|
+
EnumInfraTransportType,
|
|
48
|
+
)
|
|
49
|
+
from omnibase_infra.errors import (
|
|
50
|
+
InfraConnectionError,
|
|
51
|
+
InfraUnavailableError,
|
|
52
|
+
ModelInfraErrorContext,
|
|
53
|
+
ProtocolConfigurationError,
|
|
54
|
+
RuntimeHostError,
|
|
55
|
+
)
|
|
56
|
+
from omnibase_infra.mixins import MixinAsyncCircuitBreaker, MixinEnvelopeExtraction
|
|
57
|
+
from omnibase_infra.utils import parse_env_int
|
|
58
|
+
|
|
59
|
+
logger = logging.getLogger(__name__)
|
|
60
|
+
|
|
61
|
+
# Default configuration from environment
|
|
62
|
+
_DEFAULT_MAX_READ_SIZE: int = parse_env_int(
|
|
63
|
+
"ONEX_FS_MAX_READ_SIZE",
|
|
64
|
+
100 * 1024 * 1024, # 100 MB
|
|
65
|
+
min_value=1024,
|
|
66
|
+
max_value=1024 * 1024 * 1024, # 1 GB
|
|
67
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
68
|
+
service_name="filesystem_handler",
|
|
69
|
+
)
|
|
70
|
+
_DEFAULT_MAX_WRITE_SIZE: int = parse_env_int(
|
|
71
|
+
"ONEX_FS_MAX_WRITE_SIZE",
|
|
72
|
+
50 * 1024 * 1024, # 50 MB
|
|
73
|
+
min_value=1024,
|
|
74
|
+
max_value=500 * 1024 * 1024, # 500 MB
|
|
75
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
76
|
+
service_name="filesystem_handler",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
_SUPPORTED_OPERATIONS: frozenset[str] = frozenset(
|
|
80
|
+
{
|
|
81
|
+
"filesystem.read_file",
|
|
82
|
+
"filesystem.write_file",
|
|
83
|
+
"filesystem.list_directory",
|
|
84
|
+
"filesystem.ensure_directory",
|
|
85
|
+
"filesystem.delete_file",
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Handler ID for ModelHandlerOutput
|
|
90
|
+
HANDLER_ID_FILESYSTEM: str = "filesystem-handler"
|
|
91
|
+
|
|
92
|
+
# Size category thresholds for sanitized logging
|
|
93
|
+
_SIZE_THRESHOLD_KB: int = 1024 # 1 KB
|
|
94
|
+
_SIZE_THRESHOLD_MB: int = 1024 * 1024 # 1 MB
|
|
95
|
+
_SIZE_THRESHOLD_10MB: int = 10 * 1024 * 1024 # 10 MB
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _categorize_size(size: int) -> str:
|
|
99
|
+
"""Categorize byte size into security-safe categories.
|
|
100
|
+
|
|
101
|
+
This prevents exact payload sizes from being exposed in error messages
|
|
102
|
+
and logs, which could help attackers probe size limits.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
size: Size in bytes
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Size category: "small", "medium", "large", or "very_large"
|
|
109
|
+
"""
|
|
110
|
+
if size < _SIZE_THRESHOLD_KB:
|
|
111
|
+
return "small"
|
|
112
|
+
elif size < _SIZE_THRESHOLD_MB:
|
|
113
|
+
return "medium"
|
|
114
|
+
elif size < _SIZE_THRESHOLD_10MB:
|
|
115
|
+
return "large"
|
|
116
|
+
else:
|
|
117
|
+
return "very_large"
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class HandlerFileSystem(MixinEnvelopeExtraction, MixinAsyncCircuitBreaker):
|
|
121
|
+
"""Filesystem handler with security features for ONEX infrastructure.
|
|
122
|
+
|
|
123
|
+
Security Features:
|
|
124
|
+
- Path whitelist validation to restrict file access to allowed directories
|
|
125
|
+
- File size limits to prevent DoS attacks via memory exhaustion
|
|
126
|
+
- Symlink resolution and validation to prevent path traversal attacks
|
|
127
|
+
- All paths are resolved to absolute canonical paths before validation
|
|
128
|
+
- Circuit breaker for resilient operation
|
|
129
|
+
|
|
130
|
+
Configuration:
|
|
131
|
+
Initialize with allowed_paths to define accessible directories.
|
|
132
|
+
Configure max_read_size and max_write_size to control memory usage.
|
|
133
|
+
"""
|
|
134
|
+
|
|
135
|
+
def __init__(self, container: ModelONEXContainer | None = None) -> None:
|
|
136
|
+
"""Initialize HandlerFileSystem with optional container injection.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
container: Optional ONEX container for dependency injection.
|
|
140
|
+
When provided, enables full ONEX integration. When None,
|
|
141
|
+
handler operates in standalone mode for testing.
|
|
142
|
+
|
|
143
|
+
Note:
|
|
144
|
+
The container is stored for interface compliance with the standard ONEX
|
|
145
|
+
handler pattern and to enable future DI-based service resolution (e.g.,
|
|
146
|
+
metrics, logging, observability integration). Currently, the handler
|
|
147
|
+
operates independently for filesystem operations, but storing the container
|
|
148
|
+
ensures API consistency and enables future enhancements without breaking
|
|
149
|
+
changes.
|
|
150
|
+
"""
|
|
151
|
+
self._container = container
|
|
152
|
+
self._allowed_paths: tuple[Path, ...] = ()
|
|
153
|
+
self._max_read_size: int = _DEFAULT_MAX_READ_SIZE
|
|
154
|
+
self._max_write_size: int = _DEFAULT_MAX_WRITE_SIZE
|
|
155
|
+
self._initialized: bool = False
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def handler_type(self) -> EnumHandlerType:
|
|
159
|
+
"""Return the architectural role of this handler.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
EnumHandlerType.INFRA_HANDLER - This handler is an infrastructure
|
|
163
|
+
protocol/transport handler for filesystem operations.
|
|
164
|
+
|
|
165
|
+
Note:
|
|
166
|
+
handler_type determines lifecycle, protocol selection, and runtime
|
|
167
|
+
invocation patterns. It answers "what is this handler in the architecture?"
|
|
168
|
+
|
|
169
|
+
See Also:
|
|
170
|
+
- handler_category: Behavioral classification (EFFECT/COMPUTE)
|
|
171
|
+
- transport_type: Specific transport protocol (FILESYSTEM)
|
|
172
|
+
"""
|
|
173
|
+
return EnumHandlerType.INFRA_HANDLER
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def handler_category(self) -> EnumHandlerTypeCategory:
|
|
177
|
+
"""Return the behavioral classification of this handler.
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
EnumHandlerTypeCategory.EFFECT - This handler performs side-effecting
|
|
181
|
+
I/O operations (filesystem read/write). EFFECT handlers are not
|
|
182
|
+
deterministic and interact with external systems.
|
|
183
|
+
|
|
184
|
+
Note:
|
|
185
|
+
handler_category determines security rules, determinism guarantees,
|
|
186
|
+
replay safety, and permissions. It answers "how does this handler
|
|
187
|
+
behave at runtime?"
|
|
188
|
+
|
|
189
|
+
Categories:
|
|
190
|
+
- COMPUTE: Pure, deterministic transformations (no side effects)
|
|
191
|
+
- EFFECT: Side-effecting I/O (database, HTTP, filesystem)
|
|
192
|
+
- NONDETERMINISTIC_COMPUTE: Pure but not deterministic (UUID, random)
|
|
193
|
+
|
|
194
|
+
See Also:
|
|
195
|
+
- handler_type: Architectural role (INFRA_HANDLER/NODE_HANDLER/etc.)
|
|
196
|
+
- transport_type: Specific transport protocol (FILESYSTEM)
|
|
197
|
+
"""
|
|
198
|
+
return EnumHandlerTypeCategory.EFFECT
|
|
199
|
+
|
|
200
|
+
@property
|
|
201
|
+
def transport_type(self) -> EnumInfraTransportType:
|
|
202
|
+
"""Return the transport protocol identifier for this handler.
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
EnumInfraTransportType.FILESYSTEM - This handler uses local filesystem.
|
|
206
|
+
|
|
207
|
+
Note:
|
|
208
|
+
transport_type identifies the specific transport/protocol this handler
|
|
209
|
+
uses. It is the third dimension of the handler type system, alongside
|
|
210
|
+
handler_type (architectural role) and handler_category (behavioral
|
|
211
|
+
classification).
|
|
212
|
+
|
|
213
|
+
The three dimensions together form a complete handler classification:
|
|
214
|
+
- handler_type: INFRA_HANDLER (what it is architecturally)
|
|
215
|
+
- handler_category: EFFECT (how it behaves at runtime)
|
|
216
|
+
- transport_type: FILESYSTEM (what protocol it uses)
|
|
217
|
+
|
|
218
|
+
See Also:
|
|
219
|
+
- handler_type: Architectural role
|
|
220
|
+
- handler_category: Behavioral classification
|
|
221
|
+
"""
|
|
222
|
+
return EnumInfraTransportType.FILESYSTEM
|
|
223
|
+
|
|
224
|
+
@transport_type.setter
|
|
225
|
+
def transport_type(self, value: EnumInfraTransportType) -> None:
|
|
226
|
+
"""Prevent modification of transport_type after initialization.
|
|
227
|
+
|
|
228
|
+
The transport_type is immutable for this handler - it is always FILESYSTEM.
|
|
229
|
+
This setter raises an AttributeError if modification is attempted.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
value: The transport type value (assignment always raises error)
|
|
233
|
+
|
|
234
|
+
Raises:
|
|
235
|
+
AttributeError: Always raised - transport_type is read-only.
|
|
236
|
+
"""
|
|
237
|
+
raise AttributeError(
|
|
238
|
+
"transport_type is read-only; it is set during handler initialization"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
async def initialize(self, config: dict[str, object]) -> None:
|
|
242
|
+
"""Initialize filesystem handler with path whitelist and size limits.
|
|
243
|
+
|
|
244
|
+
Args:
|
|
245
|
+
config: Configuration dict containing:
|
|
246
|
+
- allowed_paths: Required list of allowed directory paths (strings)
|
|
247
|
+
- max_read_size: Optional max read size in bytes (default: 100 MB)
|
|
248
|
+
- max_write_size: Optional max write size in bytes (default: 50 MB)
|
|
249
|
+
- correlation_id: Optional UUID or string for error tracing
|
|
250
|
+
|
|
251
|
+
Raises:
|
|
252
|
+
ProtocolConfigurationError: If allowed_paths is missing, empty, or invalid.
|
|
253
|
+
|
|
254
|
+
Security:
|
|
255
|
+
- All allowed_paths are resolved to absolute canonical paths
|
|
256
|
+
- Non-existent paths are logged as warnings but not rejected
|
|
257
|
+
- Empty allowed_paths list is rejected for security reasons
|
|
258
|
+
"""
|
|
259
|
+
init_correlation_id = uuid4()
|
|
260
|
+
|
|
261
|
+
logger.info(
|
|
262
|
+
"Initializing %s",
|
|
263
|
+
self.__class__.__name__,
|
|
264
|
+
extra={
|
|
265
|
+
"handler": self.__class__.__name__,
|
|
266
|
+
"correlation_id": str(init_correlation_id),
|
|
267
|
+
},
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
ctx = ModelInfraErrorContext(
|
|
271
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
272
|
+
operation="initialize",
|
|
273
|
+
target_name="filesystem_handler",
|
|
274
|
+
correlation_id=init_correlation_id,
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Extract and validate allowed_paths (required)
|
|
278
|
+
allowed_paths_raw = config.get("allowed_paths")
|
|
279
|
+
if allowed_paths_raw is None:
|
|
280
|
+
raise ProtocolConfigurationError(
|
|
281
|
+
"Missing required 'allowed_paths' configuration - filesystem handler "
|
|
282
|
+
"requires explicit path whitelist for security",
|
|
283
|
+
context=ctx,
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
if (
|
|
287
|
+
not isinstance(allowed_paths_raw, list | tuple)
|
|
288
|
+
or len(allowed_paths_raw) == 0
|
|
289
|
+
):
|
|
290
|
+
raise ProtocolConfigurationError(
|
|
291
|
+
"Configuration 'allowed_paths' must be a non-empty list or tuple of directory paths",
|
|
292
|
+
context=ctx,
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
# Resolve and validate each allowed path
|
|
296
|
+
resolved_paths: list[Path] = []
|
|
297
|
+
for path_str in allowed_paths_raw:
|
|
298
|
+
if not isinstance(path_str, str):
|
|
299
|
+
raise ProtocolConfigurationError(
|
|
300
|
+
f"Invalid path in allowed_paths: expected string, got {type(path_str).__name__}",
|
|
301
|
+
context=ctx,
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
path = Path(path_str).resolve()
|
|
305
|
+
|
|
306
|
+
if not path.exists():
|
|
307
|
+
logger.warning(
|
|
308
|
+
"Allowed path does not exist (will be created on first use): %s",
|
|
309
|
+
path,
|
|
310
|
+
extra={
|
|
311
|
+
"path": str(path),
|
|
312
|
+
"correlation_id": str(init_correlation_id),
|
|
313
|
+
},
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
resolved_paths.append(path)
|
|
317
|
+
|
|
318
|
+
# Store as immutable tuple after initialization
|
|
319
|
+
self._allowed_paths = tuple(resolved_paths)
|
|
320
|
+
|
|
321
|
+
# Extract optional size limits
|
|
322
|
+
max_read_raw = config.get("max_read_size")
|
|
323
|
+
if max_read_raw is not None:
|
|
324
|
+
if isinstance(max_read_raw, int) and max_read_raw > 0:
|
|
325
|
+
self._max_read_size = max_read_raw
|
|
326
|
+
else:
|
|
327
|
+
logger.warning(
|
|
328
|
+
"Invalid max_read_size config value ignored, using default",
|
|
329
|
+
extra={
|
|
330
|
+
"provided_value": max_read_raw,
|
|
331
|
+
"default_value": self._max_read_size,
|
|
332
|
+
},
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
max_write_raw = config.get("max_write_size")
|
|
336
|
+
if max_write_raw is not None:
|
|
337
|
+
if isinstance(max_write_raw, int) and max_write_raw > 0:
|
|
338
|
+
self._max_write_size = max_write_raw
|
|
339
|
+
else:
|
|
340
|
+
logger.warning(
|
|
341
|
+
"Invalid max_write_size config value ignored, using default",
|
|
342
|
+
extra={
|
|
343
|
+
"provided_value": max_write_raw,
|
|
344
|
+
"default_value": self._max_write_size,
|
|
345
|
+
},
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
# Initialize circuit breaker for resilient I/O operations
|
|
349
|
+
self._init_circuit_breaker(
|
|
350
|
+
threshold=5,
|
|
351
|
+
reset_timeout=60.0,
|
|
352
|
+
service_name="filesystem_handler",
|
|
353
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
self._initialized = True
|
|
357
|
+
|
|
358
|
+
logger.info(
|
|
359
|
+
"%s initialized successfully",
|
|
360
|
+
self.__class__.__name__,
|
|
361
|
+
extra={
|
|
362
|
+
"handler": self.__class__.__name__,
|
|
363
|
+
"allowed_paths_count": len(self._allowed_paths),
|
|
364
|
+
"max_read_size_bytes": self._max_read_size,
|
|
365
|
+
"max_write_size_bytes": self._max_write_size,
|
|
366
|
+
"correlation_id": str(init_correlation_id),
|
|
367
|
+
},
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
async def shutdown(self) -> None:
|
|
371
|
+
"""Shutdown filesystem handler and clear configuration."""
|
|
372
|
+
self._allowed_paths = ()
|
|
373
|
+
self._initialized = False
|
|
374
|
+
logger.info("HandlerFileSystem shutdown complete")
|
|
375
|
+
|
|
376
|
+
def _validate_path_in_whitelist(
|
|
377
|
+
self, path: Path, correlation_id: UUID, operation: str
|
|
378
|
+
) -> Path:
|
|
379
|
+
"""Validate that path is within allowed directories.
|
|
380
|
+
|
|
381
|
+
This method resolves the path to its canonical form (following symlinks)
|
|
382
|
+
and verifies it is within one of the allowed directories.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
path: Path to validate
|
|
386
|
+
correlation_id: Correlation ID for error context
|
|
387
|
+
operation: Operation name for error context
|
|
388
|
+
|
|
389
|
+
Returns:
|
|
390
|
+
The resolved canonical path
|
|
391
|
+
|
|
392
|
+
Raises:
|
|
393
|
+
ProtocolConfigurationError: If path is outside allowed directories
|
|
394
|
+
or symlink points outside allowed directories
|
|
395
|
+
"""
|
|
396
|
+
ctx = ModelInfraErrorContext(
|
|
397
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
398
|
+
operation=operation,
|
|
399
|
+
target_name=str(path),
|
|
400
|
+
correlation_id=correlation_id,
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Resolve to canonical path using Path.resolve()
|
|
404
|
+
# Note: In Python 3.6+, resolve() defaults to strict=False, meaning it
|
|
405
|
+
# works for both existing and non-existing paths by resolving as much
|
|
406
|
+
# of the path as possible without requiring the full path to exist.
|
|
407
|
+
try:
|
|
408
|
+
resolved = path.resolve()
|
|
409
|
+
except OSError as e:
|
|
410
|
+
raise ProtocolConfigurationError(
|
|
411
|
+
f"Cannot resolve path: {e}",
|
|
412
|
+
context=ctx,
|
|
413
|
+
) from e
|
|
414
|
+
|
|
415
|
+
# Check if resolved path is within any allowed directory
|
|
416
|
+
for allowed in self._allowed_paths:
|
|
417
|
+
try:
|
|
418
|
+
resolved.relative_to(allowed)
|
|
419
|
+
return resolved
|
|
420
|
+
except ValueError:
|
|
421
|
+
continue
|
|
422
|
+
|
|
423
|
+
# Path is not within any allowed directory
|
|
424
|
+
raise ProtocolConfigurationError(
|
|
425
|
+
f"Path '{path}' is outside allowed directories - access denied",
|
|
426
|
+
context=ctx,
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
def _validate_symlink_target(
|
|
430
|
+
self, path: Path, correlation_id: UUID, operation: str
|
|
431
|
+
) -> None:
|
|
432
|
+
"""Validate symlink target is within allowed directories.
|
|
433
|
+
|
|
434
|
+
If the path is a symlink, this method validates that the target
|
|
435
|
+
is within the allowed directories to prevent symlink escape attacks.
|
|
436
|
+
|
|
437
|
+
Args:
|
|
438
|
+
path: Path that may be a symlink
|
|
439
|
+
correlation_id: Correlation ID for error context
|
|
440
|
+
operation: Operation name for error context
|
|
441
|
+
|
|
442
|
+
Raises:
|
|
443
|
+
ProtocolConfigurationError: If symlink target is outside allowed directories
|
|
444
|
+
"""
|
|
445
|
+
if not path.is_symlink():
|
|
446
|
+
return
|
|
447
|
+
|
|
448
|
+
ctx = ModelInfraErrorContext(
|
|
449
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
450
|
+
operation=operation,
|
|
451
|
+
target_name=str(path),
|
|
452
|
+
correlation_id=correlation_id,
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
try:
|
|
456
|
+
target = path.resolve()
|
|
457
|
+
except OSError as e:
|
|
458
|
+
raise ProtocolConfigurationError(
|
|
459
|
+
f"Cannot resolve symlink target: {e}",
|
|
460
|
+
context=ctx,
|
|
461
|
+
) from e
|
|
462
|
+
|
|
463
|
+
# Verify symlink target is within allowed directories
|
|
464
|
+
for allowed in self._allowed_paths:
|
|
465
|
+
try:
|
|
466
|
+
target.relative_to(allowed)
|
|
467
|
+
return
|
|
468
|
+
except ValueError:
|
|
469
|
+
continue
|
|
470
|
+
|
|
471
|
+
raise ProtocolConfigurationError(
|
|
472
|
+
f"Symlink '{path}' points outside allowed directories - access denied",
|
|
473
|
+
context=ctx,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
async def execute(
|
|
477
|
+
self, envelope: dict[str, object]
|
|
478
|
+
) -> ModelHandlerOutput[dict[str, object]]:
|
|
479
|
+
"""Execute filesystem operation from envelope.
|
|
480
|
+
|
|
481
|
+
Args:
|
|
482
|
+
envelope: Request envelope containing:
|
|
483
|
+
- operation: One of the supported filesystem operations
|
|
484
|
+
- payload: Operation-specific payload
|
|
485
|
+
- correlation_id: Optional correlation ID for tracing
|
|
486
|
+
- envelope_id: Optional envelope ID for causality tracking
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
ModelHandlerOutput[dict[str, object]] containing operation result
|
|
490
|
+
|
|
491
|
+
Raises:
|
|
492
|
+
RuntimeHostError: If handler not initialized
|
|
493
|
+
ProtocolConfigurationError: If operation or payload is invalid
|
|
494
|
+
"""
|
|
495
|
+
correlation_id = self._extract_correlation_id(envelope)
|
|
496
|
+
input_envelope_id = self._extract_envelope_id(envelope)
|
|
497
|
+
|
|
498
|
+
if not self._initialized:
|
|
499
|
+
ctx = ModelInfraErrorContext(
|
|
500
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
501
|
+
operation="execute",
|
|
502
|
+
target_name="filesystem_handler",
|
|
503
|
+
correlation_id=correlation_id,
|
|
504
|
+
)
|
|
505
|
+
raise RuntimeHostError(
|
|
506
|
+
"HandlerFileSystem not initialized. Call initialize() first.",
|
|
507
|
+
context=ctx,
|
|
508
|
+
)
|
|
509
|
+
|
|
510
|
+
operation = envelope.get("operation")
|
|
511
|
+
if not isinstance(operation, str):
|
|
512
|
+
ctx = ModelInfraErrorContext(
|
|
513
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
514
|
+
operation="execute",
|
|
515
|
+
target_name="filesystem_handler",
|
|
516
|
+
correlation_id=correlation_id,
|
|
517
|
+
)
|
|
518
|
+
raise ProtocolConfigurationError(
|
|
519
|
+
"Missing or invalid 'operation' in envelope", context=ctx
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
if operation not in _SUPPORTED_OPERATIONS:
|
|
523
|
+
ctx = ModelInfraErrorContext(
|
|
524
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
525
|
+
operation=operation,
|
|
526
|
+
target_name="filesystem_handler",
|
|
527
|
+
correlation_id=correlation_id,
|
|
528
|
+
)
|
|
529
|
+
raise ProtocolConfigurationError(
|
|
530
|
+
f"Operation '{operation}' not supported. Available: {', '.join(sorted(_SUPPORTED_OPERATIONS))}",
|
|
531
|
+
context=ctx,
|
|
532
|
+
)
|
|
533
|
+
|
|
534
|
+
payload = envelope.get("payload")
|
|
535
|
+
if not isinstance(payload, dict):
|
|
536
|
+
ctx = ModelInfraErrorContext(
|
|
537
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
538
|
+
operation=operation,
|
|
539
|
+
target_name="filesystem_handler",
|
|
540
|
+
correlation_id=correlation_id,
|
|
541
|
+
)
|
|
542
|
+
raise ProtocolConfigurationError(
|
|
543
|
+
"Missing or invalid 'payload' in envelope", context=ctx
|
|
544
|
+
)
|
|
545
|
+
|
|
546
|
+
# Route to appropriate operation handler
|
|
547
|
+
if operation == "filesystem.read_file":
|
|
548
|
+
return await self._execute_read_file(
|
|
549
|
+
payload, correlation_id, input_envelope_id
|
|
550
|
+
)
|
|
551
|
+
elif operation == "filesystem.write_file":
|
|
552
|
+
return await self._execute_write_file(
|
|
553
|
+
payload, correlation_id, input_envelope_id
|
|
554
|
+
)
|
|
555
|
+
elif operation == "filesystem.list_directory":
|
|
556
|
+
return await self._execute_list_directory(
|
|
557
|
+
payload, correlation_id, input_envelope_id
|
|
558
|
+
)
|
|
559
|
+
elif operation == "filesystem.ensure_directory":
|
|
560
|
+
return await self._execute_ensure_directory(
|
|
561
|
+
payload, correlation_id, input_envelope_id
|
|
562
|
+
)
|
|
563
|
+
else: # filesystem.delete_file
|
|
564
|
+
return await self._execute_delete_file(
|
|
565
|
+
payload, correlation_id, input_envelope_id
|
|
566
|
+
)
|
|
567
|
+
|
|
568
|
+
async def _execute_read_file(
|
|
569
|
+
self,
|
|
570
|
+
payload: dict[str, object],
|
|
571
|
+
correlation_id: UUID,
|
|
572
|
+
input_envelope_id: UUID,
|
|
573
|
+
) -> ModelHandlerOutput[dict[str, object]]:
|
|
574
|
+
"""Execute filesystem.read_file operation.
|
|
575
|
+
|
|
576
|
+
Payload:
|
|
577
|
+
- path: str (required) - File path to read
|
|
578
|
+
- binary: bool (optional, default False) - Read as binary (returns base64)
|
|
579
|
+
- encoding: str (optional, default "utf-8") - Text encoding
|
|
580
|
+
|
|
581
|
+
Returns:
|
|
582
|
+
Result with content, size, path, and binary flag.
|
|
583
|
+
For binary=True, content is base64-encoded string.
|
|
584
|
+
For binary=False, content is the text string.
|
|
585
|
+
|
|
586
|
+
Raises:
|
|
587
|
+
InfraConnectionError: If file not found or read fails
|
|
588
|
+
InfraUnavailableError: If file size exceeds limit or circuit breaker is open
|
|
589
|
+
"""
|
|
590
|
+
operation = "filesystem.read_file"
|
|
591
|
+
|
|
592
|
+
# Extract path (required)
|
|
593
|
+
path_raw = payload.get("path")
|
|
594
|
+
if not isinstance(path_raw, str) or not path_raw:
|
|
595
|
+
ctx = ModelInfraErrorContext(
|
|
596
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
597
|
+
operation=operation,
|
|
598
|
+
target_name="filesystem_handler",
|
|
599
|
+
correlation_id=correlation_id,
|
|
600
|
+
)
|
|
601
|
+
raise ProtocolConfigurationError(
|
|
602
|
+
"Missing or invalid 'path' in payload", context=ctx
|
|
603
|
+
)
|
|
604
|
+
|
|
605
|
+
path = Path(path_raw)
|
|
606
|
+
resolved_path = self._validate_path_in_whitelist(
|
|
607
|
+
path, correlation_id, operation
|
|
608
|
+
)
|
|
609
|
+
self._validate_symlink_target(resolved_path, correlation_id, operation)
|
|
610
|
+
|
|
611
|
+
# Extract options
|
|
612
|
+
binary = payload.get("binary", False)
|
|
613
|
+
if not isinstance(binary, bool):
|
|
614
|
+
logger.warning(
|
|
615
|
+
"Invalid binary parameter type ignored, using default",
|
|
616
|
+
extra={
|
|
617
|
+
"provided_value": binary,
|
|
618
|
+
"provided_type": type(binary).__name__,
|
|
619
|
+
"default_value": False,
|
|
620
|
+
"correlation_id": str(correlation_id),
|
|
621
|
+
},
|
|
622
|
+
)
|
|
623
|
+
binary = False
|
|
624
|
+
|
|
625
|
+
encoding = payload.get("encoding", "utf-8")
|
|
626
|
+
if not isinstance(encoding, str):
|
|
627
|
+
logger.warning(
|
|
628
|
+
"Invalid encoding parameter type ignored, using default",
|
|
629
|
+
extra={
|
|
630
|
+
"provided_value": encoding,
|
|
631
|
+
"provided_type": type(encoding).__name__,
|
|
632
|
+
"default_value": "utf-8",
|
|
633
|
+
"correlation_id": str(correlation_id),
|
|
634
|
+
},
|
|
635
|
+
)
|
|
636
|
+
encoding = "utf-8"
|
|
637
|
+
|
|
638
|
+
ctx = ModelInfraErrorContext(
|
|
639
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
640
|
+
operation=operation,
|
|
641
|
+
target_name=str(resolved_path),
|
|
642
|
+
correlation_id=correlation_id,
|
|
643
|
+
)
|
|
644
|
+
|
|
645
|
+
# Check circuit breaker before I/O operation
|
|
646
|
+
async with self._circuit_breaker_lock:
|
|
647
|
+
await self._check_circuit_breaker(operation, correlation_id)
|
|
648
|
+
|
|
649
|
+
try:
|
|
650
|
+
# Check file exists
|
|
651
|
+
if not resolved_path.exists():
|
|
652
|
+
raise InfraConnectionError(
|
|
653
|
+
f"File not found: {resolved_path.name}",
|
|
654
|
+
context=ctx,
|
|
655
|
+
)
|
|
656
|
+
|
|
657
|
+
if not resolved_path.is_file():
|
|
658
|
+
raise InfraConnectionError(
|
|
659
|
+
f"Path is not a file: {resolved_path.name}",
|
|
660
|
+
context=ctx,
|
|
661
|
+
)
|
|
662
|
+
|
|
663
|
+
# Check file size before reading
|
|
664
|
+
try:
|
|
665
|
+
file_size = resolved_path.stat().st_size
|
|
666
|
+
except OSError as e:
|
|
667
|
+
raise InfraConnectionError(
|
|
668
|
+
f"Cannot stat file: {e}",
|
|
669
|
+
context=ctx,
|
|
670
|
+
) from e
|
|
671
|
+
|
|
672
|
+
if file_size > self._max_read_size:
|
|
673
|
+
raise InfraUnavailableError(
|
|
674
|
+
f"File size ({_categorize_size(file_size)}) exceeds configured read limit",
|
|
675
|
+
context=ctx,
|
|
676
|
+
)
|
|
677
|
+
|
|
678
|
+
# Read file content
|
|
679
|
+
content: str
|
|
680
|
+
try:
|
|
681
|
+
if binary:
|
|
682
|
+
raw_bytes = resolved_path.read_bytes()
|
|
683
|
+
# Encode bytes as base64 string for JSON safety
|
|
684
|
+
content = base64.b64encode(raw_bytes).decode("ascii")
|
|
685
|
+
else:
|
|
686
|
+
content = resolved_path.read_text(encoding=encoding)
|
|
687
|
+
except OSError as e:
|
|
688
|
+
raise InfraConnectionError(
|
|
689
|
+
f"Failed to read file: {e}",
|
|
690
|
+
context=ctx,
|
|
691
|
+
) from e
|
|
692
|
+
except UnicodeDecodeError as e:
|
|
693
|
+
raise InfraConnectionError(
|
|
694
|
+
f"Failed to decode file with encoding '{encoding}': {e}",
|
|
695
|
+
context=ctx,
|
|
696
|
+
) from e
|
|
697
|
+
|
|
698
|
+
# Reset circuit breaker on success
|
|
699
|
+
async with self._circuit_breaker_lock:
|
|
700
|
+
await self._reset_circuit_breaker()
|
|
701
|
+
|
|
702
|
+
logger.debug(
|
|
703
|
+
"File read successfully",
|
|
704
|
+
extra={
|
|
705
|
+
"path": str(resolved_path),
|
|
706
|
+
"size_category": _categorize_size(file_size),
|
|
707
|
+
"binary": binary,
|
|
708
|
+
"correlation_id": str(correlation_id),
|
|
709
|
+
},
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
return ModelHandlerOutput.for_compute(
|
|
713
|
+
input_envelope_id=input_envelope_id,
|
|
714
|
+
correlation_id=correlation_id,
|
|
715
|
+
handler_id=HANDLER_ID_FILESYSTEM,
|
|
716
|
+
result={
|
|
717
|
+
"status": "success",
|
|
718
|
+
"payload": {
|
|
719
|
+
"content": content,
|
|
720
|
+
"size": file_size,
|
|
721
|
+
"path": str(resolved_path),
|
|
722
|
+
"binary": binary,
|
|
723
|
+
},
|
|
724
|
+
"correlation_id": str(correlation_id),
|
|
725
|
+
},
|
|
726
|
+
)
|
|
727
|
+
|
|
728
|
+
except (InfraConnectionError, InfraUnavailableError):
|
|
729
|
+
# Record failure for circuit breaker (infra-level failures only)
|
|
730
|
+
async with self._circuit_breaker_lock:
|
|
731
|
+
await self._record_circuit_failure(operation, correlation_id)
|
|
732
|
+
raise
|
|
733
|
+
|
|
734
|
+
async def _execute_write_file(
|
|
735
|
+
self,
|
|
736
|
+
payload: dict[str, object],
|
|
737
|
+
correlation_id: UUID,
|
|
738
|
+
input_envelope_id: UUID,
|
|
739
|
+
) -> ModelHandlerOutput[dict[str, object]]:
|
|
740
|
+
"""Execute filesystem.write_file operation.
|
|
741
|
+
|
|
742
|
+
Payload:
|
|
743
|
+
- path: str (required) - File path to write
|
|
744
|
+
- content: str (required) - Content to write.
|
|
745
|
+
For binary=True, content should be base64-encoded string.
|
|
746
|
+
- binary: bool (optional, default False) - Write as binary (expects base64 content)
|
|
747
|
+
- create_dirs: bool (optional, default False) - Create parent directories
|
|
748
|
+
|
|
749
|
+
Returns:
|
|
750
|
+
Result with path, bytes_written, and created flag
|
|
751
|
+
|
|
752
|
+
Raises:
|
|
753
|
+
InfraConnectionError: If write fails
|
|
754
|
+
InfraUnavailableError: If content size exceeds limit or circuit breaker is open
|
|
755
|
+
ProtocolConfigurationError: If base64 decoding fails for binary mode
|
|
756
|
+
"""
|
|
757
|
+
operation = "filesystem.write_file"
|
|
758
|
+
|
|
759
|
+
# Extract path (required)
|
|
760
|
+
path_raw = payload.get("path")
|
|
761
|
+
if not isinstance(path_raw, str) or not path_raw:
|
|
762
|
+
ctx = ModelInfraErrorContext(
|
|
763
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
764
|
+
operation=operation,
|
|
765
|
+
target_name="filesystem_handler",
|
|
766
|
+
correlation_id=correlation_id,
|
|
767
|
+
)
|
|
768
|
+
raise ProtocolConfigurationError(
|
|
769
|
+
"Missing or invalid 'path' in payload", context=ctx
|
|
770
|
+
)
|
|
771
|
+
|
|
772
|
+
# Extract content (required)
|
|
773
|
+
content_raw = payload.get("content")
|
|
774
|
+
if content_raw is None:
|
|
775
|
+
ctx = ModelInfraErrorContext(
|
|
776
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
777
|
+
operation=operation,
|
|
778
|
+
target_name="filesystem_handler",
|
|
779
|
+
correlation_id=correlation_id,
|
|
780
|
+
)
|
|
781
|
+
raise ProtocolConfigurationError(
|
|
782
|
+
"Missing 'content' in payload", context=ctx
|
|
783
|
+
)
|
|
784
|
+
|
|
785
|
+
# Extract binary flag (consistent with read_file API)
|
|
786
|
+
binary = payload.get("binary", False)
|
|
787
|
+
if not isinstance(binary, bool):
|
|
788
|
+
logger.warning(
|
|
789
|
+
"Invalid binary parameter type ignored, using default",
|
|
790
|
+
extra={
|
|
791
|
+
"provided_value": binary,
|
|
792
|
+
"provided_type": type(binary).__name__,
|
|
793
|
+
"default_value": False,
|
|
794
|
+
"correlation_id": str(correlation_id),
|
|
795
|
+
},
|
|
796
|
+
)
|
|
797
|
+
binary = False
|
|
798
|
+
|
|
799
|
+
ctx = ModelInfraErrorContext(
|
|
800
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
801
|
+
operation=operation,
|
|
802
|
+
target_name=str(path_raw),
|
|
803
|
+
correlation_id=correlation_id,
|
|
804
|
+
)
|
|
805
|
+
|
|
806
|
+
# Process content based on binary flag
|
|
807
|
+
content_bytes: bytes
|
|
808
|
+
content_str: str
|
|
809
|
+
|
|
810
|
+
if binary:
|
|
811
|
+
# Binary mode: expect base64-encoded string or raw bytes
|
|
812
|
+
if isinstance(content_raw, str):
|
|
813
|
+
try:
|
|
814
|
+
content_bytes = base64.b64decode(content_raw, validate=True)
|
|
815
|
+
except binascii.Error as e:
|
|
816
|
+
raise ProtocolConfigurationError(
|
|
817
|
+
f"Invalid base64 content for binary mode: {e}",
|
|
818
|
+
context=ctx,
|
|
819
|
+
) from e
|
|
820
|
+
elif isinstance(content_raw, bytes):
|
|
821
|
+
content_bytes = content_raw
|
|
822
|
+
else:
|
|
823
|
+
raise ProtocolConfigurationError(
|
|
824
|
+
f"Invalid content type for binary mode: expected str (base64) or bytes, got {type(content_raw).__name__}",
|
|
825
|
+
context=ctx,
|
|
826
|
+
)
|
|
827
|
+
content_size = len(content_bytes)
|
|
828
|
+
else:
|
|
829
|
+
# Text mode: expect string
|
|
830
|
+
if isinstance(content_raw, str):
|
|
831
|
+
content_str = content_raw
|
|
832
|
+
elif isinstance(content_raw, bytes):
|
|
833
|
+
try:
|
|
834
|
+
content_str = content_raw.decode("utf-8")
|
|
835
|
+
except UnicodeDecodeError as e:
|
|
836
|
+
raise ProtocolConfigurationError(
|
|
837
|
+
"Invalid UTF-8 bytes for text content",
|
|
838
|
+
context=ctx,
|
|
839
|
+
) from e
|
|
840
|
+
else:
|
|
841
|
+
raise ProtocolConfigurationError(
|
|
842
|
+
f"Invalid content type: expected str or bytes, got {type(content_raw).__name__}",
|
|
843
|
+
context=ctx,
|
|
844
|
+
)
|
|
845
|
+
content_size = len(content_str.encode("utf-8"))
|
|
846
|
+
|
|
847
|
+
create_dirs = payload.get("create_dirs", False)
|
|
848
|
+
if not isinstance(create_dirs, bool):
|
|
849
|
+
create_dirs = False
|
|
850
|
+
|
|
851
|
+
path = Path(path_raw)
|
|
852
|
+
|
|
853
|
+
# For write operations, validate parent directory is in whitelist
|
|
854
|
+
parent = path.parent
|
|
855
|
+
if parent.exists():
|
|
856
|
+
self._validate_path_in_whitelist(parent, correlation_id, operation)
|
|
857
|
+
else:
|
|
858
|
+
# For non-existent parents, validate the path structure
|
|
859
|
+
# Find the first existing ancestor and validate from there
|
|
860
|
+
current = parent
|
|
861
|
+
while not current.exists() and current != current.parent:
|
|
862
|
+
current = current.parent
|
|
863
|
+
|
|
864
|
+
if current.exists():
|
|
865
|
+
self._validate_path_in_whitelist(current, correlation_id, operation)
|
|
866
|
+
else:
|
|
867
|
+
raise ProtocolConfigurationError(
|
|
868
|
+
f"Path '{path}' is outside allowed directories - access denied",
|
|
869
|
+
context=ctx,
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
# Compute resolved_path for validation and return value, but preserve
|
|
873
|
+
# write_path for the actual write operation to enable O_NOFOLLOW check.
|
|
874
|
+
# We use parent.resolve() / name to avoid resolving symlinks in the final component.
|
|
875
|
+
resolved_parent = path.parent.resolve()
|
|
876
|
+
write_path = resolved_parent / path.name
|
|
877
|
+
resolved_path = path.resolve() if path.exists() else write_path
|
|
878
|
+
|
|
879
|
+
# Check circuit breaker before I/O operation
|
|
880
|
+
async with self._circuit_breaker_lock:
|
|
881
|
+
await self._check_circuit_breaker(operation, correlation_id)
|
|
882
|
+
|
|
883
|
+
try:
|
|
884
|
+
# Check content size
|
|
885
|
+
if content_size > self._max_write_size:
|
|
886
|
+
raise InfraUnavailableError(
|
|
887
|
+
f"Content size ({_categorize_size(content_size)}) exceeds configured write limit",
|
|
888
|
+
context=ctx,
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
# Check if file exists (for return value)
|
|
892
|
+
file_existed = write_path.exists()
|
|
893
|
+
|
|
894
|
+
# Create parent directories if requested
|
|
895
|
+
if create_dirs and not resolved_parent.exists():
|
|
896
|
+
try:
|
|
897
|
+
resolved_parent.mkdir(parents=True, exist_ok=True)
|
|
898
|
+
except OSError as e:
|
|
899
|
+
raise InfraConnectionError(
|
|
900
|
+
f"Failed to create parent directories: {e}",
|
|
901
|
+
context=ctx,
|
|
902
|
+
) from e
|
|
903
|
+
|
|
904
|
+
# Check if symlink exists and validate target is within allowed paths.
|
|
905
|
+
# This provides a helpful error message for symlinks pointing outside.
|
|
906
|
+
# The O_NOFOLLOW check below will reject ALL symlinks for security.
|
|
907
|
+
if write_path.is_symlink():
|
|
908
|
+
self._validate_symlink_target(write_path, correlation_id, operation)
|
|
909
|
+
|
|
910
|
+
# Write file content using O_NOFOLLOW to prevent symlink following.
|
|
911
|
+
# This eliminates the TOCTOU race condition where an attacker could
|
|
912
|
+
# replace the file with a symlink between validation and write.
|
|
913
|
+
# We use write_path (not resolved_path) to detect symlinks.
|
|
914
|
+
try:
|
|
915
|
+
# O_NOFOLLOW causes the open to fail with ELOOP if the path is a symlink
|
|
916
|
+
# This is atomic and cannot be raced, unlike checking is_symlink() first
|
|
917
|
+
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_NOFOLLOW
|
|
918
|
+
fd = os.open(str(write_path), flags, 0o644)
|
|
919
|
+
try:
|
|
920
|
+
if binary:
|
|
921
|
+
with os.fdopen(fd, "wb") as f:
|
|
922
|
+
f.write(content_bytes)
|
|
923
|
+
else:
|
|
924
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
925
|
+
f.write(content_str)
|
|
926
|
+
except Exception:
|
|
927
|
+
# fdopen takes ownership of fd, but if write fails after fdopen
|
|
928
|
+
# the context manager will close it. Re-raise to outer handler.
|
|
929
|
+
raise
|
|
930
|
+
except OSError as e:
|
|
931
|
+
if e.errno == errno.ELOOP:
|
|
932
|
+
# ELOOP indicates the path is a symlink - reject the write
|
|
933
|
+
raise ProtocolConfigurationError(
|
|
934
|
+
f"Cannot write to symlink: {write_path.name}",
|
|
935
|
+
context=ctx,
|
|
936
|
+
) from e
|
|
937
|
+
raise InfraConnectionError(
|
|
938
|
+
f"Failed to write file: {e}",
|
|
939
|
+
context=ctx,
|
|
940
|
+
) from e
|
|
941
|
+
|
|
942
|
+
# Reset circuit breaker on success
|
|
943
|
+
async with self._circuit_breaker_lock:
|
|
944
|
+
await self._reset_circuit_breaker()
|
|
945
|
+
|
|
946
|
+
logger.debug(
|
|
947
|
+
"File written successfully",
|
|
948
|
+
extra={
|
|
949
|
+
"path": str(resolved_path),
|
|
950
|
+
"size_category": _categorize_size(content_size),
|
|
951
|
+
"file_created": not file_existed,
|
|
952
|
+
"correlation_id": str(correlation_id),
|
|
953
|
+
},
|
|
954
|
+
)
|
|
955
|
+
|
|
956
|
+
return ModelHandlerOutput.for_compute(
|
|
957
|
+
input_envelope_id=input_envelope_id,
|
|
958
|
+
correlation_id=correlation_id,
|
|
959
|
+
handler_id=HANDLER_ID_FILESYSTEM,
|
|
960
|
+
result={
|
|
961
|
+
"status": "success",
|
|
962
|
+
"payload": {
|
|
963
|
+
"path": str(resolved_path),
|
|
964
|
+
"bytes_written": content_size,
|
|
965
|
+
"created": not file_existed,
|
|
966
|
+
},
|
|
967
|
+
"correlation_id": str(correlation_id),
|
|
968
|
+
},
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
except (InfraConnectionError, InfraUnavailableError):
|
|
972
|
+
# Record failure for circuit breaker (infra-level failures only)
|
|
973
|
+
async with self._circuit_breaker_lock:
|
|
974
|
+
await self._record_circuit_failure(operation, correlation_id)
|
|
975
|
+
raise
|
|
976
|
+
|
|
977
|
+
async def _execute_list_directory(
|
|
978
|
+
self,
|
|
979
|
+
payload: dict[str, object],
|
|
980
|
+
correlation_id: UUID,
|
|
981
|
+
input_envelope_id: UUID,
|
|
982
|
+
) -> ModelHandlerOutput[dict[str, object]]:
|
|
983
|
+
"""Execute filesystem.list_directory operation.
|
|
984
|
+
|
|
985
|
+
Payload:
|
|
986
|
+
- path: str (required) - Directory path to list
|
|
987
|
+
- recursive: bool (optional, default False) - List recursively
|
|
988
|
+
- pattern: str (optional) - Glob pattern to filter entries
|
|
989
|
+
|
|
990
|
+
Returns:
|
|
991
|
+
Result with entries list, count, and path
|
|
992
|
+
|
|
993
|
+
Raises:
|
|
994
|
+
InfraConnectionError: If directory not found or list fails
|
|
995
|
+
InfraUnavailableError: If circuit breaker is open
|
|
996
|
+
"""
|
|
997
|
+
operation = "filesystem.list_directory"
|
|
998
|
+
|
|
999
|
+
# Extract path (required)
|
|
1000
|
+
path_raw = payload.get("path")
|
|
1001
|
+
if not isinstance(path_raw, str) or not path_raw:
|
|
1002
|
+
ctx = ModelInfraErrorContext(
|
|
1003
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
1004
|
+
operation=operation,
|
|
1005
|
+
target_name="filesystem_handler",
|
|
1006
|
+
correlation_id=correlation_id,
|
|
1007
|
+
)
|
|
1008
|
+
raise ProtocolConfigurationError(
|
|
1009
|
+
"Missing or invalid 'path' in payload", context=ctx
|
|
1010
|
+
)
|
|
1011
|
+
|
|
1012
|
+
path = Path(path_raw)
|
|
1013
|
+
resolved_path = self._validate_path_in_whitelist(
|
|
1014
|
+
path, correlation_id, operation
|
|
1015
|
+
)
|
|
1016
|
+
self._validate_symlink_target(resolved_path, correlation_id, operation)
|
|
1017
|
+
|
|
1018
|
+
ctx = ModelInfraErrorContext(
|
|
1019
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
1020
|
+
operation=operation,
|
|
1021
|
+
target_name=str(resolved_path),
|
|
1022
|
+
correlation_id=correlation_id,
|
|
1023
|
+
)
|
|
1024
|
+
|
|
1025
|
+
# Extract options
|
|
1026
|
+
recursive = payload.get("recursive", False)
|
|
1027
|
+
if not isinstance(recursive, bool):
|
|
1028
|
+
recursive = False
|
|
1029
|
+
|
|
1030
|
+
pattern = payload.get("pattern")
|
|
1031
|
+
if pattern is not None and not isinstance(pattern, str):
|
|
1032
|
+
pattern = None
|
|
1033
|
+
|
|
1034
|
+
# Check circuit breaker before I/O operation
|
|
1035
|
+
async with self._circuit_breaker_lock:
|
|
1036
|
+
await self._check_circuit_breaker(operation, correlation_id)
|
|
1037
|
+
|
|
1038
|
+
try:
|
|
1039
|
+
# Check directory exists
|
|
1040
|
+
if not resolved_path.exists():
|
|
1041
|
+
raise InfraConnectionError(
|
|
1042
|
+
f"Directory not found: {resolved_path.name}",
|
|
1043
|
+
context=ctx,
|
|
1044
|
+
)
|
|
1045
|
+
|
|
1046
|
+
if not resolved_path.is_dir():
|
|
1047
|
+
raise InfraConnectionError(
|
|
1048
|
+
f"Path is not a directory: {resolved_path.name}",
|
|
1049
|
+
context=ctx,
|
|
1050
|
+
)
|
|
1051
|
+
|
|
1052
|
+
# List directory contents
|
|
1053
|
+
entries: list[dict[str, object]] = []
|
|
1054
|
+
try:
|
|
1055
|
+
if recursive:
|
|
1056
|
+
iterator = resolved_path.rglob("*")
|
|
1057
|
+
else:
|
|
1058
|
+
iterator = resolved_path.iterdir()
|
|
1059
|
+
|
|
1060
|
+
for entry in iterator:
|
|
1061
|
+
# Apply pattern filter if specified
|
|
1062
|
+
if pattern and not fnmatch.fnmatch(entry.name, pattern):
|
|
1063
|
+
continue
|
|
1064
|
+
|
|
1065
|
+
# Get entry metadata - use lstat() to not follow symlinks
|
|
1066
|
+
# This prevents exposing metadata from files outside the whitelist
|
|
1067
|
+
try:
|
|
1068
|
+
is_symlink = entry.is_symlink()
|
|
1069
|
+
|
|
1070
|
+
# For symlinks, check if target is within allowed paths
|
|
1071
|
+
# Skip symlinks pointing outside allowed directories to prevent
|
|
1072
|
+
# information disclosure about files outside the whitelist
|
|
1073
|
+
if is_symlink:
|
|
1074
|
+
try:
|
|
1075
|
+
resolved_target = entry.resolve()
|
|
1076
|
+
# Check if target is within any allowed directory
|
|
1077
|
+
target_allowed = False
|
|
1078
|
+
for allowed in self._allowed_paths:
|
|
1079
|
+
try:
|
|
1080
|
+
resolved_target.relative_to(allowed)
|
|
1081
|
+
target_allowed = True
|
|
1082
|
+
break
|
|
1083
|
+
except ValueError:
|
|
1084
|
+
continue
|
|
1085
|
+
if not target_allowed:
|
|
1086
|
+
# Skip symlinks pointing outside allowed paths
|
|
1087
|
+
continue
|
|
1088
|
+
except OSError:
|
|
1089
|
+
# Skip broken or unresolvable symlinks
|
|
1090
|
+
continue
|
|
1091
|
+
|
|
1092
|
+
# Use lstat() to get symlink's own metadata, not target's
|
|
1093
|
+
stat_info = entry.lstat()
|
|
1094
|
+
|
|
1095
|
+
# For is_file/is_dir, report the actual entry type
|
|
1096
|
+
# If it's a symlink, is_file()/is_dir() follow the link,
|
|
1097
|
+
# so we report based on the symlink itself
|
|
1098
|
+
entry_data: dict[str, object] = {
|
|
1099
|
+
"name": entry.name,
|
|
1100
|
+
"path": str(entry),
|
|
1101
|
+
"is_file": entry.is_file() and not is_symlink,
|
|
1102
|
+
"is_dir": entry.is_dir() and not is_symlink,
|
|
1103
|
+
"is_symlink": is_symlink,
|
|
1104
|
+
"size": stat_info.st_size,
|
|
1105
|
+
"modified": stat_info.st_mtime,
|
|
1106
|
+
}
|
|
1107
|
+
entries.append(entry_data)
|
|
1108
|
+
except OSError:
|
|
1109
|
+
# Skip entries we can't stat
|
|
1110
|
+
continue
|
|
1111
|
+
|
|
1112
|
+
except OSError as e:
|
|
1113
|
+
raise InfraConnectionError(
|
|
1114
|
+
f"Failed to list directory: {e}",
|
|
1115
|
+
context=ctx,
|
|
1116
|
+
) from e
|
|
1117
|
+
|
|
1118
|
+
# Reset circuit breaker on success
|
|
1119
|
+
async with self._circuit_breaker_lock:
|
|
1120
|
+
await self._reset_circuit_breaker()
|
|
1121
|
+
|
|
1122
|
+
logger.debug(
|
|
1123
|
+
"Directory listed successfully",
|
|
1124
|
+
extra={
|
|
1125
|
+
"path": str(resolved_path),
|
|
1126
|
+
"entry_count": len(entries),
|
|
1127
|
+
"recursive": recursive,
|
|
1128
|
+
"pattern": pattern,
|
|
1129
|
+
"correlation_id": str(correlation_id),
|
|
1130
|
+
},
|
|
1131
|
+
)
|
|
1132
|
+
|
|
1133
|
+
return ModelHandlerOutput.for_compute(
|
|
1134
|
+
input_envelope_id=input_envelope_id,
|
|
1135
|
+
correlation_id=correlation_id,
|
|
1136
|
+
handler_id=HANDLER_ID_FILESYSTEM,
|
|
1137
|
+
result={
|
|
1138
|
+
"status": "success",
|
|
1139
|
+
"payload": {
|
|
1140
|
+
"entries": entries,
|
|
1141
|
+
"count": len(entries),
|
|
1142
|
+
"path": str(resolved_path),
|
|
1143
|
+
},
|
|
1144
|
+
"correlation_id": str(correlation_id),
|
|
1145
|
+
},
|
|
1146
|
+
)
|
|
1147
|
+
|
|
1148
|
+
except (InfraConnectionError, InfraUnavailableError):
|
|
1149
|
+
# Record failure for circuit breaker (infra-level failures only)
|
|
1150
|
+
async with self._circuit_breaker_lock:
|
|
1151
|
+
await self._record_circuit_failure(operation, correlation_id)
|
|
1152
|
+
raise
|
|
1153
|
+
|
|
1154
|
+
async def _execute_ensure_directory(
|
|
1155
|
+
self,
|
|
1156
|
+
payload: dict[str, object],
|
|
1157
|
+
correlation_id: UUID,
|
|
1158
|
+
input_envelope_id: UUID,
|
|
1159
|
+
) -> ModelHandlerOutput[dict[str, object]]:
|
|
1160
|
+
"""Execute filesystem.ensure_directory operation.
|
|
1161
|
+
|
|
1162
|
+
Payload:
|
|
1163
|
+
- path: str (required) - Directory path to create
|
|
1164
|
+
- exist_ok: bool (optional, default True) - Don't error if exists
|
|
1165
|
+
|
|
1166
|
+
Returns:
|
|
1167
|
+
Result with path, created, and already_existed flags
|
|
1168
|
+
|
|
1169
|
+
Raises:
|
|
1170
|
+
InfraConnectionError: If directory creation fails
|
|
1171
|
+
InfraUnavailableError: If circuit breaker is open
|
|
1172
|
+
"""
|
|
1173
|
+
operation = "filesystem.ensure_directory"
|
|
1174
|
+
|
|
1175
|
+
# Extract path (required)
|
|
1176
|
+
path_raw = payload.get("path")
|
|
1177
|
+
if not isinstance(path_raw, str) or not path_raw:
|
|
1178
|
+
ctx = ModelInfraErrorContext(
|
|
1179
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
1180
|
+
operation=operation,
|
|
1181
|
+
target_name="filesystem_handler",
|
|
1182
|
+
correlation_id=correlation_id,
|
|
1183
|
+
)
|
|
1184
|
+
raise ProtocolConfigurationError(
|
|
1185
|
+
"Missing or invalid 'path' in payload", context=ctx
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
path = Path(path_raw)
|
|
1189
|
+
|
|
1190
|
+
# Validate the target path is within allowed directories
|
|
1191
|
+
# _validate_path_in_whitelist handles non-existent paths via resolve()
|
|
1192
|
+
self._validate_path_in_whitelist(path, correlation_id, operation)
|
|
1193
|
+
|
|
1194
|
+
ctx = ModelInfraErrorContext(
|
|
1195
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
1196
|
+
operation=operation,
|
|
1197
|
+
target_name=str(path),
|
|
1198
|
+
correlation_id=correlation_id,
|
|
1199
|
+
)
|
|
1200
|
+
|
|
1201
|
+
# Extract options
|
|
1202
|
+
exist_ok = payload.get("exist_ok", True)
|
|
1203
|
+
if not isinstance(exist_ok, bool):
|
|
1204
|
+
exist_ok = True
|
|
1205
|
+
|
|
1206
|
+
# Check circuit breaker before I/O operation
|
|
1207
|
+
async with self._circuit_breaker_lock:
|
|
1208
|
+
await self._check_circuit_breaker(operation, correlation_id)
|
|
1209
|
+
|
|
1210
|
+
try:
|
|
1211
|
+
# Check if already exists
|
|
1212
|
+
already_existed = path.exists()
|
|
1213
|
+
|
|
1214
|
+
if already_existed and not path.is_dir():
|
|
1215
|
+
raise InfraConnectionError(
|
|
1216
|
+
f"Path exists but is not a directory: {path.name}",
|
|
1217
|
+
context=ctx,
|
|
1218
|
+
)
|
|
1219
|
+
|
|
1220
|
+
# If directory exists and exist_ok=False, raise error
|
|
1221
|
+
if already_existed and not exist_ok:
|
|
1222
|
+
raise InfraConnectionError(
|
|
1223
|
+
f"Directory already exists: {path.name}",
|
|
1224
|
+
context=ctx,
|
|
1225
|
+
)
|
|
1226
|
+
|
|
1227
|
+
# Create directory
|
|
1228
|
+
created = False
|
|
1229
|
+
if not already_existed:
|
|
1230
|
+
try:
|
|
1231
|
+
# Final symlink check immediately before I/O to minimize TOCTOU window.
|
|
1232
|
+
# An attacker could create a symlink at the target path between earlier
|
|
1233
|
+
# validation and this point. Re-checking here reduces the window to
|
|
1234
|
+
# the minimum possible (between this check and the actual mkdir).
|
|
1235
|
+
# For mkdir, we also check parent directories in case a symlink was
|
|
1236
|
+
# inserted in the path hierarchy.
|
|
1237
|
+
if path.is_symlink():
|
|
1238
|
+
self._validate_symlink_target(path, correlation_id, operation)
|
|
1239
|
+
# Also check if any parent became a symlink
|
|
1240
|
+
for parent in path.parents:
|
|
1241
|
+
if parent.is_symlink():
|
|
1242
|
+
self._validate_symlink_target(
|
|
1243
|
+
parent, correlation_id, operation
|
|
1244
|
+
)
|
|
1245
|
+
# Stop at allowed paths boundary
|
|
1246
|
+
if parent in self._allowed_paths:
|
|
1247
|
+
break
|
|
1248
|
+
|
|
1249
|
+
path.mkdir(parents=True, exist_ok=exist_ok)
|
|
1250
|
+
created = True
|
|
1251
|
+
except FileExistsError:
|
|
1252
|
+
# FileExistsError is only raised when exist_ok=False
|
|
1253
|
+
# (when exist_ok=True, mkdir() silently succeeds)
|
|
1254
|
+
raise InfraConnectionError(
|
|
1255
|
+
f"Directory already exists: {path.name}",
|
|
1256
|
+
context=ctx,
|
|
1257
|
+
) from None
|
|
1258
|
+
except OSError as e:
|
|
1259
|
+
raise InfraConnectionError(
|
|
1260
|
+
f"Failed to create directory: {e}",
|
|
1261
|
+
context=ctx,
|
|
1262
|
+
) from e
|
|
1263
|
+
|
|
1264
|
+
resolved_path = path.resolve()
|
|
1265
|
+
|
|
1266
|
+
# Reset circuit breaker on success
|
|
1267
|
+
async with self._circuit_breaker_lock:
|
|
1268
|
+
await self._reset_circuit_breaker()
|
|
1269
|
+
|
|
1270
|
+
logger.debug(
|
|
1271
|
+
"Directory ensured",
|
|
1272
|
+
extra={
|
|
1273
|
+
"path": str(resolved_path),
|
|
1274
|
+
"dir_created": created,
|
|
1275
|
+
"already_existed": already_existed,
|
|
1276
|
+
"correlation_id": str(correlation_id),
|
|
1277
|
+
},
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
return ModelHandlerOutput.for_compute(
|
|
1281
|
+
input_envelope_id=input_envelope_id,
|
|
1282
|
+
correlation_id=correlation_id,
|
|
1283
|
+
handler_id=HANDLER_ID_FILESYSTEM,
|
|
1284
|
+
result={
|
|
1285
|
+
"status": "success",
|
|
1286
|
+
"payload": {
|
|
1287
|
+
"path": str(resolved_path),
|
|
1288
|
+
"created": created,
|
|
1289
|
+
"already_existed": already_existed,
|
|
1290
|
+
},
|
|
1291
|
+
"correlation_id": str(correlation_id),
|
|
1292
|
+
},
|
|
1293
|
+
)
|
|
1294
|
+
|
|
1295
|
+
except (InfraConnectionError, InfraUnavailableError):
|
|
1296
|
+
# Record failure for circuit breaker (infra-level failures only)
|
|
1297
|
+
async with self._circuit_breaker_lock:
|
|
1298
|
+
await self._record_circuit_failure(operation, correlation_id)
|
|
1299
|
+
raise
|
|
1300
|
+
|
|
1301
|
+
async def _execute_delete_file(
|
|
1302
|
+
self,
|
|
1303
|
+
payload: dict[str, object],
|
|
1304
|
+
correlation_id: UUID,
|
|
1305
|
+
input_envelope_id: UUID,
|
|
1306
|
+
) -> ModelHandlerOutput[dict[str, object]]:
|
|
1307
|
+
"""Execute filesystem.delete_file operation.
|
|
1308
|
+
|
|
1309
|
+
Payload:
|
|
1310
|
+
- path: str (required) - File path to delete
|
|
1311
|
+
- missing_ok: bool (optional, default False) - Don't error if missing
|
|
1312
|
+
|
|
1313
|
+
Returns:
|
|
1314
|
+
Result with path, deleted, and was_missing flags
|
|
1315
|
+
|
|
1316
|
+
Raises:
|
|
1317
|
+
InfraConnectionError: If delete fails or file not found (when missing_ok=False)
|
|
1318
|
+
InfraUnavailableError: If circuit breaker is open
|
|
1319
|
+
"""
|
|
1320
|
+
operation = "filesystem.delete_file"
|
|
1321
|
+
|
|
1322
|
+
# Extract path (required)
|
|
1323
|
+
path_raw = payload.get("path")
|
|
1324
|
+
if not isinstance(path_raw, str) or not path_raw:
|
|
1325
|
+
ctx = ModelInfraErrorContext(
|
|
1326
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
1327
|
+
operation=operation,
|
|
1328
|
+
target_name="filesystem_handler",
|
|
1329
|
+
correlation_id=correlation_id,
|
|
1330
|
+
)
|
|
1331
|
+
raise ProtocolConfigurationError(
|
|
1332
|
+
"Missing or invalid 'path' in payload", context=ctx
|
|
1333
|
+
)
|
|
1334
|
+
|
|
1335
|
+
path = Path(path_raw)
|
|
1336
|
+
|
|
1337
|
+
# For delete, we need to validate the parent directory is in whitelist
|
|
1338
|
+
if path.parent.exists():
|
|
1339
|
+
self._validate_path_in_whitelist(path.parent, correlation_id, operation)
|
|
1340
|
+
else:
|
|
1341
|
+
ctx = ModelInfraErrorContext(
|
|
1342
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
1343
|
+
operation=operation,
|
|
1344
|
+
target_name=str(path),
|
|
1345
|
+
correlation_id=correlation_id,
|
|
1346
|
+
)
|
|
1347
|
+
raise ProtocolConfigurationError(
|
|
1348
|
+
f"Path '{path}' is outside allowed directories - access denied",
|
|
1349
|
+
context=ctx,
|
|
1350
|
+
)
|
|
1351
|
+
|
|
1352
|
+
ctx = ModelInfraErrorContext(
|
|
1353
|
+
transport_type=EnumInfraTransportType.FILESYSTEM,
|
|
1354
|
+
operation=operation,
|
|
1355
|
+
target_name=str(path),
|
|
1356
|
+
correlation_id=correlation_id,
|
|
1357
|
+
)
|
|
1358
|
+
|
|
1359
|
+
# Extract options
|
|
1360
|
+
missing_ok = payload.get("missing_ok", False)
|
|
1361
|
+
if not isinstance(missing_ok, bool):
|
|
1362
|
+
missing_ok = False
|
|
1363
|
+
|
|
1364
|
+
# Check circuit breaker before I/O operation
|
|
1365
|
+
async with self._circuit_breaker_lock:
|
|
1366
|
+
await self._check_circuit_breaker(operation, correlation_id)
|
|
1367
|
+
|
|
1368
|
+
try:
|
|
1369
|
+
# Check if file exists
|
|
1370
|
+
was_missing = not path.exists()
|
|
1371
|
+
|
|
1372
|
+
if was_missing:
|
|
1373
|
+
if not missing_ok:
|
|
1374
|
+
raise InfraConnectionError(
|
|
1375
|
+
f"File not found: {path.name}",
|
|
1376
|
+
context=ctx,
|
|
1377
|
+
)
|
|
1378
|
+
resolved_path = path.parent.resolve() / path.name
|
|
1379
|
+
deleted = False
|
|
1380
|
+
else:
|
|
1381
|
+
# Validate full path and symlink
|
|
1382
|
+
resolved_path = self._validate_path_in_whitelist(
|
|
1383
|
+
path, correlation_id, operation
|
|
1384
|
+
)
|
|
1385
|
+
self._validate_symlink_target(resolved_path, correlation_id, operation)
|
|
1386
|
+
|
|
1387
|
+
if resolved_path.is_dir():
|
|
1388
|
+
raise InfraConnectionError(
|
|
1389
|
+
f"Path is a directory, use rmdir for directories: {path.name}",
|
|
1390
|
+
context=ctx,
|
|
1391
|
+
)
|
|
1392
|
+
|
|
1393
|
+
# Delete file
|
|
1394
|
+
try:
|
|
1395
|
+
# Final symlink check immediately before I/O to minimize TOCTOU window.
|
|
1396
|
+
# An attacker could replace the file with a symlink between earlier
|
|
1397
|
+
# validation and this point. Re-checking here reduces the window to
|
|
1398
|
+
# the minimum possible (between this check and the actual unlink).
|
|
1399
|
+
if resolved_path.is_symlink():
|
|
1400
|
+
self._validate_symlink_target(
|
|
1401
|
+
resolved_path, correlation_id, operation
|
|
1402
|
+
)
|
|
1403
|
+
|
|
1404
|
+
resolved_path.unlink()
|
|
1405
|
+
deleted = True
|
|
1406
|
+
except OSError as e:
|
|
1407
|
+
raise InfraConnectionError(
|
|
1408
|
+
f"Failed to delete file: {e}",
|
|
1409
|
+
context=ctx,
|
|
1410
|
+
) from e
|
|
1411
|
+
|
|
1412
|
+
# Reset circuit breaker on success
|
|
1413
|
+
async with self._circuit_breaker_lock:
|
|
1414
|
+
await self._reset_circuit_breaker()
|
|
1415
|
+
|
|
1416
|
+
logger.debug(
|
|
1417
|
+
"File delete operation completed",
|
|
1418
|
+
extra={
|
|
1419
|
+
"path": str(resolved_path),
|
|
1420
|
+
"deleted": deleted if not was_missing else False,
|
|
1421
|
+
"was_missing": was_missing,
|
|
1422
|
+
"correlation_id": str(correlation_id),
|
|
1423
|
+
},
|
|
1424
|
+
)
|
|
1425
|
+
|
|
1426
|
+
return ModelHandlerOutput.for_compute(
|
|
1427
|
+
input_envelope_id=input_envelope_id,
|
|
1428
|
+
correlation_id=correlation_id,
|
|
1429
|
+
handler_id=HANDLER_ID_FILESYSTEM,
|
|
1430
|
+
result={
|
|
1431
|
+
"status": "success",
|
|
1432
|
+
"payload": {
|
|
1433
|
+
"path": str(resolved_path),
|
|
1434
|
+
"deleted": deleted if not was_missing else False,
|
|
1435
|
+
"was_missing": was_missing,
|
|
1436
|
+
},
|
|
1437
|
+
"correlation_id": str(correlation_id),
|
|
1438
|
+
},
|
|
1439
|
+
)
|
|
1440
|
+
|
|
1441
|
+
except (InfraConnectionError, InfraUnavailableError):
|
|
1442
|
+
# Record failure for circuit breaker (infra-level failures only)
|
|
1443
|
+
async with self._circuit_breaker_lock:
|
|
1444
|
+
await self._record_circuit_failure(operation, correlation_id)
|
|
1445
|
+
raise
|
|
1446
|
+
|
|
1447
|
+
def describe(self) -> dict[str, object]:
|
|
1448
|
+
"""Return handler metadata and capabilities for introspection.
|
|
1449
|
+
|
|
1450
|
+
This method exposes the handler's three-dimensional type classification
|
|
1451
|
+
along with its operational configuration and capabilities.
|
|
1452
|
+
|
|
1453
|
+
Returns:
|
|
1454
|
+
dict containing:
|
|
1455
|
+
- handler_type: Architectural role from handler_type property
|
|
1456
|
+
- handler_category: Behavioral classification from handler_category property
|
|
1457
|
+
- transport_type: Protocol identifier from transport_type property
|
|
1458
|
+
- supported_operations: List of supported operations
|
|
1459
|
+
- allowed_paths: List of allowed directory paths (when initialized)
|
|
1460
|
+
- max_read_size: Maximum read size in bytes
|
|
1461
|
+
- max_write_size: Maximum write size in bytes
|
|
1462
|
+
- initialized: Whether the handler is initialized
|
|
1463
|
+
- version: Handler version string
|
|
1464
|
+
"""
|
|
1465
|
+
return {
|
|
1466
|
+
"handler_type": self.handler_type.value,
|
|
1467
|
+
"handler_category": self.handler_category.value,
|
|
1468
|
+
"transport_type": self.transport_type.value,
|
|
1469
|
+
"supported_operations": sorted(_SUPPORTED_OPERATIONS),
|
|
1470
|
+
"allowed_paths": [str(p) for p in self._allowed_paths],
|
|
1471
|
+
"max_read_size": self._max_read_size,
|
|
1472
|
+
"max_write_size": self._max_write_size,
|
|
1473
|
+
"initialized": self._initialized,
|
|
1474
|
+
"version": "0.1.0",
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
|
|
1478
|
+
__all__: list[str] = ["HandlerFileSystem"]
|