skill_runtime/
config_mapper.rs

1use anyhow::{Context, Result};
2use std::collections::HashMap;
3use wasmtime_wasi::WasiCtxBuilder;
4use zeroize::Zeroizing;
5
6use crate::instance::{InstanceConfig, InstanceManager};
7
8/// Maps instance configuration to environment variables for WASM execution
9pub struct ConfigMapper {
10    instance_manager: InstanceManager,
11}
12
13impl ConfigMapper {
14    /// Create a new config mapper
15    pub fn new(instance_manager: InstanceManager) -> Self {
16        Self { instance_manager }
17    }
18
19    /// Resolve configuration including secrets from keyring
20    /// Returns a HashMap of environment variables ready for injection
21    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        // Load instance config
33        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        // Get all config including resolved secrets
44        let resolved = config.get_all_config()?;
45
46        // Merge with explicit environment variables from config
47        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    /// Apply environment variables to WASI context builder
63    /// Converts all keys to SKILL_{KEY_NAME_UPPER} format
64    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            // Convert to SKILL_ prefix and uppercase
71            let env_key = Self::to_env_var_name(&key);
72
73            // Add to WASI context
74            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    /// Convert config key to environment variable name
83    /// Example: "aws_access_key_id" -> "SKILL_AWS_ACCESS_KEY_ID"
84    fn to_env_var_name(key: &str) -> String {
85        format!("SKILL_{}", key.to_uppercase())
86    }
87
88    /// Get redacted environment map for logging (secrets replaced with [REDACTED])
89    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    /// Support config value templating with environment variable substitution
110    /// Example: "region = ${AWS_REGION:-us-east-1}" -> "us-east-1" (if AWS_REGION not set)
111    pub fn expand_template(template: &str) -> String {
112        let mut result = template.to_string();
113
114        // Simple regex-free implementation for ${VAR:-default} syntax
115        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        // Test with environment variable set
165        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        // Test with default value
170        assert_eq!(
171            ConfigMapper::expand_template("${MISSING_VAR:-default}"),
172            "default"
173        );
174
175        // Test with no template
176        assert_eq!(ConfigMapper::expand_template("plain_text"), "plain_text");
177
178        // Test with multiple variables
179        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}