skill_runtime/
config_mapper.rs1use anyhow::{Context, Result};
2use std::collections::HashMap;
3use wasmtime_wasi::WasiCtxBuilder;
4use zeroize::Zeroizing;
5
6use crate::instance::{InstanceConfig, InstanceManager};
7
8pub struct ConfigMapper {
10 instance_manager: InstanceManager,
11}
12
13impl ConfigMapper {
14 pub fn new(instance_manager: InstanceManager) -> Self {
16 Self { instance_manager }
17 }
18
19 pub async fn resolve_config(
22 &self,
23 skill_name: &str,
24 instance_name: &str,
25 ) -> Result<HashMap<String, Zeroizing<String>>> {
26 tracing::debug!(
27 skill = %skill_name,
28 instance = %instance_name,
29 "Resolving instance configuration"
30 );
31
32 let config = self
34 .instance_manager
35 .load_instance(skill_name, instance_name)
36 .with_context(|| {
37 format!(
38 "Failed to load instance: {}/{}",
39 skill_name, instance_name
40 )
41 })?;
42
43 let resolved = config.get_all_config()?;
45
46 let mut env_vars = resolved;
48 for (key, value) in &config.environment {
49 env_vars.insert(key.clone(), Zeroizing::new(value.clone()));
50 }
51
52 tracing::debug!(
53 skill = %skill_name,
54 instance = %instance_name,
55 var_count = env_vars.len(),
56 "Resolved configuration"
57 );
58
59 Ok(env_vars)
60 }
61
62 pub fn apply_to_wasi_context(
65 &self,
66 ctx_builder: &mut WasiCtxBuilder,
67 env_vars: HashMap<String, Zeroizing<String>>,
68 ) -> Result<()> {
69 for (key, value) in env_vars {
70 let env_key = Self::to_env_var_name(&key);
72
73 ctx_builder.env(&env_key, value.as_str());
75
76 tracing::trace!(key = %env_key, "Added environment variable");
77 }
78
79 Ok(())
80 }
81
82 fn to_env_var_name(key: &str) -> String {
85 format!("SKILL_{}", key.to_uppercase())
86 }
87
88 pub fn get_redacted_env_map(
90 config: &InstanceConfig,
91 ) -> HashMap<String, String> {
92 let mut result = HashMap::new();
93
94 for (key, value) in &config.config {
95 if value.secret {
96 result.insert(key.clone(), "[REDACTED]".to_string());
97 } else {
98 result.insert(key.clone(), value.value.clone());
99 }
100 }
101
102 for (key, value) in &config.environment {
103 result.insert(key.clone(), value.clone());
104 }
105
106 result
107 }
108
109 pub fn expand_template(template: &str) -> String {
112 let mut result = template.to_string();
113
114 while let Some(start) = result.find("${") {
116 if let Some(end) = result[start..].find('}') {
117 let end = start + end;
118 let expr = &result[start + 2..end];
119
120 let value = if let Some(sep_pos) = expr.find(":-") {
121 let var_name = &expr[..sep_pos];
122 let default_value = &expr[sep_pos + 2..];
123
124 std::env::var(var_name).unwrap_or_else(|_| default_value.to_string())
125 } else {
126 std::env::var(expr).unwrap_or_default()
127 };
128
129 result.replace_range(start..=end, &value);
130 } else {
131 break;
132 }
133 }
134
135 result
136 }
137}
138
139impl Default for ConfigMapper {
140 fn default() -> Self {
141 Self::new(InstanceManager::new().expect("Failed to create InstanceManager"))
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 #[test]
150 fn test_to_env_var_name() {
151 assert_eq!(
152 ConfigMapper::to_env_var_name("aws_access_key_id"),
153 "SKILL_AWS_ACCESS_KEY_ID"
154 );
155 assert_eq!(ConfigMapper::to_env_var_name("region"), "SKILL_REGION");
156 assert_eq!(
157 ConfigMapper::to_env_var_name("max_retries"),
158 "SKILL_MAX_RETRIES"
159 );
160 }
161
162 #[test]
163 fn test_expand_template() {
164 std::env::set_var("TEST_VAR", "test_value");
166 assert_eq!(ConfigMapper::expand_template("${TEST_VAR}"), "test_value");
167 std::env::remove_var("TEST_VAR");
168
169 assert_eq!(
171 ConfigMapper::expand_template("${MISSING_VAR:-default}"),
172 "default"
173 );
174
175 assert_eq!(ConfigMapper::expand_template("plain_text"), "plain_text");
177
178 std::env::set_var("VAR1", "value1");
180 std::env::set_var("VAR2", "value2");
181 assert_eq!(
182 ConfigMapper::expand_template("${VAR1}-${VAR2}"),
183 "value1-value2"
184 );
185 std::env::remove_var("VAR1");
186 std::env::remove_var("VAR2");
187 }
188
189 #[test]
190 fn test_redacted_env_map() {
191 let mut config = InstanceConfig::default();
192 config.set_config("public_key".to_string(), "public_value".to_string(), false);
193 config.set_config(
194 "secret_key".to_string(),
195 "keyring://ref".to_string(),
196 true,
197 );
198
199 let redacted = ConfigMapper::get_redacted_env_map(&config);
200
201 assert_eq!(redacted.get("public_key"), Some(&"public_value".to_string()));
202 assert_eq!(redacted.get("secret_key"), Some(&"[REDACTED]".to_string()));
203 }
204}