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 RegistryBackendKind, RegistryConfig, ResolvedSecrets, RetrievalConfig, RouterConfig,
22 RouterStrategyConfig, ScheduledTaskConfig, ScheduledTaskKind, SchedulerConfig,
23 SchedulerDaemonConfig, SchedulerSecurityConfig, SecurityConfig, SemanticConfig, SessionsConfig,
24 SidequestConfig, SkillFilter, SkillPromptMode, SkillsConfig, SlackConfig, StoreRoutingConfig,
25 StoreRoutingStrategy, SttConfig, SubAgentConfig, SubAgentLifecycleHooks, SubagentHooks,
26 TaskSupervisorConfig, TelegramConfig, TimeoutConfig, ToolDiscoveryConfig,
27 ToolDiscoveryStrategyConfig, ToolFilterConfig, ToolPolicy, TraceConfig, TrustConfig, TuiConfig,
28 VaultConfig, VectorBackend,
29};
30
31pub use zeph_config::{
32 AutoDreamConfig, CategoryConfig, ContextStrategy, DigestConfig, MagicDocsConfig,
33 MicrocompactConfig, PersonaConfig, TrajectoryConfig, TreeConfig,
34};
35pub use zeph_config::{DiagnosticSeverity, DiagnosticsConfig, HoverConfig, LspConfig};
36pub use zeph_config::{DurableBackend, DurableConfig, RetentionPolicy};
37pub use zeph_config::{QualityConfig, TriggerPolicy};
38pub use zeph_config::{TelemetryBackend, TelemetryConfig};
39
40pub use zeph_config::{
41 ContentIsolationConfig, CustomPiiPattern, ExfiltrationGuardConfig, MemoryWriteValidationConfig,
42 PiiFilterConfig, QuarantineConfig,
43};
44pub use zeph_config::{GuardrailAction, GuardrailConfig, GuardrailFailStrategy};
45
46pub use zeph_config::A2aClientConfig;
47pub use zeph_config::A2aServerConfig;
48pub use zeph_config::ChannelSkillsConfig;
49pub use zeph_config::{CardTrustPolicy, TrustedAgentKey};
50pub use zeph_config::{FileChangedConfig, HooksConfig};
51
52pub use zeph_config::{
53 DEFAULT_DEBUG_DIR, DEFAULT_LOG_FILE, DEFAULT_SKILLS_DIR, DEFAULT_SQLITE_PATH,
54 default_debug_dir, default_log_file_path, default_skills_dir, default_sqlite_path,
55 is_legacy_default_debug_dir, is_legacy_default_log_file, is_legacy_default_skills_path,
56 is_legacy_default_sqlite_path,
57};
58
59pub use zeph_config::providers::{default_stt_language, validate_pool};
60
61pub mod migrate {
62 pub use zeph_config::migrate::*;
63}
64
65use crate::vault::{Secret, VaultProvider};
66
67pub trait SecretResolver {
72 fn resolve_secrets(
78 &mut self,
79 vault: &dyn VaultProvider,
80 ) -> impl std::future::Future<Output = Result<(), ConfigError>> + Send;
81
82 fn resolve_secrets_masked(
92 &mut self,
93 vault: &dyn VaultProvider,
94 registry: Option<&std::sync::Arc<zeph_sanitizer::secret_mask::SecretMaskRegistry>>,
95 ) -> impl std::future::Future<Output = Result<(), ConfigError>> + Send;
96}
97
98fn register_masked_secret(
102 registry: Option<&std::sync::Arc<zeph_sanitizer::secret_mask::SecretMaskRegistry>>,
103 key_name: &str,
104 value: &str,
105) {
106 if let Some(registry) = registry {
107 registry.register(
108 key_name,
109 value,
110 zeph_sanitizer::secret_mask::SecretCategory::from_key_name(key_name),
111 );
112 }
113}
114
115fn log_gonka_credential_status(has_key: bool, has_address: bool) {
116 match (has_key, has_address) {
117 (true, true) => tracing::info!("gonka wallet credentials resolved from vault"),
118 (true, false) => tracing::warn!(
119 "ZEPH_GONKA_PRIVATE_KEY is set but ZEPH_GONKA_ADDRESS is missing from vault"
120 ),
121 (false, true) => tracing::warn!(
122 "ZEPH_GONKA_ADDRESS is set but ZEPH_GONKA_PRIVATE_KEY is missing from vault"
123 ),
124 (false, false) => {}
125 }
126}
127
128impl SecretResolver for Config {
132 async fn resolve_secrets(&mut self, vault: &dyn VaultProvider) -> Result<(), ConfigError> {
133 self.resolve_secrets_masked(vault, None).await
134 }
135
136 #[allow(clippy::too_many_lines)] async fn resolve_secrets_masked(
138 &mut self,
139 vault: &dyn VaultProvider,
140 registry: Option<&std::sync::Arc<zeph_sanitizer::secret_mask::SecretMaskRegistry>>,
141 ) -> Result<(), ConfigError> {
142 if let Some(val) = vault.get_secret("ZEPH_CLAUDE_API_KEY").await? {
143 register_masked_secret(registry, "ZEPH_CLAUDE_API_KEY", &val);
144 self.secrets.claude_api_key = Some(Secret::new(val));
145 }
146 if let Some(val) = vault.get_secret("ZEPH_OPENAI_API_KEY").await? {
147 register_masked_secret(registry, "ZEPH_OPENAI_API_KEY", &val);
148 self.secrets.openai_api_key = Some(Secret::new(val));
149 }
150 if let Some(val) = vault.get_secret("ZEPH_GEMINI_API_KEY").await? {
151 register_masked_secret(registry, "ZEPH_GEMINI_API_KEY", &val);
152 self.secrets.gemini_api_key = Some(Secret::new(val));
153 }
154 if let Some(val) = vault.get_secret("ZEPH_GONKA_PRIVATE_KEY").await? {
155 register_masked_secret(registry, "ZEPH_GONKA_PRIVATE_KEY", &val);
156 self.secrets.gonka_private_key = Some(Secret::new(val));
157 }
158 if let Some(val) = vault.get_secret("ZEPH_GONKA_ADDRESS").await? {
159 self.secrets.gonka_address = Some(Secret::new(val));
163 }
164 if let Some(val) = vault.get_secret("ZEPH_COCOON_ACCESS_HASH").await? {
165 register_masked_secret(registry, "ZEPH_COCOON_ACCESS_HASH", &val);
166 self.secrets.cocoon_access_hash = Some(Secret::new(val));
167 }
168 if self.skills.registry.enabled
171 && let Some(key_name) = self.skills.registry.auth_vault_key.clone()
172 && let Some(val) = vault.get_secret(&key_name).await?
173 {
174 register_masked_secret(registry, &key_name, &val);
175 self.secrets.skill_registry_token = Some(Secret::new(val));
176 }
177 if self.tools.search.enabled
180 && let Some(val) = vault
181 .get_secret(&self.tools.search.api_key_vault_key)
182 .await?
183 {
184 register_masked_secret(registry, &self.tools.search.api_key_vault_key, &val);
185 self.secrets.web_search_api_key = Some(Secret::new(val));
186 }
187 log_gonka_credential_status(
188 self.secrets.gonka_private_key.is_some(),
189 self.secrets.gonka_address.is_some(),
190 );
191 if let Some(val) = vault.get_secret("ZEPH_TELEGRAM_TOKEN").await?
192 && let Some(tg) = self.telegram.as_mut()
193 {
194 register_masked_secret(registry, "ZEPH_TELEGRAM_TOKEN", &val);
195 tg.token = Some(val);
196 }
197 if let Some(val) = vault.get_secret("ZEPH_A2A_AUTH_TOKEN").await? {
198 if val.is_empty() {
202 tracing::warn!(
203 "ZEPH_A2A_AUTH_TOKEN resolved to an empty string; treating as not configured"
204 );
205 } else {
206 register_masked_secret(registry, "ZEPH_A2A_AUTH_TOKEN", &val);
207 self.a2a.auth_token = Some(val);
208 }
209 }
210 for entry in &self.llm.providers {
211 if entry.provider_type == crate::config::ProviderKind::Compatible
212 && let Some(ref name) = entry.name
213 {
214 let normalized: String = name
215 .to_uppercase()
216 .chars()
217 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
218 .collect();
219 let env_key = format!("ZEPH_COMPATIBLE_{normalized}_API_KEY");
220 if let Some(val) = vault.get_secret(&env_key).await? {
221 register_masked_secret(registry, &env_key, &val);
222 self.secrets
223 .compatible_api_keys
224 .insert(name.clone(), Secret::new(val));
225 }
226 }
227 }
228 if let Some(val) = vault.get_secret("ZEPH_HF_TOKEN").await? {
229 register_masked_secret(registry, "ZEPH_HF_TOKEN", &val);
230 self.classifiers.hf_token = Some(val.clone());
231 if let Some(candle) = self.llm.candle.as_mut() {
232 candle.hf_token = Some(val);
233 }
234 }
235 if let Some(val) = vault.get_secret("ZEPH_GATEWAY_TOKEN").await? {
236 if val.is_empty() {
239 tracing::warn!(
240 "ZEPH_GATEWAY_TOKEN resolved to an empty string; treating as not configured"
241 );
242 } else {
243 register_masked_secret(registry, "ZEPH_GATEWAY_TOKEN", &val);
244 self.gateway.auth_token = Some(val);
245 }
246 }
247 if let Some(val) = vault.get_secret("ZEPH_DATABASE_URL").await? {
248 register_masked_secret(registry, "ZEPH_DATABASE_URL", &val);
249 self.memory.database_url = Some(Secret::new(val));
250 }
251 if let Some(val) = vault.get_secret("ZEPH_QDRANT_API_KEY").await? {
252 register_masked_secret(registry, "ZEPH_QDRANT_API_KEY", &val);
253 self.memory.qdrant_api_key = Some(Secret::new(val));
254 }
255 if let Some(val) = vault.get_secret("ZEPH_DISCORD_TOKEN").await?
256 && let Some(dc) = self.discord.as_mut()
257 {
258 register_masked_secret(registry, "ZEPH_DISCORD_TOKEN", &val);
259 dc.token = Some(val);
260 }
261 if let Some(val) = vault.get_secret("ZEPH_DISCORD_APP_ID").await?
262 && let Some(dc) = self.discord.as_mut()
263 {
264 dc.application_id = Some(val);
267 }
268 if let Some(val) = vault.get_secret("ZEPH_SLACK_BOT_TOKEN").await?
269 && let Some(sl) = self.slack.as_mut()
270 {
271 register_masked_secret(registry, "ZEPH_SLACK_BOT_TOKEN", &val);
272 sl.bot_token = Some(val);
273 }
274 if let Some(val) = vault.get_secret("ZEPH_SLACK_SIGNING_SECRET").await?
275 && let Some(sl) = self.slack.as_mut()
276 {
277 register_masked_secret(registry, "ZEPH_SLACK_SIGNING_SECRET", &val);
278 sl.signing_secret = Some(val);
279 }
280 for key in vault.list_keys() {
281 if let Some(custom_name) = key.strip_prefix("ZEPH_SECRET_")
282 && !custom_name.is_empty()
283 && let Some(val) = vault.get_secret(&key).await?
284 {
285 let normalized = custom_name.to_lowercase().replace('-', "_");
289 register_masked_secret(registry, &key, &val);
290 self.secrets.custom.insert(normalized, Secret::new(val));
291 }
292 }
293 Ok(())
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[tokio::test]
302 #[cfg(any(test, feature = "mock"))]
303 async fn resolve_secrets_with_mock_vault() {
304 use crate::vault::MockVaultProvider;
305
306 let vault = MockVaultProvider::new()
307 .with_secret("ZEPH_CLAUDE_API_KEY", "sk-test-123")
308 .with_secret("ZEPH_TELEGRAM_TOKEN", "tg-token-456");
309
310 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
311 config.resolve_secrets(&vault).await.unwrap();
312
313 assert_eq!(
314 config.secrets.claude_api_key.as_ref().unwrap().expose(),
315 "sk-test-123"
316 );
317 if let Some(tg) = config.telegram {
318 assert_eq!(tg.token.as_deref(), Some("tg-token-456"));
319 }
320 }
321
322 #[tokio::test]
323 #[cfg(any(test, feature = "mock"))]
324 async fn resolve_gonka_secrets_both_present() {
325 use crate::vault::MockVaultProvider;
326
327 let vault = MockVaultProvider::new()
328 .with_secret("ZEPH_GONKA_PRIVATE_KEY", "gonka-priv-key-abc")
329 .with_secret("ZEPH_GONKA_ADDRESS", "gonka1xyzaddress");
330
331 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
332 config.resolve_secrets(&vault).await.unwrap();
333
334 assert_eq!(
335 config.secrets.gonka_private_key.as_ref().unwrap().expose(),
336 "gonka-priv-key-abc"
337 );
338 assert_eq!(
339 config.secrets.gonka_address.as_ref().unwrap().expose(),
340 "gonka1xyzaddress"
341 );
342 }
343
344 #[tokio::test]
345 #[cfg(any(test, feature = "mock"))]
346 async fn resolve_gonka_partial_only_private_key() {
347 use crate::vault::MockVaultProvider;
348
349 let vault =
350 MockVaultProvider::new().with_secret("ZEPH_GONKA_PRIVATE_KEY", "gonka-priv-key-only");
351
352 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
353 config.resolve_secrets(&vault).await.unwrap();
354
355 assert!(config.secrets.gonka_private_key.is_some());
356 assert!(config.secrets.gonka_address.is_none());
357 }
358
359 #[tokio::test]
362 #[cfg(any(test, feature = "mock"))]
363 async fn resolve_secrets_masked_registers_values_with_registry() {
364 use crate::vault::MockVaultProvider;
365 use std::sync::Arc;
366 use zeph_sanitizer::secret_mask::SecretMaskRegistry;
367
368 let vault = MockVaultProvider::new()
369 .with_secret("ZEPH_CLAUDE_API_KEY", "sk-claude-secret-value-1234")
370 .with_secret("ZEPH_OPENAI_API_KEY", "sk-openai-secret-value-5678");
371
372 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
373 let registry = Arc::new(SecretMaskRegistry::new());
374 config
375 .resolve_secrets_masked(&vault, Some(®istry))
376 .await
377 .unwrap();
378
379 assert_eq!(
380 registry.len(),
381 2,
382 "both resolved secrets must be registered"
383 );
384 let masked = registry.mask("token: sk-claude-secret-value-1234");
385 assert!(!masked.contains("sk-claude-secret-value-1234"));
386 assert!(masked.contains("<SECRET:api_key:"));
387 }
388
389 #[tokio::test]
390 #[cfg(any(test, feature = "mock"))]
391 async fn resolve_secrets_masked_none_registry_behaves_like_resolve_secrets() {
392 use crate::vault::MockVaultProvider;
393
394 let vault = MockVaultProvider::new().with_secret("ZEPH_CLAUDE_API_KEY", "sk-test-123");
395 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
396 config.resolve_secrets_masked(&vault, None).await.unwrap();
397
398 assert_eq!(
399 config.secrets.claude_api_key.as_ref().unwrap().expose(),
400 "sk-test-123",
401 "None registry must not change resolution behavior"
402 );
403 }
404
405 #[tokio::test]
406 #[cfg(any(test, feature = "mock"))]
407 async fn resolve_secrets_plain_delegates_without_registering() {
408 use crate::vault::MockVaultProvider;
409
410 let vault = MockVaultProvider::new().with_secret("ZEPH_OPENAI_API_KEY", "sk-openai-abc");
413 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
414 config.resolve_secrets(&vault).await.unwrap();
415
416 assert_eq!(
417 config.secrets.openai_api_key.as_ref().unwrap().expose(),
418 "sk-openai-abc"
419 );
420 }
421
422 #[tokio::test]
423 #[cfg(any(test, feature = "mock"))]
424 async fn resolve_secrets_empty_gateway_token_treated_as_not_configured() {
425 use crate::vault::MockVaultProvider;
426
427 let vault = MockVaultProvider::new().with_secret("ZEPH_GATEWAY_TOKEN", "");
430 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
431 config.resolve_secrets(&vault).await.unwrap();
432
433 assert!(config.gateway.auth_token.is_none());
434 }
435
436 #[tokio::test]
437 #[cfg(any(test, feature = "mock"))]
438 async fn resolve_secrets_empty_a2a_token_treated_as_not_configured() {
439 use crate::vault::MockVaultProvider;
440
441 let vault = MockVaultProvider::new().with_secret("ZEPH_A2A_AUTH_TOKEN", "");
442 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
443 config.resolve_secrets(&vault).await.unwrap();
444
445 assert!(config.a2a.auth_token.is_none());
446 }
447
448 #[tokio::test]
452 #[cfg(any(test, feature = "mock"))]
453 async fn resolve_secrets_web_search_enabled_resolves_key() {
454 use crate::vault::MockVaultProvider;
455
456 let vault = MockVaultProvider::new().with_secret("ZEPH_WEB_SEARCH_API_KEY", "brave-key-1");
457 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
458 config.tools.search.enabled = true;
459 config.resolve_secrets(&vault).await.unwrap();
460
461 assert_eq!(
462 config.secrets.web_search_api_key.as_ref().unwrap().expose(),
463 "brave-key-1"
464 );
465 }
466
467 #[tokio::test]
468 #[cfg(any(test, feature = "mock"))]
469 async fn resolve_secrets_web_search_disabled_key_stays_unset() {
470 use crate::vault::MockVaultProvider;
471
472 let vault = MockVaultProvider::new().with_secret("ZEPH_WEB_SEARCH_API_KEY", "brave-key-2");
478 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
479 assert!(
480 !config.tools.search.enabled,
481 "search must be disabled by default"
482 );
483 config.resolve_secrets(&vault).await.unwrap();
484
485 assert!(config.secrets.web_search_api_key.is_none());
486 }
487}