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