vtcode_safety/sandboxing/
exec_env.rs1use hashbrown::HashMap;
4use std::ffi::OsString;
5use std::path::PathBuf;
6use std::time::Duration;
7
8use tokio_util::sync::CancellationToken;
9
10use super::SandboxPermissions;
11
12#[derive(Debug, Clone, Default)]
14pub enum ExecExpiration {
15 Timeout(Duration),
17
18 #[default]
20 DefaultTimeout,
21
22 Cancellation(CancellationToken),
24}
25
26impl From<Option<u64>> for ExecExpiration {
27 fn from(timeout_ms: Option<u64>) -> Self {
28 match timeout_ms {
29 Some(ms) => Self::Timeout(Duration::from_millis(ms)),
30 None => Self::DefaultTimeout,
31 }
32 }
33}
34
35impl From<u64> for ExecExpiration {
36 fn from(timeout_ms: u64) -> Self {
37 Self::Timeout(Duration::from_millis(timeout_ms))
38 }
39}
40
41impl ExecExpiration {
42 pub fn timeout_ms(&self) -> Option<u64> {
44 match self {
45 Self::Timeout(d) => Some(u64::try_from(d.as_millis()).unwrap_or(u64::MAX)),
46 Self::DefaultTimeout => Some(30_000), Self::Cancellation(_) => None,
48 }
49 }
50
51 pub fn timeout_duration(&self) -> Option<Duration> {
53 match self {
54 Self::Timeout(d) => Some(*d),
55 Self::DefaultTimeout => Some(Duration::from_secs(30)),
56 Self::Cancellation(_) => None,
57 }
58 }
59}
60
61#[derive(Debug, Clone)]
63pub struct CommandSpec {
64 pub(crate) program: OsString,
66
67 pub(crate) args: Vec<String>,
69
70 pub(crate) cwd: PathBuf,
72
73 pub(crate) env: HashMap<String, String>,
75
76 pub(crate) expiration: ExecExpiration,
78
79 sandbox_permissions: SandboxPermissions,
81
82 justification: Option<String>,
84}
85
86impl Default for CommandSpec {
87 fn default() -> Self {
88 Self {
89 program: OsString::new(),
90 args: Vec::new(),
91 cwd: PathBuf::new(),
92 env: HashMap::new(),
93 expiration: ExecExpiration::DefaultTimeout,
94 sandbox_permissions: SandboxPermissions::UseDefault,
95 justification: None,
96 }
97 }
98}
99
100impl CommandSpec {
101 pub fn new(program: impl Into<OsString>) -> Self {
103 Self { program: program.into(), ..Default::default() }
104 }
105
106 pub fn with_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
108 self.args = args.into_iter().map(Into::into).collect();
109 self
110 }
111
112 pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
114 self.cwd = cwd.into();
115 self
116 }
117
118 pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
120 self.env = env;
121 self
122 }
123
124 pub fn with_expiration(mut self, expiration: ExecExpiration) -> Self {
126 self.expiration = expiration;
127 self
128 }
129
130 pub fn with_sandbox_permissions(mut self, permissions: SandboxPermissions) -> Self {
132 self.sandbox_permissions = permissions;
133 self
134 }
135
136 fn with_justification(mut self, justification: impl Into<String>) -> Self {
138 self.justification = Some(justification.into());
139 self
140 }
141
142 fn full_command(&self) -> Vec<OsString> {
144 let mut cmd = vec![self.program.clone()];
145 cmd.extend(self.args.iter().cloned().map(OsString::from));
146 cmd
147 }
148}
149
150#[derive(Debug, Clone)]
152pub struct ExecEnv {
153 pub program: PathBuf,
155
156 pub args: Vec<String>,
158
159 pub cwd: PathBuf,
161
162 pub env: HashMap<String, String>,
164
165 pub expiration: ExecExpiration,
167
168 pub sandbox_active: bool,
170
171 pub sandbox_type: SandboxType,
173}
174
175#[derive(Debug, Clone, PartialEq, Eq)]
182pub struct LinuxSandboxLauncher {
183 pub program: PathBuf,
185 pub prefix_args: Vec<String>,
188}
189
190impl LinuxSandboxLauncher {
191 pub fn external(helper: PathBuf) -> Self {
193 Self { program: helper, prefix_args: Vec::new() }
194 }
195
196 pub fn busybox(binary: PathBuf) -> Self {
198 Self {
199 program: binary,
200 prefix_args: vec!["sandbox-exec".to_string()],
201 }
202 }
203
204 #[cfg(target_os = "linux")]
208 pub fn resolve() -> Option<Self> {
209 if let Some(helper) = std::env::var_os("VTCODE_LINUX_SANDBOX_EXECUTABLE") {
210 return Some(Self::external(helper.into()));
211 }
212 std::env::current_exe().ok().map(Self::busybox)
213 }
214
215 #[cfg(not(target_os = "linux"))]
216 pub fn resolve() -> Option<Self> {
217 None
218 }
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
223pub enum SandboxType {
224 #[default]
226 None,
227
228 MacosSeatbelt,
230
231 LinuxLandlock,
233
234 WindowsRestrictedToken,
236}
237
238impl SandboxType {
239 pub(crate) fn platform_default() -> Self {
241 #[cfg(target_os = "macos")]
242 {
243 Self::MacosSeatbelt
244 }
245 #[cfg(target_os = "linux")]
246 {
247 Self::LinuxLandlock
248 }
249 #[cfg(target_os = "windows")]
250 {
251 Self::WindowsRestrictedToken
252 }
253 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
254 {
255 Self::None
256 }
257 }
258
259 pub(crate) fn is_available(&self) -> bool {
268 match self {
269 Self::None => true,
270 Self::MacosSeatbelt => cfg!(target_os = "macos"),
271 #[cfg(target_os = "linux")]
274 Self::LinuxLandlock => super::linux::landlock_supported(),
275 #[cfg(not(target_os = "linux"))]
276 Self::LinuxLandlock => false,
277 Self::WindowsRestrictedToken => false,
282 }
283 }
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 #[test]
291 fn test_command_spec_builder() {
292 let spec = CommandSpec::new("cat")
293 .with_args(vec!["file.txt"])
294 .with_cwd("/tmp")
295 .with_justification("testing");
296
297 assert_eq!(spec.program, OsString::from("cat"));
298 assert_eq!(spec.args, vec!["file.txt"]);
299 assert_eq!(spec.cwd, PathBuf::from("/tmp"));
300 assert_eq!(spec.justification, Some("testing".to_string()));
301 }
302
303 #[test]
304 fn test_full_command() {
305 let spec = CommandSpec::new("echo").with_args(vec!["hello", "world"]);
306
307 assert_eq!(spec.full_command(), vec![OsString::from("echo"), OsString::from("hello"), OsString::from("world")]);
308 }
309
310 #[test]
311 fn test_command_spec_accepts_path_backed_program() {
312 let program = PathBuf::from("/tmp/example-program");
313 let spec = CommandSpec::new(program.clone());
314
315 assert_eq!(spec.program, program.into_os_string());
316 }
317
318 #[test]
319 fn test_exec_expiration() {
320 let timeout = ExecExpiration::Timeout(Duration::from_secs(10));
321 assert_eq!(timeout.timeout_ms(), Some(10_000));
322
323 let default = ExecExpiration::DefaultTimeout;
324 assert_eq!(default.timeout_ms(), Some(30_000));
325 }
326}