codex_memory/
config.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3use std::env;
4use std::path::PathBuf;
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct Config {
8    /// PostgreSQL database connection URL
9    pub database_url: String,
10
11    /// Embedding service configuration
12    pub embedding: EmbeddingConfig,
13
14    /// HTTP server port
15    pub http_port: u16,
16
17    /// MCP server port (if different from HTTP)
18    pub mcp_port: Option<u16>,
19
20    /// Memory tier configuration
21    pub tier_config: TierConfig,
22
23    /// Operational settings
24    pub operational: OperationalConfig,
25
26    /// Backup and disaster recovery settings
27    pub backup: BackupConfiguration,
28
29    /// Security and compliance settings
30    pub security: SecurityConfiguration,
31
32    /// Tier manager configuration
33    pub tier_manager: TierManagerConfig,
34
35    /// Forgetting and decay configuration
36    pub forgetting: ForgettingConfig,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct EmbeddingConfig {
41    /// Embedding provider (openai or ollama)
42    pub provider: String,
43
44    /// Model name to use for embeddings
45    pub model: String,
46
47    /// API key (for OpenAI, empty for Ollama)
48    pub api_key: String,
49
50    /// Base URL for the embedding service
51    pub base_url: String,
52
53    /// Request timeout in seconds
54    pub timeout_seconds: u64,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct TierConfig {
59    /// Maximum memories in working tier (Miller's 7±2 principle: 5-9 items)
60    pub working_tier_limit: usize,
61
62    /// Maximum memories in warm tier  
63    pub warm_tier_limit: usize,
64
65    /// Days before moving from working to warm
66    pub working_to_warm_days: u32,
67
68    /// Days before moving from warm to cold
69    pub warm_to_cold_days: u32,
70
71    /// Importance threshold for tier promotion
72    pub importance_threshold: f32,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct OperationalConfig {
77    /// Maximum database connections
78    pub max_db_connections: u32,
79
80    /// Request timeout in seconds
81    pub request_timeout_seconds: u64,
82
83    /// Enable metrics endpoint
84    pub enable_metrics: bool,
85
86    /// Log level (error, warn, info, debug, trace)
87    pub log_level: String,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct BackupConfiguration {
92    /// Enable automated backups
93    pub enabled: bool,
94
95    /// Directory for backup storage
96    pub backup_directory: PathBuf,
97
98    /// WAL archive directory
99    pub wal_archive_directory: PathBuf,
100
101    /// Backup retention in days
102    pub retention_days: u32,
103
104    /// Enable backup encryption
105    pub enable_encryption: bool,
106
107    /// Backup schedule (cron format)
108    pub schedule: String,
109
110    /// Recovery time objective in minutes
111    pub rto_minutes: u32,
112
113    /// Recovery point objective in minutes
114    pub rpo_minutes: u32,
115
116    /// Enable backup verification
117    pub enable_verification: bool,
118}
119
120impl Default for Config {
121    fn default() -> Self {
122        Self {
123            database_url: "postgresql://postgres:postgres@localhost:5432/codex_memory".to_string(),
124            embedding: EmbeddingConfig::default(),
125            http_port: 8080,
126            mcp_port: None,
127            tier_config: TierConfig::default(),
128            operational: OperationalConfig::default(),
129            backup: BackupConfiguration::default(),
130            security: SecurityConfiguration::default(),
131            tier_manager: TierManagerConfig::default(),
132            forgetting: ForgettingConfig::default(),
133        }
134    }
135}
136
137impl Default for EmbeddingConfig {
138    fn default() -> Self {
139        Self {
140            provider: "ollama".to_string(),
141            model: "nomic-embed-text".to_string(),
142            api_key: String::new(),
143            base_url: "http://192.168.1.110:11434".to_string(),
144            timeout_seconds: 60,
145        }
146    }
147}
148
149impl Default for TierConfig {
150    fn default() -> Self {
151        Self {
152            working_tier_limit: 9, // Miller's 7±2 upper bound
153            warm_tier_limit: 10000,
154            working_to_warm_days: 7,
155            warm_to_cold_days: 30,
156            importance_threshold: 0.7,
157        }
158    }
159}
160
161impl Default for OperationalConfig {
162    fn default() -> Self {
163        Self {
164            max_db_connections: 10,
165            request_timeout_seconds: 30,
166            enable_metrics: true,
167            log_level: "info".to_string(),
168        }
169    }
170}
171
172impl Config {
173    /// Load configuration from environment variables with MCP-friendly error handling
174    pub fn from_env() -> Result<Self> {
175        dotenv::dotenv().ok(); // Load .env file if present
176
177        // Handle Claude Desktop MCP_ prefixed environment variables
178        if let Ok(mcp_db_url) = std::env::var("MCP_DATABASE_URL") {
179            std::env::set_var("DATABASE_URL", mcp_db_url);
180        }
181        if let Ok(mcp_provider) = std::env::var("MCP_EMBEDDING_PROVIDER") {
182            std::env::set_var("EMBEDDING_PROVIDER", mcp_provider);
183        }
184        if let Ok(mcp_model) = std::env::var("MCP_EMBEDDING_MODEL") {
185            std::env::set_var("EMBEDDING_MODEL", mcp_model);
186        }
187        if let Ok(mcp_api_key) = std::env::var("MCP_OPENAI_API_KEY") {
188            std::env::set_var("OPENAI_API_KEY", mcp_api_key);
189        }
190        if let Ok(mcp_ollama_url) = std::env::var("MCP_OLLAMA_BASE_URL") {
191            std::env::set_var("OLLAMA_BASE_URL", mcp_ollama_url);
192        }
193        if let Ok(mcp_log_level) = std::env::var("MCP_LOG_LEVEL") {
194            std::env::set_var("RUST_LOG", mcp_log_level);
195        }
196
197        let mut config = Config {
198            database_url: Self::get_database_url_from_env()
199                .map_err(|e| anyhow::anyhow!("Database configuration error: {e}"))?,
200            ..Config::default()
201        };
202
203        // Embedding configuration
204        if let Ok(provider) = env::var("EMBEDDING_PROVIDER") {
205            config.embedding.provider = provider;
206        }
207
208        if let Ok(model) = env::var("EMBEDDING_MODEL") {
209            config.embedding.model = model;
210        }
211
212        if let Ok(base_url) = env::var("EMBEDDING_BASE_URL") {
213            config.embedding.base_url = base_url;
214        }
215
216        if let Ok(timeout) = env::var("EMBEDDING_TIMEOUT_SECONDS") {
217            config.embedding.timeout_seconds = timeout
218                .parse()
219                .map_err(|e| anyhow::anyhow!("Invalid EMBEDDING_TIMEOUT_SECONDS: {}", e))?;
220        }
221
222        // API key is optional (not needed for Ollama)
223        if let Ok(api_key) = env::var("OPENAI_API_KEY") {
224            config.embedding.api_key = api_key;
225        }
226
227        // For backward compatibility, also check EMBEDDING_API_KEY
228        if let Ok(api_key) = env::var("EMBEDDING_API_KEY") {
229            config.embedding.api_key = api_key;
230        }
231
232        // Optional environment variables
233        if let Ok(port) = env::var("HTTP_PORT") {
234            config.http_port = port
235                .parse()
236                .map_err(|e| anyhow::anyhow!("Invalid HTTP_PORT: {}", e))?;
237        }
238
239        if let Ok(port) = env::var("MCP_PORT") {
240            config.mcp_port = Some(
241                port.parse()
242                    .map_err(|e| anyhow::anyhow!("Invalid MCP_PORT: {}", e))?,
243            );
244        }
245
246        // Tier configuration
247        if let Ok(limit) = env::var("WORKING_TIER_LIMIT") {
248            let parsed_limit: usize = limit
249                .parse()
250                .map_err(|e| anyhow::anyhow!("Invalid WORKING_TIER_LIMIT: {}", e))?;
251            // Enforce Miller's 7±2 principle (5-9 items)
252            if !(5..=9).contains(&parsed_limit) {
253                return Err(anyhow::anyhow!(
254                    "WORKING_TIER_LIMIT must be between 5-9 (Miller's 7±2 principle), got: {}",
255                    parsed_limit
256                ));
257            }
258            config.tier_config.working_tier_limit = parsed_limit;
259        }
260
261        if let Ok(limit) = env::var("WARM_TIER_LIMIT") {
262            config.tier_config.warm_tier_limit = limit
263                .parse()
264                .map_err(|e| anyhow::anyhow!("Invalid WARM_TIER_LIMIT: {}", e))?;
265        }
266
267        if let Ok(days) = env::var("WORKING_TO_WARM_DAYS") {
268            config.tier_config.working_to_warm_days = days
269                .parse()
270                .map_err(|e| anyhow::anyhow!("Invalid WORKING_TO_WARM_DAYS: {}", e))?;
271        }
272
273        if let Ok(days) = env::var("WARM_TO_COLD_DAYS") {
274            config.tier_config.warm_to_cold_days = days
275                .parse()
276                .map_err(|e| anyhow::anyhow!("Invalid WARM_TO_COLD_DAYS: {}", e))?;
277        }
278
279        if let Ok(threshold) = env::var("IMPORTANCE_THRESHOLD") {
280            config.tier_config.importance_threshold = threshold
281                .parse()
282                .map_err(|e| anyhow::anyhow!("Invalid IMPORTANCE_THRESHOLD: {}", e))?;
283        }
284
285        // Operational configuration
286        if let Ok(conns) = env::var("MAX_DB_CONNECTIONS") {
287            config.operational.max_db_connections = conns
288                .parse()
289                .map_err(|e| anyhow::anyhow!("Invalid MAX_DB_CONNECTIONS: {}", e))?;
290        }
291
292        if let Ok(timeout) = env::var("REQUEST_TIMEOUT_SECONDS") {
293            config.operational.request_timeout_seconds = timeout
294                .parse()
295                .map_err(|e| anyhow::anyhow!("Invalid REQUEST_TIMEOUT_SECONDS: {}", e))?;
296        }
297
298        if let Ok(enable) = env::var("ENABLE_METRICS") {
299            config.operational.enable_metrics = enable
300                .parse()
301                .map_err(|e| anyhow::anyhow!("Invalid ENABLE_METRICS: {}", e))?;
302        }
303
304        if let Ok(level) = env::var("LOG_LEVEL") {
305            config.operational.log_level = level;
306        }
307
308        Ok(config)
309    }
310
311    /// Validate the configuration
312    pub fn validate(&self) -> Result<()> {
313        if self.database_url.is_empty() {
314            return Err(anyhow::anyhow!("Database URL is required"));
315        }
316
317        // Validate embedding configuration
318        match self.embedding.provider.as_str() {
319            "openai" => {
320                if self.embedding.api_key.is_empty() {
321                    return Err(anyhow::anyhow!("API key is required for OpenAI provider"));
322                }
323            }
324            "ollama" => {
325                if self.embedding.base_url.is_empty() {
326                    return Err(anyhow::anyhow!("Base URL is required for Ollama provider"));
327                }
328            }
329            "mock" => {
330                // Mock provider for testing - no additional validation needed
331            }
332            _ => {
333                return Err(anyhow::anyhow!(
334                    "Invalid embedding provider: {}. Must be 'openai', 'ollama', or 'mock'",
335                    self.embedding.provider
336                ));
337            }
338        }
339
340        if self.embedding.model.is_empty() {
341            return Err(anyhow::anyhow!("Embedding model is required"));
342        }
343
344        // Validate Miller's 7±2 principle for working memory
345        if self.tier_config.working_tier_limit < 5 || self.tier_config.working_tier_limit > 9 {
346            return Err(anyhow::anyhow!(
347                "Working tier limit must be between 5-9 (Miller's 7±2 principle), got: {}",
348                self.tier_config.working_tier_limit
349            ));
350        }
351
352        if self.tier_config.warm_tier_limit == 0 {
353            return Err(anyhow::anyhow!("Warm tier limit must be greater than 0"));
354        }
355
356        if self.tier_config.importance_threshold < 0.0
357            || self.tier_config.importance_threshold > 1.0
358        {
359            return Err(anyhow::anyhow!(
360                "Importance threshold must be between 0.0 and 1.0"
361            ));
362        }
363
364        Ok(())
365    }
366
367    /// Get database URL from environment variables with multiple fallback options
368    /// for MCP compatibility
369    fn get_database_url_from_env() -> Result<String> {
370        // Try DATABASE_URL first (standard convention)
371        if let Ok(url) = env::var("DATABASE_URL") {
372            return Ok(url);
373        }
374
375        // Try individual components for MCP-style configuration
376        if let (Ok(host), Ok(user), Ok(db)) = (
377            env::var("DB_HOST"),
378            env::var("DB_USER"),
379            env::var("DB_NAME"),
380        ) {
381            let password = env::var("DB_PASSWORD").unwrap_or_default();
382            let port = env::var("DB_PORT").unwrap_or_else(|_| "5432".to_string());
383
384            if password.is_empty() {
385                return Ok(format!("postgresql://{user}@{host}:{port}/{db}"));
386            } else {
387                return Ok(format!("postgresql://{user}:{password}@{host}:{port}/{db}"));
388            }
389        }
390
391        // Fall back to DB_CONN if provided
392        if let Ok(conn) = env::var("DB_CONN") {
393            return Ok(conn);
394        }
395
396        Err(anyhow::anyhow!(
397            "Database credentials not found. Please provide either:\n\
398             1. DATABASE_URL environment variable, or\n\
399             2. DB_HOST, DB_USER, DB_NAME (and optionally DB_PASSWORD, DB_PORT), or\n\
400             3. DB_CONN environment variable\n\n\
401             Example:\n\
402             DATABASE_URL=postgresql://user:password@localhost:5432/database\n\
403             or\n\
404             DB_HOST=localhost\n\
405             DB_USER=myuser\n\
406             DB_PASSWORD=mypassword\n\
407             DB_NAME=mydatabase"
408        ))
409    }
410
411    /// Generate a safe connection string for logging (masks password)
412    pub fn safe_database_url(&self) -> String {
413        if let Some(at_pos) = self.database_url.find('@') {
414            if let Some(colon_pos) = self.database_url[..at_pos].rfind(':') {
415                // postgresql://user:password@host:port/db -> postgresql://user:***@host:port/db
416                let mut masked = self.database_url.clone();
417                masked.replace_range(colon_pos + 1..at_pos, "***");
418                return masked;
419            }
420        }
421        // If we can't parse it, just show the prefix
422        format!(
423            "postgresql://[credentials-hidden]{}",
424            self.database_url
425                .split_once('@')
426                .map(|(_, rest)| rest)
427                .unwrap_or("")
428        )
429    }
430
431    /// Validate MCP environment configuration
432    pub fn validate_mcp_environment(&self) -> Result<()> {
433        // Standard validation first
434        self.validate()?;
435
436        // MCP-specific validations
437        if self.embedding.provider == "openai" && self.embedding.api_key.len() < 20 {
438            return Err(anyhow::anyhow!(
439                "OpenAI API key appears to be invalid (too short)"
440            ));
441        }
442
443        // Check for reasonable port configuration
444        if self.http_port < 1024 {
445            tracing::warn!(
446                "HTTP port {} requires root privileges. Consider using port >= 1024 for MCP deployment.",
447                self.http_port
448            );
449        }
450
451        if let Some(mcp_port) = self.mcp_port {
452            if mcp_port == self.http_port {
453                return Err(anyhow::anyhow!(
454                    "MCP port and HTTP port cannot be the same ({})",
455                    mcp_port
456                ));
457            }
458        }
459
460        Ok(())
461    }
462
463    /// Create a diagnostic report for troubleshooting MCP setup
464    pub fn create_diagnostic_report(&self) -> String {
465        let mut report = String::new();
466        report.push_str("=== Agentic Memory System - MCP Configuration Report ===\n\n");
467
468        // Database configuration
469        report.push_str("Database Configuration:\n");
470        report.push_str(&format!("  Connection: {}\n", self.safe_database_url()));
471
472        // Embedding configuration
473        report.push_str("\nEmbedding Configuration:\n");
474        report.push_str(&format!("  Provider: {}\n", self.embedding.provider));
475        report.push_str(&format!("  Model: {}\n", self.embedding.model));
476        report.push_str(&format!("  Base URL: {}\n", self.embedding.base_url));
477        report.push_str(&format!("  Timeout: {}s\n", self.embedding.timeout_seconds));
478        report.push_str(&format!(
479            "  API Key: {}\n",
480            if self.embedding.api_key.is_empty() {
481                "Not set"
482            } else {
483                "***configured***"
484            }
485        ));
486
487        // Server configuration
488        report.push_str("\nServer Configuration:\n");
489        report.push_str(&format!("  HTTP Port: {}\n", self.http_port));
490        report.push_str(&format!(
491            "  MCP Port: {}\n",
492            self.mcp_port
493                .map(|p| p.to_string())
494                .unwrap_or_else(|| "Not set".to_string())
495        ));
496
497        // Memory tier configuration
498        report.push_str("\nMemory Tier Configuration:\n");
499        report.push_str(&format!(
500            "  Working Tier Limit: {}\n",
501            self.tier_config.working_tier_limit
502        ));
503        report.push_str(&format!(
504            "  Warm Tier Limit: {}\n",
505            self.tier_config.warm_tier_limit
506        ));
507        report.push_str(&format!(
508            "  Working->Warm: {} days\n",
509            self.tier_config.working_to_warm_days
510        ));
511        report.push_str(&format!(
512            "  Warm->Cold: {} days\n",
513            self.tier_config.warm_to_cold_days
514        ));
515
516        // Validation results
517        report.push_str("\nValidation Results:\n");
518        match self.validate_mcp_environment() {
519            Ok(_) => report.push_str("  ✅ All configuration checks passed\n"),
520            Err(e) => report.push_str(&format!("  ❌ Configuration error: {e}\n")),
521        }
522
523        report.push_str("\n=== End Configuration Report ===\n");
524        report
525    }
526}
527
528impl Default for BackupConfiguration {
529    fn default() -> Self {
530        Self {
531            enabled: true,
532            backup_directory: PathBuf::from("/var/lib/codex/backups"),
533            wal_archive_directory: PathBuf::from("/var/lib/codex/wal_archive"),
534            retention_days: 30,
535            enable_encryption: true,
536            schedule: "0 2 * * *".to_string(), // Daily at 2 AM
537            rto_minutes: 60,                   // 1 hour
538            rpo_minutes: 5,                    // 5 minutes
539            enable_verification: true,
540        }
541    }
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
545pub struct TierManagerConfig {
546    /// Enable automatic tier management
547    pub enabled: bool,
548
549    /// Interval between tier management scans in seconds
550    pub scan_interval_seconds: u64,
551
552    /// Batch size for migration operations (memories per batch)
553    pub migration_batch_size: usize,
554
555    /// Maximum concurrent migration tasks
556    pub max_concurrent_migrations: usize,
557
558    /// Recall probability thresholds for tier migrations
559    pub working_to_warm_threshold: f64, // P(r) < 0.7
560    pub warm_to_cold_threshold: f64,   // P(r) < 0.5
561    pub cold_to_frozen_threshold: f64, // P(r) < 0.2
562
563    /// Minimum age before considering migration (prevents rapid tier changes)
564    pub min_working_age_hours: u64,
565    pub min_warm_age_hours: u64,
566    pub min_cold_age_hours: u64,
567
568    /// Migration performance targets
569    pub target_migrations_per_second: u32,
570
571    /// Enable migration history logging
572    pub log_migrations: bool,
573
574    /// Migration failure retry configuration
575    pub max_retry_attempts: u32,
576    pub retry_delay_seconds: u64,
577
578    /// Enable metrics collection for migration monitoring
579    pub enable_metrics: bool,
580}
581
582/// Configuration for memory forgetting and decay mechanisms
583#[derive(Debug, Clone, Serialize, Deserialize)]
584pub struct ForgettingConfig {
585    /// Enable automatic forgetting cleanup jobs
586    pub enabled: bool,
587
588    /// Interval between forgetting cleanup runs in seconds
589    pub cleanup_interval_seconds: u64,
590
591    /// Batch size for processing memories during cleanup
592    pub cleanup_batch_size: usize,
593
594    /// Base decay rate for new memories (research-based default)
595    pub base_decay_rate: f64,
596
597    /// Tier-specific decay rate multipliers
598    pub working_decay_multiplier: f64,
599    pub warm_decay_multiplier: f64,
600    pub cold_decay_multiplier: f64,
601
602    /// Importance-based decay rate adjustment
603    pub importance_decay_factor: f64,
604
605    /// Age-based decay rate scaling (maximum multiplier after 60 days)
606    pub max_age_decay_multiplier: f64,
607
608    /// Enable reinforcement learning for dynamic importance scoring
609    pub enable_reinforcement_learning: bool,
610
611    /// Learning rate for reinforcement learning updates
612    pub learning_rate: f64,
613
614    /// Minimum decay rate (prevents memories from becoming permanent)
615    pub min_decay_rate: f64,
616
617    /// Maximum decay rate (prevents immediate forgetting)
618    pub max_decay_rate: f64,
619
620    /// Enable cleanup of completely forgotten memories (recall_probability < threshold)
621    pub enable_hard_deletion: bool,
622
623    /// Threshold for hard deletion of forgotten memories
624    pub hard_deletion_threshold: f64,
625
626    /// Retention period before hard deletion (days)
627    pub hard_deletion_retention_days: u32,
628
629    /// Enable performance metrics for forgetting operations
630    pub enable_metrics: bool,
631}
632
633impl Default for ForgettingConfig {
634    fn default() -> Self {
635        Self {
636            enabled: true,
637            cleanup_interval_seconds: 3600, // 1 hour - optimal for Ebbinghaus curve dynamics
638            cleanup_batch_size: 1000,       // Match consolidation job batch size for consistency
639
640            // Research-validated base decay rate from cognitive science
641            base_decay_rate: 1.0,
642
643            // Tier-specific multipliers based on access patterns
644            working_decay_multiplier: 0.5, // Slower decay for frequently accessed memories
645            warm_decay_multiplier: 1.0,    // Normal decay rate
646            cold_decay_multiplier: 1.5,    // Faster decay for infrequently accessed memories
647
648            // Importance scoring influence (0.5 = 50% reduction for high importance)
649            importance_decay_factor: 0.5,
650
651            // Age-based scaling matches math_engine.rs implementation
652            max_age_decay_multiplier: 2.0,
653
654            // Reinforcement learning for adaptive importance
655            enable_reinforcement_learning: true,
656            learning_rate: 0.1,
657
658            // Bounds to prevent extreme decay rates
659            min_decay_rate: 0.1,
660            max_decay_rate: 5.0,
661
662            // Hard deletion for completely forgotten memories
663            enable_hard_deletion: false,      // Conservative default
664            hard_deletion_threshold: 0.01,    // 1% recall probability
665            hard_deletion_retention_days: 30, // 30 day grace period
666
667            enable_metrics: true,
668        }
669    }
670}
671
672#[derive(Debug, Clone, Serialize, Deserialize)]
673pub struct SecurityConfiguration {
674    /// Enable security features
675    pub enabled: bool,
676    /// TLS configuration
677    pub tls_enabled: bool,
678    pub tls_cert_path: PathBuf,
679    pub tls_key_path: PathBuf,
680    pub tls_port: u16,
681    /// Authentication configuration
682    pub auth_enabled: bool,
683    pub jwt_secret: String,
684    pub jwt_expiry_hours: u32,
685    pub api_key_enabled: bool,
686    /// Rate limiting configuration
687    pub rate_limiting_enabled: bool,
688    pub requests_per_minute: u32,
689    pub rate_limit_burst: u32,
690    /// Audit logging configuration
691    pub audit_enabled: bool,
692    pub audit_retention_days: u32,
693    /// PII detection configuration
694    pub pii_detection_enabled: bool,
695    pub pii_mask_logs: bool,
696    /// GDPR compliance configuration
697    pub gdpr_enabled: bool,
698    pub gdpr_retention_days: u32,
699    pub right_to_be_forgotten: bool,
700    /// Secrets management configuration
701    pub vault_enabled: bool,
702    pub vault_address: Option<String>,
703    pub vault_token_path: Option<PathBuf>,
704    /// Input validation configuration
705    pub input_validation_enabled: bool,
706    pub max_request_size_mb: u32,
707}
708
709impl Default for SecurityConfiguration {
710    fn default() -> Self {
711        Self {
712            enabled: true,
713            tls_enabled: false,
714            tls_cert_path: PathBuf::from("/etc/ssl/certs/codex.crt"),
715            tls_key_path: PathBuf::from("/etc/ssl/private/codex.key"),
716            tls_port: 8443,
717            auth_enabled: true,
718            jwt_secret: "change-me-in-production".to_string(),
719            jwt_expiry_hours: 24,
720            api_key_enabled: false,
721            rate_limiting_enabled: false,
722            requests_per_minute: 100,
723            rate_limit_burst: 20,
724            audit_enabled: false,
725            audit_retention_days: 90,
726            pii_detection_enabled: false,
727            pii_mask_logs: true,
728            gdpr_enabled: false,
729            gdpr_retention_days: 730, // 2 years
730            right_to_be_forgotten: false,
731            vault_enabled: false,
732            vault_address: None,
733            vault_token_path: None,
734            input_validation_enabled: true,
735            max_request_size_mb: 10,
736        }
737    }
738}
739
740impl Default for TierManagerConfig {
741    fn default() -> Self {
742        Self {
743            enabled: true,
744            scan_interval_seconds: 300, // 5 minutes - frequent enough for responsive tier management
745            migration_batch_size: 100,  // Process in batches to avoid long-running transactions
746            max_concurrent_migrations: 4, // Balance throughput with resource usage
747
748            // Cognitive research-based thresholds for forgetting curves
749            working_to_warm_threshold: 0.7, // HIGH-002 requirement
750            warm_to_cold_threshold: 0.5,    // HIGH-002 requirement
751            cold_to_frozen_threshold: 0.2,  // HIGH-002 requirement
752
753            // Minimum ages prevent thrashing between tiers
754            min_working_age_hours: 1, // At least 1 hour in working memory
755            min_warm_age_hours: 24,   // At least 1 day in warm storage
756            min_cold_age_hours: 168,  // At least 1 week in cold storage
757
758            // Performance target from HIGH-002
759            target_migrations_per_second: 1000,
760
761            log_migrations: true,    // Track for audit and analysis
762            max_retry_attempts: 3,   // Reasonable retry policy
763            retry_delay_seconds: 60, // 1 minute between retries
764            enable_metrics: true,    // Essential for monitoring
765        }
766    }
767}
768
769#[cfg(test)]
770mod tests {
771    use super::*;
772
773    #[test]
774    fn test_default_config() {
775        let config = Config::default();
776        assert_eq!(config.http_port, 8080);
777        assert_eq!(config.embedding.model, "nomic-embed-text");
778        assert_eq!(config.embedding.provider, "ollama");
779        assert_eq!(config.tier_config.working_tier_limit, 9);
780    }
781
782    #[test]
783    fn test_config_validation() {
784        let mut config = Config::default();
785        // Ollama provider with base_url should be valid
786        assert!(config.validate().is_ok());
787
788        // OpenAI provider without API key should fail
789        config.embedding.provider = "openai".to_string();
790        config.embedding.api_key = String::new();
791        assert!(config.validate().is_err());
792
793        // OpenAI provider with API key should pass
794        config.embedding.api_key = "test-key".to_string();
795        assert!(config.validate().is_ok());
796    }
797
798    #[test]
799    fn test_embedding_config_validation() {
800        let mut config = Config::default();
801
802        // Invalid provider should fail
803        config.embedding.provider = "invalid".to_string();
804        assert!(config.validate().is_err());
805
806        // Empty model should fail
807        config.embedding.provider = "ollama".to_string();
808        config.embedding.model = String::new();
809        assert!(config.validate().is_err());
810
811        // Ollama without base_url should fail
812        config.embedding.model = "test-model".to_string();
813        config.embedding.base_url = String::new();
814        assert!(config.validate().is_err());
815    }
816}