agentic_sandbox/
firecracker.rs1use async_trait::async_trait;
14use std::path::PathBuf;
15use std::time::Duration;
16#[cfg(feature = "firecracker")]
17use tracing::info;
18#[cfg(not(feature = "firecracker"))]
19use tracing::warn;
20use tracing::{debug, instrument};
21
22use crate::{
23 error::SandboxError,
24 traits::{ExecutionResult, Sandbox},
25};
26
27#[derive(Debug, Clone)]
29pub struct FirecrackerConfig {
30 pub firecracker_bin: PathBuf,
32 pub kernel_path: PathBuf,
34 pub rootfs_path: PathBuf,
36 pub vcpu_count: u8,
38 pub mem_size_mib: u32,
40 pub timeout: Duration,
42 pub workspace: PathBuf,
44}
45
46impl Default for FirecrackerConfig {
47 fn default() -> Self {
48 Self {
49 firecracker_bin: PathBuf::from("/usr/bin/firecracker"),
50 kernel_path: PathBuf::from("/var/lib/firecracker/vmlinux"),
51 rootfs_path: PathBuf::from("/var/lib/firecracker/rootfs.ext4"),
52 vcpu_count: 2,
53 mem_size_mib: 512,
54 timeout: Duration::from_secs(60),
55 workspace: PathBuf::from("/tmp/firecracker"),
56 }
57 }
58}
59
60impl FirecrackerConfig {
61 #[must_use]
63 pub fn new(kernel: impl Into<PathBuf>, rootfs: impl Into<PathBuf>) -> Self {
64 Self {
65 kernel_path: kernel.into(),
66 rootfs_path: rootfs.into(),
67 ..Default::default()
68 }
69 }
70
71 #[must_use]
73 pub const fn vcpus(mut self, count: u8) -> Self {
74 self.vcpu_count = count;
75 self
76 }
77
78 #[must_use]
80 pub const fn memory(mut self, mib: u32) -> Self {
81 self.mem_size_mib = mib;
82 self
83 }
84
85 #[must_use]
87 pub const fn timeout(mut self, timeout: Duration) -> Self {
88 self.timeout = timeout;
89 self
90 }
91
92 #[must_use]
94 pub fn workspace(mut self, path: impl Into<PathBuf>) -> Self {
95 self.workspace = path.into();
96 self
97 }
98}
99
100#[derive(Clone)]
105pub struct FirecrackerSandbox {
106 config: FirecrackerConfig,
107}
108
109impl FirecrackerSandbox {
110 #[must_use]
112 pub fn new() -> Self {
113 Self {
114 config: FirecrackerConfig::default(),
115 }
116 }
117
118 #[must_use]
120 pub const fn with_config(config: FirecrackerConfig) -> Self {
121 Self { config }
122 }
123
124 #[must_use]
126 pub fn builder() -> FirecrackerSandboxBuilder {
127 FirecrackerSandboxBuilder::new()
128 }
129
130 pub fn check_prerequisites(&self) -> Result<(), SandboxError> {
136 if !self.config.firecracker_bin.exists() {
138 return Err(SandboxError::NotAvailable(format!(
139 "Firecracker binary not found at {}",
140 self.config.firecracker_bin.display()
141 )));
142 }
143
144 if !std::path::Path::new("/dev/kvm").exists() {
146 return Err(SandboxError::NotAvailable(
147 "KVM not available (/dev/kvm not found)".into(),
148 ));
149 }
150
151 if !self.config.kernel_path.exists() {
153 return Err(SandboxError::NotAvailable(format!(
154 "Kernel not found at {}",
155 self.config.kernel_path.display()
156 )));
157 }
158
159 if !self.config.rootfs_path.exists() {
161 return Err(SandboxError::NotAvailable(format!(
162 "Rootfs not found at {}",
163 self.config.rootfs_path.display()
164 )));
165 }
166
167 Ok(())
168 }
169
170 #[cfg(feature = "firecracker")]
172 async fn execute_with_firepilot(&self, _code: &str) -> Result<ExecutionResult, SandboxError> {
173 use firepilot::builder::drive::DriveBuilder;
174 use firepilot::builder::executor::FirecrackerExecutorBuilder;
175 use firepilot::builder::kernel::KernelBuilder;
176 use firepilot::builder::{Builder, Configuration};
177 use firepilot::machine::Machine;
178 use std::path::PathBuf;
179
180 let start = std::time::Instant::now();
181 let vm_id = uuid::Uuid::new_v4().to_string();
182
183 info!("Starting Firecracker microVM: {}", vm_id);
184
185 let workspace = self.config.workspace.join(&vm_id);
187 tokio::fs::create_dir_all(&workspace)
188 .await
189 .map_err(|e| SandboxError::ConfigError(format!("Failed to create workspace: {e}")))?;
190
191 let kernel = KernelBuilder::new()
193 .with_kernel_image_path(self.config.kernel_path.to_string_lossy().to_string())
194 .with_boot_args("console=ttyS0 reboot=k panic=1 pci=off".to_string())
195 .try_build()
196 .map_err(|e| SandboxError::ConfigError(format!("Kernel config error: {e:?}")))?;
197
198 let rootfs = DriveBuilder::new()
200 .with_drive_id("rootfs".to_string())
201 .with_path_on_host(PathBuf::from(&self.config.rootfs_path))
202 .as_root_device()
203 .try_build()
204 .map_err(|e| SandboxError::ConfigError(format!("Drive config error: {e:?}")))?;
205
206 let executor = FirecrackerExecutorBuilder::new()
208 .with_chroot(workspace.to_string_lossy().to_string())
209 .with_exec_binary(PathBuf::from(&self.config.firecracker_bin))
210 .try_build()
211 .map_err(|e| SandboxError::ConfigError(format!("Executor config error: {e:?}")))?;
212
213 let config = Configuration::new(vm_id.clone())
215 .with_kernel(kernel)
216 .with_drive(rootfs)
217 .with_executor(executor);
218
219 let mut machine = Machine::new();
221
222 machine
223 .create(config)
224 .await
225 .map_err(|e| SandboxError::StartError(format!("Failed to create VM: {e:?}")))?;
226
227 machine
228 .start()
229 .await
230 .map_err(|e| SandboxError::StartError(format!("Failed to start VM: {e:?}")))?;
231
232 debug!("MicroVM started, executing code...");
233
234 tokio::time::sleep(Duration::from_secs(2)).await;
237
238 let stdout = format!("MicroVM {vm_id} executed command");
240 let stderr = String::new();
241 let exit_code = 0;
242
243 let _ = machine.stop().await;
245 let _ = machine.kill().await;
246 let _ = tokio::fs::remove_dir_all(&workspace).await;
247
248 #[allow(clippy::cast_possible_truncation)]
249 let execution_time_ms = start.elapsed().as_millis() as u64;
250
251 Ok(ExecutionResult {
252 exit_code,
253 stdout,
254 stderr,
255 execution_time_ms,
256 artifacts: vec![],
257 })
258 }
259
260 #[cfg(not(feature = "firecracker"))]
262 async fn execute_fallback(&self, code: &str) -> Result<ExecutionResult, SandboxError> {
263 warn!("Firecracker feature not enabled, using process fallback");
264
265 let start = std::time::Instant::now();
266
267 let output = tokio::time::timeout(
268 self.config.timeout,
269 tokio::process::Command::new("sh")
270 .arg("-c")
271 .arg(code)
272 .output(),
273 )
274 .await
275 .map_err(|_| SandboxError::Timeout)?
276 .map_err(SandboxError::IoError)?;
277
278 #[allow(clippy::cast_possible_truncation)]
279 let execution_time_ms = start.elapsed().as_millis() as u64;
280
281 Ok(ExecutionResult {
282 exit_code: output.status.code().unwrap_or(-1),
283 stdout: String::from_utf8_lossy(&output.stdout).to_string(),
284 stderr: String::from_utf8_lossy(&output.stderr).to_string(),
285 execution_time_ms,
286 artifacts: vec![],
287 })
288 }
289}
290
291impl Default for FirecrackerSandbox {
292 fn default() -> Self {
293 Self::new()
294 }
295}
296
297#[async_trait]
298impl Sandbox for FirecrackerSandbox {
299 #[instrument(skip(self, code), fields(sandbox = "firecracker"))]
300 async fn execute(&self, code: &str) -> Result<ExecutionResult, SandboxError> {
301 debug!(
302 "Executing in Firecracker sandbox: {}...",
303 &code[..code.len().min(50)]
304 );
305
306 #[cfg(feature = "firecracker")]
307 {
308 self.execute_with_firepilot(code).await
309 }
310
311 #[cfg(not(feature = "firecracker"))]
312 {
313 self.execute_fallback(code).await
314 }
315 }
316
317 async fn is_ready(&self) -> Result<bool, SandboxError> {
318 self.check_prerequisites().map(|()| true)
319 }
320
321 async fn stop(&self) -> Result<(), SandboxError> {
322 Ok(())
323 }
324}
325
326#[derive(Default)]
328pub struct FirecrackerSandboxBuilder {
329 config: FirecrackerConfig,
330}
331
332impl FirecrackerSandboxBuilder {
333 #[must_use]
335 pub fn new() -> Self {
336 Self {
337 config: FirecrackerConfig::default(),
338 }
339 }
340
341 #[must_use]
343 pub fn firecracker_bin(mut self, path: impl Into<PathBuf>) -> Self {
344 self.config.firecracker_bin = path.into();
345 self
346 }
347
348 #[must_use]
350 pub fn kernel(mut self, path: impl Into<PathBuf>) -> Self {
351 self.config.kernel_path = path.into();
352 self
353 }
354
355 #[must_use]
357 pub fn rootfs(mut self, path: impl Into<PathBuf>) -> Self {
358 self.config.rootfs_path = path.into();
359 self
360 }
361
362 #[must_use]
364 pub const fn vcpus(mut self, count: u8) -> Self {
365 self.config.vcpu_count = count;
366 self
367 }
368
369 #[must_use]
371 pub const fn memory(mut self, mib: u32) -> Self {
372 self.config.mem_size_mib = mib;
373 self
374 }
375
376 #[must_use]
378 pub const fn timeout(mut self, timeout: Duration) -> Self {
379 self.config.timeout = timeout;
380 self
381 }
382
383 #[must_use]
385 pub fn workspace(mut self, path: impl Into<PathBuf>) -> Self {
386 self.config.workspace = path.into();
387 self
388 }
389
390 #[must_use]
392 pub fn build(self) -> FirecrackerSandbox {
393 FirecrackerSandbox::with_config(self.config)
394 }
395}
396
397#[cfg(test)]
398mod tests {
399 use super::*;
400
401 #[test]
402 fn test_config_builder() {
403 let config = FirecrackerConfig::new("/path/to/kernel", "/path/to/rootfs")
404 .vcpus(4)
405 .memory(1024)
406 .timeout(Duration::from_secs(120));
407
408 assert_eq!(config.vcpu_count, 4);
409 assert_eq!(config.mem_size_mib, 1024);
410 assert_eq!(config.timeout, Duration::from_secs(120));
411 }
412
413 #[test]
414 fn test_sandbox_builder() {
415 let sandbox = FirecrackerSandbox::builder()
416 .kernel("/custom/kernel")
417 .rootfs("/custom/rootfs")
418 .vcpus(2)
419 .memory(512)
420 .build();
421
422 assert_eq!(sandbox.config.vcpu_count, 2);
423 assert_eq!(sandbox.config.mem_size_mib, 512);
424 }
425
426 #[test]
427 fn test_prerequisites_missing() {
428 let sandbox = FirecrackerSandbox::builder()
429 .firecracker_bin("/nonexistent/firecracker")
430 .build();
431
432 let result = sandbox.check_prerequisites();
433 assert!(result.is_err());
434 }
435}