Skip to main content

vtcode_core/sandboxing/
debug.rs

1//! Debug utilities for testing sandbox configurations.
2//!
3//! Following the Codex pattern: "codex debug seatbelt and codex debug landlock
4//! let you test arbitrary commands through the sandbox."
5
6use std::path::Path;
7use std::process::Stdio;
8
9use anyhow::{Context, Result};
10use tokio::process::Command;
11
12use super::{CommandSpec, SandboxManager, SandboxPolicy, SandboxType};
13
14/// Result of a sandbox debug test.
15#[derive(Debug)]
16pub struct SandboxDebugResult {
17    /// Whether the command succeeded.
18    pub success: bool,
19    /// Exit code if available.
20    pub exit_code: Option<i32>,
21    /// Standard output.
22    pub stdout: String,
23    /// Standard error.
24    pub stderr: String,
25    /// The sandbox type used.
26    pub sandbox_type: SandboxType,
27    /// Whether the sandbox was actually applied.
28    pub sandbox_active: bool,
29}
30
31impl SandboxDebugResult {
32    /// Create a result indicating sandbox is not available.
33    pub fn unavailable(sandbox_type: SandboxType) -> Self {
34        Self {
35            success: false,
36            exit_code: None,
37            stdout: String::new(),
38            stderr: format!(
39                "Sandbox type {:?} is not available on this platform",
40                sandbox_type
41            ),
42            sandbox_type,
43            sandbox_active: false,
44        }
45    }
46}
47
48/// Debug sandbox configuration by running a test command.
49///
50/// This allows testing sandbox restrictions without affecting production execution.
51pub async fn debug_sandbox(
52    sandbox_type: SandboxType,
53    policy: &SandboxPolicy,
54    command: &[String],
55    cwd: &Path,
56    sandbox_executable: Option<&Path>,
57) -> Result<SandboxDebugResult> {
58    if !sandbox_type.is_available() {
59        return Ok(SandboxDebugResult::unavailable(sandbox_type));
60    }
61
62    if command.is_empty() {
63        anyhow::bail!("Command cannot be empty");
64    }
65
66    let spec = CommandSpec::new(&command[0])
67        .with_args(command[1..].to_vec())
68        .with_cwd(cwd);
69
70    let manager = SandboxManager::new();
71    let exec_env = manager
72        .transform(spec, policy, cwd, sandbox_executable)
73        .context("Failed to transform command for sandbox")?;
74
75    let mut cmd = Command::new(&exec_env.program);
76    cmd.args(&exec_env.args)
77        .current_dir(&exec_env.cwd)
78        .envs(&exec_env.env)
79        .stdout(Stdio::piped())
80        .stderr(Stdio::piped());
81
82    let output = cmd
83        .output()
84        .await
85        .context("Failed to execute sandboxed command")?;
86
87    Ok(SandboxDebugResult {
88        success: output.status.success(),
89        exit_code: output.status.code(),
90        stdout: String::from_utf8_lossy(&output.stdout).to_string(),
91        stderr: String::from_utf8_lossy(&output.stderr).to_string(),
92        sandbox_type: exec_env.sandbox_type,
93        sandbox_active: exec_env.sandbox_active,
94    })
95}
96
97/// Test if a specific path is writable under the given sandbox policy.
98pub async fn test_path_writable(
99    policy: &SandboxPolicy,
100    test_path: &Path,
101    cwd: &Path,
102    sandbox_executable: Option<&Path>,
103) -> Result<bool> {
104    let test_file = test_path.join(".vtcode_sandbox_test");
105    let test_command = vec![
106        "sh".to_string(),
107        "-c".to_string(),
108        format!(
109            "touch '{}' && rm -f '{}'",
110            test_file.display(),
111            test_file.display()
112        ),
113    ];
114
115    let result = debug_sandbox(
116        SandboxType::platform_default(),
117        policy,
118        &test_command,
119        cwd,
120        sandbox_executable,
121    )
122    .await?;
123
124    Ok(result.success)
125}
126
127/// Test if network access is blocked under the given sandbox policy.
128pub async fn test_network_blocked(
129    policy: &SandboxPolicy,
130    cwd: &Path,
131    sandbox_executable: Option<&Path>,
132) -> Result<bool> {
133    let test_command = vec![
134        "sh".to_string(),
135        "-c".to_string(),
136        "curl -s --connect-timeout 2 https://example.com > /dev/null 2>&1".to_string(),
137    ];
138
139    let result = debug_sandbox(
140        SandboxType::platform_default(),
141        policy,
142        &test_command,
143        cwd,
144        sandbox_executable,
145    )
146    .await?;
147
148    Ok(!result.success)
149}
150
151/// Get a human-readable summary of sandbox capabilities for the current platform.
152pub fn sandbox_capabilities_summary() -> String {
153    let mut summary = String::new();
154
155    summary.push_str("VT Code Sandbox Capabilities\n");
156    summary.push_str("=============================\n\n");
157
158    summary.push_str(&format!(
159        "Platform default: {:?}\n\n",
160        SandboxType::platform_default()
161    ));
162
163    summary.push_str("Available sandbox types:\n");
164    for sandbox_type in [
165        SandboxType::MacosSeatbelt,
166        SandboxType::LinuxLandlock,
167        SandboxType::WindowsRestrictedToken,
168    ] {
169        let available = if sandbox_type.is_available() {
170            "✓"
171        } else {
172            "✗"
173        };
174        summary.push_str(&format!("  {} {:?}\n", available, sandbox_type));
175    }
176
177    summary.push_str("\nSandbox policies:\n");
178    summary.push_str(
179        "  - ReadOnly: Read files, no writes except /dev/null, optional network policy\n",
180    );
181    summary
182        .push_str("  - WorkspaceWrite: Read all, write to workspace, optional network allowlist\n");
183    summary.push_str("  - DangerFullAccess: No restrictions (use with caution)\n");
184
185    summary.push_str("\nSecurity features:\n");
186    summary.push_str("  - Sensitive path blocking (~/.ssh, ~/.aws, etc.)\n");
187    summary.push_str("  - .git directory write protection\n");
188    summary.push_str("  - Environment variable sanitization\n");
189    summary.push_str("  - Seccomp syscall filtering (Linux)\n");
190    summary.push_str("  - Resource limits (memory, PIDs, disk, CPU)\n");
191
192    summary
193}
194
195/// Debug subcommand types for CLI integration.
196#[derive(Debug, Clone, Copy, PartialEq, Eq)]
197pub enum DebugSubcommand {
198    /// Test macOS Seatbelt sandbox.
199    Seatbelt,
200    /// Test Linux Landlock sandbox.
201    Landlock,
202    /// Show sandbox capabilities.
203    Capabilities,
204}
205
206impl DebugSubcommand {
207    /// Get the sandbox type for this debug subcommand.
208    pub fn sandbox_type(&self) -> SandboxType {
209        match self {
210            Self::Seatbelt => SandboxType::MacosSeatbelt,
211            Self::Landlock => SandboxType::LinuxLandlock,
212            Self::Capabilities => SandboxType::platform_default(),
213        }
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use super::*;
220
221    #[test]
222    fn test_capabilities_summary() {
223        let summary = sandbox_capabilities_summary();
224        assert!(summary.contains("VT Code Sandbox Capabilities"));
225        assert!(summary.contains("Platform default"));
226        assert!(summary.contains("ReadOnly"));
227        assert!(summary.contains("WorkspaceWrite"));
228    }
229
230    #[test]
231    fn test_debug_subcommand() {
232        assert_eq!(
233            DebugSubcommand::Seatbelt.sandbox_type(),
234            SandboxType::MacosSeatbelt
235        );
236        assert_eq!(
237            DebugSubcommand::Landlock.sandbox_type(),
238            SandboxType::LinuxLandlock
239        );
240    }
241
242    #[tokio::test]
243    async fn test_debug_sandbox_unavailable() {
244        let result = SandboxDebugResult::unavailable(SandboxType::LinuxLandlock);
245        assert!(!result.success);
246        assert!(!result.sandbox_active);
247        assert!(result.stderr.contains("not available"));
248    }
249}