Skip to main content

sh_layer3/
sandbox_runtime.rs

1//! # Sandbox Runtime
2//!
3//! 沙箱运行时:安全隔离的执行环境。
4//!
5//! ## 实现方式
6//! - 进程级隔离(默认)
7//! - 资源限制(内存、CPU、时间)
8//! - 网络策略控制
9//! - 文件系统隔离
10
11use crate::types::{Layer3Result, ToolRequest, ToolResponse};
12use async_trait::async_trait;
13use parking_lot::RwLock;
14use sh_layer1::generate_short_id;
15use std::collections::HashMap;
16use std::path::PathBuf;
17use std::process::Command;
18use std::time::{Duration, Instant};
19
20/// 沙箱运行时 trait
21///
22/// 提供安全隔离的代码执行环境。
23#[async_trait]
24pub trait SandboxRuntime: Send + Sync {
25    /// 创建沙箱
26    async fn create(&self, config: SandboxConfig) -> Layer3Result<SandboxId>;
27
28    /// 销毁沙箱
29    async fn destroy(&self, id: &SandboxId) -> Layer3Result<bool>;
30
31    /// 在沙箱中执行代码
32    async fn execute(
33        &self,
34        id: &SandboxId,
35        code: &str,
36        language: &str,
37    ) -> Layer3Result<ExecutionResult>;
38
39    /// 在沙箱中执行工具
40    async fn execute_tool(
41        &self,
42        id: &SandboxId,
43        request: ToolRequest,
44    ) -> Layer3Result<ToolResponse>;
45
46    /// 获取沙箱状态
47    async fn status(&self, id: &SandboxId) -> Layer3Result<SandboxStatus>;
48
49    /// 获取沙箱信息
50    async fn info(&self, id: &SandboxId) -> Layer3Result<Option<SandboxInfo>>;
51
52    /// 列出所有活跃沙箱
53    async fn list(&self) -> Layer3Result<Vec<SandboxInfo>>;
54
55    /// 重置沙箱
56    async fn reset(&self, id: &SandboxId) -> Layer3Result<bool>;
57}
58
59/// 沙箱 ID
60#[derive(Debug, Clone, PartialEq, Eq, Hash)]
61pub struct SandboxId(pub String);
62
63impl std::fmt::Display for SandboxId {
64    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
65        write!(f, "{}", self.0)
66    }
67}
68
69/// 沙箱配置
70#[derive(Debug, Clone)]
71pub struct SandboxConfig {
72    /// 基础镜像/环境
73    pub base_image: String,
74    /// 资源限制
75    pub limits: SandboxLimits,
76    /// 允许的网络访问
77    pub network: NetworkPolicy,
78    /// 允许的文件系统访问
79    pub filesystem: FsPolicy,
80    /// 环境变量
81    pub env_vars: HashMap<String, String>,
82    /// 工作目录
83    pub working_dir: PathBuf,
84    /// 最大执行时间(秒)
85    pub timeout_secs: u64,
86    /// 是否允许交互
87    pub interactive: bool,
88}
89
90impl Default for SandboxConfig {
91    fn default() -> Self {
92        Self {
93            base_image: "default".to_string(),
94            limits: SandboxLimits::default(),
95            network: NetworkPolicy::Disabled,
96            filesystem: FsPolicy::ReadOnly,
97            env_vars: HashMap::new(),
98            working_dir: PathBuf::from("/sandbox"),
99            timeout_secs: 30,
100            interactive: false,
101        }
102    }
103}
104
105/// 沙箱资源限制
106#[derive(Debug, Clone, Default)]
107pub struct SandboxLimits {
108    /// 最大内存(字节)
109    pub max_memory: Option<u64>,
110    /// 最大 CPU(百分比)
111    pub max_cpu_percent: Option<u32>,
112    /// 最大文件大小(字节)
113    pub max_file_size: Option<u64>,
114    /// 最大进程数
115    pub max_processes: Option<u32>,
116}
117
118/// 网络策略
119#[derive(Debug, Clone, PartialEq, Eq)]
120pub enum NetworkPolicy {
121    /// 禁止网络
122    Disabled,
123    /// 仅允许出站
124    OutboundOnly,
125    /// 允许特定端口
126    RestrictedPorts(Vec<u16>),
127    /// 完全开放(危险)
128    Full,
129}
130
131/// 文件系统策略
132#[derive(Debug, Clone, PartialEq, Eq)]
133pub enum FsPolicy {
134    /// 只读
135    ReadOnly,
136    /// 仅允许特定目录
137    RestrictedDirs(Vec<PathBuf>),
138    /// 临时可写(执行后清空)
139    TempWritable,
140    /// 完全可写(危险)
141    FullWritable,
142}
143
144/// 沙箱状态
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub enum SandboxStatus {
147    Creating,
148    Ready,
149    Running,
150    Paused,
151    Error,
152    Destroyed,
153}
154
155/// 沙箱信息
156#[derive(Debug, Clone)]
157pub struct SandboxInfo {
158    /// 沙箱 ID
159    pub id: SandboxId,
160    /// 状态
161    pub status: SandboxStatus,
162    /// 创建时间
163    pub created_at: chrono::DateTime<chrono::Utc>,
164    /// 当前内存使用
165    pub memory_used: u64,
166    /// 当前 CPU 使用
167    pub cpu_used: f32,
168    /// 执行次数
169    pub executions: u32,
170    /// 配置
171    pub config: SandboxConfig,
172}
173
174/// 执行结果
175#[derive(Debug, Clone)]
176pub struct ExecutionResult {
177    /// 标准输出
178    pub stdout: String,
179    /// 标准错误
180    pub stderr: String,
181    /// 退出码
182    pub exit_code: i32,
183    /// 执行时间(毫秒)
184    pub duration_ms: u64,
185    /// 是否超时
186    pub timed_out: bool,
187    /// 是否被杀
188    pub killed: bool,
189}
190
191impl ExecutionResult {
192    /// 创建成功结果
193    pub fn success(stdout: String) -> Self {
194        Self {
195            stdout,
196            stderr: String::new(),
197            exit_code: 0,
198            duration_ms: 0,
199            timed_out: false,
200            killed: false,
201        }
202    }
203
204    /// 创建失败结果
205    pub fn failure(stderr: String, exit_code: i32) -> Self {
206        Self {
207            stdout: String::new(),
208            stderr,
209            exit_code,
210            duration_ms: 0,
211            timed_out: false,
212            killed: false,
213        }
214    }
215
216    /// 创建超时结果
217    pub fn timeout(stdout: String, stderr: String) -> Self {
218        Self {
219            stdout,
220            stderr,
221            exit_code: -1,
222            duration_ms: 0,
223            timed_out: true,
224            killed: false,
225        }
226    }
227
228    /// 是否成功
229    pub fn is_success(&self) -> bool {
230        self.exit_code == 0 && !self.timed_out && !self.killed
231    }
232}
233
234// ============================================================================
235// Default Sandbox Runtime Implementation
236// ============================================================================
237
238/// 默认沙箱运行时实现
239///
240/// 使用进程隔离和资源限制实现沙箱。
241pub struct DefaultSandboxRuntime {
242    sandboxes: RwLock<HashMap<String, SandboxInfo>>,
243    temp_dir: PathBuf,
244}
245
246impl DefaultSandboxRuntime {
247    /// 创建新的沙箱运行时
248    pub fn new() -> Layer3Result<Self> {
249        let temp_dir = std::env::temp_dir().join("continuum_sandboxes");
250        std::fs::create_dir_all(&temp_dir)?;
251
252        Ok(Self {
253            sandboxes: RwLock::new(HashMap::new()),
254            temp_dir,
255        })
256    }
257
258    /// 获取语言的执行命令
259    fn get_language_command(language: &str) -> Option<(&'static str, &'static str)> {
260        match language.to_lowercase().as_str() {
261            "python" | "python3" | "py" => Some(("python", "-c")),
262            "javascript" | "js" | "node" => Some(("node", "-e")),
263            "ruby" | "rb" => Some(("ruby", "-e")),
264            "perl" | "pl" => Some(("perl", "-e")),
265            "bash" | "sh" | "shell" => Some(("bash", "-c")),
266            "lua" => Some(("lua", "-e")),
267            _ => None,
268        }
269    }
270
271    /// 检查命令是否可用
272    fn command_exists(cmd: &str) -> bool {
273        #[cfg(target_os = "windows")]
274        {
275            Command::new("where")
276                .arg(cmd)
277                .output()
278                .map(|o| o.status.success())
279                .unwrap_or(false)
280        }
281        #[cfg(not(target_os = "windows"))]
282        {
283            Command::new("which")
284                .arg(cmd)
285                .output()
286                .map(|o| o.status.success())
287                .unwrap_or(false)
288        }
289    }
290
291    /// 执行命令带超时
292    fn execute_with_timeout(
293        &self,
294        cmd: &str,
295        args: &[&str],
296        input: Option<&str>,
297        timeout_secs: u64,
298    ) -> ExecutionResult {
299        let start = Instant::now();
300
301        let mut command = Command::new(cmd);
302        command.args(args);
303
304        // Set environment variables
305        command.env_clear();
306        command.env("PATH", std::env::var("PATH").unwrap_or_default());
307
308        // Set working directory
309        command.current_dir(&self.temp_dir);
310
311        // Capture output
312        command.stdout(std::process::Stdio::piped());
313        command.stderr(std::process::Stdio::piped());
314
315        // Write input if provided
316        if input.is_some() {
317            command.stdin(std::process::Stdio::piped());
318        }
319
320        let spawn_result = command.spawn();
321
322        match spawn_result {
323            Ok(mut child) => {
324                // Write input if provided
325                if let Some(input_data) = input {
326                    use std::io::Write;
327                    if let Some(mut stdin) = child.stdin.take() {
328                        let _ = stdin.write_all(input_data.as_bytes());
329                    }
330                }
331
332                // Wait with timeout
333                let timeout = Duration::from_secs(timeout_secs);
334                let result = child.wait_timeout(timeout);
335
336                match result {
337                    Ok(Some(status)) => {
338                        let stdout = read_child_stdout(&mut child);
339                        let stderr = read_child_stderr(&mut child);
340                        let duration_ms = start.elapsed().as_millis() as u64;
341
342                        ExecutionResult {
343                            stdout,
344                            stderr,
345                            exit_code: status.code().unwrap_or(-1),
346                            duration_ms,
347                            timed_out: false,
348                            killed: false,
349                        }
350                    }
351                    Ok(None) => {
352                        // Timeout - kill the process
353                        let _ = child.kill();
354                        let _ = child.wait();
355                        let stdout = read_child_stdout(&mut child);
356                        let stderr = read_child_stderr(&mut child);
357
358                        ExecutionResult::timeout(stdout, stderr)
359                    }
360                    Err(e) => ExecutionResult::failure(format!("Wait error: {}", e), -1),
361                }
362            }
363            Err(e) => ExecutionResult::failure(format!("Spawn error: {}", e), -1),
364        }
365    }
366}
367
368impl Default for DefaultSandboxRuntime {
369    fn default() -> Self {
370        Self::new().expect("Failed to create DefaultSandboxRuntime")
371    }
372}
373
374#[async_trait]
375impl SandboxRuntime for DefaultSandboxRuntime {
376    async fn create(&self, config: SandboxConfig) -> Layer3Result<SandboxId> {
377        let id = SandboxId(generate_short_id());
378
379        let info = SandboxInfo {
380            id: id.clone(),
381            status: SandboxStatus::Ready,
382            created_at: chrono::Utc::now(),
383            memory_used: 0,
384            cpu_used: 0.0,
385            executions: 0,
386            config,
387        };
388
389        self.sandboxes.write().insert(id.0.clone(), info);
390        tracing::info!("Created sandbox: {}", id);
391
392        Ok(id)
393    }
394
395    async fn destroy(&self, id: &SandboxId) -> Layer3Result<bool> {
396        let mut sandboxes = self.sandboxes.write();
397        if let Some(mut info) = sandboxes.remove(&id.0) {
398            info.status = SandboxStatus::Destroyed;
399            tracing::info!("Destroyed sandbox: {}", id);
400            Ok(true)
401        } else {
402            Ok(false)
403        }
404    }
405
406    async fn execute(
407        &self,
408        id: &SandboxId,
409        code: &str,
410        language: &str,
411    ) -> Layer3Result<ExecutionResult> {
412        // Check sandbox exists and is ready
413        {
414            let sandboxes = self.sandboxes.read();
415            let info = sandboxes
416                .get(&id.0)
417                .ok_or_else(|| anyhow::anyhow!("Sandbox not found: {}", id))?;
418
419            if info.status != SandboxStatus::Ready {
420                return Err(anyhow::anyhow!("Sandbox not ready: {:?}", info.status));
421            }
422        }
423
424        // Update status to running
425        {
426            let mut sandboxes = self.sandboxes.write();
427            if let Some(info) = sandboxes.get_mut(&id.0) {
428                info.status = SandboxStatus::Running;
429            }
430        }
431
432        // Get language command
433        let (cmd, flag) = Self::get_language_command(language)
434            .ok_or_else(|| anyhow::anyhow!("Unsupported language: {}", language))?;
435
436        // Check command exists
437        if !Self::command_exists(cmd) {
438            return Err(anyhow::anyhow!("Command not found: {}", cmd));
439        }
440
441        // Execute with timeout
442        let timeout = {
443            let sandboxes = self.sandboxes.read();
444            sandboxes
445                .get(&id.0)
446                .map(|i| i.config.timeout_secs)
447                .unwrap_or(30)
448        };
449
450        let result = self.execute_with_timeout(cmd, &[flag, code], None, timeout);
451
452        // Update status and stats
453        {
454            let mut sandboxes = self.sandboxes.write();
455            if let Some(info) = sandboxes.get_mut(&id.0) {
456                info.status = SandboxStatus::Ready;
457                info.executions += 1;
458            }
459        }
460
461        Ok(result)
462    }
463
464    async fn execute_tool(
465        &self,
466        id: &SandboxId,
467        request: ToolRequest,
468    ) -> Layer3Result<ToolResponse> {
469        Err(anyhow::anyhow!(
470            "[experimental] Tool execution in sandbox is not yet implemented (sandbox_id: {}, tool: {})",
471            id, request.name
472        ))
473    }
474
475    async fn status(&self, id: &SandboxId) -> Layer3Result<SandboxStatus> {
476        let sandboxes = self.sandboxes.read();
477        let info = sandboxes
478            .get(&id.0)
479            .ok_or_else(|| anyhow::anyhow!("Sandbox not found: {}", id))?;
480        Ok(info.status)
481    }
482
483    async fn info(&self, id: &SandboxId) -> Layer3Result<Option<SandboxInfo>> {
484        let sandboxes = self.sandboxes.read();
485        Ok(sandboxes.get(&id.0).cloned())
486    }
487
488    async fn list(&self) -> Layer3Result<Vec<SandboxInfo>> {
489        Ok(self.sandboxes.read().values().cloned().collect())
490    }
491
492    async fn reset(&self, id: &SandboxId) -> Layer3Result<bool> {
493        let mut sandboxes = self.sandboxes.write();
494        if let Some(info) = sandboxes.get_mut(&id.0) {
495            info.status = SandboxStatus::Ready;
496            info.executions = 0;
497            info.memory_used = 0;
498            info.cpu_used = 0.0;
499            tracing::info!("Reset sandbox: {}", id);
500            Ok(true)
501        } else {
502            Ok(false)
503        }
504    }
505}
506
507// ============================================================================
508// Helper Functions
509// ============================================================================
510
511/// Read child process stdout
512fn read_child_stdout(child: &mut std::process::Child) -> String {
513    use std::io::Read;
514    if let Some(mut stdout) = child.stdout.take() {
515        let mut buf = String::new();
516        let _ = stdout.read_to_string(&mut buf);
517        buf
518    } else {
519        String::new()
520    }
521}
522
523/// Read child process stderr
524fn read_child_stderr(child: &mut std::process::Child) -> String {
525    use std::io::Read;
526    if let Some(mut stderr) = child.stderr.take() {
527        let mut buf = String::new();
528        let _ = stderr.read_to_string(&mut buf);
529        buf
530    } else {
531        String::new()
532    }
533}
534
535/// Extension trait for child process wait with timeout
536trait ChildWaitTimeout {
537    fn wait_timeout(
538        &mut self,
539        timeout: Duration,
540    ) -> std::io::Result<Option<std::process::ExitStatus>>;
541}
542
543impl ChildWaitTimeout for std::process::Child {
544    fn wait_timeout(
545        &mut self,
546        timeout: Duration,
547    ) -> std::io::Result<Option<std::process::ExitStatus>> {
548        let start = Instant::now();
549
550        loop {
551            match self.try_wait()? {
552                Some(status) => return Ok(Some(status)),
553                None => {
554                    if start.elapsed() >= timeout {
555                        return Ok(None);
556                    }
557                    std::thread::sleep(Duration::from_millis(10));
558                }
559            }
560        }
561    }
562}
563
564#[cfg(test)]
565mod tests {
566    use super::*;
567
568    #[test]
569    fn test_sandbox_config_default() {
570        let config = SandboxConfig::default();
571        assert_eq!(config.timeout_secs, 30);
572        assert_eq!(config.network, NetworkPolicy::Disabled);
573    }
574
575    #[test]
576    fn test_execution_result_success() {
577        let result = ExecutionResult::success("hello".to_string());
578        assert!(result.is_success());
579    }
580
581    #[test]
582    fn test_execution_result_timeout() {
583        let result = ExecutionResult::timeout("out".to_string(), "err".to_string());
584        assert!(result.timed_out);
585        assert!(!result.is_success());
586    }
587
588    #[test]
589    fn test_sandbox_id_display() {
590        let id = SandboxId("abc123".to_string());
591        assert_eq!(format!("{}", id), "abc123");
592    }
593
594    #[test]
595    fn test_language_command_mapping() {
596        assert!(DefaultSandboxRuntime::get_language_command("python").is_some());
597        assert!(DefaultSandboxRuntime::get_language_command("javascript").is_some());
598        assert!(DefaultSandboxRuntime::get_language_command("bash").is_some());
599        assert!(DefaultSandboxRuntime::get_language_command("unknown").is_none());
600    }
601
602    #[tokio::test]
603    async fn test_sandbox_create_and_destroy() {
604        let runtime = DefaultSandboxRuntime::new().unwrap();
605        let config = SandboxConfig::default();
606
607        let id = runtime.create(config).await.unwrap();
608        assert!(!id.0.is_empty());
609
610        let status = runtime.status(&id).await.unwrap();
611        assert_eq!(status, SandboxStatus::Ready);
612
613        let destroyed = runtime.destroy(&id).await.unwrap();
614        assert!(destroyed);
615
616        let status = runtime.status(&id).await;
617        assert!(status.is_err());
618    }
619
620    #[tokio::test]
621    async fn test_sandbox_list() {
622        let runtime = DefaultSandboxRuntime::new().unwrap();
623
624        let id1 = runtime.create(SandboxConfig::default()).await.unwrap();
625        let id2 = runtime.create(SandboxConfig::default()).await.unwrap();
626
627        let list = runtime.list().await.unwrap();
628        assert_eq!(list.len(), 2);
629
630        runtime.destroy(&id1).await.unwrap();
631        runtime.destroy(&id2).await.unwrap();
632
633        let list = runtime.list().await.unwrap();
634        assert!(list.is_empty());
635    }
636
637    #[tokio::test]
638    async fn test_sandbox_reset() {
639        let runtime = DefaultSandboxRuntime::new().unwrap();
640        let id = runtime.create(SandboxConfig::default()).await.unwrap();
641
642        // Manually increment executions
643        {
644            let mut sandboxes = runtime.sandboxes.write();
645            if let Some(info) = sandboxes.get_mut(&id.0) {
646                info.executions = 5;
647            }
648        }
649
650        let reset = runtime.reset(&id).await.unwrap();
651        assert!(reset);
652
653        let info = runtime.info(&id).await.unwrap().unwrap();
654        assert_eq!(info.executions, 0);
655
656        runtime.destroy(&id).await.unwrap();
657    }
658
659    #[test]
660    fn test_network_policy_equality() {
661        assert_eq!(NetworkPolicy::Disabled, NetworkPolicy::Disabled);
662        assert_ne!(NetworkPolicy::Disabled, NetworkPolicy::Full);
663    }
664
665    #[test]
666    fn test_fs_policy_equality() {
667        assert_eq!(FsPolicy::ReadOnly, FsPolicy::ReadOnly);
668        assert_ne!(FsPolicy::ReadOnly, FsPolicy::FullWritable);
669    }
670
671    #[test]
672    fn test_execution_result_failure() {
673        let result = ExecutionResult::failure("error".to_string(), 1);
674        assert!(!result.is_success());
675        assert_eq!(result.exit_code, 1);
676    }
677
678    #[tokio::test]
679    async fn test_execute_tool_returns_contextual_experimental_error() {
680        let runtime = DefaultSandboxRuntime::new().unwrap();
681        let sandbox_id = runtime.create(SandboxConfig::default()).await.unwrap();
682        let request = ToolRequest {
683            call_id: "call_1".to_string(),
684            name: "read_file".to_string(),
685            arguments: serde_json::json!({"path": "README.md"}),
686        };
687
688        let err = runtime
689            .execute_tool(&sandbox_id, request)
690            .await
691            .unwrap_err();
692        let message = err.to_string();
693
694        assert!(message.contains("[experimental]"));
695        assert!(message.contains(&sandbox_id.to_string()));
696        assert!(message.contains("read_file"));
697    }
698}