1use std::fmt;
2use std::path::PathBuf;
3
4use secrecy::SecretString;
5
6use crate::error::{GuardError, GuardResult};
7
8#[derive(Clone)]
10pub struct GuardConfig {
11 pub db_path: PathBuf,
13 pub jwt_secret: SecretString,
15 pub policy_dir: Option<PathBuf>,
17 pub sensitive_resources: Vec<String>,
19 pub audit_flush_interval_ms: u64,
21 pub audit_batch_size: usize,
23 pub business_hours_start_hour: u8,
25 pub business_hours_end_hour: u8,
27}
28
29impl fmt::Debug for GuardConfig {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 f.debug_struct("GuardConfig")
32 .field("db_path", &self.db_path)
33 .field("jwt_secret", &"***REDACTED***")
34 .field("policy_dir", &self.policy_dir)
35 .field("sensitive_resources", &self.sensitive_resources)
36 .field("audit_flush_interval_ms", &self.audit_flush_interval_ms)
37 .field("audit_batch_size", &self.audit_batch_size)
38 .field("business_hours_start_hour", &self.business_hours_start_hour)
39 .field("business_hours_end_hour", &self.business_hours_end_hour)
40 .finish()
41 }
42}
43
44impl GuardConfig {
45 pub fn from_env() -> GuardResult<Self> {
47 let jwt_secret = std::env::var("CLAW_GUARD_JWT_SECRET")
48 .map(|value| SecretString::new(value.into_boxed_str()))
49 .map_err(|_| GuardError::ConfigError("CLAW_GUARD_JWT_SECRET is required".to_owned()))?;
50 let db_path = std::env::var("CLAW_GUARD_DB_PATH")
51 .map(PathBuf::from)
52 .unwrap_or_else(|_| PathBuf::from("claw_guard.db"));
53 let policy_dir = std::env::var("CLAW_GUARD_POLICY_DIR")
54 .ok()
55 .map(PathBuf::from)
56 .filter(|value| !value.as_os_str().is_empty());
57 let sensitive_resources = std::env::var("CLAW_GUARD_SENSITIVE_RESOURCES")
58 .unwrap_or_default()
59 .split(',')
60 .filter_map(|value| {
61 let trimmed = value.trim();
62 (!trimmed.is_empty()).then(|| trimmed.to_owned())
63 })
64 .collect();
65
66 Ok(Self {
67 db_path,
68 jwt_secret,
69 policy_dir,
70 sensitive_resources,
71 audit_flush_interval_ms: parse_or_default("CLAW_GUARD_AUDIT_FLUSH_INTERVAL_MS", 100)?,
72 audit_batch_size: parse_or_default("CLAW_GUARD_AUDIT_BATCH_SIZE", 500)?,
73 business_hours_start_hour: parse_or_default("CLAW_GUARD_BUSINESS_HOURS_START", 8)?,
74 business_hours_end_hour: parse_or_default("CLAW_GUARD_BUSINESS_HOURS_END", 18)?,
75 })
76 }
77
78 pub fn sqlite_connection_string(&self) -> String {
80 let path = self.db_path.to_string_lossy();
81 if path == ":memory:" {
82 "sqlite::memory:".to_owned()
83 } else if path.starts_with("sqlite:") {
84 path.into_owned()
85 } else {
86 format!("sqlite://{}?mode=rwc", path)
87 }
88 }
89}
90
91fn parse_or_default<T>(name: &str, default: T) -> GuardResult<T>
92where
93 T: std::str::FromStr,
94 T::Err: fmt::Display,
95{
96 match std::env::var(name) {
97 Ok(value) => value
98 .parse::<T>()
99 .map_err(|error| GuardError::ConfigError(format!("invalid {name}: {error}"))),
100 Err(_) => Ok(default),
101 }
102}