Skip to main content

ai_sandbox/sandboxing/
mod.rs

1//! Sandbox Manager - Cross-platform sandbox abstraction
2
3#![allow(dead_code)]
4
5#[cfg(target_os = "macos")]
6pub mod seatbelt;
7
8#[cfg(target_os = "macos")]
9pub use seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
10
11use std::collections::HashMap;
12#[allow(unused_imports)]
13use std::ffi::OsString;
14use std::path::{Path, PathBuf};
15
16/// Platform-specific sandbox types
17#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
18pub enum SandboxType {
19    /// No sandboxing
20    #[default]
21    None,
22    /// macOS Seatbelt (sandbox-exec)
23    MacosSeatbelt,
24    /// Linux Seccomp/Bubblewrap/Landlock
25    LinuxSeccomp,
26    /// Windows Restricted Token
27    WindowsRestrictedToken,
28    /// FreeBSD Capsicum
29    FreeBSDCapsicum,
30    /// OpenBSD pledge
31    OpenBSDPledge,
32}
33
34impl SandboxType {
35    pub fn as_metric_tag(self) -> &'static str {
36        match self {
37            SandboxType::None => "none",
38            SandboxType::MacosSeatbelt => "seatbelt",
39            SandboxType::LinuxSeccomp => "seccomp",
40            SandboxType::WindowsRestrictedToken => "windows_sandbox",
41            SandboxType::FreeBSDCapsicum => "capsicum",
42            SandboxType::OpenBSDPledge => "pledge",
43        }
44    }
45
46    /// Get the name of this sandbox type
47    pub fn name(&self) -> &'static str {
48        match self {
49            SandboxType::None => "none",
50            SandboxType::MacosSeatbelt => "seatbelt",
51            SandboxType::LinuxSeccomp => "linux-seccomp",
52            SandboxType::WindowsRestrictedToken => "windows-restricted-token",
53            SandboxType::FreeBSDCapsicum => "capsicum",
54            SandboxType::OpenBSDPledge => "pledge",
55        }
56    }
57}
58
59/// Sandbox preference setting
60#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
61pub enum SandboxablePreference {
62    /// Automatically select based on platform
63    #[default]
64    Auto,
65    /// Require sandboxing
66    Require,
67    /// Forbid sandboxing
68    Forbid,
69}
70
71/// Network sandbox policy
72#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
73pub enum NetworkSandboxPolicy {
74    /// Full network access
75    #[default]
76    FullAccess,
77    /// No network access
78    NoAccess,
79    /// Allow localhost only
80    Localhost,
81    /// Use system proxy
82    Proxy,
83}
84
85/// File system sandbox policy
86#[derive(Clone, Debug, PartialEq, Eq, Default)]
87pub enum FileSystemSandboxPolicy {
88    /// Full filesystem access
89    #[default]
90    FullAccess,
91    /// Read-only access
92    ReadOnly,
93    /// Workspace-only write access
94    WorkspaceWrite {
95        /// Allowed writable roots
96        writable_roots: Vec<PathBuf>,
97    },
98    /// External sandbox (no policy applied by us)
99    External,
100}
101
102/// Sandbox policy definition
103#[derive(Clone, Debug, PartialEq, Eq)]
104pub enum SandboxPolicy {
105    /// No sandboxing - full access (deprecated: use ReadOnly as default)
106    DangerFullAccess,
107    /// Read-only sandbox (安全的默认选项)
108    ReadOnly {
109        file_system: FileSystemSandboxPolicy,
110        network_access: NetworkSandboxPolicy,
111    },
112    /// External sandbox with network control
113    ExternalSandbox {
114        network_access: NetworkSandboxPolicy,
115    },
116    /// Workspace write access
117    WorkspaceWrite {
118        writable_roots: Vec<PathBuf>,
119        network_access: NetworkSandboxPolicy,
120    },
121}
122
123impl Default for SandboxPolicy {
124    /// 默认使用安全的 ReadOnly 策略
125    fn default() -> Self {
126        SandboxPolicy::ReadOnly {
127            file_system: FileSystemSandboxPolicy::ReadOnly,
128            network_access: NetworkSandboxPolicy::NoAccess,
129        }
130    }
131}
132
133impl SandboxPolicy {
134    /// Get the network policy from this sandbox policy
135    pub fn network_policy(&self) -> NetworkSandboxPolicy {
136        match self {
137            SandboxPolicy::DangerFullAccess => NetworkSandboxPolicy::FullAccess,
138            SandboxPolicy::ReadOnly { network_access, .. } => *network_access,
139            SandboxPolicy::ExternalSandbox { network_access } => *network_access,
140            SandboxPolicy::WorkspaceWrite { network_access, .. } => *network_access,
141        }
142    }
143
144    /// Get the filesystem policy from this sandbox policy
145    pub fn filesystem_policy(&self) -> FileSystemSandboxPolicy {
146        match self {
147            SandboxPolicy::DangerFullAccess => FileSystemSandboxPolicy::FullAccess,
148            SandboxPolicy::ReadOnly { file_system, .. } => file_system.clone(),
149            SandboxPolicy::ExternalSandbox { .. } => FileSystemSandboxPolicy::External,
150            SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
151                // Security check: if writable_roots is empty, downgrade to ReadOnly
152                if writable_roots.is_empty() {
153                    FileSystemSandboxPolicy::ReadOnly
154                } else {
155                    FileSystemSandboxPolicy::WorkspaceWrite {
156                        writable_roots: writable_roots.clone(),
157                    }
158                }
159            }
160        }
161    }
162
163    /// Check if a path contains path traversal attack attempts
164    pub fn contains_path_traversal(path: &Path) -> bool {
165        let path_str = path.to_string_lossy();
166
167        // Check for ".." pattern
168        if path_str.contains("..") {
169            return true;
170        }
171
172        // Check for "./" or "/." patterns (hidden files or current directory)
173        if path_str.contains("/.") || path_str.contains("./") {
174            return true;
175        }
176
177        false
178    }
179
180    /// 验证策略是否安全(用于创建沙箱请求前的检查)
181    /// This method is also available via the SandboxPolicyExt trait
182    pub fn is_safe(&self) -> bool {
183        match self {
184            // DangerFullAccess is not secure
185            SandboxPolicy::DangerFullAccess => false,
186            // ReadOnly is secure by default
187            SandboxPolicy::ReadOnly { .. } => true,
188            // ExternalSandbox is not controlled by us, treat as potentially insecure
189            SandboxPolicy::ExternalSandbox { .. } => false,
190            // WorkspaceWrite must have non-empty writable_roots and no path traversal
191            SandboxPolicy::WorkspaceWrite { writable_roots, .. } => {
192                if writable_roots.is_empty() {
193                    return false;
194                }
195                // Check all paths for path traversal attacks
196                !writable_roots
197                    .iter()
198                    .any(|p| SandboxPolicy::contains_path_traversal(p))
199            }
200        }
201    }
202}
203
204/// Trait to extend SandboxPolicy with additional security checks
205pub trait SandboxPolicyExt {
206    fn is_safe(&self) -> bool;
207}
208
209impl SandboxPolicyExt for SandboxPolicy {
210    fn is_safe(&self) -> bool {
211        SandboxPolicy::is_safe(self)
212    }
213}
214/// A command to be executed with sandboxing
215#[derive(Debug)]
216pub struct SandboxCommand {
217    pub program: OsString,
218    pub args: Vec<String>,
219    pub cwd: PathBuf,
220    pub env: HashMap<String, String>,
221}
222
223/// The transformed request ready for execution
224#[derive(Debug)]
225pub struct SandboxExecRequest {
226    pub command: Vec<String>,
227    pub cwd: PathBuf,
228    pub env: HashMap<String, String>,
229    pub sandbox: SandboxType,
230    pub sandbox_policy: SandboxPolicy,
231    pub file_system_policy: FileSystemSandboxPolicy,
232    pub network_policy: NetworkSandboxPolicy,
233    pub arg0: Option<String>,
234}
235
236/// Sandbox transformation error
237#[derive(Debug)]
238pub enum SandboxTransformError {
239    MissingLinuxSandboxExecutable,
240    #[cfg(not(target_os = "macos"))]
241    SeatbeltUnavailable,
242    PlatformNotSupported,
243    /// Policy is not safe (e.g., empty writable_roots or path traversal detected)
244    UnsafePolicy(String),
245}
246
247impl std::fmt::Display for SandboxTransformError {
248    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
249        match self {
250            Self::MissingLinuxSandboxExecutable => {
251                write!(f, "missing linux-sandbox executable path")
252            }
253            #[cfg(not(target_os = "macos"))]
254            Self::SeatbeltUnavailable => write!(f, "seatbelt sandbox is only available on macOS"),
255            Self::PlatformNotSupported => write!(f, "sandbox is not supported on this platform"),
256            Self::UnsafePolicy(reason) => write!(f, "unsafe policy: {}", reason),
257        }
258    }
259}
260
261impl std::error::Error for SandboxTransformError {}
262
263/// Get the appropriate sandbox type for the current platform
264pub fn get_platform_sandbox(windows_sandbox_enabled: bool) -> Option<SandboxType> {
265    if cfg!(target_os = "macos") {
266        Some(SandboxType::MacosSeatbelt)
267    } else if cfg!(target_os = "linux") {
268        Some(SandboxType::LinuxSeccomp)
269    } else if cfg!(target_os = "freebsd") {
270        Some(SandboxType::FreeBSDCapsicum)
271    } else if cfg!(target_os = "openbsd") {
272        Some(SandboxType::OpenBSDPledge)
273    } else if cfg!(target_os = "windows") {
274        if windows_sandbox_enabled {
275            Some(SandboxType::WindowsRestrictedToken)
276        } else {
277            None
278        }
279    } else {
280        None
281    }
282}
283
284/// Sandbox Manager - creates sandboxed execution requests
285#[derive(Default)]
286pub struct SandboxManager;
287
288impl SandboxManager {
289    pub fn new() -> Self {
290        Self
291    }
292
293    /// Select initial sandbox type based on preferences
294    #[allow(unused_variables)]
295    pub fn select_initial(
296        &self,
297        file_system_policy: &FileSystemSandboxPolicy,
298        network_policy: NetworkSandboxPolicy,
299        pref: SandboxablePreference,
300        windows_sandbox_enabled: bool,
301    ) -> SandboxType {
302        match pref {
303            SandboxablePreference::Forbid => SandboxType::None,
304            SandboxablePreference::Require => {
305                get_platform_sandbox(windows_sandbox_enabled).unwrap_or(SandboxType::None)
306            }
307            SandboxablePreference::Auto => {
308                let platform_sandbox = get_platform_sandbox(windows_sandbox_enabled);
309                // Always use platform sandbox for Auto mode
310                platform_sandbox.unwrap_or(SandboxType::None)
311            }
312        }
313    }
314
315    /// Create a sandbox execution request
316    pub fn create_exec_request(
317        &self,
318        command: SandboxCommand,
319        policy: SandboxPolicy,
320    ) -> Result<SandboxExecRequest, SandboxTransformError> {
321        // SECURITY: Validate policy before creating execution request
322        if !policy.is_safe() {
323            return Err(SandboxTransformError::UnsafePolicy(
324                "Policy failed safety check: empty writable_roots or path traversal detected"
325                    .to_string(),
326            ));
327        }
328
329        let sandbox = self.select_initial(
330            &FileSystemSandboxPolicy::default(),
331            NetworkSandboxPolicy::default(),
332            SandboxablePreference::Auto,
333            false,
334        );
335        self.transform_command(command, policy, sandbox, None)
336    }
337
338    /// Transform a command for sandbox execution
339    pub fn transform_command(
340        &self,
341        command: SandboxCommand,
342        policy: SandboxPolicy,
343        sandbox: SandboxType,
344        _linux_sandbox_exe: Option<&Path>,
345    ) -> Result<SandboxExecRequest, SandboxTransformError> {
346        let argv: Vec<OsString> = std::iter::once(command.program)
347            .chain(command.args.iter().map(OsString::from))
348            .collect();
349
350        let (argv, arg0_override) = match sandbox {
351            SandboxType::None => (os_argv_to_strings(argv), None),
352            #[cfg(target_os = "macos")]
353            SandboxType::MacosSeatbelt => {
354                let args = crate::sandboxing::seatbelt::create_seatbelt_command_args_for_policies(
355                    os_argv_to_strings(argv),
356                    &policy.filesystem_policy(),
357                    policy.network_policy(),
358                    std::path::Path::new("."),
359                    false,
360                    None,
361                );
362                let mut full_command = vec![MACOS_PATH_TO_SEATBELT_EXECUTABLE.to_string()];
363                full_command.extend(args);
364                (full_command, None)
365            }
366            #[cfg(not(target_os = "macos"))]
367            SandboxType::MacosSeatbelt => return Err(SandboxTransformError::SeatbeltUnavailable),
368            SandboxType::LinuxSeccomp => {
369                let exe = _linux_sandbox_exe
370                    .ok_or(SandboxTransformError::MissingLinuxSandboxExecutable)?;
371                let args = create_linux_sandbox_args(&policy, command.cwd.as_path());
372                let mut full_command = vec![exe.to_string_lossy().to_string()];
373                full_command.extend(args);
374                (full_command, Some("linux-sandbox".to_string()))
375            }
376            #[cfg(target_os = "windows")]
377            SandboxType::WindowsRestrictedToken => (os_argv_to_strings(argv), None),
378            #[cfg(not(target_os = "windows"))]
379            SandboxType::WindowsRestrictedToken => (os_argv_to_strings(argv), None),
380            #[cfg(target_os = "freebsd")]
381            SandboxType::FreeBSDCapsicum => (os_argv_to_strings(argv), None),
382            #[cfg(not(target_os = "freebsd"))]
383            SandboxType::FreeBSDCapsicum => (os_argv_to_strings(argv), None),
384            #[cfg(target_os = "openbsd")]
385            SandboxType::OpenBSDPledge => (os_argv_to_strings(argv), None),
386            #[cfg(not(target_os = "openbsd"))]
387            SandboxType::OpenBSDPledge => (os_argv_to_strings(argv), None),
388        };
389
390        Ok(SandboxExecRequest {
391            command: argv,
392            cwd: command.cwd,
393            env: command.env,
394            sandbox,
395            sandbox_policy: policy.clone(),
396            file_system_policy: policy.filesystem_policy(),
397            network_policy: policy.network_policy(),
398            arg0: arg0_override,
399        })
400    }
401}
402
403fn os_argv_to_strings(argv: Vec<OsString>) -> Vec<String> {
404    argv.into_iter()
405        .map(|s| {
406            s.into_string()
407                .unwrap_or_else(|s| s.to_string_lossy().into_owned())
408        })
409        .collect()
410}
411
412fn should_require_platform_sandbox(
413    file_system_policy: &FileSystemSandboxPolicy,
414    network_policy: NetworkSandboxPolicy,
415) -> bool {
416    !matches!(file_system_policy, FileSystemSandboxPolicy::FullAccess)
417        || !matches!(network_policy, NetworkSandboxPolicy::FullAccess)
418}
419
420#[cfg(target_os = "macos")]
421#[allow(dead_code)]
422fn create_seatbelt_command_args(_policy: &SandboxPolicy) -> Vec<String> {
423    vec!["-p".to_string(), "(version 1)".to_string()]
424}
425
426#[cfg(target_os = "linux")]
427fn create_linux_sandbox_args(policy: &SandboxPolicy, cwd: &Path) -> Vec<String> {
428    crate::linux_sandbox::create_linux_sandbox_command_args_for_policies(vec![], cwd, policy, false)
429}
430
431#[cfg(not(target_os = "linux"))]
432#[allow(dead_code)]
433fn create_linux_sandbox_args(_policy: &SandboxPolicy, _cwd: &Path) -> Vec<String> {
434    vec![]
435}
436
437#[cfg(test)]
438#[allow(clippy::assertions_on_constants)]
439mod tests {
440    use super::*;
441
442    #[test]
443    fn test_get_platform_sandbox() {
444        // On Windows, sandbox requires windows_sandbox_enabled = true
445        // Use true to ensure test passes on all platforms
446        #[cfg(target_os = "windows")]
447        let result = get_platform_sandbox(true);
448        #[cfg(not(target_os = "windows"))]
449        let result = get_platform_sandbox(false);
450
451        #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
452        assert!(result.is_some());
453        #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
454        assert!(result.is_none());
455    }
456
457    #[test]
458    #[cfg(target_os = "macos")]
459    fn test_sandbox_manager_create_request() {
460        let manager = SandboxManager::new();
461        let command = SandboxCommand {
462            program: OsString::from("ls"),
463            args: vec!["-la".to_string()],
464            cwd: PathBuf::from("/tmp"),
465            env: HashMap::new(),
466        };
467
468        let result = manager.create_exec_request(command, SandboxPolicy::default());
469        assert!(result.is_ok());
470    }
471
472    #[test]
473    #[cfg(not(target_os = "macos"))]
474    fn test_sandbox_manager_create_request() {
475        // Skip test on non-macOS platforms as it requires platform-specific sandbox executable
476        // The test verifies that create_exec_request works, but it requires a sandbox executable
477        // which is only available on macOS (seatbelt)
478    }
479
480    // ============================================================================
481    // 破坏性测试 - 沙箱策略安全性验证
482    // ============================================================================
483
484    #[test]
485    fn test_default_policy_should_be_secure() {
486        // 默认策略应该是安全的,不应该是 DangerFullAccess
487        // 根据安全最佳实践,默认应该拒绝访问
488        let default_policy = SandboxPolicy::default();
489
490        // 默认策略不应该是完全无限制的
491        assert!(
492            !matches!(default_policy, SandboxPolicy::DangerFullAccess),
493            "默认策略不应该是 DangerFullAccess,这是安全漏洞!"
494        );
495    }
496
497    #[test]
498    fn test_workspace_write_rejects_empty_paths() {
499        // WorkspaceWrite 应该拒绝空的 writable_roots
500        let empty_policy = SandboxPolicy::WorkspaceWrite {
501            writable_roots: vec![],
502            network_access: NetworkSandboxPolicy::NoAccess,
503        };
504
505        // 获取文件系统策略并验证 - 空路径应该被降级为 ReadOnly
506        let fs_policy = empty_policy.filesystem_policy();
507        match fs_policy {
508            FileSystemSandboxPolicy::ReadOnly => {
509                // 预期行为:空路径被降级为只读 - 测试通过
510            }
511            FileSystemSandboxPolicy::WorkspaceWrite { writable_roots } => {
512                assert!(
513                    !writable_roots.is_empty(),
514                    "空的 writable_roots 应该被拒绝或自动处理"
515                );
516            }
517            _ => {
518                panic!("Unexpected filesystem policy variant");
519            }
520        }
521
522        // 验证 is_safe 方法
523        assert!(
524            !empty_policy.is_safe(),
525            "空 writable_roots 的 WorkspaceWrite 应该是不安全的"
526        );
527
528        // 验证非空的是安全的
529        let safe_policy = SandboxPolicy::WorkspaceWrite {
530            writable_roots: vec![PathBuf::from("/tmp")],
531            network_access: NetworkSandboxPolicy::NoAccess,
532        };
533        assert!(
534            safe_policy.is_safe(),
535            "有有效路径的 WorkspaceWrite 应该是安全的"
536        );
537    }
538
539    #[test]
540    fn test_workspace_write_rejects_path_traversal() {
541        // WorkspaceWrite 应该拒绝包含路径遍历的路径
542        let traversal_paths = vec![
543            PathBuf::from("/tmp/../etc"),
544            PathBuf::from("/tmp/../../etc"),
545            PathBuf::from("/home/../../../root"),
546            PathBuf::from("/tmp/./secret"),
547        ];
548
549        for path in traversal_paths {
550            let policy = SandboxPolicy::WorkspaceWrite {
551                writable_roots: vec![path.clone()],
552                network_access: NetworkSandboxPolicy::NoAccess,
553            };
554
555            // 包含路径遍历的路径应该被认为是不安全的
556            assert!(
557                !policy.is_safe(),
558                "包含路径遍历的路径 {:?} 应该被认为是不安全的",
559                path
560            );
561        }
562    }
563
564    #[test]
565    fn test_workspace_write_accepts_valid_paths() {
566        // WorkspaceWrite 应该接受有效的规范化路径
567        let valid_paths = vec![
568            PathBuf::from("/tmp"),
569            PathBuf::from("/home/user/workspace"),
570            PathBuf::from("/var/data"),
571        ];
572
573        for path in valid_paths {
574            let policy = SandboxPolicy::WorkspaceWrite {
575                writable_roots: vec![path.clone()],
576                network_access: NetworkSandboxPolicy::NoAccess,
577            };
578
579            assert!(policy.is_safe(), "有效路径 {:?} 应该被认为安全的", path);
580        }
581    }
582
583    #[test]
584    fn test_readonly_policy_structure() {
585        // 测试只读策略结构
586        let policy = SandboxPolicy::ReadOnly {
587            file_system: FileSystemSandboxPolicy::ReadOnly,
588            network_access: NetworkSandboxPolicy::NoAccess,
589        };
590
591        match policy {
592            SandboxPolicy::ReadOnly {
593                file_system,
594                network_access,
595            } => {
596                assert!(matches!(file_system, FileSystemSandboxPolicy::ReadOnly));
597                assert!(matches!(network_access, NetworkSandboxPolicy::NoAccess));
598            }
599            _ => panic!("Expected ReadOnly policy"),
600        }
601    }
602
603    #[test]
604    fn test_workspace_policy_validates_paths() {
605        // 测试工作区策略路径验证
606        let policy = SandboxPolicy::WorkspaceWrite {
607            writable_roots: vec![PathBuf::from("/tmp"), PathBuf::from("/home/user/workspace")],
608            network_access: NetworkSandboxPolicy::Localhost,
609        };
610
611        match policy {
612            SandboxPolicy::WorkspaceWrite {
613                writable_roots,
614                network_access,
615            } => {
616                assert_eq!(writable_roots.len(), 2);
617                assert!(matches!(network_access, NetworkSandboxPolicy::Localhost));
618            }
619            _ => panic!("Expected WorkspaceWrite policy"),
620        }
621    }
622
623    #[test]
624    fn test_network_policy_variants() {
625        // 测试网络策略变体
626        assert!(matches!(
627            NetworkSandboxPolicy::default(),
628            NetworkSandboxPolicy::FullAccess
629        ));
630        assert!(matches!(
631            NetworkSandboxPolicy::NoAccess,
632            NetworkSandboxPolicy::NoAccess
633        ));
634        assert!(matches!(
635            NetworkSandboxPolicy::Localhost,
636            NetworkSandboxPolicy::Localhost
637        ));
638        assert!(matches!(
639            NetworkSandboxPolicy::Proxy,
640            NetworkSandboxPolicy::Proxy
641        ));
642    }
643
644    #[test]
645    fn test_filesystem_policy_variants() {
646        // 测试文件系统策略变体
647        assert!(matches!(
648            FileSystemSandboxPolicy::default(),
649            FileSystemSandboxPolicy::FullAccess
650        ));
651        assert!(matches!(
652            FileSystemSandboxPolicy::ReadOnly,
653            FileSystemSandboxPolicy::ReadOnly
654        ));
655        assert!(matches!(
656            FileSystemSandboxPolicy::External,
657            FileSystemSandboxPolicy::External
658        ));
659
660        // 测试 WorkspaceWrite 变体
661        let ws = FileSystemSandboxPolicy::WorkspaceWrite {
662            writable_roots: vec![PathBuf::from("/tmp")],
663        };
664        assert!(matches!(ws, FileSystemSandboxPolicy::WorkspaceWrite { .. }));
665    }
666
667    #[test]
668    fn test_sandbox_type_all_variants() {
669        // 测试所有沙箱类型
670        let types = vec![
671            SandboxType::None,
672            SandboxType::MacosSeatbelt,
673            SandboxType::LinuxSeccomp,
674            SandboxType::WindowsRestrictedToken,
675            SandboxType::FreeBSDCapsicum,
676            SandboxType::OpenBSDPledge,
677        ];
678
679        for sandbox_type in types {
680            let name = sandbox_type.name();
681            let tag = sandbox_type.as_metric_tag();
682            assert!(!name.is_empty());
683            assert!(!tag.is_empty());
684        }
685    }
686
687    #[test]
688    fn test_sandbox_command_validation() {
689        // 测试 SandboxCommand 验证
690        let command = SandboxCommand {
691            program: OsString::from("ls"),
692            args: vec!["-la".to_string(), "/tmp".to_string()],
693            cwd: PathBuf::from("/tmp"),
694            env: HashMap::new(),
695        };
696
697        assert!(!command.program.is_empty());
698        assert!(!command.args.is_empty());
699        assert!(command.cwd.exists() || command.cwd.to_string_lossy() == "/tmp");
700    }
701
702    #[test]
703    fn test_sandbox_preference_variants() {
704        // 测试沙箱偏好设置
705        assert!(matches!(
706            SandboxablePreference::default(),
707            SandboxablePreference::Auto
708        ));
709        assert!(matches!(
710            SandboxablePreference::Require,
711            SandboxablePreference::Require
712        ));
713        assert!(matches!(
714            SandboxablePreference::Forbid,
715            SandboxablePreference::Forbid
716        ));
717    }
718
719    // ============================================================================
720    // 破坏性测试 - 边界条件和错误处理
721    // ============================================================================
722
723    #[test]
724    fn test_empty_program_name() {
725        // 测试空程序名
726        let manager = SandboxManager::new();
727        let command = SandboxCommand {
728            program: OsString::from(""),
729            args: vec![],
730            cwd: PathBuf::from("/tmp"),
731            env: HashMap::new(),
732        };
733
734        // 应该能创建请求,但不保证能执行
735        let result = manager.create_exec_request(command, SandboxPolicy::default());
736        // 空程序名可能导致错误
737        assert!(result.is_ok() || result.is_err());
738    }
739
740    #[test]
741    fn test_very_long_program_name() {
742        // 测试超长程序名
743        let manager = SandboxManager::new();
744        let long_name = "A".repeat(10000);
745        let command = SandboxCommand {
746            program: OsString::from(long_name),
747            args: vec![],
748            cwd: PathBuf::from("/tmp"),
749            env: HashMap::new(),
750        };
751
752        let result = manager.create_exec_request(command, SandboxPolicy::default());
753        // 长程序名应该被处理(可能返回错误但不崩溃)
754        assert!(result.is_ok() || result.is_err());
755    }
756
757    #[test]
758    fn test_special_characters_in_args() {
759        // 测试参数中的特殊字符
760        let manager = SandboxManager::new();
761        let command = SandboxCommand {
762            program: OsString::from("ls"),
763            args: vec![
764                "-la".to_string(),
765                "/tmp".to_string(),
766                ";rm -rf /".to_string(),
767                "|cat /etc/passwd".to_string(),
768                "`whoami`".to_string(),
769                "$(id)".to_string(),
770            ],
771            cwd: PathBuf::from("/tmp"),
772            env: HashMap::new(),
773        };
774
775        let result = manager.create_exec_request(command, SandboxPolicy::default());
776        // 特殊字符应该被处理(可能返回错误但不崩溃)
777        assert!(result.is_ok() || result.is_err());
778    }
779
780    #[test]
781    fn test_empty_cwd() {
782        // 测试空工作目录
783        let manager = SandboxManager::new();
784        let command = SandboxCommand {
785            program: OsString::from("ls"),
786            args: vec![],
787            cwd: PathBuf::from(""),
788            env: HashMap::new(),
789        };
790
791        let result = manager.create_exec_request(command, SandboxPolicy::default());
792        // 空 cwd 可能导致错误或使用默认目录
793        assert!(result.is_ok() || result.is_err());
794    }
795
796    #[test]
797    fn test_nonexistent_cwd() {
798        // 测试不存在的工作目录
799        let manager = SandboxManager::new();
800        let command = SandboxCommand {
801            program: OsString::from("ls"),
802            args: vec![],
803            cwd: PathBuf::from("/nonexistent/path/that/does/not/exist"),
804            env: HashMap::new(),
805        };
806
807        let result = manager.create_exec_request(command, SandboxPolicy::default());
808        // 应该能创建请求(可能在执行时验证目录,不一定要在创建时)
809        assert!(result.is_ok() || result.is_err());
810    }
811
812    // ============================================================================
813    // 破坏性测试 - Policy Clone 和序列化
814    // ============================================================================
815
816    #[test]
817    fn test_policy_clone() {
818        // 测试策略克隆
819        let policy = SandboxPolicy::ReadOnly {
820            file_system: FileSystemSandboxPolicy::ReadOnly,
821            network_access: NetworkSandboxPolicy::NoAccess,
822        };
823
824        let cloned = policy.clone();
825        assert_eq!(policy, cloned);
826    }
827
828    #[test]
829    fn test_policy_debug_format() {
830        // 测试策略调试格式
831        let policy = SandboxPolicy::WorkspaceWrite {
832            writable_roots: vec![PathBuf::from("/tmp")],
833            network_access: NetworkSandboxPolicy::Localhost,
834        };
835
836        let debug_str = format!("{:?}", policy);
837        assert!(!debug_str.is_empty());
838    }
839
840    // ============================================================================
841    // 新增测试: 验证 network_policy() 和 filesystem_policy() 方法
842    // ============================================================================
843
844    #[test]
845    fn test_sandbox_policy_network_policy_method() {
846        // 测试 SandboxPolicy::network_policy() 方法
847        let policy_full = SandboxPolicy::DangerFullAccess;
848        assert_eq!(
849            policy_full.network_policy(),
850            NetworkSandboxPolicy::FullAccess
851        );
852
853        let policy_readonly = SandboxPolicy::ReadOnly {
854            file_system: FileSystemSandboxPolicy::ReadOnly,
855            network_access: NetworkSandboxPolicy::NoAccess,
856        };
857        assert_eq!(
858            policy_readonly.network_policy(),
859            NetworkSandboxPolicy::NoAccess
860        );
861
862        let policy_external = SandboxPolicy::ExternalSandbox {
863            network_access: NetworkSandboxPolicy::Localhost,
864        };
865        assert_eq!(
866            policy_external.network_policy(),
867            NetworkSandboxPolicy::Localhost
868        );
869
870        let policy_workspace = SandboxPolicy::WorkspaceWrite {
871            writable_roots: vec![PathBuf::from("/tmp")],
872            network_access: NetworkSandboxPolicy::Proxy,
873        };
874        assert_eq!(
875            policy_workspace.network_policy(),
876            NetworkSandboxPolicy::Proxy
877        );
878    }
879
880    #[test]
881    fn test_sandbox_policy_filesystem_policy_method() {
882        // 测试 SandboxPolicy::filesystem_policy() 方法
883        let policy_full = SandboxPolicy::DangerFullAccess;
884        assert_eq!(
885            policy_full.filesystem_policy(),
886            FileSystemSandboxPolicy::FullAccess
887        );
888
889        let policy_readonly = SandboxPolicy::ReadOnly {
890            file_system: FileSystemSandboxPolicy::ReadOnly,
891            network_access: NetworkSandboxPolicy::NoAccess,
892        };
893        assert_eq!(
894            policy_readonly.filesystem_policy(),
895            FileSystemSandboxPolicy::ReadOnly
896        );
897
898        let policy_external = SandboxPolicy::ExternalSandbox {
899            network_access: NetworkSandboxPolicy::Localhost,
900        };
901        assert_eq!(
902            policy_external.filesystem_policy(),
903            FileSystemSandboxPolicy::External
904        );
905
906        let policy_workspace = SandboxPolicy::WorkspaceWrite {
907            writable_roots: vec![PathBuf::from("/tmp"), PathBuf::from("/home")],
908            network_access: NetworkSandboxPolicy::FullAccess,
909        };
910        let fs_policy = policy_workspace.filesystem_policy();
911        match fs_policy {
912            FileSystemSandboxPolicy::WorkspaceWrite { writable_roots } => {
913                assert_eq!(writable_roots.len(), 2);
914            }
915            _ => panic!("Expected WorkspaceWrite variant"),
916        }
917    }
918
919    #[test]
920    #[cfg(target_os = "macos")]
921    fn test_sandbox_exec_request_carries_network_policy() {
922        // 测试 SandboxExecRequest 正确携带 network_policy
923        let manager = SandboxManager::new();
924
925        // 测试 NoAccess 策略
926        let policy = SandboxPolicy::ReadOnly {
927            file_system: FileSystemSandboxPolicy::ReadOnly,
928            network_access: NetworkSandboxPolicy::NoAccess,
929        };
930        let command = SandboxCommand {
931            program: OsString::from("ls"),
932            args: vec![],
933            cwd: PathBuf::from("/tmp"),
934            env: HashMap::new(),
935        };
936        let request = manager.create_exec_request(command, policy).unwrap();
937        assert_eq!(request.network_policy, NetworkSandboxPolicy::NoAccess);
938
939        // 测试 Localhost 策略
940        let policy_localhost = SandboxPolicy::ReadOnly {
941            file_system: FileSystemSandboxPolicy::ReadOnly,
942            network_access: NetworkSandboxPolicy::Localhost,
943        };
944        let command = SandboxCommand {
945            program: OsString::from("ls"),
946            args: vec![],
947            cwd: PathBuf::from("/tmp"),
948            env: HashMap::new(),
949        };
950        let request = manager
951            .create_exec_request(command, policy_localhost)
952            .unwrap();
953        assert_eq!(request.network_policy, NetworkSandboxPolicy::Localhost);
954    }
955
956    #[test]
957    #[cfg(not(target_os = "macos"))]
958    fn test_sandbox_exec_request_carries_network_policy() {
959        // Skip on non-macOS as it requires platform-specific sandbox executable
960    }
961
962    #[test]
963    #[cfg(target_os = "macos")]
964    fn test_sandbox_exec_request_carries_filesystem_policy() {
965        // 测试 SandboxExecRequest 正确携带 file_system_policy
966        let manager = SandboxManager::new();
967
968        // 测试 ReadOnly 策略
969        let policy = SandboxPolicy::ReadOnly {
970            file_system: FileSystemSandboxPolicy::ReadOnly,
971            network_access: NetworkSandboxPolicy::FullAccess,
972        };
973        let command = SandboxCommand {
974            program: OsString::from("ls"),
975            args: vec![],
976            cwd: PathBuf::from("/tmp"),
977            env: HashMap::new(),
978        };
979        let request = manager.create_exec_request(command, policy).unwrap();
980        assert_eq!(
981            request.file_system_policy,
982            FileSystemSandboxPolicy::ReadOnly
983        );
984
985        // 测试 WorkspaceWrite 策略
986        let policy_workspace = SandboxPolicy::WorkspaceWrite {
987            writable_roots: vec![PathBuf::from("/workspace")],
988            network_access: NetworkSandboxPolicy::FullAccess,
989        };
990        let command = SandboxCommand {
991            program: OsString::from("ls"),
992            args: vec![],
993            cwd: PathBuf::from("/tmp"),
994            env: HashMap::new(),
995        };
996        let request = manager
997            .create_exec_request(command, policy_workspace)
998            .unwrap();
999        match request.file_system_policy {
1000            FileSystemSandboxPolicy::WorkspaceWrite { writable_roots } => {
1001                assert_eq!(writable_roots.len(), 1);
1002            }
1003            _ => panic!("Expected WorkspaceWrite variant"),
1004        }
1005    }
1006
1007    #[test]
1008    #[cfg(not(target_os = "macos"))]
1009    fn test_sandbox_exec_request_carries_filesystem_policy() {
1010        // Skip on non-macOS as it requires platform-specific sandbox executable
1011    }
1012}