skill_context/
secrets.rs

1//! Secrets configuration types.
2//!
3//! This module defines secret management configuration and provider types
4//! for execution contexts.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9/// Secrets configuration.
10#[derive(Debug, Clone, Default, Serialize, Deserialize)]
11#[serde(rename_all = "snake_case")]
12pub struct SecretsConfig {
13    /// Individual secret definitions.
14    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
15    pub secrets: HashMap<String, SecretDefinition>,
16
17    /// Secret provider configuration.
18    #[serde(default, skip_serializing_if = "Vec::is_empty")]
19    pub providers: Vec<SecretProviderConfig>,
20}
21
22impl SecretsConfig {
23    /// Create a new empty secrets configuration.
24    pub fn new() -> Self {
25        Self::default()
26    }
27
28    /// Add a secret definition.
29    pub fn with_secret(mut self, key: impl Into<String>, definition: SecretDefinition) -> Self {
30        self.secrets.insert(key.into(), definition);
31        self
32    }
33
34    /// Add a required secret with environment variable injection.
35    pub fn with_required_env_secret(
36        mut self,
37        key: impl Into<String>,
38        env_var: impl Into<String>,
39        description: impl Into<String>,
40    ) -> Self {
41        let key = key.into();
42        self.secrets.insert(
43            key.clone(),
44            SecretDefinition {
45                key: key.clone(),
46                description: Some(description.into()),
47                required: true,
48                provider: None,
49                env_var: Some(env_var.into()),
50                file_path: None,
51                file_mode: None,
52            },
53        );
54        self
55    }
56
57    /// Add a required secret that is written to a file.
58    pub fn with_required_file_secret(
59        mut self,
60        key: impl Into<String>,
61        file_path: impl Into<String>,
62        description: impl Into<String>,
63    ) -> Self {
64        let key = key.into();
65        self.secrets.insert(
66            key.clone(),
67            SecretDefinition {
68                key: key.clone(),
69                description: Some(description.into()),
70                required: true,
71                provider: None,
72                env_var: None,
73                file_path: Some(file_path.into()),
74                file_mode: Some("0600".to_string()),
75            },
76        );
77        self
78    }
79
80    /// Add a secret provider configuration.
81    pub fn with_provider(mut self, provider: SecretProviderConfig) -> Self {
82        self.providers.push(provider);
83        self
84    }
85
86    /// Get all secret keys.
87    pub fn keys(&self) -> Vec<&str> {
88        self.secrets.keys().map(|s| s.as_str()).collect()
89    }
90
91    /// Get all required secret keys.
92    pub fn required_keys(&self) -> Vec<&str> {
93        self.secrets
94            .iter()
95            .filter(|(_, def)| def.required)
96            .map(|(k, _)| k.as_str())
97            .collect()
98    }
99
100    /// Get all optional secret keys.
101    pub fn optional_keys(&self) -> Vec<&str> {
102        self.secrets
103            .iter()
104            .filter(|(_, def)| !def.required)
105            .map(|(k, _)| k.as_str())
106            .collect()
107    }
108
109    /// Get a secret definition by key.
110    pub fn get(&self, key: &str) -> Option<&SecretDefinition> {
111        self.secrets.get(key)
112    }
113
114    /// Check if a secret is defined.
115    pub fn contains(&self, key: &str) -> bool {
116        self.secrets.contains_key(key)
117    }
118
119    /// Get the number of secrets defined.
120    pub fn len(&self) -> usize {
121        self.secrets.len()
122    }
123
124    /// Check if there are no secrets defined.
125    pub fn is_empty(&self) -> bool {
126        self.secrets.is_empty()
127    }
128}
129
130/// Definition of a single secret.
131#[derive(Debug, Clone, Serialize, Deserialize)]
132#[serde(rename_all = "snake_case")]
133pub struct SecretDefinition {
134    /// Secret key name.
135    pub key: String,
136
137    /// Human-readable description.
138    #[serde(default, skip_serializing_if = "Option::is_none")]
139    pub description: Option<String>,
140
141    /// Whether this secret is required.
142    #[serde(default)]
143    pub required: bool,
144
145    /// Provider to use (defaults to platform keychain).
146    #[serde(default, skip_serializing_if = "Option::is_none")]
147    pub provider: Option<String>,
148
149    /// Environment variable name to inject as.
150    #[serde(default, skip_serializing_if = "Option::is_none")]
151    pub env_var: Option<String>,
152
153    /// File path to write secret to (for file-based secrets).
154    #[serde(default, skip_serializing_if = "Option::is_none")]
155    pub file_path: Option<String>,
156
157    /// File permissions (octal, e.g., "0600").
158    #[serde(default, skip_serializing_if = "Option::is_none")]
159    pub file_mode: Option<String>,
160}
161
162impl SecretDefinition {
163    /// Create a new required secret definition.
164    pub fn required(key: impl Into<String>) -> Self {
165        let key = key.into();
166        Self {
167            key: key.clone(),
168            description: None,
169            required: true,
170            provider: None,
171            env_var: None,
172            file_path: None,
173            file_mode: None,
174        }
175    }
176
177    /// Create a new optional secret definition.
178    pub fn optional(key: impl Into<String>) -> Self {
179        let key = key.into();
180        Self {
181            key: key.clone(),
182            description: None,
183            required: false,
184            provider: None,
185            env_var: None,
186            file_path: None,
187            file_mode: None,
188        }
189    }
190
191    /// Set the description.
192    pub fn with_description(mut self, description: impl Into<String>) -> Self {
193        self.description = Some(description.into());
194        self
195    }
196
197    /// Set the provider.
198    pub fn with_provider(mut self, provider: impl Into<String>) -> Self {
199        self.provider = Some(provider.into());
200        self
201    }
202
203    /// Set the environment variable to inject as.
204    pub fn inject_as_env(mut self, env_var: impl Into<String>) -> Self {
205        self.env_var = Some(env_var.into());
206        self
207    }
208
209    /// Set the file path to write to.
210    pub fn write_to_file(mut self, path: impl Into<String>) -> Self {
211        self.file_path = Some(path.into());
212        self
213    }
214
215    /// Set the file mode.
216    pub fn with_file_mode(mut self, mode: impl Into<String>) -> Self {
217        self.file_mode = Some(mode.into());
218        self
219    }
220
221    /// Check if this secret should be injected as an environment variable.
222    pub fn has_env_var(&self) -> bool {
223        self.env_var.is_some()
224    }
225
226    /// Check if this secret should be written to a file.
227    pub fn has_file_path(&self) -> bool {
228        self.file_path.is_some()
229    }
230
231    /// Get the injection targets for this secret.
232    pub fn injection_targets(&self) -> Vec<SecretInjectionTarget> {
233        let mut targets = Vec::new();
234        if let Some(ref env_var) = self.env_var {
235            targets.push(SecretInjectionTarget::EnvVar(env_var.clone()));
236        }
237        if let Some(ref file_path) = self.file_path {
238            targets.push(SecretInjectionTarget::File {
239                path: file_path.clone(),
240                mode: self.file_mode.clone(),
241            });
242        }
243        targets
244    }
245}
246
247/// Where a secret should be injected.
248#[derive(Debug, Clone, PartialEq)]
249pub enum SecretInjectionTarget {
250    /// Inject as environment variable.
251    EnvVar(String),
252    /// Write to file.
253    File {
254        /// File path to write the secret to.
255        path: String,
256        /// File permissions (octal, e.g., "0600").
257        mode: Option<String>,
258    },
259}
260
261/// Configuration for a secret provider.
262#[derive(Debug, Clone, Serialize, Deserialize)]
263#[serde(rename_all = "snake_case", tag = "type")]
264pub enum SecretProviderConfig {
265    /// Platform keychain (default).
266    Keychain,
267
268    /// Environment variable (for CI/CD).
269    EnvironmentVariable {
270        /// Prefix for environment variable names.
271        prefix: String,
272    },
273
274    /// File-based secrets.
275    File {
276        /// Path to secrets file.
277        path: String,
278        /// File format.
279        format: SecretFileFormat,
280    },
281
282    /// External secret manager.
283    External {
284        /// Provider type.
285        provider_type: ExternalSecretProvider,
286        /// Provider-specific configuration.
287        #[serde(default)]
288        config: HashMap<String, String>,
289    },
290}
291
292impl SecretProviderConfig {
293    /// Create a keychain provider config.
294    pub fn keychain() -> Self {
295        Self::Keychain
296    }
297
298    /// Create an environment variable provider config.
299    pub fn environment_variable(prefix: impl Into<String>) -> Self {
300        Self::EnvironmentVariable {
301            prefix: prefix.into(),
302        }
303    }
304
305    /// Create a file provider config.
306    pub fn file(path: impl Into<String>, format: SecretFileFormat) -> Self {
307        Self::File {
308            path: path.into(),
309            format,
310        }
311    }
312
313    /// Create an external provider config.
314    pub fn external(provider_type: ExternalSecretProvider) -> Self {
315        Self::External {
316            provider_type,
317            config: HashMap::new(),
318        }
319    }
320
321    /// Get the provider name.
322    pub fn name(&self) -> &'static str {
323        match self {
324            Self::Keychain => "keychain",
325            Self::EnvironmentVariable { .. } => "environment",
326            Self::File { .. } => "file",
327            Self::External { provider_type, .. } => provider_type.name(),
328        }
329    }
330}
331
332/// External secret provider type.
333#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
334#[serde(rename_all = "snake_case")]
335pub enum ExternalSecretProvider {
336    /// HashiCorp Vault.
337    Vault,
338    /// AWS Secrets Manager.
339    AwsSecretsManager,
340    /// Google Cloud Secret Manager.
341    GcpSecretManager,
342    /// Azure Key Vault.
343    AzureKeyVault,
344    /// 1Password CLI.
345    OnePassword,
346    /// Doppler.
347    Doppler,
348}
349
350impl ExternalSecretProvider {
351    /// Get the provider name.
352    pub fn name(&self) -> &'static str {
353        match self {
354            Self::Vault => "vault",
355            Self::AwsSecretsManager => "aws-secrets-manager",
356            Self::GcpSecretManager => "gcp-secret-manager",
357            Self::AzureKeyVault => "azure-key-vault",
358            Self::OnePassword => "1password",
359            Self::Doppler => "doppler",
360        }
361    }
362
363    /// Get a human-readable display name.
364    pub fn display_name(&self) -> &'static str {
365        match self {
366            Self::Vault => "HashiCorp Vault",
367            Self::AwsSecretsManager => "AWS Secrets Manager",
368            Self::GcpSecretManager => "GCP Secret Manager",
369            Self::AzureKeyVault => "Azure Key Vault",
370            Self::OnePassword => "1Password",
371            Self::Doppler => "Doppler",
372        }
373    }
374}
375
376/// Secret file format.
377#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
378#[serde(rename_all = "snake_case")]
379pub enum SecretFileFormat {
380    /// KEY=value format.
381    Env,
382    /// JSON object.
383    Json,
384    /// YAML file.
385    Yaml,
386    /// Single secret per file (raw content).
387    Raw,
388}
389
390impl SecretFileFormat {
391    /// Get the file extension for this format.
392    pub fn extension(&self) -> &'static str {
393        match self {
394            Self::Env => "env",
395            Self::Json => "json",
396            Self::Yaml => "yaml",
397            Self::Raw => "txt",
398        }
399    }
400}
401
402#[cfg(test)]
403mod tests {
404    use super::*;
405
406    #[test]
407    fn test_secrets_config_builder() {
408        let config = SecretsConfig::new()
409            .with_required_env_secret("api-key", "API_KEY", "API authentication key")
410            .with_required_file_secret("db-password", "/run/secrets/db", "Database password");
411
412        assert_eq!(config.secrets.len(), 2);
413        assert!(config.secrets.get("api-key").unwrap().required);
414        assert!(config.secrets.get("api-key").unwrap().env_var.is_some());
415        assert!(config.secrets.get("db-password").unwrap().file_path.is_some());
416    }
417
418    #[test]
419    fn test_secret_definition_builder() {
420        let secret = SecretDefinition::required("api-key")
421            .with_description("API key for authentication")
422            .inject_as_env("API_KEY")
423            .with_provider("keychain");
424
425        assert!(secret.required);
426        assert_eq!(secret.env_var, Some("API_KEY".to_string()));
427        assert_eq!(secret.provider, Some("keychain".to_string()));
428    }
429
430    #[test]
431    fn test_secret_injection_targets() {
432        let secret = SecretDefinition::required("multi-target")
433            .inject_as_env("SECRET_VAR")
434            .write_to_file("/run/secrets/key")
435            .with_file_mode("0400");
436
437        let targets = secret.injection_targets();
438        assert_eq!(targets.len(), 2);
439        assert!(targets.contains(&SecretInjectionTarget::EnvVar("SECRET_VAR".to_string())));
440        assert!(targets.contains(&SecretInjectionTarget::File {
441            path: "/run/secrets/key".to_string(),
442            mode: Some("0400".to_string()),
443        }));
444    }
445
446    #[test]
447    fn test_secret_provider_config() {
448        let keychain = SecretProviderConfig::keychain();
449        assert_eq!(keychain.name(), "keychain");
450
451        let env = SecretProviderConfig::environment_variable("SECRET_");
452        assert_eq!(env.name(), "environment");
453
454        let file = SecretProviderConfig::file("/secrets.json", SecretFileFormat::Json);
455        assert_eq!(file.name(), "file");
456
457        let vault = SecretProviderConfig::external(ExternalSecretProvider::Vault);
458        assert_eq!(vault.name(), "vault");
459    }
460
461    #[test]
462    fn test_secrets_config_queries() {
463        let config = SecretsConfig::new()
464            .with_secret("required-key", SecretDefinition::required("required-key"))
465            .with_secret("optional-key", SecretDefinition::optional("optional-key"));
466
467        assert_eq!(config.required_keys(), vec!["required-key"]);
468        assert_eq!(config.optional_keys(), vec!["optional-key"]);
469        assert!(config.contains("required-key"));
470        assert!(!config.contains("nonexistent"));
471    }
472
473    #[test]
474    fn test_secrets_config_serialization() {
475        let config = SecretsConfig::new()
476            .with_required_env_secret("api-key", "API_KEY", "API key")
477            .with_provider(SecretProviderConfig::keychain());
478
479        let json = serde_json::to_string(&config).unwrap();
480        let deserialized: SecretsConfig = serde_json::from_str(&json).unwrap();
481
482        assert_eq!(config.secrets.len(), deserialized.secrets.len());
483        assert_eq!(config.providers.len(), deserialized.providers.len());
484    }
485
486    #[test]
487    fn test_external_provider_names() {
488        assert_eq!(ExternalSecretProvider::Vault.name(), "vault");
489        assert_eq!(
490            ExternalSecretProvider::AwsSecretsManager.display_name(),
491            "AWS Secrets Manager"
492        );
493    }
494}