Skip to main content

zeph_core/
config.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Extension trait for resolving vault secrets into a Config.
5//!
6//! This trait is defined in zeph-core (not in zeph-config) due to Rust's orphan rule:
7//! implementing a foreign trait on a foreign type requires the trait to be defined locally.
8
9// Re-export Config types from zeph-config for internal use.
10pub use zeph_config::{
11    AcpConfig, AcpLspConfig, AcpTransport, AgentConfig, CandleConfig, CandleInlineConfig,
12    CascadeClassifierMode, CascadeConfig, ClassifiersConfig, CompressionConfig,
13    CompressionStrategy, Config, ConfigError, CostConfig, DaemonConfig, DebugConfig, DetectorMode,
14    DiscordConfig, DocumentConfig, DumpFormat, ExperimentConfig, ExperimentSchedule, FocusConfig,
15    GatewayConfig, GenerationParams, GraphConfig, HookDef, HookMatcher, HookType, IndexConfig,
16    LearningConfig, LlmConfig, LlmRoutingStrategy, LogRotation, LoggingConfig, MAX_TOKENS_CAP,
17    McpConfig, McpOAuthConfig, McpServerConfig, McpTrustLevel, MemoryConfig, MemoryScope,
18    NoteLinkingConfig, OAuthTokenStorage, ObservabilityConfig, OrchestrationConfig, PermissionMode,
19    ProviderEntry, ProviderKind, ProviderName, PruningStrategy, RateLimitConfig, ResolvedSecrets,
20    RouterConfig, RouterStrategyConfig, ScheduledTaskConfig, ScheduledTaskKind, SchedulerConfig,
21    SecurityConfig, SemanticConfig, SessionsConfig, SidequestConfig, SkillFilter, SkillPromptMode,
22    SkillsConfig, SlackConfig, StoreRoutingConfig, StoreRoutingStrategy, SttConfig, SubAgentConfig,
23    SubAgentLifecycleHooks, SubagentHooks, TelegramConfig, TimeoutConfig, ToolDiscoveryConfig,
24    ToolDiscoveryStrategyConfig, ToolFilterConfig, ToolPolicy, TraceConfig, TrustConfig, TuiConfig,
25    VaultConfig, VectorBackend,
26};
27
28pub use zeph_config::{ContextStrategy, DigestConfig, PersonaConfig};
29pub use zeph_config::{DiagnosticSeverity, DiagnosticsConfig, HoverConfig, LspConfig};
30
31pub use zeph_config::{
32    ContentIsolationConfig, CustomPiiPattern, ExfiltrationGuardConfig, MemoryWriteValidationConfig,
33    PiiFilterConfig, QuarantineConfig,
34};
35pub use zeph_config::{GuardrailAction, GuardrailConfig, GuardrailFailStrategy};
36
37pub use zeph_config::A2aServerConfig;
38pub use zeph_config::ChannelSkillsConfig;
39pub use zeph_config::{FileChangedConfig, HooksConfig};
40
41pub use zeph_config::{
42    DEFAULT_DEBUG_DIR, DEFAULT_LOG_FILE, DEFAULT_SKILLS_DIR, DEFAULT_SQLITE_PATH,
43    default_debug_dir, default_log_file_path, default_skills_dir, default_sqlite_path,
44    is_legacy_default_debug_dir, is_legacy_default_log_file, is_legacy_default_skills_path,
45    is_legacy_default_sqlite_path,
46};
47
48pub use zeph_config::providers::{default_stt_language, default_stt_provider, validate_pool};
49
50pub mod migrate {
51    pub use zeph_config::migrate::*;
52}
53
54use crate::vault::{Secret, VaultProvider};
55
56/// Extension trait for resolving vault secrets into a [`Config`].
57///
58/// Implemented for [`Config`] in `zeph-core` because `VaultProvider` lives here.
59/// Call with `use zeph_core::config::SecretResolver` in scope.
60pub trait SecretResolver {
61    /// Populate `secrets` fields from the vault.
62    ///
63    /// # Errors
64    ///
65    /// Returns an error if the vault backend fails.
66    fn resolve_secrets(
67        &mut self,
68        vault: &dyn VaultProvider,
69    ) -> impl std::future::Future<Output = Result<(), ConfigError>> + Send;
70}
71
72impl SecretResolver for Config {
73    async fn resolve_secrets(&mut self, vault: &dyn VaultProvider) -> Result<(), ConfigError> {
74        if let Some(val) = vault.get_secret("ZEPH_CLAUDE_API_KEY").await? {
75            self.secrets.claude_api_key = Some(Secret::new(val));
76        }
77        if let Some(val) = vault.get_secret("ZEPH_OPENAI_API_KEY").await? {
78            self.secrets.openai_api_key = Some(Secret::new(val));
79        }
80        if let Some(val) = vault.get_secret("ZEPH_GEMINI_API_KEY").await? {
81            self.secrets.gemini_api_key = Some(Secret::new(val));
82        }
83        if let Some(val) = vault.get_secret("ZEPH_TELEGRAM_TOKEN").await?
84            && let Some(tg) = self.telegram.as_mut()
85        {
86            tg.token = Some(val);
87        }
88        if let Some(val) = vault.get_secret("ZEPH_A2A_AUTH_TOKEN").await? {
89            self.a2a.auth_token = Some(val);
90        }
91        for entry in &self.llm.providers {
92            if entry.provider_type == crate::config::ProviderKind::Compatible
93                && let Some(ref name) = entry.name
94            {
95                let env_key = format!("ZEPH_COMPATIBLE_{}_API_KEY", name.to_uppercase());
96                if let Some(val) = vault.get_secret(&env_key).await? {
97                    self.secrets
98                        .compatible_api_keys
99                        .insert(name.clone(), Secret::new(val));
100                }
101            }
102        }
103        if let Some(val) = vault.get_secret("ZEPH_HF_TOKEN").await? {
104            self.classifiers.hf_token = Some(val.clone());
105            if let Some(candle) = self.llm.candle.as_mut() {
106                candle.hf_token = Some(val);
107            }
108        }
109        if let Some(val) = vault.get_secret("ZEPH_GATEWAY_TOKEN").await? {
110            self.gateway.auth_token = Some(val);
111        }
112        if let Some(val) = vault.get_secret("ZEPH_DATABASE_URL").await? {
113            self.memory.database_url = Some(val);
114        }
115        if let Some(val) = vault.get_secret("ZEPH_DISCORD_TOKEN").await?
116            && let Some(dc) = self.discord.as_mut()
117        {
118            dc.token = Some(val);
119        }
120        if let Some(val) = vault.get_secret("ZEPH_DISCORD_APP_ID").await?
121            && let Some(dc) = self.discord.as_mut()
122        {
123            dc.application_id = Some(val);
124        }
125        if let Some(val) = vault.get_secret("ZEPH_SLACK_BOT_TOKEN").await?
126            && let Some(sl) = self.slack.as_mut()
127        {
128            sl.bot_token = Some(val);
129        }
130        if let Some(val) = vault.get_secret("ZEPH_SLACK_SIGNING_SECRET").await?
131            && let Some(sl) = self.slack.as_mut()
132        {
133            sl.signing_secret = Some(val);
134        }
135        for key in vault.list_keys() {
136            if let Some(custom_name) = key.strip_prefix("ZEPH_SECRET_")
137                && !custom_name.is_empty()
138                && let Some(val) = vault.get_secret(&key).await?
139            {
140                // Canonical form uses underscores. Both `_` and `-` in vault key names
141                // are normalized to `_` so that ZEPH_SECRET_MY-KEY and ZEPH_SECRET_MY_KEY
142                // both map to "my_key", matching SKILL.md requires-secrets parsing.
143                let normalized = custom_name.to_lowercase().replace('-', "_");
144                self.secrets.custom.insert(normalized, Secret::new(val));
145            }
146        }
147        Ok(())
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[tokio::test]
156    #[cfg(any(test, feature = "mock"))]
157    async fn resolve_secrets_with_mock_vault() {
158        use crate::vault::MockVaultProvider;
159
160        let vault = MockVaultProvider::new()
161            .with_secret("ZEPH_CLAUDE_API_KEY", "sk-test-123")
162            .with_secret("ZEPH_TELEGRAM_TOKEN", "tg-token-456");
163
164        let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
165        config.resolve_secrets(&vault).await.unwrap();
166
167        assert_eq!(
168            config.secrets.claude_api_key.as_ref().unwrap().expose(),
169            "sk-test-123"
170        );
171        if let Some(tg) = config.telegram {
172            assert_eq!(tg.token.as_deref(), Some("tg-token-456"));
173        }
174    }
175}