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 log_gonka_credential_status(
178 self.secrets.gonka_private_key.is_some(),
179 self.secrets.gonka_address.is_some(),
180 );
181 if let Some(val) = vault.get_secret("ZEPH_TELEGRAM_TOKEN").await?
182 && let Some(tg) = self.telegram.as_mut()
183 {
184 register_masked_secret(registry, "ZEPH_TELEGRAM_TOKEN", &val);
185 tg.token = Some(val);
186 }
187 if let Some(val) = vault.get_secret("ZEPH_A2A_AUTH_TOKEN").await? {
188 if val.is_empty() {
192 tracing::warn!(
193 "ZEPH_A2A_AUTH_TOKEN resolved to an empty string; treating as not configured"
194 );
195 } else {
196 register_masked_secret(registry, "ZEPH_A2A_AUTH_TOKEN", &val);
197 self.a2a.auth_token = Some(val);
198 }
199 }
200 for entry in &self.llm.providers {
201 if entry.provider_type == crate::config::ProviderKind::Compatible
202 && let Some(ref name) = entry.name
203 {
204 let normalized: String = name
205 .to_uppercase()
206 .chars()
207 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
208 .collect();
209 let env_key = format!("ZEPH_COMPATIBLE_{normalized}_API_KEY");
210 if let Some(val) = vault.get_secret(&env_key).await? {
211 register_masked_secret(registry, &env_key, &val);
212 self.secrets
213 .compatible_api_keys
214 .insert(name.clone(), Secret::new(val));
215 }
216 }
217 }
218 if let Some(val) = vault.get_secret("ZEPH_HF_TOKEN").await? {
219 register_masked_secret(registry, "ZEPH_HF_TOKEN", &val);
220 self.classifiers.hf_token = Some(val.clone());
221 if let Some(candle) = self.llm.candle.as_mut() {
222 candle.hf_token = Some(val);
223 }
224 }
225 if let Some(val) = vault.get_secret("ZEPH_GATEWAY_TOKEN").await? {
226 if val.is_empty() {
229 tracing::warn!(
230 "ZEPH_GATEWAY_TOKEN resolved to an empty string; treating as not configured"
231 );
232 } else {
233 register_masked_secret(registry, "ZEPH_GATEWAY_TOKEN", &val);
234 self.gateway.auth_token = Some(val);
235 }
236 }
237 if let Some(val) = vault.get_secret("ZEPH_DATABASE_URL").await? {
238 register_masked_secret(registry, "ZEPH_DATABASE_URL", &val);
239 self.memory.database_url = Some(Secret::new(val));
240 }
241 if let Some(val) = vault.get_secret("ZEPH_QDRANT_API_KEY").await? {
242 register_masked_secret(registry, "ZEPH_QDRANT_API_KEY", &val);
243 self.memory.qdrant_api_key = Some(Secret::new(val));
244 }
245 if let Some(val) = vault.get_secret("ZEPH_DISCORD_TOKEN").await?
246 && let Some(dc) = self.discord.as_mut()
247 {
248 register_masked_secret(registry, "ZEPH_DISCORD_TOKEN", &val);
249 dc.token = Some(val);
250 }
251 if let Some(val) = vault.get_secret("ZEPH_DISCORD_APP_ID").await?
252 && let Some(dc) = self.discord.as_mut()
253 {
254 dc.application_id = Some(val);
257 }
258 if let Some(val) = vault.get_secret("ZEPH_SLACK_BOT_TOKEN").await?
259 && let Some(sl) = self.slack.as_mut()
260 {
261 register_masked_secret(registry, "ZEPH_SLACK_BOT_TOKEN", &val);
262 sl.bot_token = Some(val);
263 }
264 if let Some(val) = vault.get_secret("ZEPH_SLACK_SIGNING_SECRET").await?
265 && let Some(sl) = self.slack.as_mut()
266 {
267 register_masked_secret(registry, "ZEPH_SLACK_SIGNING_SECRET", &val);
268 sl.signing_secret = Some(val);
269 }
270 for key in vault.list_keys() {
271 if let Some(custom_name) = key.strip_prefix("ZEPH_SECRET_")
272 && !custom_name.is_empty()
273 && let Some(val) = vault.get_secret(&key).await?
274 {
275 let normalized = custom_name.to_lowercase().replace('-', "_");
279 register_masked_secret(registry, &key, &val);
280 self.secrets.custom.insert(normalized, Secret::new(val));
281 }
282 }
283 Ok(())
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[tokio::test]
292 #[cfg(any(test, feature = "mock"))]
293 async fn resolve_secrets_with_mock_vault() {
294 use crate::vault::MockVaultProvider;
295
296 let vault = MockVaultProvider::new()
297 .with_secret("ZEPH_CLAUDE_API_KEY", "sk-test-123")
298 .with_secret("ZEPH_TELEGRAM_TOKEN", "tg-token-456");
299
300 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
301 config.resolve_secrets(&vault).await.unwrap();
302
303 assert_eq!(
304 config.secrets.claude_api_key.as_ref().unwrap().expose(),
305 "sk-test-123"
306 );
307 if let Some(tg) = config.telegram {
308 assert_eq!(tg.token.as_deref(), Some("tg-token-456"));
309 }
310 }
311
312 #[tokio::test]
313 #[cfg(any(test, feature = "mock"))]
314 async fn resolve_gonka_secrets_both_present() {
315 use crate::vault::MockVaultProvider;
316
317 let vault = MockVaultProvider::new()
318 .with_secret("ZEPH_GONKA_PRIVATE_KEY", "gonka-priv-key-abc")
319 .with_secret("ZEPH_GONKA_ADDRESS", "gonka1xyzaddress");
320
321 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
322 config.resolve_secrets(&vault).await.unwrap();
323
324 assert_eq!(
325 config.secrets.gonka_private_key.as_ref().unwrap().expose(),
326 "gonka-priv-key-abc"
327 );
328 assert_eq!(
329 config.secrets.gonka_address.as_ref().unwrap().expose(),
330 "gonka1xyzaddress"
331 );
332 }
333
334 #[tokio::test]
335 #[cfg(any(test, feature = "mock"))]
336 async fn resolve_gonka_partial_only_private_key() {
337 use crate::vault::MockVaultProvider;
338
339 let vault =
340 MockVaultProvider::new().with_secret("ZEPH_GONKA_PRIVATE_KEY", "gonka-priv-key-only");
341
342 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
343 config.resolve_secrets(&vault).await.unwrap();
344
345 assert!(config.secrets.gonka_private_key.is_some());
346 assert!(config.secrets.gonka_address.is_none());
347 }
348
349 #[tokio::test]
352 #[cfg(any(test, feature = "mock"))]
353 async fn resolve_secrets_masked_registers_values_with_registry() {
354 use crate::vault::MockVaultProvider;
355 use std::sync::Arc;
356 use zeph_sanitizer::secret_mask::SecretMaskRegistry;
357
358 let vault = MockVaultProvider::new()
359 .with_secret("ZEPH_CLAUDE_API_KEY", "sk-claude-secret-value-1234")
360 .with_secret("ZEPH_OPENAI_API_KEY", "sk-openai-secret-value-5678");
361
362 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
363 let registry = Arc::new(SecretMaskRegistry::new());
364 config
365 .resolve_secrets_masked(&vault, Some(®istry))
366 .await
367 .unwrap();
368
369 assert_eq!(
370 registry.len(),
371 2,
372 "both resolved secrets must be registered"
373 );
374 let masked = registry.mask("token: sk-claude-secret-value-1234");
375 assert!(!masked.contains("sk-claude-secret-value-1234"));
376 assert!(masked.contains("<SECRET:api_key:"));
377 }
378
379 #[tokio::test]
380 #[cfg(any(test, feature = "mock"))]
381 async fn resolve_secrets_masked_none_registry_behaves_like_resolve_secrets() {
382 use crate::vault::MockVaultProvider;
383
384 let vault = MockVaultProvider::new().with_secret("ZEPH_CLAUDE_API_KEY", "sk-test-123");
385 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
386 config.resolve_secrets_masked(&vault, None).await.unwrap();
387
388 assert_eq!(
389 config.secrets.claude_api_key.as_ref().unwrap().expose(),
390 "sk-test-123",
391 "None registry must not change resolution behavior"
392 );
393 }
394
395 #[tokio::test]
396 #[cfg(any(test, feature = "mock"))]
397 async fn resolve_secrets_plain_delegates_without_registering() {
398 use crate::vault::MockVaultProvider;
399
400 let vault = MockVaultProvider::new().with_secret("ZEPH_OPENAI_API_KEY", "sk-openai-abc");
403 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
404 config.resolve_secrets(&vault).await.unwrap();
405
406 assert_eq!(
407 config.secrets.openai_api_key.as_ref().unwrap().expose(),
408 "sk-openai-abc"
409 );
410 }
411
412 #[tokio::test]
413 #[cfg(any(test, feature = "mock"))]
414 async fn resolve_secrets_empty_gateway_token_treated_as_not_configured() {
415 use crate::vault::MockVaultProvider;
416
417 let vault = MockVaultProvider::new().with_secret("ZEPH_GATEWAY_TOKEN", "");
420 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
421 config.resolve_secrets(&vault).await.unwrap();
422
423 assert!(config.gateway.auth_token.is_none());
424 }
425
426 #[tokio::test]
427 #[cfg(any(test, feature = "mock"))]
428 async fn resolve_secrets_empty_a2a_token_treated_as_not_configured() {
429 use crate::vault::MockVaultProvider;
430
431 let vault = MockVaultProvider::new().with_secret("ZEPH_A2A_AUTH_TOKEN", "");
432 let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
433 config.resolve_secrets(&vault).await.unwrap();
434
435 assert!(config.a2a.auth_token.is_none());
436 }
437}