skill_context/providers/
env.rs1use async_trait::async_trait;
9use zeroize::Zeroizing;
10
11use super::{SecretProvider, SecretValue};
12use crate::ContextError;
13
14pub struct EnvironmentProvider {
24 prefix: String,
26}
27
28impl EnvironmentProvider {
29 pub fn new(prefix: impl Into<String>) -> Self {
39 Self {
40 prefix: prefix.into(),
41 }
42 }
43
44 pub fn without_prefix() -> Self {
46 Self {
47 prefix: String::new(),
48 }
49 }
50
51 fn build_env_var(&self, context_id: &str, key: &str) -> String {
53 let context_part = context_id.to_uppercase().replace('-', "_").replace('.', "_");
54 let key_part = key.to_uppercase().replace('-', "_").replace('.', "_");
55 format!("{}{}__{}", self.prefix, context_part, key_part)
56 }
57}
58
59#[async_trait]
60impl SecretProvider for EnvironmentProvider {
61 async fn get_secret(
62 &self,
63 context_id: &str,
64 key: &str,
65 ) -> Result<Option<SecretValue>, ContextError> {
66 let env_var = self.build_env_var(context_id, key);
67
68 match std::env::var(&env_var) {
69 Ok(value) => {
70 tracing::debug!(
71 context_id = context_id,
72 key = key,
73 env_var = env_var,
74 "Retrieved secret from environment"
75 );
76 Ok(Some(Zeroizing::new(value)))
77 }
78 Err(std::env::VarError::NotPresent) => Ok(None),
79 Err(std::env::VarError::NotUnicode(_)) => {
80 tracing::warn!(
81 context_id = context_id,
82 key = key,
83 env_var = env_var,
84 "Environment variable contains invalid UTF-8"
85 );
86 Err(ContextError::SecretProvider(format!(
87 "Environment variable '{}' contains invalid UTF-8",
88 env_var
89 )))
90 }
91 }
92 }
93
94 async fn set_secret(
95 &self,
96 context_id: &str,
97 key: &str,
98 _value: &str,
99 ) -> Result<(), ContextError> {
100 let env_var = self.build_env_var(context_id, key);
101 tracing::warn!(
102 context_id = context_id,
103 key = key,
104 env_var = env_var,
105 "Attempted to set secret via environment provider (read-only)"
106 );
107 Err(ContextError::SecretProvider(
108 "Environment provider is read-only. Cannot set secrets.".to_string(),
109 ))
110 }
111
112 async fn delete_secret(&self, context_id: &str, key: &str) -> Result<(), ContextError> {
113 let env_var = self.build_env_var(context_id, key);
114 tracing::warn!(
115 context_id = context_id,
116 key = key,
117 env_var = env_var,
118 "Attempted to delete secret via environment provider (read-only)"
119 );
120 Err(ContextError::SecretProvider(
121 "Environment provider is read-only. Cannot delete secrets.".to_string(),
122 ))
123 }
124
125 async fn list_keys(&self, context_id: &str) -> Result<Vec<String>, ContextError> {
126 let context_prefix = format!(
127 "{}{}__",
128 self.prefix,
129 context_id.to_uppercase().replace('-', "_").replace('.', "_")
130 );
131
132 let keys: Vec<String> = std::env::vars()
133 .filter_map(|(k, _)| {
134 if k.starts_with(&context_prefix) {
135 Some(
136 k[context_prefix.len()..]
137 .to_lowercase()
138 .replace('_', "-"),
139 )
140 } else {
141 None
142 }
143 })
144 .collect();
145
146 Ok(keys)
147 }
148
149 fn name(&self) -> &'static str {
150 "environment"
151 }
152
153 fn is_read_only(&self) -> bool {
154 true
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn test_env_var_naming() {
164 let provider = EnvironmentProvider::new("SECRET_");
165
166 assert_eq!(
167 provider.build_env_var("my-context", "api-key"),
168 "SECRET_MY_CONTEXT__API_KEY"
169 );
170
171 assert_eq!(
172 provider.build_env_var("production.api", "database.password"),
173 "SECRET_PRODUCTION_API__DATABASE_PASSWORD"
174 );
175 }
176
177 #[test]
178 fn test_no_prefix() {
179 let provider = EnvironmentProvider::without_prefix();
180
181 assert_eq!(
182 provider.build_env_var("context", "key"),
183 "CONTEXT__KEY"
184 );
185 }
186
187 #[tokio::test]
188 async fn test_get_from_env() {
189 let provider = EnvironmentProvider::new("TEST_SECRET_");
190
191 std::env::set_var("TEST_SECRET_MY_CTX__MY_KEY", "my-secret-value");
193
194 let result = provider.get_secret("my-ctx", "my-key").await.unwrap();
195 assert!(result.is_some());
196 assert_eq!(&*result.unwrap(), "my-secret-value");
197
198 std::env::remove_var("TEST_SECRET_MY_CTX__MY_KEY");
200 }
201
202 #[tokio::test]
203 async fn test_get_nonexistent() {
204 let provider = EnvironmentProvider::new("NONEXISTENT_PREFIX_");
205
206 let result = provider
207 .get_secret("context", "key")
208 .await
209 .unwrap();
210
211 assert!(result.is_none());
212 }
213
214 #[tokio::test]
215 async fn test_set_is_read_only() {
216 let provider = EnvironmentProvider::new("TEST_");
217
218 let result = provider.set_secret("ctx", "key", "value").await;
219
220 assert!(result.is_err());
221 assert!(provider.is_read_only());
222 }
223
224 #[tokio::test]
225 async fn test_delete_is_read_only() {
226 let provider = EnvironmentProvider::new("TEST_");
227
228 let result = provider.delete_secret("ctx", "key").await;
229
230 assert!(result.is_err());
231 }
232
233 #[tokio::test]
234 async fn test_list_keys() {
235 let provider = EnvironmentProvider::new("TEST_LIST_");
236
237 std::env::set_var("TEST_LIST_MY_CTX__KEY_ONE", "value1");
239 std::env::set_var("TEST_LIST_MY_CTX__KEY_TWO", "value2");
240 std::env::set_var("TEST_LIST_OTHER_CTX__KEY", "value3");
241
242 let keys = provider.list_keys("my-ctx").await.unwrap();
243
244 assert!(keys.contains(&"key-one".to_string()));
245 assert!(keys.contains(&"key-two".to_string()));
246 assert!(!keys.contains(&"key".to_string())); std::env::remove_var("TEST_LIST_MY_CTX__KEY_ONE");
250 std::env::remove_var("TEST_LIST_MY_CTX__KEY_TWO");
251 std::env::remove_var("TEST_LIST_OTHER_CTX__KEY");
252 }
253}