1use gestura_core_foundation::AppError;
22use std::collections::HashMap;
23use std::path::PathBuf;
24
25#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
29pub struct SandboxConfig {
30 pub max_memory_mb: u64,
32 pub max_cpu_time_secs: u64,
34 pub allowed_read_paths: Vec<PathBuf>,
36 pub allowed_write_paths: Vec<PathBuf>,
38 pub allowed_hosts: Vec<String>,
40 pub env_vars: HashMap<String, String>,
42}
43
44impl Default for SandboxConfig {
45 fn default() -> Self {
46 Self {
47 max_memory_mb: 512,
48 max_cpu_time_secs: 300,
49 allowed_read_paths: vec![],
50 allowed_write_paths: vec![],
51 allowed_hosts: vec![],
52 env_vars: HashMap::new(),
53 }
54 }
55}
56
57impl SandboxConfig {
58 pub fn new() -> Self {
60 Self::default()
61 }
62
63 pub fn with_memory_limit(mut self, mb: u64) -> Self {
65 self.max_memory_mb = mb;
66 self
67 }
68
69 pub fn with_cpu_limit(mut self, secs: u64) -> Self {
71 self.max_cpu_time_secs = secs;
72 self
73 }
74
75 pub fn with_read_path(mut self, path: PathBuf) -> Self {
77 self.allowed_read_paths.push(path);
78 self
79 }
80
81 pub fn with_write_path(mut self, path: PathBuf) -> Self {
83 self.allowed_write_paths.push(path);
84 self
85 }
86
87 pub fn with_allowed_host(mut self, host: String) -> Self {
89 self.allowed_hosts.push(host);
90 self
91 }
92
93 pub fn with_env(mut self, key: String, value: String) -> Self {
95 self.env_vars.insert(key, value);
96 self
97 }
98}
99
100pub struct SandboxManager {
105 configs: HashMap<String, SandboxConfig>,
106}
107
108impl Default for SandboxManager {
109 fn default() -> Self {
110 Self::new()
111 }
112}
113
114impl SandboxManager {
115 pub fn new() -> Self {
117 Self {
118 configs: HashMap::new(),
119 }
120 }
121
122 pub fn register_agent(&mut self, agent_id: &str, config: SandboxConfig) {
124 tracing::debug!(
125 agent_id = %agent_id,
126 max_memory_mb = config.max_memory_mb,
127 max_cpu_time_secs = config.max_cpu_time_secs,
128 "Registering sandbox config for agent"
129 );
130 self.configs.insert(agent_id.to_string(), config);
131 }
132
133 pub fn unregister_agent(&mut self, agent_id: &str) {
135 self.configs.remove(agent_id);
136 }
137
138 pub fn get_config(&self, agent_id: &str) -> SandboxConfig {
140 self.configs.get(agent_id).cloned().unwrap_or_default()
141 }
142
143 pub fn has_agent(&self, agent_id: &str) -> bool {
145 self.configs.contains_key(agent_id)
146 }
147
148 pub fn list_agents(&self) -> Vec<String> {
150 self.configs.keys().cloned().collect()
151 }
152
153 pub fn apply_sandbox(
157 &self,
158 agent_id: &str,
159 mut cmd: tokio::process::Command,
160 ) -> tokio::process::Command {
161 let config = self.get_config(agent_id);
162
163 #[cfg(unix)]
165 {
166 cmd.env(
167 "RLIMIT_AS",
168 (config.max_memory_mb * 1024 * 1024).to_string(),
169 );
170 cmd.env("RLIMIT_CPU", config.max_cpu_time_secs.to_string());
171 }
172
173 cmd.env_clear();
175 for (key, value) in &config.env_vars {
176 cmd.env(key, value);
177 }
178
179 cmd.env("GESTURA_AGENT_ID", agent_id);
181 cmd.env("GESTURA_SANDBOX", "1");
182
183 tracing::info!("Applied sandbox config for agent: {}", agent_id);
184 cmd
185 }
186
187 pub fn validate_file_access(
191 &self,
192 agent_id: &str,
193 path: &PathBuf,
194 write_access: bool,
195 ) -> Result<(), AppError> {
196 let config = self.get_config(agent_id);
197 let access_type = if write_access { "write" } else { "read" };
198
199 tracing::debug!(
200 agent_id = %agent_id,
201 path = ?path,
202 access_type = %access_type,
203 "Validating file access for agent"
204 );
205
206 if write_access {
207 for allowed_path in &config.allowed_write_paths {
208 if path.starts_with(allowed_path) {
209 tracing::debug!(
210 agent_id = %agent_id,
211 path = ?path,
212 matched_pattern = ?allowed_path,
213 "File write access granted"
214 );
215 return Ok(());
216 }
217 }
218 tracing::warn!(
219 agent_id = %agent_id,
220 path = ?path,
221 allowed_write_paths = ?config.allowed_write_paths,
222 "File write access denied"
223 );
224 Err(AppError::PermissionDenied(format!(
225 "Write access denied for agent {} to path: {:?}",
226 agent_id, path
227 )))
228 } else {
229 for allowed_path in &config.allowed_read_paths {
231 if path.starts_with(allowed_path) {
232 return Ok(());
233 }
234 }
235 for allowed_path in &config.allowed_write_paths {
237 if path.starts_with(allowed_path) {
238 return Ok(());
239 }
240 }
241 tracing::warn!(
242 agent_id = %agent_id,
243 path = ?path,
244 allowed_read_paths = ?config.allowed_read_paths,
245 allowed_write_paths = ?config.allowed_write_paths,
246 "File read access denied"
247 );
248 Err(AppError::PermissionDenied(format!(
249 "Read access denied for agent {} to path: {:?}",
250 agent_id, path
251 )))
252 }
253 }
254
255 pub fn validate_network_access(&self, agent_id: &str, host: &str) -> Result<(), AppError> {
259 let config = self.get_config(agent_id);
260
261 tracing::debug!(
262 agent_id = %agent_id,
263 host = %host,
264 "Validating network access for agent"
265 );
266
267 if config.allowed_hosts.is_empty() {
268 return Ok(());
269 }
270
271 for allowed_host in &config.allowed_hosts {
272 if host == allowed_host || host.ends_with(&format!(".{}", allowed_host)) {
273 return Ok(());
274 }
275 }
276
277 tracing::warn!(
278 agent_id = %agent_id,
279 host = %host,
280 allowed_hosts = ?config.allowed_hosts,
281 "Network access denied"
282 );
283 Err(AppError::PermissionDenied(format!(
284 "Network access denied for agent {} to host: {}",
285 agent_id, host
286 )))
287 }
288}
289
290pub fn create_default_sandbox(agent_type: &str) -> SandboxConfig {
294 tracing::debug!(agent_type = %agent_type, "Creating default sandbox config");
295
296 let mut config = SandboxConfig::default();
297
298 match agent_type {
299 "voice-agent" => {
300 config.max_memory_mb = 256;
301 config.max_cpu_time_secs = 60;
302 if let Some(temp_dir) = std::env::temp_dir().to_str() {
303 config.allowed_read_paths.push(PathBuf::from(temp_dir));
304 }
305 }
306 "mcp-agent" => {
307 config.max_memory_mb = 512;
308 config.max_cpu_time_secs = 300;
309 config.allowed_hosts = vec!["localhost".to_string(), "127.0.0.1".to_string()];
310 }
311 "default-agent" => {
312 config.max_memory_mb = 128;
313 config.max_cpu_time_secs = 120;
314 }
315 _ => {
316 tracing::debug!(
317 agent_type = %agent_type,
318 "Unknown agent type, using restrictive defaults"
319 );
320 config.max_memory_mb = 64;
321 config.max_cpu_time_secs = 30;
322 }
323 }
324
325 tracing::debug!(
326 agent_type = %agent_type,
327 max_memory_mb = config.max_memory_mb,
328 max_cpu_time_secs = config.max_cpu_time_secs,
329 "Created sandbox config"
330 );
331
332 config
333}
334
335#[cfg(test)]
336mod tests {
337 use super::*;
338
339 #[test]
340 fn test_sandbox_config_defaults() {
341 let config = SandboxConfig::default();
342 assert_eq!(config.max_memory_mb, 512);
343 assert_eq!(config.max_cpu_time_secs, 300);
344 assert!(config.allowed_read_paths.is_empty());
345 assert!(config.allowed_write_paths.is_empty());
346 assert!(config.allowed_hosts.is_empty());
347 }
348
349 #[test]
350 fn test_sandbox_config_builder() {
351 let config = SandboxConfig::new()
352 .with_memory_limit(256)
353 .with_cpu_limit(60)
354 .with_read_path(PathBuf::from("/tmp"))
355 .with_write_path(PathBuf::from("/var/app"))
356 .with_allowed_host("localhost".to_string())
357 .with_env("KEY".to_string(), "VALUE".to_string());
358
359 assert_eq!(config.max_memory_mb, 256);
360 assert_eq!(config.max_cpu_time_secs, 60);
361 assert_eq!(config.allowed_read_paths.len(), 1);
362 assert_eq!(config.allowed_write_paths.len(), 1);
363 assert_eq!(config.allowed_hosts.len(), 1);
364 assert_eq!(config.env_vars.get("KEY"), Some(&"VALUE".to_string()));
365 }
366
367 #[test]
368 fn test_sandbox_config_creation() {
369 let voice_config = create_default_sandbox("voice-agent");
370 assert_eq!(voice_config.max_memory_mb, 256);
371 assert_eq!(voice_config.max_cpu_time_secs, 60);
372
373 let mcp_config = create_default_sandbox("mcp-agent");
374 assert_eq!(mcp_config.max_memory_mb, 512);
375 assert!(mcp_config.allowed_hosts.contains(&"localhost".to_string()));
376
377 let default_config = create_default_sandbox("default-agent");
378 assert_eq!(default_config.max_memory_mb, 128);
379
380 let unknown_config = create_default_sandbox("unknown");
381 assert_eq!(unknown_config.max_memory_mb, 64);
382 }
383
384 #[test]
385 fn test_sandbox_manager_registration() {
386 let mut manager = SandboxManager::new();
387 assert!(!manager.has_agent("test"));
388
389 let config = SandboxConfig::default();
390 manager.register_agent("test", config);
391 assert!(manager.has_agent("test"));
392
393 let agents = manager.list_agents();
394 assert!(agents.contains(&"test".to_string()));
395
396 manager.unregister_agent("test");
397 assert!(!manager.has_agent("test"));
398 }
399
400 #[test]
401 fn test_file_access_validation() {
402 let mut manager = SandboxManager::new();
403 let config = SandboxConfig::new()
404 .with_read_path(PathBuf::from("/tmp"))
405 .with_write_path(PathBuf::from("/var/app"));
406 manager.register_agent("test-agent", config);
407
408 assert!(
409 manager
410 .validate_file_access("test-agent", &PathBuf::from("/tmp/test.txt"), false)
411 .is_ok()
412 );
413 assert!(
414 manager
415 .validate_file_access("test-agent", &PathBuf::from("/var/app/data.json"), true)
416 .is_ok()
417 );
418 assert!(
419 manager
420 .validate_file_access("test-agent", &PathBuf::from("/var/app/data.json"), false)
421 .is_ok()
422 );
423 assert!(
424 manager
425 .validate_file_access("test-agent", &PathBuf::from("/etc/passwd"), false)
426 .is_err()
427 );
428 assert!(
429 manager
430 .validate_file_access("test-agent", &PathBuf::from("/tmp/test.txt"), true)
431 .is_err()
432 );
433 }
434
435 #[test]
436 fn test_network_access_validation() {
437 let mut manager = SandboxManager::new();
438 let config = SandboxConfig::new()
439 .with_allowed_host("localhost".to_string())
440 .with_allowed_host("api.example.com".to_string());
441 manager.register_agent("test-agent", config);
442
443 assert!(
444 manager
445 .validate_network_access("test-agent", "localhost")
446 .is_ok()
447 );
448 assert!(
449 manager
450 .validate_network_access("test-agent", "api.example.com")
451 .is_ok()
452 );
453 assert!(
454 manager
455 .validate_network_access("test-agent", "sub.api.example.com")
456 .is_ok()
457 );
458 assert!(
459 manager
460 .validate_network_access("test-agent", "evil.com")
461 .is_err()
462 );
463 }
464
465 #[test]
466 fn test_network_access_permissive_default() {
467 let mut manager = SandboxManager::new();
468 let config = SandboxConfig::default();
469 manager.register_agent("permissive", config);
470 assert!(
471 manager
472 .validate_network_access("permissive", "any.host.com")
473 .is_ok()
474 );
475 }
476}