Skip to main content

symbi_runtime/
config.rs

1//! Configuration management module for Symbiont runtime
2//!
3//! Provides centralized configuration handling with validation, environment
4//! variable abstraction, and secure defaults.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::env;
9use std::path::PathBuf;
10use thiserror::Error;
11
12/// Configuration errors
13#[derive(Debug, Error)]
14pub enum ConfigError {
15    #[error("Missing required configuration: {key}")]
16    MissingRequired { key: String },
17
18    #[error("Invalid configuration value for {key}: {reason}")]
19    InvalidValue { key: String, reason: String },
20
21    #[error("Environment variable error: {message}")]
22    EnvError { message: String },
23
24    #[error("IO error reading config file: {message}")]
25    IoError { message: String },
26
27    #[error("Configuration parsing error: {message}")]
28    ParseError { message: String },
29}
30
31/// Main application configuration
32#[derive(Debug, Clone, Serialize, Deserialize, Default)]
33pub struct Config {
34    /// API configuration
35    pub api: ApiConfig,
36    /// Database configuration
37    pub database: DatabaseConfig,
38    /// Logging configuration
39    pub logging: LoggingConfig,
40    /// Security configuration
41    pub security: SecurityConfig,
42    /// Storage configuration
43    pub storage: StorageConfig,
44    /// SLM-first configuration
45    pub slm: Option<Slm>,
46    /// Routing configuration
47    pub routing: Option<crate::routing::RoutingConfig>,
48    /// Native execution configuration (optional)
49    pub native_execution: Option<NativeExecutionConfig>,
50    /// AgentPin integration configuration (optional)
51    pub agentpin: Option<crate::integrations::agentpin::AgentPinConfig>,
52    /// CLI executor configuration (optional, requires `cli-executor` feature)
53    #[cfg(feature = "cli-executor")]
54    pub cli_executor: Option<CliExecutorConfigToml>,
55}
56
57/// API configuration.
58///
59/// `auth_token` is redacted in `Debug` output and zeroised on drop to avoid
60/// leaking bearer tokens via `tracing::debug!` of a `Config` value or via
61/// heap inspection after free.
62#[derive(Clone, Serialize, Deserialize)]
63pub struct ApiConfig {
64    /// API server port
65    pub port: u16,
66    /// API server host
67    pub host: String,
68    /// API authentication token (securely handled)
69    #[serde(skip_serializing)]
70    pub auth_token: Option<String>,
71    /// Request timeout in seconds
72    pub timeout_seconds: u64,
73    /// Maximum request body size in bytes
74    pub max_body_size: usize,
75}
76
77impl std::fmt::Debug for ApiConfig {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        f.debug_struct("ApiConfig")
80            .field("port", &self.port)
81            .field("host", &self.host)
82            .field(
83                "auth_token",
84                &self.auth_token.as_ref().map(|_| "[REDACTED]"),
85            )
86            .field("timeout_seconds", &self.timeout_seconds)
87            .field("max_body_size", &self.max_body_size)
88            .finish()
89    }
90}
91
92impl Drop for ApiConfig {
93    fn drop(&mut self) {
94        use zeroize::Zeroize;
95        if let Some(ref mut t) = self.auth_token {
96            t.zeroize();
97        }
98    }
99}
100
101/// Database configuration.
102///
103/// `url` and `redis_url` may embed credentials and are redacted in `Debug`
104/// output and zeroised on drop.
105#[derive(Clone, Serialize, Deserialize)]
106pub struct DatabaseConfig {
107    /// PostgreSQL connection URL
108    #[serde(skip_serializing)]
109    pub url: Option<String>,
110    /// Redis connection URL
111    #[serde(skip_serializing)]
112    pub redis_url: Option<String>,
113    /// Qdrant vector database URL
114    pub qdrant_url: String,
115    /// Qdrant collection name
116    pub qdrant_collection: String,
117    /// Vector dimension
118    pub vector_dimension: usize,
119}
120
121impl std::fmt::Debug for DatabaseConfig {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        f.debug_struct("DatabaseConfig")
124            .field("url", &self.url.as_ref().map(|_| "[REDACTED]"))
125            .field("redis_url", &self.redis_url.as_ref().map(|_| "[REDACTED]"))
126            .field("qdrant_url", &self.qdrant_url)
127            .field("qdrant_collection", &self.qdrant_collection)
128            .field("vector_dimension", &self.vector_dimension)
129            .finish()
130    }
131}
132
133impl Drop for DatabaseConfig {
134    fn drop(&mut self) {
135        use zeroize::Zeroize;
136        if let Some(ref mut u) = self.url {
137            u.zeroize();
138        }
139        if let Some(ref mut u) = self.redis_url {
140            u.zeroize();
141        }
142    }
143}
144
145/// Logging configuration
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct LoggingConfig {
148    /// Log level
149    pub level: String,
150    /// Log format
151    pub format: LogFormat,
152    /// Enable structured logging
153    pub structured: bool,
154}
155
156/// Log format options
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub enum LogFormat {
159    Json,
160    Pretty,
161    Compact,
162}
163
164/// Security configuration
165#[derive(Debug, Clone, Serialize, Deserialize)]
166pub struct SecurityConfig {
167    /// Encryption key provider
168    pub key_provider: KeyProvider,
169    /// Enable/disable features
170    pub enable_compression: bool,
171    pub enable_backups: bool,
172    pub enable_safety_checks: bool,
173}
174
175/// Key provider configuration
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub enum KeyProvider {
178    Environment { var_name: String },
179    File { path: PathBuf },
180    Keychain { service: String, account: String },
181}
182
183/// Native execution configuration (non-isolated host execution)
184/// ⚠️ WARNING: Use only in trusted development environments
185#[derive(Debug, Clone, Serialize, Deserialize)]
186pub struct NativeExecutionConfig {
187    /// Allow native execution without Docker/isolation
188    pub enabled: bool,
189    /// Default executable for native execution
190    pub default_executable: String,
191    /// Working directory for native execution
192    pub working_directory: PathBuf,
193    /// Enforce resource limits even in native mode
194    pub enforce_resource_limits: bool,
195    /// Maximum memory in MB
196    pub max_memory_mb: Option<u64>,
197    /// Maximum CPU time in seconds
198    pub max_cpu_seconds: Option<u64>,
199    /// Maximum execution time (timeout) in seconds
200    pub max_execution_time_seconds: u64,
201    /// Allowed executables (empty = all allowed)
202    pub allowed_executables: Vec<String>,
203}
204
205impl Default for NativeExecutionConfig {
206    fn default() -> Self {
207        Self {
208            enabled: false, // Disabled by default for safety
209            default_executable: "bash".to_string(),
210            working_directory: PathBuf::from("/tmp/symbiont-native"),
211            enforce_resource_limits: true,
212            max_memory_mb: Some(2048),
213            max_cpu_seconds: Some(300),
214            max_execution_time_seconds: 300,
215            allowed_executables: vec![], // Empty — must be explicitly configured
216        }
217    }
218}
219
220/// CLI executor TOML configuration for AI CLI tool orchestration.
221/// Gated behind the `cli-executor` feature.
222#[cfg(feature = "cli-executor")]
223#[derive(Debug, Clone, Serialize, Deserialize, Default)]
224pub struct CliExecutorConfigToml {
225    /// Wall-clock timeout in seconds (default: 600).
226    pub max_runtime_seconds: Option<u64>,
227    /// Idle timeout in seconds — kill if no output for this long (default: 120).
228    pub idle_timeout_seconds: Option<u64>,
229    /// Maximum output bytes per stream (default: 10MB).
230    pub max_output_bytes: Option<usize>,
231    /// Per-adapter configurations keyed by adapter name.
232    pub adapters: Option<HashMap<String, AdapterConfigToml>>,
233}
234
235/// Per-adapter TOML configuration.
236#[cfg(feature = "cli-executor")]
237#[derive(Debug, Clone, Serialize, Deserialize, Default)]
238pub struct AdapterConfigToml {
239    /// Path or name of the executable.
240    pub executable: Option<String>,
241    /// Model override for this adapter.
242    pub model: Option<String>,
243    /// Maximum agentic turns (adapter-specific).
244    pub max_turns: Option<u32>,
245    /// Allowed tools list (adapter-specific).
246    pub allowed_tools: Option<Vec<String>>,
247    /// Disallowed tools list (adapter-specific).
248    pub disallowed_tools: Option<Vec<String>>,
249}
250
251/// Storage configuration
252#[derive(Debug, Clone, Serialize, Deserialize)]
253pub struct StorageConfig {
254    /// Context storage path
255    pub context_path: PathBuf,
256    /// Git clone base path
257    pub git_clone_path: PathBuf,
258    /// Backup directory
259    pub backup_path: PathBuf,
260    /// Maximum context size in MB
261    pub max_context_size_mb: u64,
262}
263
264/// SLM-first configuration
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct Slm {
267    /// Enable SLM-first mode globally
268    pub enabled: bool,
269    /// Model allow list configuration
270    pub model_allow_lists: ModelAllowListConfig,
271    /// Named sandbox profiles for different security tiers
272    pub sandbox_profiles: HashMap<String, SandboxProfile>,
273    /// Default sandbox profile name
274    pub default_sandbox_profile: String,
275}
276
277/// Model allow list configuration with hierarchical overrides
278#[derive(Debug, Clone, Serialize, Deserialize, Default)]
279pub struct ModelAllowListConfig {
280    /// Global model definitions available system-wide
281    pub global_models: Vec<Model>,
282    /// Agent-specific model mappings (agent_id -> model_ids)
283    pub agent_model_maps: HashMap<String, Vec<String>>,
284    /// Allow runtime API-based overrides
285    pub allow_runtime_overrides: bool,
286}
287
288/// Individual model definition
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct Model {
291    /// Unique model identifier
292    pub id: String,
293    /// Human-readable name
294    pub name: String,
295    /// Model provider/source
296    pub provider: ModelProvider,
297    /// Model capabilities
298    pub capabilities: Vec<ModelCapability>,
299    /// Resource requirements for this model
300    pub resource_requirements: ModelResourceRequirements,
301}
302
303/// Model provider configuration
304#[derive(Debug, Clone, Serialize, Deserialize)]
305pub enum ModelProvider {
306    HuggingFace { model_path: String },
307    LocalFile { file_path: PathBuf },
308    OpenAI { model_name: String },
309    Anthropic { model_name: String },
310    Custom { endpoint_url: String },
311}
312
313/// Model capability enumeration
314#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
315pub enum ModelCapability {
316    TextGeneration,
317    CodeGeneration,
318    Reasoning,
319    ToolUse,
320    FunctionCalling,
321    Embeddings,
322}
323
324/// Model resource requirements
325#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct ModelResourceRequirements {
327    /// Minimum memory required in MB
328    pub min_memory_mb: u64,
329    /// Preferred CPU cores
330    pub preferred_cpu_cores: f32,
331    /// GPU requirements
332    pub gpu_requirements: Option<GpuRequirements>,
333}
334
335/// GPU requirements specification
336#[derive(Debug, Clone, Serialize, Deserialize)]
337pub struct GpuRequirements {
338    /// Minimum VRAM in MB
339    pub min_vram_mb: u64,
340    /// Required compute capability
341    pub compute_capability: String,
342}
343
344/// Sandbox profile for SLM runners with comprehensive controls
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct SandboxProfile {
347    /// Resource allocation and limits
348    pub resources: ResourceConstraints,
349    /// Filesystem access controls
350    pub filesystem: FilesystemControls,
351    /// Process execution limits
352    pub process_limits: ProcessLimits,
353    /// Network access policies
354    pub network: NetworkPolicy,
355    /// Security settings
356    pub security: SecuritySettings,
357}
358
359/// Resource constraints for sandbox
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct ResourceConstraints {
362    /// Maximum memory allocation in MB
363    pub max_memory_mb: u64,
364    /// Maximum CPU cores (fractional allowed, e.g., 1.5)
365    pub max_cpu_cores: f32,
366    /// Maximum disk space in MB
367    pub max_disk_mb: u64,
368    /// GPU access configuration
369    pub gpu_access: GpuAccess,
370    /// I/O bandwidth limits
371    pub max_io_bandwidth_mbps: Option<u64>,
372}
373
374/// Filesystem access controls
375#[derive(Debug, Clone, Serialize, Deserialize)]
376pub struct FilesystemControls {
377    /// Allowed read paths (glob patterns supported)
378    pub read_paths: Vec<String>,
379    /// Allowed write paths (glob patterns supported)
380    pub write_paths: Vec<String>,
381    /// Explicitly denied paths (takes precedence)
382    pub denied_paths: Vec<String>,
383    /// Allow temporary file creation
384    pub allow_temp_files: bool,
385    /// Maximum file size in MB
386    pub max_file_size_mb: u64,
387}
388
389/// Process execution limits
390#[derive(Debug, Clone, Serialize, Deserialize)]
391pub struct ProcessLimits {
392    /// Maximum number of child processes
393    pub max_child_processes: u32,
394    /// Maximum execution time in seconds
395    pub max_execution_time_seconds: u64,
396    /// Allowed system calls (seccomp filter)
397    pub allowed_syscalls: Vec<String>,
398    /// Process priority (nice value)
399    pub process_priority: i8,
400}
401
402/// Network access policy
403#[derive(Debug, Clone, Serialize, Deserialize)]
404pub struct NetworkPolicy {
405    /// Network access mode
406    pub access_mode: NetworkAccessMode,
407    /// Allowed destinations (when mode is Restricted)
408    pub allowed_destinations: Vec<NetworkDestination>,
409    /// Maximum bandwidth in Mbps
410    pub max_bandwidth_mbps: Option<u64>,
411}
412
413/// Network access mode enumeration
414#[derive(Debug, Clone, Serialize, Deserialize)]
415pub enum NetworkAccessMode {
416    /// No network access
417    None,
418    /// Restricted to specific destinations
419    Restricted,
420    /// Full network access
421    Full,
422}
423
424/// Network destination specification
425#[derive(Debug, Clone, Serialize, Deserialize)]
426pub struct NetworkDestination {
427    /// Host (can be IP or domain)
428    pub host: String,
429    /// Port (optional, defaults to any)
430    pub port: Option<u16>,
431    /// Protocol restriction
432    pub protocol: Option<NetworkProtocol>,
433}
434
435/// Network protocol enumeration
436#[derive(Debug, Clone, Serialize, Deserialize)]
437pub enum NetworkProtocol {
438    TCP,
439    UDP,
440    HTTP,
441    HTTPS,
442}
443
444/// GPU access configuration
445#[derive(Debug, Clone, Serialize, Deserialize)]
446pub enum GpuAccess {
447    /// No GPU access
448    None,
449    /// Shared GPU access with memory limit
450    Shared { max_memory_mb: u64 },
451    /// Exclusive GPU access
452    Exclusive,
453}
454
455/// Security settings for sandbox
456#[derive(Debug, Clone, Serialize, Deserialize)]
457pub struct SecuritySettings {
458    /// Enable additional syscall filtering
459    pub strict_syscall_filtering: bool,
460    /// Disable debugging interfaces
461    pub disable_debugging: bool,
462    /// Enable audit logging
463    pub enable_audit_logging: bool,
464    /// Encryption requirements
465    pub require_encryption: bool,
466}
467
468impl Default for ApiConfig {
469    fn default() -> Self {
470        Self {
471            port: 8080,
472            host: "127.0.0.1".to_string(),
473            auth_token: None,
474            timeout_seconds: 60,
475            max_body_size: 16 * 1024 * 1024, // 16MB
476        }
477    }
478}
479
480impl Default for DatabaseConfig {
481    fn default() -> Self {
482        Self {
483            url: None,
484            redis_url: None,
485            qdrant_url: "http://localhost:6333".to_string(),
486            qdrant_collection: "agent_knowledge".to_string(),
487            vector_dimension: 1536,
488        }
489    }
490}
491
492impl Default for LoggingConfig {
493    fn default() -> Self {
494        Self {
495            level: "info".to_string(),
496            format: LogFormat::Pretty,
497            structured: false,
498        }
499    }
500}
501
502impl Default for SecurityConfig {
503    fn default() -> Self {
504        Self {
505            key_provider: KeyProvider::Environment {
506                var_name: "SYMBIONT_SECRET_KEY".to_string(),
507            },
508            enable_compression: true,
509            enable_backups: true,
510            enable_safety_checks: true,
511        }
512    }
513}
514
515impl Default for StorageConfig {
516    fn default() -> Self {
517        Self {
518            context_path: PathBuf::from("./agent_storage"),
519            git_clone_path: PathBuf::from("./temp_repos"),
520            backup_path: PathBuf::from("./backups"),
521            max_context_size_mb: 100,
522        }
523    }
524}
525
526impl Default for Slm {
527    fn default() -> Self {
528        let mut profiles = HashMap::new();
529        profiles.insert("secure".to_string(), SandboxProfile::secure_default());
530        profiles.insert("standard".to_string(), SandboxProfile::standard_default());
531
532        Self {
533            enabled: false,
534            model_allow_lists: ModelAllowListConfig::default(),
535            sandbox_profiles: profiles,
536            default_sandbox_profile: "secure".to_string(),
537        }
538    }
539}
540
541impl SandboxProfile {
542    /// Create a secure default profile
543    pub fn secure_default() -> Self {
544        Self {
545            resources: ResourceConstraints {
546                max_memory_mb: 512,
547                max_cpu_cores: 1.0,
548                max_disk_mb: 100,
549                gpu_access: GpuAccess::None,
550                max_io_bandwidth_mbps: Some(10),
551            },
552            filesystem: FilesystemControls {
553                read_paths: vec!["/tmp/sandbox/*".to_string()],
554                write_paths: vec!["/tmp/sandbox/output/*".to_string()],
555                denied_paths: vec!["/etc/*".to_string(), "/proc/*".to_string()],
556                allow_temp_files: true,
557                max_file_size_mb: 10,
558            },
559            process_limits: ProcessLimits {
560                max_child_processes: 0,
561                max_execution_time_seconds: 300,
562                allowed_syscalls: vec!["read".to_string(), "write".to_string(), "open".to_string()],
563                process_priority: 19,
564            },
565            network: NetworkPolicy {
566                access_mode: NetworkAccessMode::None,
567                allowed_destinations: vec![],
568                max_bandwidth_mbps: None,
569            },
570            security: SecuritySettings {
571                strict_syscall_filtering: true,
572                disable_debugging: true,
573                enable_audit_logging: true,
574                require_encryption: true,
575            },
576        }
577    }
578
579    /// Create a standard default profile (less restrictive)
580    pub fn standard_default() -> Self {
581        Self {
582            resources: ResourceConstraints {
583                max_memory_mb: 1024,
584                max_cpu_cores: 2.0,
585                max_disk_mb: 500,
586                gpu_access: GpuAccess::Shared {
587                    max_memory_mb: 1024,
588                },
589                max_io_bandwidth_mbps: Some(50),
590            },
591            filesystem: FilesystemControls {
592                read_paths: vec!["/tmp/*".to_string(), "/home/sandbox/*".to_string()],
593                write_paths: vec!["/tmp/*".to_string(), "/home/sandbox/*".to_string()],
594                denied_paths: vec!["/etc/passwd".to_string(), "/etc/shadow".to_string()],
595                allow_temp_files: true,
596                max_file_size_mb: 100,
597            },
598            process_limits: ProcessLimits {
599                max_child_processes: 5,
600                max_execution_time_seconds: 600,
601                allowed_syscalls: vec![], // Empty means allow all
602                process_priority: 0,
603            },
604            network: NetworkPolicy {
605                access_mode: NetworkAccessMode::Restricted,
606                allowed_destinations: vec![NetworkDestination {
607                    host: "api.openai.com".to_string(),
608                    port: Some(443),
609                    protocol: Some(NetworkProtocol::HTTPS),
610                }],
611                max_bandwidth_mbps: Some(100),
612            },
613            security: SecuritySettings {
614                strict_syscall_filtering: false,
615                disable_debugging: false,
616                enable_audit_logging: true,
617                require_encryption: false,
618            },
619        }
620    }
621
622    /// Validate sandbox profile configuration
623    pub fn validate(&self) -> Result<(), Box<dyn std::error::Error>> {
624        // Validate resource constraints
625        if self.resources.max_memory_mb == 0 {
626            return Err("max_memory_mb must be > 0".into());
627        }
628        if self.resources.max_cpu_cores <= 0.0 {
629            return Err("max_cpu_cores must be > 0".into());
630        }
631
632        // Validate filesystem paths
633        for path in &self.filesystem.read_paths {
634            if path.is_empty() {
635                return Err("read_paths cannot contain empty strings".into());
636            }
637        }
638
639        // Validate process limits
640        if self.process_limits.max_execution_time_seconds == 0 {
641            return Err("max_execution_time_seconds must be > 0".into());
642        }
643
644        Ok(())
645    }
646}
647
648impl Slm {
649    /// Validate the SLM configuration
650    pub fn validate(&self) -> Result<(), ConfigError> {
651        // Validate default sandbox profile exists
652        if !self
653            .sandbox_profiles
654            .contains_key(&self.default_sandbox_profile)
655        {
656            return Err(ConfigError::InvalidValue {
657                key: "slm.default_sandbox_profile".to_string(),
658                reason: format!(
659                    "Profile '{}' not found in sandbox_profiles",
660                    self.default_sandbox_profile
661                ),
662            });
663        }
664
665        // Validate model definitions have unique IDs
666        let mut model_ids = std::collections::HashSet::new();
667        for model in &self.model_allow_lists.global_models {
668            if !model_ids.insert(&model.id) {
669                return Err(ConfigError::InvalidValue {
670                    key: "slm.model_allow_lists.global_models".to_string(),
671                    reason: format!("Duplicate model ID: {}", model.id),
672                });
673            }
674        }
675
676        // Validate agent model mappings reference existing models
677        for (agent_id, model_ids) in &self.model_allow_lists.agent_model_maps {
678            for model_id in model_ids {
679                if !self
680                    .model_allow_lists
681                    .global_models
682                    .iter()
683                    .any(|m| &m.id == model_id)
684                {
685                    return Err(ConfigError::InvalidValue {
686                        key: format!("slm.model_allow_lists.agent_model_maps.{}", agent_id),
687                        reason: format!("Model ID '{}' not found in global_models", model_id),
688                    });
689                }
690            }
691        }
692
693        // Validate sandbox profiles
694        for (profile_name, profile) in &self.sandbox_profiles {
695            profile.validate().map_err(|e| ConfigError::InvalidValue {
696                key: format!("slm.sandbox_profiles.{}", profile_name),
697                reason: e.to_string(),
698            })?;
699        }
700
701        Ok(())
702    }
703
704    /// Get allowed models for a specific agent
705    pub fn get_allowed_models(&self, agent_id: &str) -> Vec<&Model> {
706        // Check agent-specific mappings first
707        if let Some(model_ids) = self.model_allow_lists.agent_model_maps.get(agent_id) {
708            self.model_allow_lists
709                .global_models
710                .iter()
711                .filter(|model| model_ids.contains(&model.id))
712                .collect()
713        } else {
714            // Fall back to all global models if no specific mapping
715            self.model_allow_lists.global_models.iter().collect()
716        }
717    }
718}
719
720impl Config {
721    /// Load configuration from environment variables and defaults
722    pub fn from_env() -> Result<Self, ConfigError> {
723        let mut config = Self::default();
724
725        // Load API configuration
726        if let Ok(port) = env::var("API_PORT") {
727            config.api.port = port.parse().map_err(|_| ConfigError::InvalidValue {
728                key: "API_PORT".to_string(),
729                reason: "Invalid port number".to_string(),
730            })?;
731        }
732
733        if let Ok(host) = env::var("API_HOST") {
734            config.api.host = host;
735        }
736
737        // Load and validate auth token if present
738        if let Ok(token) = env::var("API_AUTH_TOKEN") {
739            match Self::validate_auth_token(&token) {
740                Ok(validated_token) => {
741                    config.api.auth_token = Some(validated_token);
742                }
743                Err(e) => {
744                    tracing::error!("Invalid API_AUTH_TOKEN: {}", e);
745                    eprintln!("⚠️  ERROR: Invalid API_AUTH_TOKEN: {}", e);
746                    // Don't set the token if it's invalid
747                }
748            }
749        }
750
751        // Load database configuration
752        if let Ok(db_url) = env::var("DATABASE_URL") {
753            config.database.url = Some(db_url);
754        }
755
756        if let Ok(redis_url) = env::var("REDIS_URL") {
757            config.database.redis_url = Some(redis_url);
758        }
759
760        if let Ok(qdrant_url) = env::var("QDRANT_URL") {
761            config.database.qdrant_url = qdrant_url;
762        }
763
764        // Load logging configuration
765        if let Ok(log_level) = env::var("LOG_LEVEL") {
766            config.logging.level = log_level;
767        }
768
769        // Load security configuration
770        if let Ok(key_var) = env::var("SYMBIONT_SECRET_KEY_VAR") {
771            config.security.key_provider = KeyProvider::Environment { var_name: key_var };
772        }
773
774        // Load storage configuration
775        if let Ok(context_path) = env::var("CONTEXT_STORAGE_PATH") {
776            config.storage.context_path = PathBuf::from(context_path);
777        }
778
779        if let Ok(git_path) = env::var("GIT_CLONE_BASE_PATH") {
780            config.storage.git_clone_path = PathBuf::from(git_path);
781        }
782
783        if let Ok(backup_path) = env::var("BACKUP_DIRECTORY") {
784            config.storage.backup_path = PathBuf::from(backup_path);
785        }
786
787        Ok(config)
788    }
789
790    /// Load configuration from file
791    pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, ConfigError> {
792        let content = std::fs::read_to_string(path).map_err(|e| ConfigError::IoError {
793            message: e.to_string(),
794        })?;
795
796        let config: Self = toml::from_str(&content).map_err(|e| ConfigError::ParseError {
797            message: e.to_string(),
798        })?;
799
800        Ok(config)
801    }
802
803    /// Validate configuration
804    pub fn validate(&self) -> Result<(), ConfigError> {
805        // Validate port range
806        if self.api.port == 0 {
807            return Err(ConfigError::InvalidValue {
808                key: "api.port".to_string(),
809                reason: "Port cannot be 0".to_string(),
810            });
811        }
812
813        // Validate log level
814        let valid_levels = ["error", "warn", "info", "debug", "trace"];
815        if !valid_levels.contains(&self.logging.level.as_str()) {
816            return Err(ConfigError::InvalidValue {
817                key: "logging.level".to_string(),
818                reason: format!("Must be one of: {}", valid_levels.join(", ")),
819            });
820        }
821
822        // Validate vector dimension
823        if self.database.vector_dimension == 0 {
824            return Err(ConfigError::InvalidValue {
825                key: "database.vector_dimension".to_string(),
826                reason: "Vector dimension must be > 0".to_string(),
827            });
828        }
829
830        // Validate SLM configuration if enabled
831        if let Some(slm) = &self.slm {
832            if slm.enabled {
833                slm.validate()?;
834            }
835        }
836
837        Ok(())
838    }
839
840    /// Get API auth token securely
841    pub fn get_api_auth_token(&self) -> Result<String, ConfigError> {
842        match &self.api.auth_token {
843            Some(token) => Ok(token.clone()),
844            None => Err(ConfigError::MissingRequired {
845                key: "API_AUTH_TOKEN".to_string(),
846            }),
847        }
848    }
849
850    /// Get database URL securely
851    pub fn get_database_url(&self) -> Result<String, ConfigError> {
852        match &self.database.url {
853            Some(url) => Ok(url.clone()),
854            None => Err(ConfigError::MissingRequired {
855                key: "DATABASE_URL".to_string(),
856            }),
857        }
858    }
859
860    /// Get secret key based on provider configuration
861    pub fn get_secret_key(&self) -> Result<String, ConfigError> {
862        match &self.security.key_provider {
863            KeyProvider::Environment { var_name } => {
864                env::var(var_name).map_err(|_| ConfigError::MissingRequired {
865                    key: var_name.clone(),
866                })
867            }
868            KeyProvider::File { path } => std::fs::read_to_string(path)
869                .map(|s| s.trim().to_string())
870                .map_err(|e| ConfigError::IoError {
871                    message: e.to_string(),
872                }),
873            KeyProvider::Keychain { service, account } => {
874                #[cfg(feature = "keychain")]
875                {
876                    use keyring::Entry;
877                    let entry =
878                        Entry::new(service, account).map_err(|e| ConfigError::EnvError {
879                            message: e.to_string(),
880                        })?;
881                    entry.get_password().map_err(|e| ConfigError::EnvError {
882                        message: e.to_string(),
883                    })
884                }
885                #[cfg(not(feature = "keychain"))]
886                {
887                    Err(ConfigError::EnvError {
888                        message: "Keychain support not enabled".to_string(),
889                    })
890                }
891            }
892        }
893    }
894
895    /// Validate an authentication token for security best practices
896    ///
897    /// Returns an error if the token:
898    /// - Is empty or only whitespace
899    /// - Is too short (< 8 characters)
900    /// - Matches known weak/default tokens
901    /// - Contains only whitespace
902    ///
903    /// Returns Ok(trimmed_token) if validation passes
904    fn validate_auth_token(token: &str) -> Result<String, ConfigError> {
905        // Trim whitespace
906        let trimmed = token.trim();
907
908        // Check if empty
909        if trimmed.is_empty() {
910            return Err(ConfigError::InvalidValue {
911                key: "auth_token".to_string(),
912                reason: "Token cannot be empty".to_string(),
913            });
914        }
915
916        // Check for known weak/default tokens (case-insensitive) before length check
917        // so that short weak tokens like "dev" get the correct error message
918        let weak_tokens = [
919            "dev",
920            "test",
921            "password",
922            "secret",
923            "token",
924            "api_key",
925            "12345678",
926            "admin",
927            "root",
928            "default",
929            "changeme",
930            "letmein",
931            "qwerty",
932            "abc123",
933            "password123",
934        ];
935
936        if weak_tokens.contains(&trimmed.to_lowercase().as_str()) {
937            return Err(ConfigError::InvalidValue {
938                key: "auth_token".to_string(),
939                reason: format!(
940                    "Token '{}' is a known weak/default token. Use a strong random token instead.",
941                    trimmed
942                ),
943            });
944        }
945
946        // Check minimum length
947        if trimmed.len() < 8 {
948            return Err(ConfigError::InvalidValue {
949                key: "auth_token".to_string(),
950                reason: "Token must be at least 8 characters long".to_string(),
951            });
952        }
953
954        // Warn if token appears to be weak (all same character, sequential, etc.)
955        if trimmed
956            .chars()
957            .all(|c| c == trimmed.chars().next().unwrap())
958        {
959            tracing::warn!("⚠️  Auth token appears weak (all same character)");
960        }
961
962        // Check for potential secrets in token (bcrypt hashes, jwt tokens, etc. are OK)
963        if trimmed.contains(' ') && !trimmed.starts_with("Bearer ") {
964            return Err(ConfigError::InvalidValue {
965                key: "auth_token".to_string(),
966                reason: "Token should not contain spaces (unless it's a Bearer token)".to_string(),
967            });
968        }
969
970        Ok(trimmed.to_string())
971    }
972}
973
974#[cfg(test)]
975mod tests {
976    use super::*;
977    use serial_test::serial;
978    use std::collections::HashMap;
979    use std::env;
980    use std::path::PathBuf;
981
982    #[test]
983    fn test_default_config() {
984        let config = Config::default();
985        assert_eq!(config.api.port, 8080);
986        assert_eq!(config.api.host, "127.0.0.1");
987        assert!(config.validate().is_ok());
988    }
989
990    #[test]
991    #[serial]
992    fn test_config_from_env() {
993        env::set_var("API_PORT", "9090");
994        env::set_var("API_HOST", "0.0.0.0");
995        env::set_var("LOG_LEVEL", "debug");
996
997        let config = Config::from_env().unwrap();
998        assert_eq!(config.api.port, 9090);
999        assert_eq!(config.api.host, "0.0.0.0");
1000        assert_eq!(config.logging.level, "debug");
1001
1002        // Cleanup
1003        env::remove_var("API_PORT");
1004        env::remove_var("API_HOST");
1005        env::remove_var("LOG_LEVEL");
1006    }
1007
1008    #[test]
1009    fn test_invalid_port() {
1010        let mut config = Config::default();
1011        config.api.port = 0;
1012        assert!(config.validate().is_err());
1013    }
1014
1015    #[test]
1016    fn test_invalid_log_level() {
1017        let mut config = Config::default();
1018        config.logging.level = "invalid".to_string();
1019        assert!(config.validate().is_err());
1020    }
1021
1022    // SLM Configuration Tests
1023    #[test]
1024    fn test_slm_default_config() {
1025        let slm = Slm::default();
1026        assert!(!slm.enabled);
1027        assert_eq!(slm.default_sandbox_profile, "secure");
1028        assert!(slm.sandbox_profiles.contains_key("secure"));
1029        assert!(slm.sandbox_profiles.contains_key("standard"));
1030        assert!(slm.validate().is_ok());
1031    }
1032
1033    #[test]
1034    fn test_slm_validation_invalid_default_profile() {
1035        let slm = Slm {
1036            default_sandbox_profile: "nonexistent".to_string(),
1037            ..Default::default()
1038        };
1039
1040        let result = slm.validate();
1041        assert!(result.is_err());
1042        if let Err(ConfigError::InvalidValue { key, reason }) = result {
1043            assert_eq!(key, "slm.default_sandbox_profile");
1044            assert!(reason.contains("nonexistent"));
1045        }
1046    }
1047
1048    #[test]
1049    fn test_slm_validation_duplicate_model_ids() {
1050        let model1 = Model {
1051            id: "duplicate".to_string(),
1052            name: "Model 1".to_string(),
1053            provider: ModelProvider::LocalFile {
1054                file_path: PathBuf::from("/tmp/model1.gguf"),
1055            },
1056            capabilities: vec![ModelCapability::TextGeneration],
1057            resource_requirements: ModelResourceRequirements {
1058                min_memory_mb: 512,
1059                preferred_cpu_cores: 1.0,
1060                gpu_requirements: None,
1061            },
1062        };
1063
1064        let model2 = Model {
1065            id: "duplicate".to_string(), // Same ID
1066            name: "Model 2".to_string(),
1067            provider: ModelProvider::LocalFile {
1068                file_path: PathBuf::from("/tmp/model2.gguf"),
1069            },
1070            capabilities: vec![ModelCapability::CodeGeneration],
1071            resource_requirements: ModelResourceRequirements {
1072                min_memory_mb: 1024,
1073                preferred_cpu_cores: 2.0,
1074                gpu_requirements: None,
1075            },
1076        };
1077
1078        let mut slm = Slm::default();
1079        slm.model_allow_lists.global_models = vec![model1, model2];
1080
1081        let result = slm.validate();
1082        assert!(result.is_err());
1083        if let Err(ConfigError::InvalidValue { key, reason }) = result {
1084            assert_eq!(key, "slm.model_allow_lists.global_models");
1085            assert!(reason.contains("Duplicate model ID: duplicate"));
1086        }
1087    }
1088
1089    #[test]
1090    fn test_slm_validation_invalid_agent_model_mapping() {
1091        let model = Model {
1092            id: "test_model".to_string(),
1093            name: "Test Model".to_string(),
1094            provider: ModelProvider::LocalFile {
1095                file_path: PathBuf::from("/tmp/test.gguf"),
1096            },
1097            capabilities: vec![ModelCapability::TextGeneration],
1098            resource_requirements: ModelResourceRequirements {
1099                min_memory_mb: 512,
1100                preferred_cpu_cores: 1.0,
1101                gpu_requirements: None,
1102            },
1103        };
1104
1105        let mut slm = Slm::default();
1106        slm.model_allow_lists.global_models = vec![model];
1107
1108        let mut agent_model_maps = HashMap::new();
1109        agent_model_maps.insert(
1110            "test_agent".to_string(),
1111            vec!["nonexistent_model".to_string()],
1112        );
1113        slm.model_allow_lists.agent_model_maps = agent_model_maps;
1114
1115        let result = slm.validate();
1116        assert!(result.is_err());
1117        if let Err(ConfigError::InvalidValue { key, reason }) = result {
1118            assert_eq!(key, "slm.model_allow_lists.agent_model_maps.test_agent");
1119            assert!(reason.contains("Model ID 'nonexistent_model' not found"));
1120        }
1121    }
1122
1123    #[test]
1124    fn test_slm_get_allowed_models_with_agent_mapping() {
1125        let model1 = Model {
1126            id: "model1".to_string(),
1127            name: "Model 1".to_string(),
1128            provider: ModelProvider::LocalFile {
1129                file_path: PathBuf::from("/tmp/model1.gguf"),
1130            },
1131            capabilities: vec![ModelCapability::TextGeneration],
1132            resource_requirements: ModelResourceRequirements {
1133                min_memory_mb: 512,
1134                preferred_cpu_cores: 1.0,
1135                gpu_requirements: None,
1136            },
1137        };
1138
1139        let model2 = Model {
1140            id: "model2".to_string(),
1141            name: "Model 2".to_string(),
1142            provider: ModelProvider::LocalFile {
1143                file_path: PathBuf::from("/tmp/model2.gguf"),
1144            },
1145            capabilities: vec![ModelCapability::CodeGeneration],
1146            resource_requirements: ModelResourceRequirements {
1147                min_memory_mb: 1024,
1148                preferred_cpu_cores: 2.0,
1149                gpu_requirements: None,
1150            },
1151        };
1152
1153        let mut slm = Slm::default();
1154        slm.model_allow_lists.global_models = vec![model1, model2];
1155
1156        let mut agent_model_maps = HashMap::new();
1157        agent_model_maps.insert("agent1".to_string(), vec!["model1".to_string()]);
1158        slm.model_allow_lists.agent_model_maps = agent_model_maps;
1159
1160        // Agent with specific mapping should only get their models
1161        let allowed_models = slm.get_allowed_models("agent1");
1162        assert_eq!(allowed_models.len(), 1);
1163        assert_eq!(allowed_models[0].id, "model1");
1164
1165        // Agent without mapping should get all global models
1166        let allowed_models = slm.get_allowed_models("agent2");
1167        assert_eq!(allowed_models.len(), 2);
1168    }
1169
1170    // Sandbox Profile Tests
1171    #[test]
1172    fn test_sandbox_profile_secure_default() {
1173        let profile = SandboxProfile::secure_default();
1174        assert_eq!(profile.resources.max_memory_mb, 512);
1175        assert_eq!(profile.resources.max_cpu_cores, 1.0);
1176        assert!(matches!(profile.resources.gpu_access, GpuAccess::None));
1177        assert!(matches!(
1178            profile.network.access_mode,
1179            NetworkAccessMode::None
1180        ));
1181        assert!(profile.security.strict_syscall_filtering);
1182        assert!(profile.validate().is_ok());
1183    }
1184
1185    #[test]
1186    fn test_sandbox_profile_standard_default() {
1187        let profile = SandboxProfile::standard_default();
1188        assert_eq!(profile.resources.max_memory_mb, 1024);
1189        assert_eq!(profile.resources.max_cpu_cores, 2.0);
1190        assert!(matches!(
1191            profile.resources.gpu_access,
1192            GpuAccess::Shared { .. }
1193        ));
1194        assert!(matches!(
1195            profile.network.access_mode,
1196            NetworkAccessMode::Restricted
1197        ));
1198        assert!(!profile.security.strict_syscall_filtering);
1199        assert!(profile.validate().is_ok());
1200    }
1201
1202    #[test]
1203    fn test_sandbox_profile_validation_zero_memory() {
1204        let mut profile = SandboxProfile::secure_default();
1205        profile.resources.max_memory_mb = 0;
1206
1207        let result = profile.validate();
1208        assert!(result.is_err());
1209        assert!(result
1210            .unwrap_err()
1211            .to_string()
1212            .contains("max_memory_mb must be > 0"));
1213    }
1214
1215    #[test]
1216    fn test_sandbox_profile_validation_zero_cpu() {
1217        let mut profile = SandboxProfile::secure_default();
1218        profile.resources.max_cpu_cores = 0.0;
1219
1220        let result = profile.validate();
1221        assert!(result.is_err());
1222        assert!(result
1223            .unwrap_err()
1224            .to_string()
1225            .contains("max_cpu_cores must be > 0"));
1226    }
1227
1228    #[test]
1229    fn test_sandbox_profile_validation_empty_read_path() {
1230        let mut profile = SandboxProfile::secure_default();
1231        profile.filesystem.read_paths.push("".to_string());
1232
1233        let result = profile.validate();
1234        assert!(result.is_err());
1235        assert!(result
1236            .unwrap_err()
1237            .to_string()
1238            .contains("read_paths cannot contain empty strings"));
1239    }
1240
1241    #[test]
1242    fn test_sandbox_profile_validation_zero_execution_time() {
1243        let mut profile = SandboxProfile::secure_default();
1244        profile.process_limits.max_execution_time_seconds = 0;
1245
1246        let result = profile.validate();
1247        assert!(result.is_err());
1248        assert!(result
1249            .unwrap_err()
1250            .to_string()
1251            .contains("max_execution_time_seconds must be > 0"));
1252    }
1253
1254    // Model Configuration Tests
1255    #[test]
1256    fn test_model_provider_variants() {
1257        let huggingface_model = Model {
1258            id: "hf_model".to_string(),
1259            name: "HuggingFace Model".to_string(),
1260            provider: ModelProvider::HuggingFace {
1261                model_path: "microsoft/DialoGPT-medium".to_string(),
1262            },
1263            capabilities: vec![ModelCapability::TextGeneration],
1264            resource_requirements: ModelResourceRequirements {
1265                min_memory_mb: 512,
1266                preferred_cpu_cores: 1.0,
1267                gpu_requirements: None,
1268            },
1269        };
1270
1271        let openai_model = Model {
1272            id: "openai_model".to_string(),
1273            name: "OpenAI Model".to_string(),
1274            provider: ModelProvider::OpenAI {
1275                model_name: "gpt-3.5-turbo".to_string(),
1276            },
1277            capabilities: vec![ModelCapability::TextGeneration, ModelCapability::Reasoning],
1278            resource_requirements: ModelResourceRequirements {
1279                min_memory_mb: 0, // Cloud model
1280                preferred_cpu_cores: 0.0,
1281                gpu_requirements: None,
1282            },
1283        };
1284
1285        assert_eq!(huggingface_model.id, "hf_model");
1286        assert_eq!(openai_model.id, "openai_model");
1287    }
1288
1289    #[test]
1290    fn test_model_capabilities() {
1291        let all_capabilities = vec![
1292            ModelCapability::TextGeneration,
1293            ModelCapability::CodeGeneration,
1294            ModelCapability::Reasoning,
1295            ModelCapability::ToolUse,
1296            ModelCapability::FunctionCalling,
1297            ModelCapability::Embeddings,
1298        ];
1299
1300        let model = Model {
1301            id: "full_model".to_string(),
1302            name: "Full Capability Model".to_string(),
1303            provider: ModelProvider::LocalFile {
1304                file_path: PathBuf::from("/tmp/full.gguf"),
1305            },
1306            capabilities: all_capabilities.clone(),
1307            resource_requirements: ModelResourceRequirements {
1308                min_memory_mb: 2048,
1309                preferred_cpu_cores: 4.0,
1310                gpu_requirements: Some(GpuRequirements {
1311                    min_vram_mb: 8192,
1312                    compute_capability: "7.5".to_string(),
1313                }),
1314            },
1315        };
1316
1317        assert_eq!(model.capabilities.len(), 6);
1318        for capability in &all_capabilities {
1319            assert!(model.capabilities.contains(capability));
1320        }
1321    }
1322
1323    // Configuration File Tests
1324    #[test]
1325    fn test_config_validation_vector_dimension() {
1326        let mut config = Config::default();
1327        config.database.vector_dimension = 0;
1328
1329        let result = config.validate();
1330        assert!(result.is_err());
1331        if let Err(ConfigError::InvalidValue { key, reason }) = result {
1332            assert_eq!(key, "database.vector_dimension");
1333            assert!(reason.contains("Vector dimension must be > 0"));
1334        }
1335    }
1336
1337    #[test]
1338    fn test_config_validation_with_slm() {
1339        let mut config = Config::default();
1340        let slm = Slm {
1341            enabled: true,
1342            default_sandbox_profile: "invalid".to_string(), // This should cause validation to fail
1343            ..Default::default()
1344        };
1345        config.slm = Some(slm);
1346
1347        let result = config.validate();
1348        assert!(result.is_err());
1349    }
1350
1351    #[test]
1352    fn test_config_secret_key_retrieval() {
1353        // Test environment variable key provider
1354        env::set_var("TEST_SECRET_KEY", "test_secret_123");
1355
1356        let mut config = Config::default();
1357        config.security.key_provider = KeyProvider::Environment {
1358            var_name: "TEST_SECRET_KEY".to_string(),
1359        };
1360
1361        let key = config.get_secret_key();
1362        assert!(key.is_ok());
1363        assert_eq!(key.unwrap(), "test_secret_123");
1364
1365        env::remove_var("TEST_SECRET_KEY");
1366    }
1367
1368    #[test]
1369    fn test_config_secret_key_missing() {
1370        let mut config = Config::default();
1371        config.security.key_provider = KeyProvider::Environment {
1372            var_name: "NONEXISTENT_KEY".to_string(),
1373        };
1374
1375        let result = config.get_secret_key();
1376        assert!(result.is_err());
1377        if let Err(ConfigError::MissingRequired { key }) = result {
1378            assert_eq!(key, "NONEXISTENT_KEY");
1379        }
1380    }
1381
1382    #[test]
1383    fn test_network_policy_configurations() {
1384        // Test restricted network access
1385        let destination = NetworkDestination {
1386            host: "api.openai.com".to_string(),
1387            port: Some(443),
1388            protocol: Some(NetworkProtocol::HTTPS),
1389        };
1390
1391        let network_policy = NetworkPolicy {
1392            access_mode: NetworkAccessMode::Restricted,
1393            allowed_destinations: vec![destination],
1394            max_bandwidth_mbps: Some(100),
1395        };
1396
1397        let profile = SandboxProfile {
1398            resources: ResourceConstraints {
1399                max_memory_mb: 1024,
1400                max_cpu_cores: 2.0,
1401                max_disk_mb: 500,
1402                gpu_access: GpuAccess::None,
1403                max_io_bandwidth_mbps: Some(50),
1404            },
1405            filesystem: FilesystemControls {
1406                read_paths: vec!["/tmp/*".to_string()],
1407                write_paths: vec!["/tmp/output/*".to_string()],
1408                denied_paths: vec!["/etc/*".to_string()],
1409                allow_temp_files: true,
1410                max_file_size_mb: 10,
1411            },
1412            process_limits: ProcessLimits {
1413                max_child_processes: 2,
1414                max_execution_time_seconds: 300,
1415                allowed_syscalls: vec!["read".to_string(), "write".to_string()],
1416                process_priority: 0,
1417            },
1418            network: network_policy,
1419            security: SecuritySettings {
1420                strict_syscall_filtering: true,
1421                disable_debugging: true,
1422                enable_audit_logging: true,
1423                require_encryption: false,
1424            },
1425        };
1426
1427        assert!(profile.validate().is_ok());
1428        assert!(matches!(
1429            profile.network.access_mode,
1430            NetworkAccessMode::Restricted
1431        ));
1432        assert_eq!(profile.network.allowed_destinations.len(), 1);
1433        assert_eq!(
1434            profile.network.allowed_destinations[0].host,
1435            "api.openai.com"
1436        );
1437    }
1438
1439    #[test]
1440    fn test_gpu_requirements_configurations() {
1441        let gpu_requirements = GpuRequirements {
1442            min_vram_mb: 4096,
1443            compute_capability: "8.0".to_string(),
1444        };
1445
1446        let model = Model {
1447            id: "gpu_model".to_string(),
1448            name: "GPU Model".to_string(),
1449            provider: ModelProvider::LocalFile {
1450                file_path: PathBuf::from("/tmp/gpu.gguf"),
1451            },
1452            capabilities: vec![ModelCapability::TextGeneration],
1453            resource_requirements: ModelResourceRequirements {
1454                min_memory_mb: 1024,
1455                preferred_cpu_cores: 2.0,
1456                gpu_requirements: Some(gpu_requirements),
1457            },
1458        };
1459
1460        assert!(model.resource_requirements.gpu_requirements.is_some());
1461        let gpu_req = model.resource_requirements.gpu_requirements.unwrap();
1462        assert_eq!(gpu_req.min_vram_mb, 4096);
1463        assert_eq!(gpu_req.compute_capability, "8.0");
1464    }
1465
1466    #[test]
1467    #[serial]
1468    fn test_config_from_env_invalid_port() {
1469        env::set_var("API_PORT", "invalid");
1470
1471        let result = Config::from_env();
1472        assert!(result.is_err());
1473        if let Err(ConfigError::InvalidValue { key, reason }) = result {
1474            assert_eq!(key, "API_PORT");
1475            assert!(reason.contains("Invalid port number"));
1476        }
1477
1478        env::remove_var("API_PORT");
1479    }
1480
1481    #[test]
1482    fn test_api_auth_token_missing() {
1483        let config = Config::default();
1484
1485        let result = config.get_api_auth_token();
1486        assert!(result.is_err());
1487        if let Err(ConfigError::MissingRequired { key }) = result {
1488            assert_eq!(key, "API_AUTH_TOKEN");
1489        }
1490    }
1491
1492    #[test]
1493    fn test_database_url_missing() {
1494        let config = Config::default();
1495
1496        let result = config.get_database_url();
1497        assert!(result.is_err());
1498        if let Err(ConfigError::MissingRequired { key }) = result {
1499            assert_eq!(key, "DATABASE_URL");
1500        }
1501    }
1502
1503    // ============================================================================
1504    // Security Tests for Token Validation
1505    // ============================================================================
1506
1507    #[test]
1508    fn test_validate_auth_token_valid_strong_token() {
1509        let tokens = vec![
1510            "MySecureToken123",
1511            "a1b2c3d4e5f6g7h8",
1512            "production_token_2024",
1513            "Bearer_abc123def456",
1514        ];
1515
1516        for token in tokens {
1517            let result = Config::validate_auth_token(token);
1518            assert!(result.is_ok(), "Token '{}' should be valid", token);
1519            assert_eq!(result.unwrap(), token.trim());
1520        }
1521    }
1522
1523    #[test]
1524    fn test_validate_auth_token_empty() {
1525        assert!(Config::validate_auth_token("").is_err());
1526        assert!(Config::validate_auth_token("   ").is_err());
1527        assert!(Config::validate_auth_token("\t\n").is_err());
1528    }
1529
1530    #[test]
1531    fn test_validate_auth_token_too_short() {
1532        let short_tokens = vec!["abc", "12345", "short", "1234567"];
1533
1534        for token in short_tokens {
1535            let result = Config::validate_auth_token(token);
1536            assert!(
1537                result.is_err(),
1538                "Token '{}' should be rejected (too short)",
1539                token
1540            );
1541
1542            if let Err(ConfigError::InvalidValue { reason, .. }) = result {
1543                assert!(reason.contains("at least 8 characters"));
1544            }
1545        }
1546    }
1547
1548    #[test]
1549    fn test_validate_auth_token_weak_defaults() {
1550        let weak_tokens = vec![
1551            "dev", "test", "password", "secret", "token", "admin", "root", "default", "changeme",
1552            "12345678",
1553        ];
1554
1555        for token in weak_tokens {
1556            let result = Config::validate_auth_token(token);
1557            assert!(result.is_err(), "Weak token '{}' should be rejected", token);
1558
1559            if let Err(ConfigError::InvalidValue { reason, .. }) = result {
1560                assert!(
1561                    reason.contains("weak/default token"),
1562                    "Expected 'weak/default token' message for '{}', got: {}",
1563                    token,
1564                    reason
1565                );
1566            }
1567        }
1568    }
1569
1570    #[test]
1571    fn test_validate_auth_token_case_insensitive_weak_check() {
1572        let tokens = vec!["DEV", "Test", "PASSWORD", "Admin", "ROOT"];
1573
1574        for token in tokens {
1575            let result = Config::validate_auth_token(token);
1576            assert!(
1577                result.is_err(),
1578                "Token '{}' should be rejected (case-insensitive)",
1579                token
1580            );
1581        }
1582    }
1583
1584    #[test]
1585    fn test_validate_auth_token_with_spaces() {
1586        // Spaces should be rejected unless it's a Bearer token
1587        let result = Config::validate_auth_token("my token here");
1588        assert!(result.is_err());
1589
1590        if let Err(ConfigError::InvalidValue { reason, .. }) = result {
1591            assert!(reason.contains("should not contain spaces"));
1592        }
1593    }
1594
1595    #[test]
1596    fn test_validate_auth_token_trims_whitespace() {
1597        let result = Config::validate_auth_token("  validtoken123  ");
1598        assert!(result.is_ok());
1599        assert_eq!(result.unwrap(), "validtoken123");
1600    }
1601
1602    #[test]
1603    fn test_validate_auth_token_minimum_length_boundary() {
1604        // Exactly 8 characters should pass
1605        assert!(Config::validate_auth_token("12345678").is_err()); // Weak token
1606        assert!(Config::validate_auth_token("abcdefgh").is_ok());
1607
1608        // 7 characters should fail
1609        assert!(Config::validate_auth_token("abcdefg").is_err());
1610    }
1611
1612    #[test]
1613    #[serial]
1614    fn test_validate_auth_token_integration_with_from_env() {
1615        // Test that validation is called when loading from environment
1616
1617        // Set a weak token
1618        env::set_var("API_AUTH_TOKEN", "dev");
1619        let config = Config::from_env().unwrap();
1620        // Token should be rejected, so it shouldn't be set
1621        assert!(config.api.auth_token.is_none());
1622        env::remove_var("API_AUTH_TOKEN");
1623
1624        // Set a strong token
1625        env::set_var("API_AUTH_TOKEN", "strong_secure_token_12345");
1626        let config = Config::from_env().unwrap();
1627        assert!(config.api.auth_token.is_some());
1628        // `ApiConfig` implements `Drop` (zeroises the token), so we can't
1629        // move `auth_token` out for the `unwrap()` — clone first.
1630        assert_eq!(
1631            config.api.auth_token.clone().unwrap(),
1632            "strong_secure_token_12345"
1633        );
1634        env::remove_var("API_AUTH_TOKEN");
1635    }
1636
1637    #[test]
1638    fn test_validate_auth_token_special_characters_allowed() {
1639        let tokens = vec![
1640            "token-with-dashes",
1641            "token_with_underscores",
1642            "token.with.dots",
1643            "token@with#special$chars",
1644        ];
1645
1646        for token in tokens {
1647            let result = Config::validate_auth_token(token);
1648            assert!(
1649                result.is_ok(),
1650                "Token '{}' with special chars should be valid",
1651                token
1652            );
1653        }
1654    }
1655}