Skip to main content

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