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