1pub use zeph_config::{
11 AcpAuthMethod, AcpConfig, AcpLspConfig, AcpTransport, AdditionalDir, AdditionalDirError,
12 AgentConfig, CandleConfig, CandleDevice, CandleInlineConfig, CandleSource,
13 CascadeClassifierMode, CascadeConfig, ClassifiersConfig, CompressionConfig,
14 CompressionStrategy, Config, ConfigError, ContextFormat, CostConfig, DaemonConfig, DebugConfig,
15 DetectorMode, DiscordConfig, DocumentConfig, DumpFormat, ExperimentConfig, ExperimentSchedule,
16 FocusConfig, GatewayConfig, GenerationParams, GonkaNode, GraphConfig, HookAction, HookDef,
17 HookMatcher, IndexConfig, LearningConfig, LlmConfig, LlmRoutingStrategy, LogRotation,
18 LoggingConfig, MAX_TOKENS_CAP, McpConfig, McpOAuthConfig, McpServerConfig, McpTrustLevel,
19 MemoryConfig, MemoryScope, NoteLinkingConfig, OAuthTokenStorage, OrchestrationConfig,
20 PermissionMode, ProviderEntry, ProviderKind, ProviderName, PruningStrategy, RateLimitConfig,
21 ResolvedSecrets, RetrievalConfig, RouterConfig, RouterStrategyConfig, ScheduledTaskConfig,
22 ScheduledTaskKind, SchedulerConfig, SchedulerDaemonConfig, SchedulerSecurityConfig,
23 SecurityConfig, SemanticConfig, SessionsConfig, SidequestConfig, SkillFilter, SkillPromptMode,
24 SkillsConfig, SlackConfig, StoreRoutingConfig, StoreRoutingStrategy, SttConfig, SubAgentConfig,
25 SubAgentLifecycleHooks, SubagentHooks, TaskSupervisorConfig, TelegramConfig, TimeoutConfig,
26 ToolDiscoveryConfig, ToolDiscoveryStrategyConfig, ToolFilterConfig, ToolPolicy, TraceConfig,
27 TrustConfig, TuiConfig, VaultConfig, VectorBackend,
28};
29
30pub use zeph_config::{
31 AutoDreamConfig, CategoryConfig, ContextStrategy, DigestConfig, MagicDocsConfig,
32 MicrocompactConfig, PersonaConfig, TrajectoryConfig, TreeConfig,
33};
34pub use zeph_config::{DiagnosticSeverity, DiagnosticsConfig, HoverConfig, LspConfig};
35pub use zeph_config::{DurableBackend, DurableConfig, RetentionPolicy};
36pub use zeph_config::{QualityConfig, TriggerPolicy};
37pub use zeph_config::{TelemetryBackend, TelemetryConfig};
38
39pub use zeph_config::{
40 ContentIsolationConfig, CustomPiiPattern, ExfiltrationGuardConfig, MemoryWriteValidationConfig,
41 PiiFilterConfig, QuarantineConfig,
42};
43pub use zeph_config::{GuardrailAction, GuardrailConfig, GuardrailFailStrategy};
44
45pub use zeph_config::A2aServerConfig;
46pub use zeph_config::ChannelSkillsConfig;
47pub use zeph_config::{FileChangedConfig, HooksConfig};
48
49pub use zeph_config::{
50 DEFAULT_DEBUG_DIR, DEFAULT_LOG_FILE, DEFAULT_SKILLS_DIR, DEFAULT_SQLITE_PATH,
51 default_debug_dir, default_log_file_path, default_skills_dir, default_sqlite_path,
52 is_legacy_default_debug_dir, is_legacy_default_log_file, is_legacy_default_skills_path,
53 is_legacy_default_sqlite_path,
54};
55
56pub use zeph_config::providers::{default_stt_language, validate_pool};
57
58pub mod migrate {
59 pub use zeph_config::migrate::*;
60}
61
62use crate::vault::{Secret, VaultProvider};
63
64pub trait SecretResolver {
69 fn resolve_secrets(
75 &mut self,
76 vault: &dyn VaultProvider,
77 ) -> impl std::future::Future<Output = Result<(), ConfigError>> + Send;
78
79 fn resolve_secrets_masked(
89 &mut self,
90 vault: &dyn VaultProvider,
91 registry: Option<&std::sync::Arc<zeph_sanitizer::secret_mask::SecretMaskRegistry>>,
92 ) -> impl std::future::Future<Output = Result<(), ConfigError>> + Send;
93}
94
95fn register_masked_secret(
99 registry: Option<&std::sync::Arc<zeph_sanitizer::secret_mask::SecretMaskRegistry>>,
100 key_name: &str,
101 value: &str,
102) {
103 if let Some(registry) = registry {
104 registry.register(
105 key_name,
106 value,
107 zeph_sanitizer::secret_mask::SecretCategory::from_key_name(key_name),
108 );
109 }
110}
111
112fn log_gonka_credential_status(has_key: bool, has_address: bool) {
113 match (has_key, has_address) {
114 (true, true) => tracing::info!("gonka wallet credentials resolved from vault"),
115 (true, false) => tracing::warn!(
116 "ZEPH_GONKA_PRIVATE_KEY is set but ZEPH_GONKA_ADDRESS is missing from vault"
117 ),
118 (false, true) => tracing::warn!(
119 "ZEPH_GONKA_ADDRESS is set but ZEPH_GONKA_PRIVATE_KEY is missing from vault"
120 ),
121 (false, false) => {}
122 }
123}
124
125impl SecretResolver for Config {
129 async fn resolve_secrets(&mut self, vault: &dyn VaultProvider) -> Result<(), ConfigError> {
130 self.resolve_secrets_masked(vault, None).await
131 }
132
133 #[allow(clippy::too_many_lines)] async fn resolve_secrets_masked(
135 &mut self,
136 vault: &dyn VaultProvider,
137 registry: Option<&std::sync::Arc<zeph_sanitizer::secret_mask::SecretMaskRegistry>>,
138 ) -> Result<(), ConfigError> {
139 if let Some(val) = vault.get_secret("ZEPH_CLAUDE_API_KEY").await? {
140 register_masked_secret(registry, "ZEPH_CLAUDE_API_KEY", &val);
141 self.secrets.claude_api_key = Some(Secret::new(val));
142 }
143 if let Some(val) = vault.get_secret("ZEPH_OPENAI_API_KEY").await? {
144 register_masked_secret(registry, "ZEPH_OPENAI_API_KEY", &val);
145 self.secrets.openai_api_key = Some(Secret::new(val));
146 }
147 if let Some(val) = vault.get_secret("ZEPH_GEMINI_API_KEY").await? {
148 register_masked_secret(registry, "ZEPH_GEMINI_API_KEY", &val);
149 self.secrets.gemini_api_key = Some(Secret::new(val));
150 }
151 if let Some(val) = vault.get_secret("ZEPH_GONKA_PRIVATE_KEY").await? {
152 register_masked_secret(registry, "ZEPH_GONKA_PRIVATE_KEY", &val);
153 self.secrets.gonka_private_key = Some(Secret::new(val));
154 }
155 if let Some(val) = vault.get_secret("ZEPH_GONKA_ADDRESS").await? {
156 self.secrets.gonka_address = Some(Secret::new(val));
160 }
161 if let Some(val) = vault.get_secret("ZEPH_COCOON_ACCESS_HASH").await? {
162 register_masked_secret(registry, "ZEPH_COCOON_ACCESS_HASH", &val);
163 self.secrets.cocoon_access_hash = Some(Secret::new(val));
164 }
165 log_gonka_credential_status(
166 self.secrets.gonka_private_key.is_some(),
167 self.secrets.gonka_address.is_some(),
168 );
169 if let Some(val) = vault.get_secret("ZEPH_TELEGRAM_TOKEN").await?
170 && let Some(tg) = self.telegram.as_mut()
171 {
172 register_masked_secret(registry, "ZEPH_TELEGRAM_TOKEN", &val);
173 tg.token = Some(val);
174 }
175 if let Some(val) = vault.get_secret("ZEPH_A2A_AUTH_TOKEN").await? {
176 register_masked_secret(registry, "ZEPH_A2A_AUTH_TOKEN", &val);
177 self.a2a.auth_token = Some(val);
178 }
179 for entry in &self.llm.providers {
180 if entry.provider_type == crate::config::ProviderKind::Compatible
181 && let Some(ref name) = entry.name
182 {
183 let normalized: String = name
184 .to_uppercase()
185 .chars()
186 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
187 .collect();
188 let env_key = format!("ZEPH_COMPATIBLE_{normalized}_API_KEY");
189 if let Some(val) = vault.get_secret(&env_key).await? {
190 register_masked_secret(registry, &env_key, &val);
191 self.secrets
192 .compatible_api_keys
193 .insert(name.clone(), Secret::new(val));
194 }
195 }
196 }
197 if let Some(val) = vault.get_secret("ZEPH_HF_TOKEN").await? {
198 register_masked_secret(registry, "ZEPH_HF_TOKEN", &val);
199 self.classifiers.hf_token = Some(val.clone());
200 if let Some(candle) = self.llm.candle.as_mut() {
201 candle.hf_token = Some(val);
202 }
203 }
204 if let Some(val) = vault.get_secret("ZEPH_GATEWAY_TOKEN").await? {
205 register_masked_secret(registry, "ZEPH_GATEWAY_TOKEN", &val);
206 self.gateway.auth_token = Some(val);
207 }
208 if let Some(val) = vault.get_secret("ZEPH_DATABASE_URL").await? {
209 register_masked_secret(registry, "ZEPH_DATABASE_URL", &val);
210 self.memory.database_url = Some(val);
211 }
212 if let Some(val) = vault.get_secret("ZEPH_QDRANT_API_KEY").await? {
213 register_masked_secret(registry, "ZEPH_QDRANT_API_KEY", &val);
214 self.memory.qdrant_api_key = Some(Secret::new(val));
215 }
216 if let Some(val) = vault.get_secret("ZEPH_DISCORD_TOKEN").await?
217 && let Some(dc) = self.discord.as_mut()
218 {
219 register_masked_secret(registry, "ZEPH_DISCORD_TOKEN", &val);
220 dc.token = Some(val);
221 }
222 if let Some(val) = vault.get_secret("ZEPH_DISCORD_APP_ID").await?
223 && let Some(dc) = self.discord.as_mut()
224 {
225 dc.application_id = Some(val);
228 }
229 if let Some(val) = vault.get_secret("ZEPH_SLACK_BOT_TOKEN").await?
230 && let Some(sl) = self.slack.as_mut()
231 {
232 register_masked_secret(registry, "ZEPH_SLACK_BOT_TOKEN", &val);
233 sl.bot_token = Some(val);
234 }
235 if let Some(val) = vault.get_secret("ZEPH_SLACK_SIGNING_SECRET").await?
236 && let Some(sl) = self.slack.as_mut()
237 {
238 register_masked_secret(registry, "ZEPH_SLACK_SIGNING_SECRET", &val);
239 sl.signing_secret = Some(val);
240 }
241 for key in vault.list_keys() {
242 if let Some(custom_name) = key.strip_prefix("ZEPH_SECRET_")
243 && !custom_name.is_empty()
244 && let Some(val) = vault.get_secret(&key).await?
245 {
246 let normalized = custom_name.to_lowercase().replace('-', "_");
250 register_masked_secret(registry, &key, &val);
251 self.secrets.custom.insert(normalized, Secret::new(val));
252 }
253 }
254 Ok(())
255 }
256}
257
258#[cfg(test)]
259mod tests {
260 use super::*;
261
262 #[tokio::test]
263 #[cfg(any(test, feature = "mock"))]
264 async fn resolve_secrets_with_mock_vault() {
265 use crate::vault::MockVaultProvider;
266
267 let vault = MockVaultProvider::new()
268 .with_secret("ZEPH_CLAUDE_API_KEY", "sk-test-123")
269 .with_secret("ZEPH_TELEGRAM_TOKEN", "tg-token-456");
270
271 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
272 config.resolve_secrets(&vault).await.unwrap();
273
274 assert_eq!(
275 config.secrets.claude_api_key.as_ref().unwrap().expose(),
276 "sk-test-123"
277 );
278 if let Some(tg) = config.telegram {
279 assert_eq!(tg.token.as_deref(), Some("tg-token-456"));
280 }
281 }
282
283 #[tokio::test]
284 #[cfg(any(test, feature = "mock"))]
285 async fn resolve_gonka_secrets_both_present() {
286 use crate::vault::MockVaultProvider;
287
288 let vault = MockVaultProvider::new()
289 .with_secret("ZEPH_GONKA_PRIVATE_KEY", "gonka-priv-key-abc")
290 .with_secret("ZEPH_GONKA_ADDRESS", "gonka1xyzaddress");
291
292 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
293 config.resolve_secrets(&vault).await.unwrap();
294
295 assert_eq!(
296 config.secrets.gonka_private_key.as_ref().unwrap().expose(),
297 "gonka-priv-key-abc"
298 );
299 assert_eq!(
300 config.secrets.gonka_address.as_ref().unwrap().expose(),
301 "gonka1xyzaddress"
302 );
303 }
304
305 #[tokio::test]
306 #[cfg(any(test, feature = "mock"))]
307 async fn resolve_gonka_partial_only_private_key() {
308 use crate::vault::MockVaultProvider;
309
310 let vault =
311 MockVaultProvider::new().with_secret("ZEPH_GONKA_PRIVATE_KEY", "gonka-priv-key-only");
312
313 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
314 config.resolve_secrets(&vault).await.unwrap();
315
316 assert!(config.secrets.gonka_private_key.is_some());
317 assert!(config.secrets.gonka_address.is_none());
318 }
319
320 #[tokio::test]
323 #[cfg(any(test, feature = "mock"))]
324 async fn resolve_secrets_masked_registers_values_with_registry() {
325 use crate::vault::MockVaultProvider;
326 use std::sync::Arc;
327 use zeph_sanitizer::secret_mask::SecretMaskRegistry;
328
329 let vault = MockVaultProvider::new()
330 .with_secret("ZEPH_CLAUDE_API_KEY", "sk-claude-secret-value-1234")
331 .with_secret("ZEPH_OPENAI_API_KEY", "sk-openai-secret-value-5678");
332
333 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
334 let registry = Arc::new(SecretMaskRegistry::new());
335 config
336 .resolve_secrets_masked(&vault, Some(®istry))
337 .await
338 .unwrap();
339
340 assert_eq!(
341 registry.len(),
342 2,
343 "both resolved secrets must be registered"
344 );
345 let masked = registry.mask("token: sk-claude-secret-value-1234");
346 assert!(!masked.contains("sk-claude-secret-value-1234"));
347 assert!(masked.contains("<SECRET:api_key:"));
348 }
349
350 #[tokio::test]
351 #[cfg(any(test, feature = "mock"))]
352 async fn resolve_secrets_masked_none_registry_behaves_like_resolve_secrets() {
353 use crate::vault::MockVaultProvider;
354
355 let vault = MockVaultProvider::new().with_secret("ZEPH_CLAUDE_API_KEY", "sk-test-123");
356 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
357 config.resolve_secrets_masked(&vault, None).await.unwrap();
358
359 assert_eq!(
360 config.secrets.claude_api_key.as_ref().unwrap().expose(),
361 "sk-test-123",
362 "None registry must not change resolution behavior"
363 );
364 }
365
366 #[tokio::test]
367 #[cfg(any(test, feature = "mock"))]
368 async fn resolve_secrets_plain_delegates_without_registering() {
369 use crate::vault::MockVaultProvider;
370
371 let vault = MockVaultProvider::new().with_secret("ZEPH_OPENAI_API_KEY", "sk-openai-abc");
374 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
375 config.resolve_secrets(&vault).await.unwrap();
376
377 assert_eq!(
378 config.secrets.openai_api_key.as_ref().unwrap().expose(),
379 "sk-openai-abc"
380 );
381 }
382}