1mod env;
5mod types;
6
7#[cfg(test)]
8mod tests;
9
10pub use types::*;
11pub use zeph_tools::AutonomyLevel;
12
13use std::path::Path;
14
15use crate::vault::VaultProvider;
16
17#[derive(Debug, thiserror::Error)]
18pub enum ConfigError {
19 #[error("failed to read config file: {0}")]
20 Io(#[from] std::io::Error),
21 #[error("failed to parse config file: {0}")]
22 Parse(#[from] toml::de::Error),
23 #[error("config validation failed: {0}")]
24 Validation(String),
25 #[error("vault error: {0}")]
26 Vault(#[from] anyhow::Error),
27}
28
29impl Config {
30 pub fn load(path: &Path) -> Result<Self, ConfigError> {
38 let mut config = if path.exists() {
39 let content = std::fs::read_to_string(path)?;
40 toml::from_str::<Self>(&content)?
41 } else {
42 Self::default()
43 };
44
45 config.apply_env_overrides();
46 Ok(config)
47 }
48
49 pub fn validate(&self) -> Result<(), ConfigError> {
55 if self.memory.history_limit > 10_000 {
56 return Err(ConfigError::Validation(format!(
57 "history_limit must be <= 10000, got {}",
58 self.memory.history_limit
59 )));
60 }
61 if self.memory.context_budget_tokens > 1_000_000 {
62 return Err(ConfigError::Validation(format!(
63 "context_budget_tokens must be <= 1000000, got {}",
64 self.memory.context_budget_tokens
65 )));
66 }
67 if self.agent.max_tool_iterations > 100 {
68 return Err(ConfigError::Validation(format!(
69 "max_tool_iterations must be <= 100, got {}",
70 self.agent.max_tool_iterations
71 )));
72 }
73 if self.a2a.rate_limit == 0 {
74 return Err(ConfigError::Validation("a2a.rate_limit must be > 0".into()));
75 }
76 if self.gateway.rate_limit == 0 {
77 return Err(ConfigError::Validation(
78 "gateway.rate_limit must be > 0".into(),
79 ));
80 }
81 if self.gateway.max_body_size > 10_485_760 {
82 return Err(ConfigError::Validation(format!(
83 "gateway.max_body_size must be <= 10485760 (10 MiB), got {}",
84 self.gateway.max_body_size
85 )));
86 }
87 if self.memory.token_safety_margin <= 0.0 {
88 return Err(ConfigError::Validation(format!(
89 "token_safety_margin must be > 0.0, got {}",
90 self.memory.token_safety_margin
91 )));
92 }
93 if self.memory.tool_call_cutoff == 0 {
94 return Err(ConfigError::Validation(
95 "tool_call_cutoff must be >= 1".into(),
96 ));
97 }
98 if let crate::config::CompressionStrategy::Proactive {
99 threshold_tokens,
100 max_summary_tokens,
101 } = &self.memory.compression.strategy
102 {
103 if *threshold_tokens < 1_000 {
104 return Err(ConfigError::Validation(format!(
105 "compression.threshold_tokens must be >= 1000, got {threshold_tokens}"
106 )));
107 }
108 if *max_summary_tokens < 128 {
109 return Err(ConfigError::Validation(format!(
110 "compression.max_summary_tokens must be >= 128, got {max_summary_tokens}"
111 )));
112 }
113 }
114 if !self.memory.compaction_threshold.is_finite()
115 || self.memory.compaction_threshold <= 0.0
116 || self.memory.compaction_threshold >= 1.0
117 {
118 return Err(ConfigError::Validation(format!(
119 "compaction_threshold must be in (0.0, 1.0) exclusive, got {}",
120 self.memory.compaction_threshold
121 )));
122 }
123 if !self.memory.deferred_apply_threshold.is_finite()
124 || self.memory.deferred_apply_threshold <= 0.0
125 || self.memory.deferred_apply_threshold >= 1.0
126 {
127 return Err(ConfigError::Validation(format!(
128 "deferred_apply_threshold must be in (0.0, 1.0) exclusive, got {}",
129 self.memory.deferred_apply_threshold
130 )));
131 }
132 if self.memory.deferred_apply_threshold >= self.memory.compaction_threshold {
133 return Err(ConfigError::Validation(format!(
134 "deferred_apply_threshold ({}) must be less than compaction_threshold ({})",
135 self.memory.deferred_apply_threshold, self.memory.compaction_threshold,
136 )));
137 }
138 self.experiments.validate()?;
139 Ok(())
140 }
141
142 pub async fn resolve_secrets(&mut self, vault: &dyn VaultProvider) -> Result<(), ConfigError> {
148 use crate::vault::Secret;
149
150 if let Some(val) = vault.get_secret("ZEPH_CLAUDE_API_KEY").await? {
151 self.secrets.claude_api_key = Some(Secret::new(val));
152 }
153 if let Some(val) = vault.get_secret("ZEPH_OPENAI_API_KEY").await? {
154 self.secrets.openai_api_key = Some(Secret::new(val));
155 }
156 if let Some(val) = vault.get_secret("ZEPH_TELEGRAM_TOKEN").await? {
157 let tg = self.telegram.get_or_insert(TelegramConfig {
158 token: None,
159 allowed_users: Vec::new(),
160 });
161 tg.token = Some(val);
162 }
163 if let Some(val) = vault.get_secret("ZEPH_A2A_AUTH_TOKEN").await? {
164 self.a2a.auth_token = Some(val);
165 }
166 if let Some(ref entries) = self.llm.compatible {
167 for entry in entries {
168 let env_key = format!("ZEPH_COMPATIBLE_{}_API_KEY", entry.name.to_uppercase());
169 if let Some(val) = vault.get_secret(&env_key).await? {
170 self.secrets
171 .compatible_api_keys
172 .insert(entry.name.clone(), Secret::new(val));
173 }
174 }
175 }
176 if let Some(val) = vault.get_secret("ZEPH_GATEWAY_TOKEN").await? {
177 self.gateway.auth_token = Some(val);
178 }
179 if let Some(val) = vault.get_secret("ZEPH_DISCORD_TOKEN").await? {
180 let dc = self.discord.get_or_insert(DiscordConfig {
181 token: None,
182 application_id: None,
183 allowed_user_ids: Vec::new(),
184 allowed_role_ids: Vec::new(),
185 allowed_channel_ids: Vec::new(),
186 });
187 dc.token = Some(val);
188 }
189 if let Some(val) = vault.get_secret("ZEPH_DISCORD_APP_ID").await?
190 && let Some(dc) = self.discord.as_mut()
191 {
192 dc.application_id = Some(val);
193 }
194 if let Some(val) = vault.get_secret("ZEPH_SLACK_BOT_TOKEN").await? {
195 let sl = self.slack.get_or_insert(SlackConfig {
196 bot_token: None,
197 signing_secret: None,
198 webhook_host: "127.0.0.1".into(),
199 port: 3000,
200 allowed_user_ids: Vec::new(),
201 allowed_channel_ids: Vec::new(),
202 });
203 sl.bot_token = Some(val);
204 }
205 if let Some(val) = vault.get_secret("ZEPH_SLACK_SIGNING_SECRET").await?
206 && let Some(sl) = self.slack.as_mut()
207 {
208 sl.signing_secret = Some(val);
209 }
210 for key in vault.list_keys() {
211 if let Some(custom_name) = key.strip_prefix("ZEPH_SECRET_")
212 && !custom_name.is_empty()
213 && let Some(val) = vault.get_secret(&key).await?
214 {
215 let normalized = custom_name.to_lowercase().replace('-', "_");
219 self.secrets.custom.insert(normalized, Secret::new(val));
220 }
221 }
222 Ok(())
223 }
224}