codetether_agent/secrets/
mod.rs1use 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#[allow(dead_code)]
15const DEFAULT_SECRETS_PATH: &str = "secret/data/codetether/providers";
16
17#[derive(Clone)]
19pub struct SecretsManager {
20 client: Option<Arc<VaultClient>>,
21 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 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 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 pub fn is_connected(&self) -> bool {
79 self.client.is_some()
80 }
81
82 pub async fn get_api_key(&self, provider_id: &str) -> Result<Option<String>> {
84 {
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 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 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 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 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 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 pub async fn clear_cache(&self) {
160 let mut cache = self.cache.write().await;
161 cache.clear();
162 }
163}
164
165#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
167pub struct VaultConfig {
168 pub address: String,
170
171 pub token: String,
173
174 #[serde(default)]
176 pub mount: Option<String>,
177
178 #[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#[derive(Clone, serde::Serialize, serde::Deserialize)]
196pub struct ProviderSecrets {
197 #[serde(default)]
199 pub api_key: Option<String>,
200
201 #[serde(default)]
203 pub base_url: Option<String>,
204
205 #[serde(default)]
207 pub organization: Option<String>,
208
209 #[serde(default)]
211 pub headers: Option<HashMap<String, String>>,
212
213 #[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 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 pub fn api_key_len(&self) -> Option<usize> {
239 self.api_key.as_ref().map(|k| k.len())
240 }
241}
242
243static SECRETS_MANAGER: tokio::sync::OnceCell<SecretsManager> = tokio::sync::OnceCell::const_new();
245
246pub 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
255pub 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
263pub fn secrets_manager() -> Option<&'static SecretsManager> {
265 SECRETS_MANAGER.get()
266}
267
268pub 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
276pub 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
284pub 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}