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