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