skill_context/
environment.rs

1//! Environment variable configuration types.
2//!
3//! This module defines environment variable configuration and value types
4//! for execution contexts.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::PathBuf;
9
10/// Environment variable configuration.
11#[derive(Debug, Clone, Default, Serialize, Deserialize)]
12#[serde(rename_all = "snake_case")]
13pub struct EnvironmentConfig {
14    /// Static environment variables.
15    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
16    pub variables: HashMap<String, EnvValue>,
17
18    /// Environment files to load (.env format).
19    #[serde(default, skip_serializing_if = "Vec::is_empty")]
20    pub env_files: Vec<EnvFileRef>,
21
22    /// Environment variable prefixes to pass through from host.
23    #[serde(default, skip_serializing_if = "Vec::is_empty")]
24    pub passthrough_prefixes: Vec<String>,
25
26    /// Specific host env vars to pass through.
27    #[serde(default, skip_serializing_if = "Vec::is_empty")]
28    pub passthrough_vars: Vec<String>,
29}
30
31impl EnvironmentConfig {
32    /// Create a new empty environment configuration.
33    pub fn new() -> Self {
34        Self::default()
35    }
36
37    /// Add a plain text variable.
38    pub fn with_var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
39        self.variables
40            .insert(key.into(), EnvValue::Plain(value.into()));
41        self
42    }
43
44    /// Add a variable reference.
45    pub fn with_reference(mut self, key: impl Into<String>, ref_var: impl Into<String>) -> Self {
46        self.variables
47            .insert(key.into(), EnvValue::Reference(ref_var.into()));
48        self
49    }
50
51    /// Add a secret reference.
52    pub fn with_secret(mut self, key: impl Into<String>, secret_ref: SecretRef) -> Self {
53        self.variables
54            .insert(key.into(), EnvValue::Secret(secret_ref));
55        self
56    }
57
58    /// Add an environment file to load.
59    pub fn with_env_file(mut self, path: impl Into<String>) -> Self {
60        self.env_files.push(EnvFileRef {
61            path: path.into(),
62            required: true,
63            prefix: None,
64        });
65        self
66    }
67
68    /// Add an optional environment file.
69    pub fn with_optional_env_file(mut self, path: impl Into<String>) -> Self {
70        self.env_files.push(EnvFileRef {
71            path: path.into(),
72            required: false,
73            prefix: None,
74        });
75        self
76    }
77
78    /// Add a passthrough prefix.
79    pub fn with_passthrough_prefix(mut self, prefix: impl Into<String>) -> Self {
80        self.passthrough_prefixes.push(prefix.into());
81        self
82    }
83
84    /// Add a specific passthrough variable.
85    pub fn with_passthrough_var(mut self, var: impl Into<String>) -> Self {
86        self.passthrough_vars.push(var.into());
87        self
88    }
89
90    /// Get all variable keys.
91    pub fn variable_keys(&self) -> Vec<&str> {
92        self.variables.keys().map(|s| s.as_str()).collect()
93    }
94
95    /// Check if a variable is a secret reference.
96    pub fn is_secret(&self, key: &str) -> bool {
97        self.variables
98            .get(key)
99            .map(|v| matches!(v, EnvValue::Secret(_)))
100            .unwrap_or(false)
101    }
102
103    /// Get all secret references.
104    pub fn secret_refs(&self) -> Vec<(&str, &SecretRef)> {
105        self.variables
106            .iter()
107            .filter_map(|(k, v)| match v {
108                EnvValue::Secret(r) => Some((k.as_str(), r)),
109                _ => None,
110            })
111            .collect()
112    }
113}
114
115/// Environment variable value.
116#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
117#[serde(rename_all = "snake_case", tag = "type", content = "value")]
118pub enum EnvValue {
119    /// Plain text value.
120    Plain(String),
121
122    /// Reference to another env var: `${VAR_NAME}`.
123    Reference(String),
124
125    /// Reference to a secret: `secret://context/key`.
126    Secret(SecretRef),
127
128    /// Generated value (e.g., UUID, timestamp).
129    Generated(GeneratedValue),
130
131    /// Value from file.
132    FromFile(PathBuf),
133}
134
135impl EnvValue {
136    /// Create a plain text value.
137    pub fn plain(value: impl Into<String>) -> Self {
138        Self::Plain(value.into())
139    }
140
141    /// Create a reference to another env var.
142    pub fn reference(var_name: impl Into<String>) -> Self {
143        Self::Reference(var_name.into())
144    }
145
146    /// Create a secret reference.
147    pub fn secret(context_id: impl Into<String>, key: impl Into<String>) -> Self {
148        Self::Secret(SecretRef::new(context_id, key))
149    }
150
151    /// Create a generated UUID value.
152    pub fn uuid() -> Self {
153        Self::Generated(GeneratedValue::Uuid)
154    }
155
156    /// Create a generated timestamp value.
157    pub fn timestamp() -> Self {
158        Self::Generated(GeneratedValue::Timestamp)
159    }
160
161    /// Create a value from file.
162    pub fn from_file(path: impl Into<PathBuf>) -> Self {
163        Self::FromFile(path.into())
164    }
165
166    /// Check if this is a plain value.
167    pub fn is_plain(&self) -> bool {
168        matches!(self, Self::Plain(_))
169    }
170
171    /// Check if this is a secret reference.
172    pub fn is_secret(&self) -> bool {
173        matches!(self, Self::Secret(_))
174    }
175
176    /// Check if this requires resolution.
177    pub fn needs_resolution(&self) -> bool {
178        !matches!(self, Self::Plain(_))
179    }
180}
181
182/// Reference to a secret value.
183#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
184#[serde(rename_all = "snake_case")]
185pub struct SecretRef {
186    /// Context ID containing the secret (use "." for current context).
187    pub context_id: String,
188
189    /// Secret key name.
190    pub key: String,
191}
192
193impl SecretRef {
194    /// Create a new secret reference.
195    pub fn new(context_id: impl Into<String>, key: impl Into<String>) -> Self {
196        Self {
197            context_id: context_id.into(),
198            key: key.into(),
199        }
200    }
201
202    /// Create a reference to a secret in the current context.
203    pub fn current(key: impl Into<String>) -> Self {
204        Self::new(".", key)
205    }
206
207    /// Check if this references the current context.
208    pub fn is_current_context(&self) -> bool {
209        self.context_id == "."
210    }
211
212    /// Parse a secret reference string like `secret://context/key`.
213    pub fn parse(s: &str) -> Option<Self> {
214        let s = s.strip_prefix("secret://")?;
215        let parts: Vec<&str> = s.splitn(2, '/').collect();
216        if parts.len() == 2 {
217            Some(Self::new(parts[0], parts[1]))
218        } else {
219            None
220        }
221    }
222
223    /// Convert to a secret reference string.
224    pub fn to_uri(&self) -> String {
225        format!("secret://{}/{}", self.context_id, self.key)
226    }
227}
228
229/// Generated value type.
230#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
231#[serde(rename_all = "snake_case", tag = "generator")]
232pub enum GeneratedValue {
233    /// Generate a UUID v4.
234    Uuid,
235
236    /// Generate current timestamp (ISO 8601).
237    Timestamp,
238
239    /// Generate a random string.
240    RandomString {
241        /// Length of the random string.
242        length: usize,
243    },
244
245    /// Generate a hash of another value.
246    Hash {
247        /// Hash algorithm (sha256, blake3, etc.).
248        algorithm: String,
249        /// Value to hash (can be a variable reference).
250        of: String,
251    },
252}
253
254impl GeneratedValue {
255    /// Create a random string generator.
256    pub fn random_string(length: usize) -> Self {
257        Self::RandomString { length }
258    }
259
260    /// Create a hash generator.
261    pub fn hash(algorithm: impl Into<String>, of: impl Into<String>) -> Self {
262        Self::Hash {
263            algorithm: algorithm.into(),
264            of: of.into(),
265        }
266    }
267
268    /// Generate the value.
269    pub fn generate(&self) -> String {
270        match self {
271            Self::Uuid => uuid::Uuid::new_v4().to_string(),
272            Self::Timestamp => chrono::Utc::now().to_rfc3339(),
273            Self::RandomString { length } => {
274                use std::iter;
275                const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
276                let mut rng = rand_simple();
277                iter::repeat_with(|| CHARSET[rng.next() % CHARSET.len()])
278                    .map(|c| c as char)
279                    .take(*length)
280                    .collect()
281            }
282            Self::Hash { algorithm, of } => {
283                // For now, just use a simple hash representation
284                // In production, this would use the actual hash algorithm
285                format!("{}:{}", algorithm, of)
286            }
287        }
288    }
289}
290
291// Simple random number generator for random strings
292struct SimpleRng(u64);
293
294impl SimpleRng {
295    fn next(&mut self) -> usize {
296        self.0 = self.0.wrapping_mul(6364136223846793005).wrapping_add(1);
297        (self.0 >> 33) as usize
298    }
299}
300
301fn rand_simple() -> SimpleRng {
302    use std::time::{SystemTime, UNIX_EPOCH};
303    let seed = SystemTime::now()
304        .duration_since(UNIX_EPOCH)
305        .map(|d| d.as_nanos() as u64)
306        .unwrap_or(0);
307    SimpleRng(seed)
308}
309
310/// Reference to an environment file.
311#[derive(Debug, Clone, Serialize, Deserialize)]
312#[serde(rename_all = "snake_case")]
313pub struct EnvFileRef {
314    /// Path to .env file (supports glob patterns).
315    pub path: String,
316
317    /// Whether file must exist.
318    #[serde(default = "default_true")]
319    pub required: bool,
320
321    /// Optional prefix to add to all vars from this file.
322    #[serde(default, skip_serializing_if = "Option::is_none")]
323    pub prefix: Option<String>,
324}
325
326fn default_true() -> bool {
327    true
328}
329
330impl EnvFileRef {
331    /// Create a new required env file reference.
332    pub fn new(path: impl Into<String>) -> Self {
333        Self {
334            path: path.into(),
335            required: true,
336            prefix: None,
337        }
338    }
339
340    /// Create an optional env file reference.
341    pub fn optional(path: impl Into<String>) -> Self {
342        Self {
343            path: path.into(),
344            required: false,
345            prefix: None,
346        }
347    }
348
349    /// Set a prefix for all variables from this file.
350    pub fn with_prefix(mut self, prefix: impl Into<String>) -> Self {
351        self.prefix = Some(prefix.into());
352        self
353    }
354}
355
356#[cfg(test)]
357mod tests {
358    use super::*;
359
360    #[test]
361    fn test_env_config_builder() {
362        let config = EnvironmentConfig::new()
363            .with_var("LOG_LEVEL", "debug")
364            .with_reference("API_URL", "PRODUCTION_API_URL")
365            .with_passthrough_prefix("AWS_")
366            .with_passthrough_var("PATH");
367
368        assert_eq!(config.variables.len(), 2);
369        assert!(config.passthrough_prefixes.contains(&"AWS_".to_string()));
370        assert!(config.passthrough_vars.contains(&"PATH".to_string()));
371    }
372
373    #[test]
374    fn test_env_value_types() {
375        let plain = EnvValue::plain("value");
376        let reference = EnvValue::reference("OTHER_VAR");
377        let secret = EnvValue::secret("my-context", "api-key");
378        let uuid = EnvValue::uuid();
379
380        assert!(plain.is_plain());
381        assert!(!plain.needs_resolution());
382
383        assert!(!reference.is_plain());
384        assert!(reference.needs_resolution());
385
386        assert!(secret.is_secret());
387        assert!(secret.needs_resolution());
388
389        assert!(uuid.needs_resolution());
390    }
391
392    #[test]
393    fn test_secret_ref_parsing() {
394        let ref1 = SecretRef::parse("secret://my-context/api-key").unwrap();
395        assert_eq!(ref1.context_id, "my-context");
396        assert_eq!(ref1.key, "api-key");
397        assert!(!ref1.is_current_context());
398
399        let ref2 = SecretRef::parse("secret://./local-key").unwrap();
400        assert!(ref2.is_current_context());
401
402        assert!(SecretRef::parse("invalid").is_none());
403        assert!(SecretRef::parse("other://scheme").is_none());
404    }
405
406    #[test]
407    fn test_secret_ref_uri() {
408        let secret_ref = SecretRef::new("ctx", "key");
409        assert_eq!(secret_ref.to_uri(), "secret://ctx/key");
410    }
411
412    #[test]
413    fn test_generated_value() {
414        let uuid = GeneratedValue::Uuid.generate();
415        assert_eq!(uuid.len(), 36); // UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
416
417        let timestamp = GeneratedValue::Timestamp.generate();
418        assert!(timestamp.contains("T")); // ISO 8601 format
419
420        let random = GeneratedValue::random_string(10).generate();
421        assert_eq!(random.len(), 10);
422    }
423
424    #[test]
425    fn test_env_config_serialization() {
426        let config = EnvironmentConfig::new()
427            .with_var("KEY", "value")
428            .with_secret("SECRET_KEY", SecretRef::current("my-secret"));
429
430        let json = serde_json::to_string(&config).unwrap();
431        let deserialized: EnvironmentConfig = serde_json::from_str(&json).unwrap();
432
433        assert_eq!(config.variables.len(), deserialized.variables.len());
434    }
435
436    #[test]
437    fn test_env_file_ref() {
438        let required = EnvFileRef::new(".env.production");
439        assert!(required.required);
440
441        let optional = EnvFileRef::optional(".env.local").with_prefix("LOCAL_");
442        assert!(!optional.required);
443        assert_eq!(optional.prefix, Some("LOCAL_".to_string()));
444    }
445}