1use anyhow::{Context, Result};
2use std::path::PathBuf;
3use wasmtime_wasi::{
4 ResourceTable, WasiCtx, WasiCtxBuilder, WasiView,
5};
6
7use crate::instance::InstanceConfig;
8
9pub 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
27pub 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 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 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 pub fn env_from_config(mut self, config: &InstanceConfig) -> Self {
63 for (key, value) in &config.environment {
65 self.env_vars.push((key.clone(), value.clone()));
66 }
67 self
68 }
69
70 pub fn args(mut self, args: Vec<String>) -> Self {
72 self.args = args;
73 self
74 }
75
76 pub fn inherit_stdio(mut self, inherit: bool) -> Self {
78 self.inherit_stdio = inherit;
79 self
80 }
81
82 pub fn build(self) -> Result<HostState> {
84 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 for (key, value) in &self.env_vars {
92 builder.env(key, value);
93 }
94
95 builder.env("SKILL_INSTANCE_ID", &self.instance_id);
97
98 builder.args(&self.args);
100
101 if self.inherit_stdio {
103 builder.inherit_stdio();
104 }
105
106 let wasi = builder.build();
112 let table = ResourceTable::new();
113
114 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
135pub 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 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 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}