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(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(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, Copy, PartialEq, Eq, Default)]
177pub enum SandboxType {
178 #[default]
180 None,
181
182 MacosSeatbelt,
184
185 LinuxLandlock,
187
188 WindowsRestrictedToken,
190}
191
192impl SandboxType {
193 pub(crate) fn platform_default() -> Self {
195 #[cfg(target_os = "macos")]
196 {
197 Self::MacosSeatbelt
198 }
199 #[cfg(target_os = "linux")]
200 {
201 Self::LinuxLandlock
202 }
203 #[cfg(target_os = "windows")]
204 {
205 Self::WindowsRestrictedToken
206 }
207 #[cfg(not(any(target_os = "macos", target_os = "linux", target_os = "windows")))]
208 {
209 Self::None
210 }
211 }
212
213 pub(crate) fn is_available(&self) -> bool {
215 match self {
216 Self::None => true,
217 Self::MacosSeatbelt => cfg!(target_os = "macos"),
218 Self::LinuxLandlock => cfg!(target_os = "linux"),
219 Self::WindowsRestrictedToken => cfg!(target_os = "windows"),
220 }
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227
228 #[test]
229 fn test_command_spec_builder() {
230 let spec = CommandSpec::new("cat")
231 .with_args(vec!["file.txt"])
232 .with_cwd("/tmp")
233 .with_justification("testing");
234
235 assert_eq!(spec.program, OsString::from("cat"));
236 assert_eq!(spec.args, vec!["file.txt"]);
237 assert_eq!(spec.cwd, PathBuf::from("/tmp"));
238 assert_eq!(spec.justification, Some("testing".to_string()));
239 }
240
241 #[test]
242 fn test_full_command() {
243 let spec = CommandSpec::new("echo").with_args(vec!["hello", "world"]);
244
245 assert_eq!(spec.full_command(), vec![OsString::from("echo"), OsString::from("hello"), OsString::from("world")]);
246 }
247
248 #[test]
249 fn test_command_spec_accepts_path_backed_program() {
250 let program = PathBuf::from("/tmp/example-program");
251 let spec = CommandSpec::new(program.clone());
252
253 assert_eq!(spec.program, program.into_os_string());
254 }
255
256 #[test]
257 fn test_exec_expiration() {
258 let timeout = ExecExpiration::Timeout(Duration::from_secs(10));
259 assert_eq!(timeout.timeout_ms(), Some(10_000));
260
261 let default = ExecExpiration::DefaultTimeout;
262 assert_eq!(default.timeout_ms(), Some(30_000));
263 }
264}