# Provider Loader System # Dynamic provider loading and interface validation use registry.nu * use interface.nu * use ../utils/logging.nu * # Load provider dynamically with validation (cached) export def load-provider [name: string] { # Check cache first - provider loading happens multiple times due to wrapper scripts let cache_key = $"PROVIDER_CACHE_($name)" if ($cache_key in ($env | columns)) { return ($env | get $cache_key) } # Silent loading - only log debug, not errors for repeated loads if ($env.PROVISIONING_DEBUG? | default false) { log-debug $"Loading provider: ($name)" "provider-loader" } # Check if provider is available if not (is-provider-available $name) { if ($env.PROVISIONING_DEBUG? | default false) { log-debug $"Provider ($name) not found or not available" "provider-loader" } load-env { $cache_key: {} } return {} } # Get provider registry entry let provider_entry = (get-provider-entry $name) # Load the provider module let provider_instance = if ($provider_entry.type == "core") { load-core-provider $provider_entry } else { load-extension-provider $provider_entry } if not ($provider_instance | is-empty) { # IMPORTANT: Skip subprocess-based validation for extension providers. # Child nu processes don't inherit NICKEL_IMPORT_PATH or the provisioning env, # so validate-provider-interface always reports functions missing even when valid. # (Same documented fix as registry.nu:132-146 and load-extension-provider above) # Core providers are loaded from known paths where subprocess context is reliable. let skip_validation = ($provider_entry.type == "extension") let validation = if $skip_validation { { valid: true, missing_functions: [] } } else { validate-provider-interface $name $provider_instance } if $validation.valid { load-env { $cache_key: $provider_instance } $provider_instance } else { if ($env.PROVISIONING_DEBUG? | default false) { log-error $"Provider ($name) failed interface validation" "provider-loader" log-error $"Missing functions: ($validation.missing_functions | str join ', ')" "provider-loader" } load-env { $cache_key: {} } {} } } else { if ($env.PROVISIONING_DEBUG? | default false) { log-error $"Failed to load provider module for ($name)" "provider-loader" } load-env { $cache_key: {} } {} } } # Load core provider def load-core-provider [provider_entry: record] { # For core providers, use direct module loading # Core providers should be in the core library path let module_path = $provider_entry.entry_point # Create provider instance record { name: $provider_entry.name type: "core" loaded: true entry_point: $module_path load_time: (date now) } } # Load extension provider def load-extension-provider [provider_entry: record] { # IMPORTANT: Do NOT spawn a child nu process to validate the provider. # Child processes don't inherit NICKEL_IMPORT_PATH or the provisioning env, # causing all providers to fail validation even though they are valid. # (Same reason registry.nu skips subprocess validation — see registry.nu:132-146) # Just verify the file exists and create the instance directly. let module_path = $provider_entry.entry_point if not ($module_path | path exists) { log-error $"Provider module not found: ($module_path)" "provider-loader" return {} } { name: $provider_entry.name type: "extension" loaded: true entry_point: $module_path load_time: (date now) metadata: {} } } # Get provider instance (with caching) export def get-provider [name: string] { # Check if already loaded in this session let cache_key = $"PROVIDER_LOADED_($name)" let cached_value = if ($cache_key in ($env | columns)) { $env | get $cache_key } else { null } if $cached_value != null { return $cached_value } # Load and cache the provider let provider = (load-provider $name) if not ($provider | is-empty) { load-env { $cache_key: $provider } } $provider } # Call a provider function dynamically export def call-provider-function [ provider_name: string function_name: string ...args ] { # Get provider entry let provider_entry = (get-provider-entry $provider_name) if ($provider_entry | is-empty) { log-error $"Provider ($provider_name) not found" "provider-loader" return null } # Use direct import and call via a wrapper script let temp_dir = ($env.TMPDIR? | default "/tmp") # Save arguments as a list to a single file let args_file = ($temp_dir | path join $"provider_args_(random chars).nuon") $args | to nuon | save --force $args_file # Create wrapper script that loads args and calls function # Build individual arg references based on count let wrapper_script = ($temp_dir | path join $"provider_wrapper_(random chars).nu") let arg_count = ($args | length) let call_line = if $arg_count == 0 { $"($function_name) | to json" } else if $arg_count == 1 { $"($function_name) \(\$args | get 0\) | to json" } else if $arg_count == 2 { $"($function_name) \(\$args | get 0\) \(\$args | get 1\) | to json" } else if $arg_count == 3 { $"($function_name) \(\$args | get 0\) \(\$args | get 1\) \(\$args | get 2\) | to json" } else if $arg_count == 4 { $"($function_name) \(\$args | get 0\) \(\$args | get 1\) \(\$args | get 2\) \(\$args | get 3\) | to json" } else if $arg_count == 5 { $"($function_name) \(\$args | get 0\) \(\$args | get 1\) \(\$args | get 2\) \(\$args | get 3\) \(\$args | get 4\) | to json" } else { log-error $"Too many arguments \(($arg_count)\) for provider function" "provider-loader" return null } let script_content = $" use ($provider_entry.entry_point) * let args = \(open ($args_file)\) ($call_line) " $script_content | save --force $wrapper_script # Execute the wrapper script let result = (do --ignore-errors { nu $wrapper_script } | complete) # Clean up temp files if ($args_file | path exists) { rm -f $args_file } if ($wrapper_script | path exists) { rm -f $wrapper_script } # Return result if successful, null otherwise if $result.exit_code == 0 { # Parse output: always try JSON first (handles strings, bools, records, lists) # The wrapper script serializes all return values with | to json, so bare JSON # strings like "91.98.28.202" must go through from json to strip the quotes. let output = ($result.stdout | str trim) if ($output | is-empty) { null } else { let parsed = (do -i { $output | from json }) let value = if ($parsed | is-empty) { $output } else { $parsed } log-debug $"($provider_name)::($function_name) → ($value)" "provider-loader" $value } } else { log-error $"Provider function call failed: ($result.stderr)" "provider-loader" null } } # Get required provider functions def get-required-functions [] { [ "get-provider-metadata" "query_servers" "server_exists" "check_server_requirements" ] } # Validate provider interface compliance def validate-provider-interface [provider_name: string, provider_instance: record] { let required_functions = (get-required-functions) mut missing_functions = [] mut valid = true # Check if provider file exists let provider_file = $provider_instance.entry_point if not ($provider_file | path exists) { return { valid: false missing_functions: ["provider file not found"] provider: $provider_name } } # Check each required function for func in $required_functions { let check_cmd = $"nu -c \"use ($provider_file) *; help commands | where name == '($func)' | length\"" let check_result = (nu -c $check_cmd | complete) if $check_result.exit_code == 0 { let func_count = ($check_result.stdout | str trim | into int) if $func_count == 0 { $missing_functions = ($missing_functions | append $func) $valid = false } } else { $missing_functions = ($missing_functions | append $func) $valid = false } } { valid: $valid missing_functions: $missing_functions provider: $provider_name checked_functions: ($required_functions | length) validation_time: (date now) } } # Load multiple providers export def load-providers [provider_names: list] { mut results = { successful: 0 failed: 0 total: ($provider_names | length) details: [] } for provider_name in $provider_names { let result = (load-provider $provider_name) if not ($result | is-empty) { $results.successful = ($results.successful + 1) $results.details = ($results.details | append { provider: $provider_name status: "success" loaded: true }) } else { $results.failed = ($results.failed + 1) $results.details = ($results.details | append { provider: $provider_name status: "failed" loaded: false }) } } $results } # Check provider health export def check-provider-health [provider_name: string] { let health_check = { provider: $provider_name available: false loadable: false interface_valid: false metadata_accessible: false timestamp: (date now) } # Check if provider is available let available = (is-provider-available $provider_name) let updated_health = ($health_check | merge { available: $available }) if not $available { return $updated_health } # Try to load provider let provider_instance = (load-provider $provider_name) let loadable = not ($provider_instance | is-empty) let updated_health = ($updated_health | merge { loadable: $loadable }) if not $loadable { return $updated_health } # Check interface validation let validation = (validate-provider-interface $provider_name $provider_instance) let updated_health = ($updated_health | merge { interface_valid: $validation.valid }) # Check metadata access let provider_entry = (get-provider-entry $provider_name) let metadata_cmd = $"nu -c \"use ($provider_entry.entry_point) *; get-provider-metadata\"" let metadata_result = (nu -c $metadata_cmd | complete) let metadata_accessible = ($metadata_result.exit_code == 0) $updated_health | merge { metadata_accessible: $metadata_accessible } } # Check health of all providers export def check-all-providers-health [] { let providers = (list-providers --available-only) $providers | each {|provider| check-provider-health $provider.name } } # Get loader statistics export def get-loader-stats [] { let provider_stats = (get-provider-stats) let health_checks = (check-all-providers-health) { total_providers: $provider_stats.total_providers available_providers: $provider_stats.available_providers healthy_providers: ($health_checks | where interface_valid == true | length) last_check: (date now) } }