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