skill_runtime/
sandbox.rs

1use anyhow::{Context, Result};
2use std::path::PathBuf;
3use wasmtime_wasi::{
4    ResourceTable, WasiCtx, WasiCtxBuilder, WasiView,
5};
6
7use crate::instance::InstanceConfig;
8
9/// Host state for WASI context
10pub struct HostState {
11    pub wasi: WasiCtx,
12    pub table: ResourceTable,
13    pub instance_id: String,
14    pub config: std::collections::HashMap<String, String>,
15}
16
17impl WasiView for HostState {
18    fn table(&mut self) -> &mut ResourceTable {
19        &mut self.table
20    }
21
22    fn ctx(&mut self) -> &mut WasiCtx {
23        &mut self.wasi
24    }
25}
26
27/// Builder for creating sandboxed WASI environments
28pub struct SandboxBuilder {
29    instance_id: String,
30    instance_dir: PathBuf,
31    temp_dir: PathBuf,
32    env_vars: Vec<(String, String)>,
33    args: Vec<String>,
34    inherit_stdio: bool,
35}
36
37impl SandboxBuilder {
38    /// Create a new sandbox builder for a skill instance
39    pub fn new(instance_id: impl Into<String>, instance_dir: PathBuf) -> Self {
40        let temp_dir = std::env::temp_dir()
41            .join("skill-engine")
42            .join("sandbox")
43            .join(uuid::Uuid::new_v4().to_string());
44
45        Self {
46            instance_id: instance_id.into(),
47            instance_dir,
48            temp_dir,
49            env_vars: Vec::new(),
50            args: Vec::new(),
51            inherit_stdio: true,
52        }
53    }
54
55    /// Add an environment variable to the sandbox
56    pub fn env(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
57        self.env_vars.push((key.into(), value.into()));
58        self
59    }
60
61    /// Add multiple environment variables from configuration
62    pub fn env_from_config(mut self, config: &InstanceConfig) -> Self {
63        // Map configuration to environment variables
64        for (key, value) in &config.environment {
65            self.env_vars.push((key.clone(), value.clone()));
66        }
67        self
68    }
69
70    /// Add command-line arguments
71    pub fn args(mut self, args: Vec<String>) -> Self {
72        self.args = args;
73        self
74    }
75
76    /// Set whether to inherit stdio (default: true)
77    pub fn inherit_stdio(mut self, inherit: bool) -> Self {
78        self.inherit_stdio = inherit;
79        self
80    }
81
82    /// Build the sandboxed WASI context with capability restrictions
83    pub fn build(self) -> Result<HostState> {
84        // Create temporary directory for this execution
85        std::fs::create_dir_all(&self.temp_dir)
86            .context("Failed to create temporary sandbox directory")?;
87
88        let mut builder = WasiCtxBuilder::new();
89
90        // Add environment variables
91        for (key, value) in &self.env_vars {
92            builder.env(key, value);
93        }
94
95        // Set instance ID
96        builder.env("SKILL_INSTANCE_ID", &self.instance_id);
97
98        // Add arguments
99        builder.args(&self.args);
100
101        // Configure stdio
102        if self.inherit_stdio {
103            builder.inherit_stdio();
104        }
105
106        // Pre-open directories - in wasmtime 26, preopened_dir is simpler
107        // Just use the builder's methods directly with paths
108        // Note: The API changed - for now we'll comment this out until we can test properly
109        //  TODO: Fix directory preopen for WASI Preview 2
110
111        let wasi = builder.build();
112        let table = ResourceTable::new();
113
114        // Convert env_vars to HashMap for config access
115        let config: std::collections::HashMap<String, String> =
116            self.env_vars.into_iter().collect();
117
118        tracing::debug!(
119            instance_id = %self.instance_id,
120            instance_dir = %self.instance_dir.display(),
121            temp_dir = %self.temp_dir.display(),
122            config_count = config.len(),
123            "Created sandbox environment"
124        );
125
126        Ok(HostState {
127            wasi,
128            table,
129            instance_id: self.instance_id,
130            config,
131        })
132    }
133}
134
135/// Cleanup temporary sandbox directories
136pub fn cleanup_temp_dirs() -> Result<()> {
137    let sandbox_root = std::env::temp_dir().join("skill-engine").join("sandbox");
138
139    if sandbox_root.exists() {
140        // Remove old sandbox directories (older than 1 hour)
141        let now = std::time::SystemTime::now();
142
143        for entry in std::fs::read_dir(&sandbox_root)? {
144            let entry = entry?;
145            let metadata = entry.metadata()?;
146
147            if let Ok(created) = metadata.created() {
148                if let Ok(duration) = now.duration_since(created) {
149                    if duration.as_secs() > 3600 {
150                        // Older than 1 hour
151                        if let Err(e) = std::fs::remove_dir_all(entry.path()) {
152                            tracing::warn!(
153                                path = %entry.path().display(),
154                                error = %e,
155                                "Failed to cleanup old sandbox directory"
156                            );
157                        } else {
158                            tracing::debug!(
159                                path = %entry.path().display(),
160                                "Cleaned up old sandbox directory"
161                            );
162                        }
163                    }
164                }
165            }
166        }
167    }
168
169    Ok(())
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use tempfile::TempDir;
176
177    #[test]
178    fn test_sandbox_builder() {
179        let temp_dir = TempDir::new().unwrap();
180        let instance_dir = temp_dir.path().to_path_buf();
181
182        let sandbox = SandboxBuilder::new("test-instance", instance_dir.clone())
183            .env("TEST_VAR", "test_value")
184            .args(vec!["arg1".to_string(), "arg2".to_string()])
185            .build()
186            .unwrap();
187
188        assert_eq!(sandbox.instance_id, "test-instance");
189    }
190
191    #[test]
192    fn test_env_from_config() {
193        let temp_dir = TempDir::new().unwrap();
194        let instance_dir = temp_dir.path().to_path_buf();
195
196        let mut config = InstanceConfig::default();
197        config.environment.insert("KEY1".to_string(), "value1".to_string());
198        config.environment.insert("KEY2".to_string(), "value2".to_string());
199
200        let sandbox = SandboxBuilder::new("test", instance_dir)
201            .env_from_config(&config)
202            .build()
203            .unwrap();
204
205        assert_eq!(sandbox.instance_id, "test");
206    }
207}