Skip to main content

codetether_agent/secrets/
mod.rs

1//! Secrets management via HashiCorp Vault
2//!
3//! All API keys and secrets are loaded exclusively from HashiCorp Vault.
4//! Environment variables are NOT used for secrets.
5
6use anyhow::{Context, Result};
7use std::collections::HashMap;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10use vaultrs::client::{VaultClient, VaultClientSettingsBuilder};
11use vaultrs::kv2;
12
13/// Path in Vault where provider secrets are stored
14#[allow(dead_code)]
15const DEFAULT_SECRETS_PATH: &str = "secret/data/codetether/providers";
16
17/// Vault-based secrets manager
18#[derive(Clone)]
19pub struct SecretsManager {
20    client: Option<Arc<VaultClient>>,
21    /// Cache of loaded API keys (provider_id -> api_key)
22    pub cache: Arc<RwLock<HashMap<String, String>>>,
23    mount: String,
24    path: String,
25}
26
27impl Default for SecretsManager {
28    fn default() -> Self {
29        Self {
30            client: None,
31            cache: Arc::new(RwLock::new(HashMap::new())),
32            mount: "secret".to_string(),
33            path: "codetether/providers".to_string(),
34        }
35    }
36}
37
38impl SecretsManager {
39    /// Create a new secrets manager with Vault configuration
40    pub async fn new(config: &VaultConfig) -> Result<Self> {
41        let settings = VaultClientSettingsBuilder::default()
42            .address(&config.address)
43            .token(&config.token)
44            .build()
45            .context("Failed to build Vault client settings")?;
46
47        let client = VaultClient::new(settings).context("Failed to create Vault client")?;
48
49        Ok(Self {
50            client: Some(Arc::new(client)),
51            cache: Arc::new(RwLock::new(HashMap::new())),
52            mount: config.mount.clone().unwrap_or_else(|| "secret".to_string()),
53            path: config
54                .path
55                .clone()
56                .unwrap_or_else(|| "codetether/providers".to_string()),
57        })
58    }
59
60    /// Try to create from environment (for initial bootstrap only)
61    pub async fn from_env() -> Result<Self> {
62        let address = std::env::var("VAULT_ADDR").context("VAULT_ADDR not set")?;
63        let token = std::env::var("VAULT_TOKEN").context("VAULT_TOKEN not set")?;
64        let mount = std::env::var("VAULT_MOUNT").ok();
65        let path = std::env::var("VAULT_SECRETS_PATH").ok();
66
67        let config = VaultConfig {
68            address,
69            token,
70            mount,
71            path,
72        };
73
74        Self::new(&config).await
75    }
76
77    /// Check if Vault is configured and connected
78    pub fn is_connected(&self) -> bool {
79        self.client.is_some()
80    }
81
82    /// Get an API key for a provider from Vault
83    pub async fn get_api_key(&self, provider_id: &str) -> Result<Option<String>> {
84        // Check cache first
85        {
86            let cache = self.cache.read().await;
87            if let Some(key) = cache.get(provider_id) {
88                return Ok(Some(key.clone()));
89            }
90        }
91
92        // Fetch from Vault
93        let client = match &self.client {
94            Some(c) => c,
95            None => return Ok(None),
96        };
97
98        let secret_path = format!("{}/{}", self.path, provider_id);
99
100        match kv2::read::<ProviderSecrets>(client.as_ref(), &self.mount, &secret_path).await {
101            Ok(secret) => {
102                // Cache the result
103                if let Some(ref api_key) = secret.api_key {
104                    let mut cache = self.cache.write().await;
105                    cache.insert(provider_id.to_string(), api_key.clone());
106                }
107                Ok(secret.api_key)
108            }
109            Err(vaultrs::error::ClientError::APIError { code: 404, .. }) => Ok(None),
110            Err(e) => {
111                tracing::warn!("Failed to fetch secret for {}: {}", provider_id, e);
112                Ok(None)
113            }
114        }
115    }
116
117    /// Get all secrets for a provider
118    pub async fn get_provider_secrets(&self, provider_id: &str) -> Result<Option<ProviderSecrets>> {
119        let client = match &self.client {
120            Some(c) => c,
121            None => return Ok(None),
122        };
123
124        let secret_path = format!("{}/{}", self.path, provider_id);
125
126        match kv2::read::<ProviderSecrets>(client.as_ref(), &self.mount, &secret_path).await {
127            Ok(secret) => Ok(Some(secret)),
128            Err(vaultrs::error::ClientError::APIError { code: 404, .. }) => Ok(None),
129            Err(e) => {
130                tracing::warn!("Failed to fetch secrets for {}: {}", provider_id, e);
131                Ok(None)
132            }
133        }
134    }
135
136    /// Check if a provider has an API key in Vault
137    pub async fn has_api_key(&self, provider_id: &str) -> bool {
138        matches!(self.get_api_key(provider_id).await, Ok(Some(_)))
139    }
140
141    /// List all providers that have secrets configured
142    pub async fn list_configured_providers(&self) -> Result<Vec<String>> {
143        let client = match &self.client {
144            Some(c) => c,
145            None => return Ok(Vec::new()),
146        };
147
148        match kv2::list(client.as_ref(), &self.mount, &self.path).await {
149            Ok(keys) => Ok(keys),
150            Err(vaultrs::error::ClientError::APIError { code: 404, .. }) => Ok(Vec::new()),
151            Err(e) => {
152                tracing::warn!("Failed to list providers: {}", e);
153                Ok(Vec::new())
154            }
155        }
156    }
157
158    /// Clear the cache (useful when secrets are rotated)
159    pub async fn clear_cache(&self) {
160        let mut cache = self.cache.write().await;
161        cache.clear();
162    }
163}
164
165/// Vault configuration
166#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
167pub struct VaultConfig {
168    /// Vault server address (e.g., "https://vault.example.com:8200")
169    pub address: String,
170
171    /// Vault token for authentication
172    pub token: String,
173
174    /// KV secrets engine mount path (default: "secret")
175    #[serde(default)]
176    pub mount: Option<String>,
177
178    /// Path prefix for provider secrets (default: "codetether/providers")
179    #[serde(default)]
180    pub path: Option<String>,
181}
182
183impl Default for VaultConfig {
184    fn default() -> Self {
185        Self {
186            address: String::new(),
187            token: String::new(),
188            mount: Some("secret".to_string()),
189            path: Some("codetether/providers".to_string()),
190        }
191    }
192}
193
194/// Provider secrets stored in Vault
195#[derive(Clone, serde::Serialize, serde::Deserialize)]
196pub struct ProviderSecrets {
197    /// API key for the provider
198    #[serde(default)]
199    pub api_key: Option<String>,
200
201    /// Base URL override
202    #[serde(default)]
203    pub base_url: Option<String>,
204
205    /// Organization ID (for OpenAI)
206    #[serde(default)]
207    pub organization: Option<String>,
208
209    /// Additional headers as JSON
210    #[serde(default)]
211    pub headers: Option<HashMap<String, String>>,
212
213    /// Any provider-specific extra fields
214    #[serde(flatten)]
215    pub extra: HashMap<String, serde_json::Value>,
216}
217
218impl std::fmt::Debug for ProviderSecrets {
219    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
220        f.debug_struct("ProviderSecrets")
221            .field("api_key", &self.api_key.as_ref().map(|_| "<REDACTED>"))
222            .field("api_key_len", &self.api_key.as_ref().map(|k| k.len()))
223            .field("base_url", &self.base_url)
224            .field("organization", &self.organization)
225            .field("headers_present", &self.headers.is_some())
226            .field("extra_fields", &self.extra.len())
227            .finish()
228    }
229}
230
231impl ProviderSecrets {
232    /// Check if API key is present and valid (non-empty)
233    pub fn has_valid_api_key(&self) -> bool {
234        self.api_key.as_ref().map(|k| !k.is_empty()).unwrap_or(false)
235    }
236    
237    /// Get API key length without exposing the key
238    pub fn api_key_len(&self) -> Option<usize> {
239        self.api_key.as_ref().map(|k| k.len())
240    }
241}
242
243/// Global secrets manager instance
244static SECRETS_MANAGER: tokio::sync::OnceCell<SecretsManager> = tokio::sync::OnceCell::const_new();
245
246/// Initialize the global secrets manager
247pub async fn init_secrets_manager(config: &VaultConfig) -> Result<()> {
248    let manager = SecretsManager::new(config).await?;
249    SECRETS_MANAGER
250        .set(manager)
251        .map_err(|_| anyhow::anyhow!("Secrets manager already initialized"))?;
252    Ok(())
253}
254
255/// Initialize the global secrets manager from an existing manager instance
256pub fn init_from_manager(manager: SecretsManager) -> Result<()> {
257    SECRETS_MANAGER
258        .set(manager)
259        .map_err(|_| anyhow::anyhow!("Secrets manager already initialized"))?;
260    Ok(())
261}
262
263/// Get the global secrets manager
264pub fn secrets_manager() -> Option<&'static SecretsManager> {
265    SECRETS_MANAGER.get()
266}
267
268/// Get API key for a provider (convenience function)
269pub async fn get_api_key(provider_id: &str) -> Option<String> {
270    match SECRETS_MANAGER.get() {
271        Some(manager) => manager.get_api_key(provider_id).await.ok().flatten(),
272        None => None,
273    }
274}
275
276/// Check if a provider has an API key (convenience function)
277pub async fn has_api_key(provider_id: &str) -> bool {
278    match SECRETS_MANAGER.get() {
279        Some(manager) => manager.has_api_key(provider_id).await,
280        None => false,
281    }
282}
283
284/// Get full provider secrets (convenience function)
285pub async fn get_provider_secrets(provider_id: &str) -> Option<ProviderSecrets> {
286    match SECRETS_MANAGER.get() {
287        Some(manager) => manager.get_provider_secrets(provider_id).await.ok().flatten(),
288        None => None,
289    }
290}