vtcode_core/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(d.as_millis() as u64),
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 program: OsString,
66
67 pub args: Vec<String>,
69
70 pub cwd: PathBuf,
72
73 pub env: HashMap<String, String>,
75
76 pub expiration: ExecExpiration,
78
79 pub sandbox_permissions: SandboxPermissions,
81
82 pub 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 {
104 program: program.into(),
105 ..Default::default()
106 }
107 }
108
109 pub fn with_args(mut self, args: impl IntoIterator<Item = impl Into<String>>) -> Self {
111 self.args = args.into_iter().map(Into::into).collect();
112 self
113 }
114
115 pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
117 self.cwd = cwd.into();
118 self
119 }
120
121 pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
123 self.env = env;
124 self
125 }
126
127 pub fn with_expiration(mut self, expiration: ExecExpiration) -> Self {
129 self.expiration = expiration;
130 self
131 }
132
133 pub fn with_sandbox_permissions(mut self, permissions: SandboxPermissions) -> Self {
135 self.sandbox_permissions = permissions;
136 self
137 }
138
139 pub fn with_justification(mut self, justification: impl Into<String>) -> Self {
141 self.justification = Some(justification.into());
142 self
143 }
144
145 pub fn full_command(&self) -> Vec<OsString> {
147 let mut cmd = vec![self.program.clone()];
148 cmd.extend(self.args.iter().cloned().map(OsString::from));
149 cmd
150 }
151}
152
153#[derive(Debug, Clone)]
155pub struct ExecEnv {
156 pub program: PathBuf,
158
159 pub args: Vec<String>,
161
162 pub cwd: PathBuf,
164
165 pub env: HashMap<String, String>,
167
168 pub expiration: ExecExpiration,
170
171 pub sandbox_active: bool,
173
174 pub sandbox_type: SandboxType,
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
180pub enum SandboxType {
181 #[default]
183 None,
184
185 MacosSeatbelt,
187
188 LinuxLandlock,
190
191 WindowsRestrictedToken,
193}
194
195impl SandboxType {
196 pub fn platform_default() -> Self {
198 #[cfg(target_os = "macos")]
199 {
200 Self::MacosSeatbelt
201 }
202 #[cfg(target_os = "linux")]
203 {
204 Self::LinuxLandlock
205 }
206 #[cfg(target_os = "windows")]
207 {
208 Self::WindowsRestrictedToken
209 }
210 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
211 {
212 Self::None
213 }
214 }
215
216 pub fn is_available(&self) -> bool {
218 match self {
219 Self::None => true,
220 Self::MacosSeatbelt => cfg!(target_os = "macos"),
221 Self::LinuxLandlock => cfg!(target_os = "linux"),
222 Self::WindowsRestrictedToken => cfg!(target_os = "windows"),
223 }
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_command_spec_builder() {
233 let spec = CommandSpec::new("cat")
234 .with_args(vec!["file.txt"])
235 .with_cwd("/tmp")
236 .with_justification("testing");
237
238 assert_eq!(spec.program, OsString::from("cat"));
239 assert_eq!(spec.args, vec!["file.txt"]);
240 assert_eq!(spec.cwd, PathBuf::from("/tmp"));
241 assert_eq!(spec.justification, Some("testing".to_string()));
242 }
243
244 #[test]
245 fn test_full_command() {
246 let spec = CommandSpec::new("echo").with_args(vec!["hello", "world"]);
247
248 assert_eq!(
249 spec.full_command(),
250 vec![
251 OsString::from("echo"),
252 OsString::from("hello"),
253 OsString::from("world")
254 ]
255 );
256 }
257
258 #[test]
259 fn test_command_spec_accepts_path_backed_program() {
260 let program = PathBuf::from("/tmp/example-program");
261 let spec = CommandSpec::new(program.clone());
262
263 assert_eq!(spec.program, program.into_os_string());
264 }
265
266 #[test]
267 fn test_exec_expiration() {
268 let timeout = ExecExpiration::Timeout(Duration::from_secs(10));
269 assert_eq!(timeout.timeout_ms(), Some(10_000));
270
271 let default = ExecExpiration::DefaultTimeout;
272 assert_eq!(default.timeout_ms(), Some(30_000));
273 }
274}