vtcode_safety/sandboxing/
debug.rs1use 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#[derive(Debug)]
16pub struct SandboxDebugResult {
17 success: bool,
19 exit_code: Option<i32>,
21 stdout: String,
23 stderr: String,
25 sandbox_type: SandboxType,
27 sandbox_active: bool,
29}
30
31impl SandboxDebugResult {
32 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#[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
95pub 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
114pub 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
131pub 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
167pub enum DebugSubcommand {
168 Seatbelt,
170 Landlock,
172 Capabilities,
174}
175
176impl DebugSubcommand {
177 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}