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
33#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct EmbeddingConfig {
35 pub provider: String,
37
38 pub model: String,
40
41 pub api_key: String,
43
44 pub base_url: String,
46
47 pub timeout_seconds: u64,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct TierConfig {
53 pub working_tier_limit: usize,
55
56 pub warm_tier_limit: usize,
58
59 pub working_to_warm_days: u32,
61
62 pub warm_to_cold_days: u32,
64
65 pub importance_threshold: f32,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct OperationalConfig {
71 pub max_db_connections: u32,
73
74 pub request_timeout_seconds: u64,
76
77 pub enable_metrics: bool,
79
80 pub log_level: String,
82}
83
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct BackupConfiguration {
86 pub enabled: bool,
88
89 pub backup_directory: PathBuf,
91
92 pub wal_archive_directory: PathBuf,
94
95 pub retention_days: u32,
97
98 pub enable_encryption: bool,
100
101 pub schedule: String,
103
104 pub rto_minutes: u32,
106
107 pub rpo_minutes: u32,
109
110 pub enable_verification: bool,
112}
113
114impl Default for Config {
115 fn default() -> Self {
116 Self {
117 database_url: "postgresql://postgres:postgres@localhost:5432/codex_memory".to_string(),
118 embedding: EmbeddingConfig::default(),
119 http_port: 8080,
120 mcp_port: None,
121 tier_config: TierConfig::default(),
122 operational: OperationalConfig::default(),
123 backup: BackupConfiguration::default(),
124 security: SecurityConfiguration::default(),
125 }
126 }
127}
128
129impl Default for EmbeddingConfig {
130 fn default() -> Self {
131 Self {
132 provider: "ollama".to_string(),
133 model: "nomic-embed-text".to_string(),
134 api_key: String::new(),
135 base_url: "http://192.168.1.110:11434".to_string(),
136 timeout_seconds: 60,
137 }
138 }
139}
140
141impl Default for TierConfig {
142 fn default() -> Self {
143 Self {
144 working_tier_limit: 1000,
145 warm_tier_limit: 10000,
146 working_to_warm_days: 7,
147 warm_to_cold_days: 30,
148 importance_threshold: 0.7,
149 }
150 }
151}
152
153impl Default for OperationalConfig {
154 fn default() -> Self {
155 Self {
156 max_db_connections: 10,
157 request_timeout_seconds: 30,
158 enable_metrics: true,
159 log_level: "info".to_string(),
160 }
161 }
162}
163
164impl Config {
165 pub fn from_env() -> Result<Self> {
167 dotenv::dotenv().ok(); if let Ok(mcp_db_url) = std::env::var("MCP_DATABASE_URL") {
171 std::env::set_var("DATABASE_URL", mcp_db_url);
172 }
173 if let Ok(mcp_provider) = std::env::var("MCP_EMBEDDING_PROVIDER") {
174 std::env::set_var("EMBEDDING_PROVIDER", mcp_provider);
175 }
176 if let Ok(mcp_model) = std::env::var("MCP_EMBEDDING_MODEL") {
177 std::env::set_var("EMBEDDING_MODEL", mcp_model);
178 }
179 if let Ok(mcp_api_key) = std::env::var("MCP_OPENAI_API_KEY") {
180 std::env::set_var("OPENAI_API_KEY", mcp_api_key);
181 }
182 if let Ok(mcp_ollama_url) = std::env::var("MCP_OLLAMA_BASE_URL") {
183 std::env::set_var("OLLAMA_BASE_URL", mcp_ollama_url);
184 }
185 if let Ok(mcp_log_level) = std::env::var("MCP_LOG_LEVEL") {
186 std::env::set_var("RUST_LOG", mcp_log_level);
187 }
188
189 let mut config = Config::default();
190
191 config.database_url = Self::get_database_url_from_env()
193 .map_err(|e| anyhow::anyhow!("Database configuration error: {}", e))?;
194
195 if let Ok(provider) = env::var("EMBEDDING_PROVIDER") {
197 config.embedding.provider = provider;
198 }
199
200 if let Ok(model) = env::var("EMBEDDING_MODEL") {
201 config.embedding.model = model;
202 }
203
204 if let Ok(base_url) = env::var("EMBEDDING_BASE_URL") {
205 config.embedding.base_url = base_url;
206 }
207
208 if let Ok(timeout) = env::var("EMBEDDING_TIMEOUT_SECONDS") {
209 config.embedding.timeout_seconds = timeout
210 .parse()
211 .map_err(|e| anyhow::anyhow!("Invalid EMBEDDING_TIMEOUT_SECONDS: {}", e))?;
212 }
213
214 if let Ok(api_key) = env::var("OPENAI_API_KEY") {
216 config.embedding.api_key = api_key;
217 }
218
219 if let Ok(api_key) = env::var("EMBEDDING_API_KEY") {
221 config.embedding.api_key = api_key;
222 }
223
224 if let Ok(port) = env::var("HTTP_PORT") {
226 config.http_port = port
227 .parse()
228 .map_err(|e| anyhow::anyhow!("Invalid HTTP_PORT: {}", e))?;
229 }
230
231 if let Ok(port) = env::var("MCP_PORT") {
232 config.mcp_port = Some(
233 port.parse()
234 .map_err(|e| anyhow::anyhow!("Invalid MCP_PORT: {}", e))?,
235 );
236 }
237
238 if let Ok(limit) = env::var("WORKING_TIER_LIMIT") {
240 config.tier_config.working_tier_limit = limit
241 .parse()
242 .map_err(|e| anyhow::anyhow!("Invalid WORKING_TIER_LIMIT: {}", e))?;
243 }
244
245 if let Ok(limit) = env::var("WARM_TIER_LIMIT") {
246 config.tier_config.warm_tier_limit = limit
247 .parse()
248 .map_err(|e| anyhow::anyhow!("Invalid WARM_TIER_LIMIT: {}", e))?;
249 }
250
251 if let Ok(days) = env::var("WORKING_TO_WARM_DAYS") {
252 config.tier_config.working_to_warm_days = days
253 .parse()
254 .map_err(|e| anyhow::anyhow!("Invalid WORKING_TO_WARM_DAYS: {}", e))?;
255 }
256
257 if let Ok(days) = env::var("WARM_TO_COLD_DAYS") {
258 config.tier_config.warm_to_cold_days = days
259 .parse()
260 .map_err(|e| anyhow::anyhow!("Invalid WARM_TO_COLD_DAYS: {}", e))?;
261 }
262
263 if let Ok(threshold) = env::var("IMPORTANCE_THRESHOLD") {
264 config.tier_config.importance_threshold = threshold
265 .parse()
266 .map_err(|e| anyhow::anyhow!("Invalid IMPORTANCE_THRESHOLD: {}", e))?;
267 }
268
269 if let Ok(conns) = env::var("MAX_DB_CONNECTIONS") {
271 config.operational.max_db_connections = conns
272 .parse()
273 .map_err(|e| anyhow::anyhow!("Invalid MAX_DB_CONNECTIONS: {}", e))?;
274 }
275
276 if let Ok(timeout) = env::var("REQUEST_TIMEOUT_SECONDS") {
277 config.operational.request_timeout_seconds = timeout
278 .parse()
279 .map_err(|e| anyhow::anyhow!("Invalid REQUEST_TIMEOUT_SECONDS: {}", e))?;
280 }
281
282 if let Ok(enable) = env::var("ENABLE_METRICS") {
283 config.operational.enable_metrics = enable
284 .parse()
285 .map_err(|e| anyhow::anyhow!("Invalid ENABLE_METRICS: {}", e))?;
286 }
287
288 if let Ok(level) = env::var("LOG_LEVEL") {
289 config.operational.log_level = level;
290 }
291
292 Ok(config)
293 }
294
295 pub fn validate(&self) -> Result<()> {
297 if self.database_url.is_empty() {
298 return Err(anyhow::anyhow!("Database URL is required"));
299 }
300
301 match self.embedding.provider.as_str() {
303 "openai" => {
304 if self.embedding.api_key.is_empty() {
305 return Err(anyhow::anyhow!("API key is required for OpenAI provider"));
306 }
307 }
308 "ollama" => {
309 if self.embedding.base_url.is_empty() {
310 return Err(anyhow::anyhow!("Base URL is required for Ollama provider"));
311 }
312 }
313 "mock" => {
314 }
316 _ => {
317 return Err(anyhow::anyhow!(
318 "Invalid embedding provider: {}. Must be 'openai', 'ollama', or 'mock'",
319 self.embedding.provider
320 ));
321 }
322 }
323
324 if self.embedding.model.is_empty() {
325 return Err(anyhow::anyhow!("Embedding model is required"));
326 }
327
328 if self.tier_config.working_tier_limit == 0 {
329 return Err(anyhow::anyhow!("Working tier limit must be greater than 0"));
330 }
331
332 if self.tier_config.warm_tier_limit == 0 {
333 return Err(anyhow::anyhow!("Warm tier limit must be greater than 0"));
334 }
335
336 if self.tier_config.importance_threshold < 0.0
337 || self.tier_config.importance_threshold > 1.0
338 {
339 return Err(anyhow::anyhow!(
340 "Importance threshold must be between 0.0 and 1.0"
341 ));
342 }
343
344 Ok(())
345 }
346
347 fn get_database_url_from_env() -> Result<String> {
350 if let Ok(url) = env::var("DATABASE_URL") {
352 return Ok(url);
353 }
354
355 if let (Ok(host), Ok(user), Ok(db)) = (
357 env::var("DB_HOST"),
358 env::var("DB_USER"),
359 env::var("DB_NAME"),
360 ) {
361 let password = env::var("DB_PASSWORD").unwrap_or_default();
362 let port = env::var("DB_PORT").unwrap_or_else(|_| "5432".to_string());
363
364 if password.is_empty() {
365 return Ok(format!("postgresql://{}@{}:{}/{}", user, host, port, db));
366 } else {
367 return Ok(format!(
368 "postgresql://{}:{}@{}:{}/{}",
369 user, password, host, port, db
370 ));
371 }
372 }
373
374 if let Ok(conn) = env::var("DB_CONN") {
376 return Ok(conn);
377 }
378
379 Err(anyhow::anyhow!(
380 "Database credentials not found. Please provide either:\n\
381 1. DATABASE_URL environment variable, or\n\
382 2. DB_HOST, DB_USER, DB_NAME (and optionally DB_PASSWORD, DB_PORT), or\n\
383 3. DB_CONN environment variable\n\n\
384 Example:\n\
385 DATABASE_URL=postgresql://user:password@localhost:5432/database\n\
386 or\n\
387 DB_HOST=localhost\n\
388 DB_USER=myuser\n\
389 DB_PASSWORD=mypassword\n\
390 DB_NAME=mydatabase"
391 ))
392 }
393
394 pub fn safe_database_url(&self) -> String {
396 if let Some(at_pos) = self.database_url.find('@') {
397 if let Some(colon_pos) = self.database_url[..at_pos].rfind(':') {
398 let mut masked = self.database_url.clone();
400 masked.replace_range(colon_pos + 1..at_pos, "***");
401 return masked;
402 }
403 }
404 format!(
406 "postgresql://[credentials-hidden]{}",
407 self.database_url
408 .split_once('@')
409 .map(|(_, rest)| rest)
410 .unwrap_or("")
411 )
412 }
413
414 pub fn validate_mcp_environment(&self) -> Result<()> {
416 self.validate()?;
418
419 if self.embedding.provider == "openai" && self.embedding.api_key.len() < 20 {
421 return Err(anyhow::anyhow!(
422 "OpenAI API key appears to be invalid (too short)"
423 ));
424 }
425
426 if self.http_port < 1024 {
428 tracing::warn!(
429 "HTTP port {} requires root privileges. Consider using port >= 1024 for MCP deployment.",
430 self.http_port
431 );
432 }
433
434 if let Some(mcp_port) = self.mcp_port {
435 if mcp_port == self.http_port {
436 return Err(anyhow::anyhow!(
437 "MCP port and HTTP port cannot be the same ({})",
438 mcp_port
439 ));
440 }
441 }
442
443 Ok(())
444 }
445
446 pub fn create_diagnostic_report(&self) -> String {
448 let mut report = String::new();
449 report.push_str("=== Agentic Memory System - MCP Configuration Report ===\n\n");
450
451 report.push_str("Database Configuration:\n");
453 report.push_str(&format!(" Connection: {}\n", self.safe_database_url()));
454
455 report.push_str("\nEmbedding Configuration:\n");
457 report.push_str(&format!(" Provider: {}\n", self.embedding.provider));
458 report.push_str(&format!(" Model: {}\n", self.embedding.model));
459 report.push_str(&format!(" Base URL: {}\n", self.embedding.base_url));
460 report.push_str(&format!(" Timeout: {}s\n", self.embedding.timeout_seconds));
461 report.push_str(&format!(
462 " API Key: {}\n",
463 if self.embedding.api_key.is_empty() {
464 "Not set"
465 } else {
466 "***configured***"
467 }
468 ));
469
470 report.push_str("\nServer Configuration:\n");
472 report.push_str(&format!(" HTTP Port: {}\n", self.http_port));
473 report.push_str(&format!(
474 " MCP Port: {}\n",
475 self.mcp_port
476 .map(|p| p.to_string())
477 .unwrap_or_else(|| "Not set".to_string())
478 ));
479
480 report.push_str("\nMemory Tier Configuration:\n");
482 report.push_str(&format!(
483 " Working Tier Limit: {}\n",
484 self.tier_config.working_tier_limit
485 ));
486 report.push_str(&format!(
487 " Warm Tier Limit: {}\n",
488 self.tier_config.warm_tier_limit
489 ));
490 report.push_str(&format!(
491 " Working->Warm: {} days\n",
492 self.tier_config.working_to_warm_days
493 ));
494 report.push_str(&format!(
495 " Warm->Cold: {} days\n",
496 self.tier_config.warm_to_cold_days
497 ));
498
499 report.push_str("\nValidation Results:\n");
501 match self.validate_mcp_environment() {
502 Ok(_) => report.push_str(" ✅ All configuration checks passed\n"),
503 Err(e) => report.push_str(&format!(" ❌ Configuration error: {}\n", e)),
504 }
505
506 report.push_str("\n=== End Configuration Report ===\n");
507 report
508 }
509}
510
511impl Default for BackupConfiguration {
512 fn default() -> Self {
513 Self {
514 enabled: true,
515 backup_directory: PathBuf::from("/var/lib/codex/backups"),
516 wal_archive_directory: PathBuf::from("/var/lib/codex/wal_archive"),
517 retention_days: 30,
518 enable_encryption: true,
519 schedule: "0 2 * * *".to_string(), rto_minutes: 60, rpo_minutes: 5, enable_verification: true,
523 }
524 }
525}
526
527#[derive(Debug, Clone, Serialize, Deserialize)]
528pub struct SecurityConfiguration {
529 pub enabled: bool,
531 pub tls_enabled: bool,
533 pub tls_cert_path: PathBuf,
534 pub tls_key_path: PathBuf,
535 pub tls_port: u16,
536 pub auth_enabled: bool,
538 pub jwt_secret: String,
539 pub jwt_expiry_hours: u32,
540 pub api_key_enabled: bool,
541 pub rate_limiting_enabled: bool,
543 pub requests_per_minute: u32,
544 pub rate_limit_burst: u32,
545 pub audit_enabled: bool,
547 pub audit_retention_days: u32,
548 pub pii_detection_enabled: bool,
550 pub pii_mask_logs: bool,
551 pub gdpr_enabled: bool,
553 pub gdpr_retention_days: u32,
554 pub right_to_be_forgotten: bool,
555 pub vault_enabled: bool,
557 pub vault_address: Option<String>,
558 pub vault_token_path: Option<PathBuf>,
559 pub input_validation_enabled: bool,
561 pub max_request_size_mb: u32,
562}
563
564impl Default for SecurityConfiguration {
565 fn default() -> Self {
566 Self {
567 enabled: true,
568 tls_enabled: false,
569 tls_cert_path: PathBuf::from("/etc/ssl/certs/codex.crt"),
570 tls_key_path: PathBuf::from("/etc/ssl/private/codex.key"),
571 tls_port: 8443,
572 auth_enabled: false,
573 jwt_secret: "change-me-in-production".to_string(),
574 jwt_expiry_hours: 24,
575 api_key_enabled: false,
576 rate_limiting_enabled: false,
577 requests_per_minute: 100,
578 rate_limit_burst: 20,
579 audit_enabled: false,
580 audit_retention_days: 90,
581 pii_detection_enabled: false,
582 pii_mask_logs: true,
583 gdpr_enabled: false,
584 gdpr_retention_days: 730, right_to_be_forgotten: false,
586 vault_enabled: false,
587 vault_address: None,
588 vault_token_path: None,
589 input_validation_enabled: true,
590 max_request_size_mb: 10,
591 }
592 }
593}
594
595#[cfg(test)]
596mod tests {
597 use super::*;
598
599 #[test]
600 fn test_default_config() {
601 let config = Config::default();
602 assert_eq!(config.http_port, 8080);
603 assert_eq!(config.embedding.model, "nomic-embed-text");
604 assert_eq!(config.embedding.provider, "ollama");
605 assert_eq!(config.tier_config.working_tier_limit, 1000);
606 }
607
608 #[test]
609 fn test_config_validation() {
610 let mut config = Config::default();
611 assert!(config.validate().is_ok());
613
614 config.embedding.provider = "openai".to_string();
616 config.embedding.api_key = String::new();
617 assert!(config.validate().is_err());
618
619 config.embedding.api_key = "test-key".to_string();
621 assert!(config.validate().is_ok());
622 }
623
624 #[test]
625 fn test_embedding_config_validation() {
626 let mut config = Config::default();
627
628 config.embedding.provider = "invalid".to_string();
630 assert!(config.validate().is_err());
631
632 config.embedding.provider = "ollama".to_string();
634 config.embedding.model = String::new();
635 assert!(config.validate().is_err());
636
637 config.embedding.model = "test-model".to_string();
639 config.embedding.base_url = String::new();
640 assert!(config.validate().is_err());
641 }
642}