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 {
190 database_url: Self::get_database_url_from_env()
191 .map_err(|e| anyhow::anyhow!("Database configuration error: {e}"))?,
192 ..Config::default()
193 };
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!("postgresql://{user}:{password}@{host}:{port}/{db}"));
368 }
369 }
370
371 if let Ok(conn) = env::var("DB_CONN") {
373 return Ok(conn);
374 }
375
376 Err(anyhow::anyhow!(
377 "Database credentials not found. Please provide either:\n\
378 1. DATABASE_URL environment variable, or\n\
379 2. DB_HOST, DB_USER, DB_NAME (and optionally DB_PASSWORD, DB_PORT), or\n\
380 3. DB_CONN environment variable\n\n\
381 Example:\n\
382 DATABASE_URL=postgresql://user:password@localhost:5432/database\n\
383 or\n\
384 DB_HOST=localhost\n\
385 DB_USER=myuser\n\
386 DB_PASSWORD=mypassword\n\
387 DB_NAME=mydatabase"
388 ))
389 }
390
391 pub fn safe_database_url(&self) -> String {
393 if let Some(at_pos) = self.database_url.find('@') {
394 if let Some(colon_pos) = self.database_url[..at_pos].rfind(':') {
395 let mut masked = self.database_url.clone();
397 masked.replace_range(colon_pos + 1..at_pos, "***");
398 return masked;
399 }
400 }
401 format!(
403 "postgresql://[credentials-hidden]{}",
404 self.database_url
405 .split_once('@')
406 .map(|(_, rest)| rest)
407 .unwrap_or("")
408 )
409 }
410
411 pub fn validate_mcp_environment(&self) -> Result<()> {
413 self.validate()?;
415
416 if self.embedding.provider == "openai" && self.embedding.api_key.len() < 20 {
418 return Err(anyhow::anyhow!(
419 "OpenAI API key appears to be invalid (too short)"
420 ));
421 }
422
423 if self.http_port < 1024 {
425 tracing::warn!(
426 "HTTP port {} requires root privileges. Consider using port >= 1024 for MCP deployment.",
427 self.http_port
428 );
429 }
430
431 if let Some(mcp_port) = self.mcp_port {
432 if mcp_port == self.http_port {
433 return Err(anyhow::anyhow!(
434 "MCP port and HTTP port cannot be the same ({})",
435 mcp_port
436 ));
437 }
438 }
439
440 Ok(())
441 }
442
443 pub fn create_diagnostic_report(&self) -> String {
445 let mut report = String::new();
446 report.push_str("=== Agentic Memory System - MCP Configuration Report ===\n\n");
447
448 report.push_str("Database Configuration:\n");
450 report.push_str(&format!(" Connection: {}\n", self.safe_database_url()));
451
452 report.push_str("\nEmbedding Configuration:\n");
454 report.push_str(&format!(" Provider: {}\n", self.embedding.provider));
455 report.push_str(&format!(" Model: {}\n", self.embedding.model));
456 report.push_str(&format!(" Base URL: {}\n", self.embedding.base_url));
457 report.push_str(&format!(" Timeout: {}s\n", self.embedding.timeout_seconds));
458 report.push_str(&format!(
459 " API Key: {}\n",
460 if self.embedding.api_key.is_empty() {
461 "Not set"
462 } else {
463 "***configured***"
464 }
465 ));
466
467 report.push_str("\nServer Configuration:\n");
469 report.push_str(&format!(" HTTP Port: {}\n", self.http_port));
470 report.push_str(&format!(
471 " MCP Port: {}\n",
472 self.mcp_port
473 .map(|p| p.to_string())
474 .unwrap_or_else(|| "Not set".to_string())
475 ));
476
477 report.push_str("\nMemory Tier Configuration:\n");
479 report.push_str(&format!(
480 " Working Tier Limit: {}\n",
481 self.tier_config.working_tier_limit
482 ));
483 report.push_str(&format!(
484 " Warm Tier Limit: {}\n",
485 self.tier_config.warm_tier_limit
486 ));
487 report.push_str(&format!(
488 " Working->Warm: {} days\n",
489 self.tier_config.working_to_warm_days
490 ));
491 report.push_str(&format!(
492 " Warm->Cold: {} days\n",
493 self.tier_config.warm_to_cold_days
494 ));
495
496 report.push_str("\nValidation Results:\n");
498 match self.validate_mcp_environment() {
499 Ok(_) => report.push_str(" ✅ All configuration checks passed\n"),
500 Err(e) => report.push_str(&format!(" ❌ Configuration error: {e}\n")),
501 }
502
503 report.push_str("\n=== End Configuration Report ===\n");
504 report
505 }
506}
507
508impl Default for BackupConfiguration {
509 fn default() -> Self {
510 Self {
511 enabled: true,
512 backup_directory: PathBuf::from("/var/lib/codex/backups"),
513 wal_archive_directory: PathBuf::from("/var/lib/codex/wal_archive"),
514 retention_days: 30,
515 enable_encryption: true,
516 schedule: "0 2 * * *".to_string(), rto_minutes: 60, rpo_minutes: 5, enable_verification: true,
520 }
521 }
522}
523
524#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct SecurityConfiguration {
526 pub enabled: bool,
528 pub tls_enabled: bool,
530 pub tls_cert_path: PathBuf,
531 pub tls_key_path: PathBuf,
532 pub tls_port: u16,
533 pub auth_enabled: bool,
535 pub jwt_secret: String,
536 pub jwt_expiry_hours: u32,
537 pub api_key_enabled: bool,
538 pub rate_limiting_enabled: bool,
540 pub requests_per_minute: u32,
541 pub rate_limit_burst: u32,
542 pub audit_enabled: bool,
544 pub audit_retention_days: u32,
545 pub pii_detection_enabled: bool,
547 pub pii_mask_logs: bool,
548 pub gdpr_enabled: bool,
550 pub gdpr_retention_days: u32,
551 pub right_to_be_forgotten: bool,
552 pub vault_enabled: bool,
554 pub vault_address: Option<String>,
555 pub vault_token_path: Option<PathBuf>,
556 pub input_validation_enabled: bool,
558 pub max_request_size_mb: u32,
559}
560
561impl Default for SecurityConfiguration {
562 fn default() -> Self {
563 Self {
564 enabled: true,
565 tls_enabled: false,
566 tls_cert_path: PathBuf::from("/etc/ssl/certs/codex.crt"),
567 tls_key_path: PathBuf::from("/etc/ssl/private/codex.key"),
568 tls_port: 8443,
569 auth_enabled: false,
570 jwt_secret: "change-me-in-production".to_string(),
571 jwt_expiry_hours: 24,
572 api_key_enabled: false,
573 rate_limiting_enabled: false,
574 requests_per_minute: 100,
575 rate_limit_burst: 20,
576 audit_enabled: false,
577 audit_retention_days: 90,
578 pii_detection_enabled: false,
579 pii_mask_logs: true,
580 gdpr_enabled: false,
581 gdpr_retention_days: 730, right_to_be_forgotten: false,
583 vault_enabled: false,
584 vault_address: None,
585 vault_token_path: None,
586 input_validation_enabled: true,
587 max_request_size_mb: 10,
588 }
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595
596 #[test]
597 fn test_default_config() {
598 let config = Config::default();
599 assert_eq!(config.http_port, 8080);
600 assert_eq!(config.embedding.model, "nomic-embed-text");
601 assert_eq!(config.embedding.provider, "ollama");
602 assert_eq!(config.tier_config.working_tier_limit, 1000);
603 }
604
605 #[test]
606 fn test_config_validation() {
607 let mut config = Config::default();
608 assert!(config.validate().is_ok());
610
611 config.embedding.provider = "openai".to_string();
613 config.embedding.api_key = String::new();
614 assert!(config.validate().is_err());
615
616 config.embedding.api_key = "test-key".to_string();
618 assert!(config.validate().is_ok());
619 }
620
621 #[test]
622 fn test_embedding_config_validation() {
623 let mut config = Config::default();
624
625 config.embedding.provider = "invalid".to_string();
627 assert!(config.validate().is_err());
628
629 config.embedding.provider = "ollama".to_string();
631 config.embedding.model = String::new();
632 assert!(config.validate().is_err());
633
634 config.embedding.model = "test-model".to_string();
636 config.embedding.base_url = String::new();
637 assert!(config.validate().is_err());
638 }
639}