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::{
29    AutoDreamConfig, CategoryConfig, ContextStrategy, DigestConfig, MagicDocsConfig,
30    MicrocompactConfig, PersonaConfig, TrajectoryConfig, TreeConfig,
31};
32pub use zeph_config::{DiagnosticSeverity, DiagnosticsConfig, HoverConfig, LspConfig};
33
34pub use zeph_config::{
35    ContentIsolationConfig, CustomPiiPattern, ExfiltrationGuardConfig, MemoryWriteValidationConfig,
36    PiiFilterConfig, QuarantineConfig,
37};
38pub use zeph_config::{GuardrailAction, GuardrailConfig, GuardrailFailStrategy};
39
40pub use zeph_config::A2aServerConfig;
41pub use zeph_config::ChannelSkillsConfig;
42pub use zeph_config::{FileChangedConfig, HooksConfig};
43
44pub use zeph_config::{
45    DEFAULT_DEBUG_DIR, DEFAULT_LOG_FILE, DEFAULT_SKILLS_DIR, DEFAULT_SQLITE_PATH,
46    default_debug_dir, default_log_file_path, default_skills_dir, default_sqlite_path,
47    is_legacy_default_debug_dir, is_legacy_default_log_file, is_legacy_default_skills_path,
48    is_legacy_default_sqlite_path,
49};
50
51pub use zeph_config::providers::{default_stt_language, default_stt_provider, validate_pool};
52
53pub mod migrate {
54    pub use zeph_config::migrate::*;
55}
56
57use crate::vault::{Secret, VaultProvider};
58
59/// Extension trait for resolving vault secrets into a [`Config`].
60///
61/// Implemented for [`Config`] in `zeph-core` because `VaultProvider` lives here.
62/// Call with `use zeph_core::config::SecretResolver` in scope.
63pub trait SecretResolver {
64    /// Populate `secrets` fields from the vault.
65    ///
66    /// # Errors
67    ///
68    /// Returns an error if the vault backend fails.
69    fn resolve_secrets(
70        &mut self,
71        vault: &dyn VaultProvider,
72    ) -> impl std::future::Future<Output = Result<(), ConfigError>> + Send;
73}
74
75impl SecretResolver for Config {
76    async fn resolve_secrets(&mut self, vault: &dyn VaultProvider) -> Result<(), ConfigError> {
77        if let Some(val) = vault.get_secret("ZEPH_CLAUDE_API_KEY").await? {
78            self.secrets.claude_api_key = Some(Secret::new(val));
79        }
80        if let Some(val) = vault.get_secret("ZEPH_OPENAI_API_KEY").await? {
81            self.secrets.openai_api_key = Some(Secret::new(val));
82        }
83        if let Some(val) = vault.get_secret("ZEPH_GEMINI_API_KEY").await? {
84            self.secrets.gemini_api_key = Some(Secret::new(val));
85        }
86        if let Some(val) = vault.get_secret("ZEPH_TELEGRAM_TOKEN").await?
87            && let Some(tg) = self.telegram.as_mut()
88        {
89            tg.token = Some(val);
90        }
91        if let Some(val) = vault.get_secret("ZEPH_A2A_AUTH_TOKEN").await? {
92            self.a2a.auth_token = Some(val);
93        }
94        for entry in &self.llm.providers {
95            if entry.provider_type == crate::config::ProviderKind::Compatible
96                && let Some(ref name) = entry.name
97            {
98                let env_key = format!("ZEPH_COMPATIBLE_{}_API_KEY", name.to_uppercase());
99                if let Some(val) = vault.get_secret(&env_key).await? {
100                    self.secrets
101                        .compatible_api_keys
102                        .insert(name.clone(), Secret::new(val));
103                }
104            }
105        }
106        if let Some(val) = vault.get_secret("ZEPH_HF_TOKEN").await? {
107            self.classifiers.hf_token = Some(val.clone());
108            if let Some(candle) = self.llm.candle.as_mut() {
109                candle.hf_token = Some(val);
110            }
111        }
112        if let Some(val) = vault.get_secret("ZEPH_GATEWAY_TOKEN").await? {
113            self.gateway.auth_token = Some(val);
114        }
115        if let Some(val) = vault.get_secret("ZEPH_DATABASE_URL").await? {
116            self.memory.database_url = Some(val);
117        }
118        if let Some(val) = vault.get_secret("ZEPH_DISCORD_TOKEN").await?
119            && let Some(dc) = self.discord.as_mut()
120        {
121            dc.token = Some(val);
122        }
123        if let Some(val) = vault.get_secret("ZEPH_DISCORD_APP_ID").await?
124            && let Some(dc) = self.discord.as_mut()
125        {
126            dc.application_id = Some(val);
127        }
128        if let Some(val) = vault.get_secret("ZEPH_SLACK_BOT_TOKEN").await?
129            && let Some(sl) = self.slack.as_mut()
130        {
131            sl.bot_token = Some(val);
132        }
133        if let Some(val) = vault.get_secret("ZEPH_SLACK_SIGNING_SECRET").await?
134            && let Some(sl) = self.slack.as_mut()
135        {
136            sl.signing_secret = Some(val);
137        }
138        for key in vault.list_keys() {
139            if let Some(custom_name) = key.strip_prefix("ZEPH_SECRET_")
140                && !custom_name.is_empty()
141                && let Some(val) = vault.get_secret(&key).await?
142            {
143                // Canonical form uses underscores. Both `_` and `-` in vault key names
144                // are normalized to `_` so that ZEPH_SECRET_MY-KEY and ZEPH_SECRET_MY_KEY
145                // both map to "my_key", matching SKILL.md requires-secrets parsing.
146                let normalized = custom_name.to_lowercase().replace('-', "_");
147                self.secrets.custom.insert(normalized, Secret::new(val));
148            }
149        }
150        Ok(())
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[tokio::test]
159    #[cfg(any(test, feature = "mock"))]
160    async fn resolve_secrets_with_mock_vault() {
161        use crate::vault::MockVaultProvider;
162
163        let vault = MockVaultProvider::new()
164            .with_secret("ZEPH_CLAUDE_API_KEY", "sk-test-123")
165            .with_secret("ZEPH_TELEGRAM_TOKEN", "tg-token-456");
166
167        let mut config = Config::load(std::path::Path::new("/nonexistent/config.toml")).unwrap();
168        config.resolve_secrets(&vault).await.unwrap();
169
170        assert_eq!(
171            config.secrets.claude_api_key.as_ref().unwrap().expose(),
172            "sk-test-123"
173        );
174        if let Some(tg) = config.telegram {
175            assert_eq!(tg.token.as_deref(), Some("tg-token-456"));
176        }
177    }
178}