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 pub database_url: String,
10
11 pub embedding: EmbeddingConfig,
13
14 pub http_port: u16,
16
17 pub mcp_port: Option<u16>,
19
20 pub tier_config: TierConfig,
22
23 pub operational: OperationalConfig,
25
26 pub backup: BackupConfiguration,
28
29 pub security: SecurityConfiguration,
31
32 pub tier_manager: TierManagerConfig,
34
35 pub forgetting: ForgettingConfig,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct EmbeddingConfig {
41 pub provider: String,
43
44 pub model: String,
46
47 pub api_key: String,
49
50 pub base_url: String,
52
53 pub timeout_seconds: u64,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
58pub struct TierConfig {
59 pub working_tier_limit: usize,
61
62 pub warm_tier_limit: usize,
64
65 pub working_to_warm_days: u32,
67
68 pub warm_to_cold_days: u32,
70
71 pub importance_threshold: f32,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct OperationalConfig {
77 pub max_db_connections: u32,
79
80 pub request_timeout_seconds: u64,
82
83 pub enable_metrics: bool,
85
86 pub log_level: String,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct BackupConfiguration {
92 pub enabled: bool,
94
95 pub backup_directory: PathBuf,
97
98 pub wal_archive_directory: PathBuf,
100
101 pub retention_days: u32,
103
104 pub enable_encryption: bool,
106
107 pub schedule: String,
109
110 pub rto_minutes: u32,
112
113 pub rpo_minutes: u32,
115
116 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, 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 pub fn from_env() -> Result<Self> {
175 dotenv::dotenv().ok(); 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 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 if let Ok(api_key) = env::var("OPENAI_API_KEY") {
224 config.embedding.api_key = api_key;
225 }
226
227 if let Ok(api_key) = env::var("EMBEDDING_API_KEY") {
229 config.embedding.api_key = api_key;
230 }
231
232 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 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 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 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 pub fn validate(&self) -> Result<()> {
313 if self.database_url.is_empty() {
314 return Err(anyhow::anyhow!("Database URL is required"));
315 }
316
317 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 }
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 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 fn get_database_url_from_env() -> Result<String> {
370 if let Ok(url) = env::var("DATABASE_URL") {
372 return Ok(url);
373 }
374
375 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 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 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 let mut masked = self.database_url.clone();
417 masked.replace_range(colon_pos + 1..at_pos, "***");
418 return masked;
419 }
420 }
421 format!(
423 "postgresql://[credentials-hidden]{}",
424 self.database_url
425 .split_once('@')
426 .map(|(_, rest)| rest)
427 .unwrap_or("")
428 )
429 }
430
431 pub fn validate_mcp_environment(&self) -> Result<()> {
433 self.validate()?;
435
436 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 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 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 report.push_str("Database Configuration:\n");
470 report.push_str(&format!(" Connection: {}\n", self.safe_database_url()));
471
472 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 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 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 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(), rto_minutes: 60, rpo_minutes: 5, enable_verification: true,
540 }
541 }
542}
543
544#[derive(Debug, Clone, Serialize, Deserialize)]
545pub struct TierManagerConfig {
546 pub enabled: bool,
548
549 pub scan_interval_seconds: u64,
551
552 pub migration_batch_size: usize,
554
555 pub max_concurrent_migrations: usize,
557
558 pub working_to_warm_threshold: f64, pub warm_to_cold_threshold: f64, pub cold_to_frozen_threshold: f64, pub min_working_age_hours: u64,
565 pub min_warm_age_hours: u64,
566 pub min_cold_age_hours: u64,
567
568 pub target_migrations_per_second: u32,
570
571 pub log_migrations: bool,
573
574 pub max_retry_attempts: u32,
576 pub retry_delay_seconds: u64,
577
578 pub enable_metrics: bool,
580}
581
582#[derive(Debug, Clone, Serialize, Deserialize)]
584pub struct ForgettingConfig {
585 pub enabled: bool,
587
588 pub cleanup_interval_seconds: u64,
590
591 pub cleanup_batch_size: usize,
593
594 pub base_decay_rate: f64,
596
597 pub working_decay_multiplier: f64,
599 pub warm_decay_multiplier: f64,
600 pub cold_decay_multiplier: f64,
601
602 pub importance_decay_factor: f64,
604
605 pub max_age_decay_multiplier: f64,
607
608 pub enable_reinforcement_learning: bool,
610
611 pub learning_rate: f64,
613
614 pub min_decay_rate: f64,
616
617 pub max_decay_rate: f64,
619
620 pub enable_hard_deletion: bool,
622
623 pub hard_deletion_threshold: f64,
625
626 pub hard_deletion_retention_days: u32,
628
629 pub enable_metrics: bool,
631}
632
633impl Default for ForgettingConfig {
634 fn default() -> Self {
635 Self {
636 enabled: true,
637 cleanup_interval_seconds: 3600, cleanup_batch_size: 1000, base_decay_rate: 1.0,
642
643 working_decay_multiplier: 0.5, warm_decay_multiplier: 1.0, cold_decay_multiplier: 1.5, importance_decay_factor: 0.5,
650
651 max_age_decay_multiplier: 2.0,
653
654 enable_reinforcement_learning: true,
656 learning_rate: 0.1,
657
658 min_decay_rate: 0.1,
660 max_decay_rate: 5.0,
661
662 enable_hard_deletion: false, hard_deletion_threshold: 0.01, hard_deletion_retention_days: 30, enable_metrics: true,
668 }
669 }
670}
671
672#[derive(Debug, Clone, Serialize, Deserialize)]
673pub struct SecurityConfiguration {
674 pub enabled: bool,
676 pub tls_enabled: bool,
678 pub tls_cert_path: PathBuf,
679 pub tls_key_path: PathBuf,
680 pub tls_port: u16,
681 pub auth_enabled: bool,
683 pub jwt_secret: String,
684 pub jwt_expiry_hours: u32,
685 pub api_key_enabled: bool,
686 pub rate_limiting_enabled: bool,
688 pub requests_per_minute: u32,
689 pub rate_limit_burst: u32,
690 pub audit_enabled: bool,
692 pub audit_retention_days: u32,
693 pub pii_detection_enabled: bool,
695 pub pii_mask_logs: bool,
696 pub gdpr_enabled: bool,
698 pub gdpr_retention_days: u32,
699 pub right_to_be_forgotten: bool,
700 pub vault_enabled: bool,
702 pub vault_address: Option<String>,
703 pub vault_token_path: Option<PathBuf>,
704 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, 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, migration_batch_size: 100, max_concurrent_migrations: 4, working_to_warm_threshold: 0.7, warm_to_cold_threshold: 0.5, cold_to_frozen_threshold: 0.2, min_working_age_hours: 1, min_warm_age_hours: 24, min_cold_age_hours: 168, target_migrations_per_second: 1000,
760
761 log_migrations: true, max_retry_attempts: 3, retry_delay_seconds: 60, enable_metrics: true, }
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 assert!(config.validate().is_ok());
787
788 config.embedding.provider = "openai".to_string();
790 config.embedding.api_key = String::new();
791 assert!(config.validate().is_err());
792
793 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 config.embedding.provider = "invalid".to_string();
804 assert!(config.validate().is_err());
805
806 config.embedding.provider = "ollama".to_string();
808 config.embedding.model = String::new();
809 assert!(config.validate().is_err());
810
811 config.embedding.model = "test-model".to_string();
813 config.embedding.base_url = String::new();
814 assert!(config.validate().is_err());
815 }
816}