1use crate::error::LagoResult;
2use serde::{Deserialize, Serialize};
3use std::pin::Pin;
4
5type BoxFuture<'a, T> = Pin<Box<dyn std::future::Future<Output = T> + Send + 'a>>;
7
8#[derive(
13 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize, Default,
14)]
15#[serde(rename_all = "snake_case")]
16pub enum SandboxTier {
17 #[default]
19 None,
20 Basic,
22 Process,
24 Container,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct SandboxConfig {
31 pub tier: SandboxTier,
32 #[serde(default)]
33 pub allowed_paths: Vec<String>,
34 #[serde(default)]
35 pub allowed_commands: Vec<String>,
36 #[serde(default = "default_true")]
37 pub network_access: bool,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub max_memory_mb: Option<u64>,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub max_cpu_seconds: Option<u64>,
42}
43
44fn default_true() -> bool {
45 true
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct SandboxExecRequest {
51 pub command: String,
52 #[serde(default)]
53 pub args: Vec<String>,
54 #[serde(default)]
55 pub env: Vec<(String, String)>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub working_dir: Option<String>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub stdin: Option<String>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub timeout_ms: Option<u64>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct SandboxExecResult {
67 pub exit_code: i32,
68 pub stdout: String,
69 pub stderr: String,
70 pub duration_ms: u64,
71}
72
73pub trait Sandbox: Send + Sync {
81 fn tier(&self) -> SandboxTier;
83
84 fn config(&self) -> &SandboxConfig;
86
87 fn execute(&self, request: SandboxExecRequest) -> BoxFuture<'_, LagoResult<SandboxExecResult>>;
89
90 fn read_file(&self, path: &str) -> BoxFuture<'_, LagoResult<Vec<u8>>>;
92
93 fn write_file(&self, path: &str, data: &[u8]) -> BoxFuture<'_, LagoResult<()>>;
95
96 fn destroy(&self) -> BoxFuture<'_, LagoResult<()>>;
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn sandbox_tier_ordering() {
106 assert!(SandboxTier::None < SandboxTier::Basic);
107 assert!(SandboxTier::Basic < SandboxTier::Process);
108 assert!(SandboxTier::Process < SandboxTier::Container);
109 }
110
111 #[test]
112 fn sandbox_tier_serde_roundtrip() {
113 for tier in [
114 SandboxTier::None,
115 SandboxTier::Basic,
116 SandboxTier::Process,
117 SandboxTier::Container,
118 ] {
119 let json = serde_json::to_string(&tier).unwrap();
120 let back: SandboxTier = serde_json::from_str(&json).unwrap();
121 assert_eq!(back, tier);
122 }
123 assert_eq!(
124 serde_json::to_string(&SandboxTier::None).unwrap(),
125 "\"none\""
126 );
127 assert_eq!(
128 serde_json::to_string(&SandboxTier::Container).unwrap(),
129 "\"container\""
130 );
131 }
132
133 #[test]
134 fn sandbox_tier_default() {
135 assert_eq!(SandboxTier::default(), SandboxTier::None);
136 }
137
138 #[test]
139 fn sandbox_config_serde_roundtrip() {
140 let config = SandboxConfig {
141 tier: SandboxTier::Process,
142 allowed_paths: vec!["/tmp".to_string(), "/workspace".to_string()],
143 allowed_commands: vec!["cargo".to_string(), "rustc".to_string()],
144 network_access: false,
145 max_memory_mb: Some(512),
146 max_cpu_seconds: Some(60),
147 };
148 let json = serde_json::to_string(&config).unwrap();
149 let back: SandboxConfig = serde_json::from_str(&json).unwrap();
150 assert_eq!(back.tier, SandboxTier::Process);
151 assert_eq!(back.allowed_paths.len(), 2);
152 assert!(!back.network_access);
153 assert_eq!(back.max_memory_mb, Some(512));
154 }
155
156 #[test]
157 fn sandbox_config_defaults() {
158 let json = r#"{"tier": "basic"}"#;
159 let config: SandboxConfig = serde_json::from_str(json).unwrap();
160 assert_eq!(config.tier, SandboxTier::Basic);
161 assert!(config.allowed_paths.is_empty());
162 assert!(config.allowed_commands.is_empty());
163 assert!(config.network_access); assert!(config.max_memory_mb.is_none());
165 }
166
167 #[test]
168 fn sandbox_exec_request_serde_roundtrip() {
169 let req = SandboxExecRequest {
170 command: "cargo".to_string(),
171 args: vec!["test".to_string()],
172 env: vec![("RUST_LOG".to_string(), "debug".to_string())],
173 working_dir: Some("/workspace".to_string()),
174 stdin: None,
175 timeout_ms: Some(30000),
176 };
177 let json = serde_json::to_string(&req).unwrap();
178 let back: SandboxExecRequest = serde_json::from_str(&json).unwrap();
179 assert_eq!(back.command, "cargo");
180 assert_eq!(back.args, vec!["test"]);
181 assert_eq!(back.timeout_ms, Some(30000));
182 }
183
184 #[test]
185 fn sandbox_exec_result_serde_roundtrip() {
186 let result = SandboxExecResult {
187 exit_code: 0,
188 stdout: "ok".to_string(),
189 stderr: String::new(),
190 duration_ms: 1234,
191 };
192 let json = serde_json::to_string(&result).unwrap();
193 let back: SandboxExecResult = serde_json::from_str(&json).unwrap();
194 assert_eq!(back.exit_code, 0);
195 assert_eq!(back.duration_ms, 1234);
196 }
197
198 #[test]
199 fn sandbox_tier_ge_comparison() {
200 let required = SandboxTier::Process;
202 assert!(SandboxTier::Process >= required);
203 assert!(SandboxTier::Container >= required);
204 assert!(SandboxTier::Basic < required);
205 assert!(SandboxTier::None < required);
206 }
207}