1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::env;
9use std::path::PathBuf;
10use thiserror::Error;
11
12#[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#[derive(Debug, Clone, Serialize, Deserialize, Default)]
33pub struct Config {
34 pub api: ApiConfig,
36 pub database: DatabaseConfig,
38 pub logging: LoggingConfig,
40 pub security: SecurityConfig,
42 pub storage: StorageConfig,
44 pub slm: Option<Slm>,
46 pub routing: Option<crate::routing::RoutingConfig>,
48 pub native_execution: Option<NativeExecutionConfig>,
50 pub agentpin: Option<crate::integrations::agentpin::AgentPinConfig>,
52 #[cfg(feature = "cli-executor")]
54 pub cli_executor: Option<CliExecutorConfigToml>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct ApiConfig {
60 pub port: u16,
62 pub host: String,
64 #[serde(skip_serializing)]
66 pub auth_token: Option<String>,
67 pub timeout_seconds: u64,
69 pub max_body_size: usize,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct DatabaseConfig {
76 #[serde(skip_serializing)]
78 pub url: Option<String>,
79 #[serde(skip_serializing)]
81 pub redis_url: Option<String>,
82 pub qdrant_url: String,
84 pub qdrant_collection: String,
86 pub vector_dimension: usize,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct LoggingConfig {
93 pub level: String,
95 pub format: LogFormat,
97 pub structured: bool,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103pub enum LogFormat {
104 Json,
105 Pretty,
106 Compact,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct SecurityConfig {
112 pub key_provider: KeyProvider,
114 pub enable_compression: bool,
116 pub enable_backups: bool,
117 pub enable_safety_checks: bool,
118}
119
120#[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#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct NativeExecutionConfig {
132 pub enabled: bool,
134 pub default_executable: String,
136 pub working_directory: PathBuf,
138 pub enforce_resource_limits: bool,
140 pub max_memory_mb: Option<u64>,
142 pub max_cpu_seconds: Option<u64>,
144 pub max_execution_time_seconds: u64,
146 pub allowed_executables: Vec<String>,
148}
149
150impl Default for NativeExecutionConfig {
151 fn default() -> Self {
152 Self {
153 enabled: false, 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![], }
162 }
163}
164
165#[cfg(feature = "cli-executor")]
168#[derive(Debug, Clone, Serialize, Deserialize, Default)]
169pub struct CliExecutorConfigToml {
170 pub max_runtime_seconds: Option<u64>,
172 pub idle_timeout_seconds: Option<u64>,
174 pub max_output_bytes: Option<usize>,
176 pub adapters: Option<HashMap<String, AdapterConfigToml>>,
178}
179
180#[cfg(feature = "cli-executor")]
182#[derive(Debug, Clone, Serialize, Deserialize, Default)]
183pub struct AdapterConfigToml {
184 pub executable: Option<String>,
186 pub model: Option<String>,
188 pub max_turns: Option<u32>,
190 pub allowed_tools: Option<Vec<String>>,
192 pub disallowed_tools: Option<Vec<String>>,
194}
195
196#[derive(Debug, Clone, Serialize, Deserialize)]
198pub struct StorageConfig {
199 pub context_path: PathBuf,
201 pub git_clone_path: PathBuf,
203 pub backup_path: PathBuf,
205 pub max_context_size_mb: u64,
207}
208
209#[derive(Debug, Clone, Serialize, Deserialize)]
211pub struct Slm {
212 pub enabled: bool,
214 pub model_allow_lists: ModelAllowListConfig,
216 pub sandbox_profiles: HashMap<String, SandboxProfile>,
218 pub default_sandbox_profile: String,
220}
221
222#[derive(Debug, Clone, Serialize, Deserialize, Default)]
224pub struct ModelAllowListConfig {
225 pub global_models: Vec<Model>,
227 pub agent_model_maps: HashMap<String, Vec<String>>,
229 pub allow_runtime_overrides: bool,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct Model {
236 pub id: String,
238 pub name: String,
240 pub provider: ModelProvider,
242 pub capabilities: Vec<ModelCapability>,
244 pub resource_requirements: ModelResourceRequirements,
246}
247
248#[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#[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#[derive(Debug, Clone, Serialize, Deserialize)]
271pub struct ModelResourceRequirements {
272 pub min_memory_mb: u64,
274 pub preferred_cpu_cores: f32,
276 pub gpu_requirements: Option<GpuRequirements>,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct GpuRequirements {
283 pub min_vram_mb: u64,
285 pub compute_capability: String,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct SandboxProfile {
292 pub resources: ResourceConstraints,
294 pub filesystem: FilesystemControls,
296 pub process_limits: ProcessLimits,
298 pub network: NetworkPolicy,
300 pub security: SecuritySettings,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize)]
306pub struct ResourceConstraints {
307 pub max_memory_mb: u64,
309 pub max_cpu_cores: f32,
311 pub max_disk_mb: u64,
313 pub gpu_access: GpuAccess,
315 pub max_io_bandwidth_mbps: Option<u64>,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct FilesystemControls {
322 pub read_paths: Vec<String>,
324 pub write_paths: Vec<String>,
326 pub denied_paths: Vec<String>,
328 pub allow_temp_files: bool,
330 pub max_file_size_mb: u64,
332}
333
334#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct ProcessLimits {
337 pub max_child_processes: u32,
339 pub max_execution_time_seconds: u64,
341 pub allowed_syscalls: Vec<String>,
343 pub process_priority: i8,
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize)]
349pub struct NetworkPolicy {
350 pub access_mode: NetworkAccessMode,
352 pub allowed_destinations: Vec<NetworkDestination>,
354 pub max_bandwidth_mbps: Option<u64>,
356}
357
358#[derive(Debug, Clone, Serialize, Deserialize)]
360pub enum NetworkAccessMode {
361 None,
363 Restricted,
365 Full,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct NetworkDestination {
372 pub host: String,
374 pub port: Option<u16>,
376 pub protocol: Option<NetworkProtocol>,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize)]
382pub enum NetworkProtocol {
383 TCP,
384 UDP,
385 HTTP,
386 HTTPS,
387}
388
389#[derive(Debug, Clone, Serialize, Deserialize)]
391pub enum GpuAccess {
392 None,
394 Shared { max_memory_mb: u64 },
396 Exclusive,
398}
399
400#[derive(Debug, Clone, Serialize, Deserialize)]
402pub struct SecuritySettings {
403 pub strict_syscall_filtering: bool,
405 pub disable_debugging: bool,
407 pub enable_audit_logging: bool,
409 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, }
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 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 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![], 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 pub fn validate(&self) -> Result<(), Box<dyn std::error::Error>> {
569 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 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 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 pub fn validate(&self) -> Result<(), ConfigError> {
596 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 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 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 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 pub fn get_allowed_models(&self, agent_id: &str) -> Vec<&Model> {
651 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 self.model_allow_lists.global_models.iter().collect()
661 }
662 }
663}
664
665impl Config {
666 pub fn from_env() -> Result<Self, ConfigError> {
668 let mut config = Self::default();
669
670 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 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 }
693 }
694 }
695
696 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 if let Ok(log_level) = env::var("LOG_LEVEL") {
711 config.logging.level = log_level;
712 }
713
714 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 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 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 pub fn validate(&self) -> Result<(), ConfigError> {
750 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 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 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 if let Some(slm) = &self.slm {
777 if slm.enabled {
778 slm.validate()?;
779 }
780 }
781
782 Ok(())
783 }
784
785 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 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 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 fn validate_auth_token(token: &str) -> Result<String, ConfigError> {
850 let trimmed = token.trim();
852
853 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 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 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 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 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 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 #[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(), 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 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 let allowed_models = slm.get_allowed_models("agent2");
1112 assert_eq!(allowed_models.len(), 2);
1113 }
1114
1115 #[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 #[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, 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 #[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(), ..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 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 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 #[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 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 assert!(Config::validate_auth_token("12345678").is_err()); assert!(Config::validate_auth_token("abcdefgh").is_ok());
1552
1553 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 env::set_var("API_AUTH_TOKEN", "dev");
1564 let config = Config::from_env().unwrap();
1565 assert!(config.api.auth_token.is_none());
1567 env::remove_var("API_AUTH_TOKEN");
1568
1569 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}