Skip to main content

vtcode_core/sandboxing/
exec_env.rs

1//! Command specification and execution environment types.
2
3use 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/// Mechanism to terminate an exec invocation before it finishes naturally.
13#[derive(Debug, Clone, Default)]
14pub enum ExecExpiration {
15    /// Timeout after a specified duration.
16    Timeout(Duration),
17
18    /// Use the default timeout.
19    #[default]
20    DefaultTimeout,
21
22    /// Cancel via a cancellation token.
23    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    /// Get the timeout in milliseconds, if applicable.
43    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), // 30 second default
47            Self::Cancellation(_) => None,
48        }
49    }
50
51    /// Get the timeout duration, if applicable.
52    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/// Specification for a command to be executed.
62#[derive(Debug, Clone)]
63pub struct CommandSpec {
64    /// The program to execute.
65    pub program: OsString,
66
67    /// Arguments to pass to the program.
68    pub args: Vec<String>,
69
70    /// Working directory for the command.
71    pub cwd: PathBuf,
72
73    /// Environment variables to set.
74    pub env: HashMap<String, String>,
75
76    /// Expiration mechanism for the command.
77    pub expiration: ExecExpiration,
78
79    /// Sandbox permissions for this command.
80    pub sandbox_permissions: SandboxPermissions,
81
82    /// Optional justification for why the command needs to run.
83    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    /// Create a new command specification.
102    pub fn new(program: impl Into<OsString>) -> Self {
103        Self {
104            program: program.into(),
105            ..Default::default()
106        }
107    }
108
109    /// Add arguments to the command.
110    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    /// Set the working directory.
116    pub fn with_cwd(mut self, cwd: impl Into<PathBuf>) -> Self {
117        self.cwd = cwd.into();
118        self
119    }
120
121    /// Set environment variables.
122    pub fn with_env(mut self, env: HashMap<String, String>) -> Self {
123        self.env = env;
124        self
125    }
126
127    /// Set the expiration.
128    pub fn with_expiration(mut self, expiration: ExecExpiration) -> Self {
129        self.expiration = expiration;
130        self
131    }
132
133    /// Set sandbox permissions.
134    pub fn with_sandbox_permissions(mut self, permissions: SandboxPermissions) -> Self {
135        self.sandbox_permissions = permissions;
136        self
137    }
138
139    /// Set a justification.
140    pub fn with_justification(mut self, justification: impl Into<String>) -> Self {
141        self.justification = Some(justification.into());
142        self
143    }
144
145    /// Get the full command as a vector.
146    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/// The prepared execution environment after sandbox transformation.
154#[derive(Debug, Clone)]
155pub struct ExecEnv {
156    /// The program to execute (may be wrapped).
157    pub program: PathBuf,
158
159    /// Arguments to the program (may include sandbox wrapper args).
160    pub args: Vec<String>,
161
162    /// Working directory.
163    pub cwd: PathBuf,
164
165    /// Environment variables.
166    pub env: HashMap<String, String>,
167
168    /// Expiration mechanism.
169    pub expiration: ExecExpiration,
170
171    /// Whether the sandbox is active.
172    pub sandbox_active: bool,
173
174    /// Type of sandbox applied.
175    pub sandbox_type: SandboxType,
176}
177
178/// Type of sandbox being used.
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
180pub enum SandboxType {
181    /// No sandbox applied.
182    #[default]
183    None,
184
185    /// macOS Seatbelt sandbox.
186    MacosSeatbelt,
187
188    /// Linux Landlock + Seccomp sandbox.
189    LinuxLandlock,
190
191    /// Windows restricted token sandbox.
192    WindowsRestrictedToken,
193}
194
195impl SandboxType {
196    /// Get the platform-appropriate sandbox type.
197    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    /// Check if this sandbox type is available on the current platform.
217    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}