Skip to main content

vtcode_config/core/
sandbox.rs

1//! Sandbox configuration for VT Code
2//!
3//! Implements configuration for the sandbox system following the AI sandbox field guide's
4//! three-question model:
5//! - **Boundary**: What is shared between code and host
6//! - **Policy**: What can code touch (files, network, devices, syscalls)
7//! - **Lifecycle**: What survives between runs
8
9use crate::env_helpers::default_true;
10use serde::{Deserialize, Serialize};
11
12/// Sandbox configuration
13#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
14#[derive(Debug, Clone, Deserialize, Serialize)]
15pub struct SandboxConfig {
16    /// Enable sandboxing for command execution
17    #[serde(default = "default_false")]
18    pub enabled: bool,
19
20    /// Default sandbox mode
21    #[serde(default)]
22    pub default_mode: SandboxMode,
23
24    /// Network egress configuration
25    #[serde(default)]
26    pub network: NetworkConfig,
27
28    /// Sensitive path blocking configuration
29    #[serde(default)]
30    pub sensitive_paths: SensitivePathsConfig,
31
32    /// Resource limits configuration
33    #[serde(default)]
34    pub resource_limits: ResourceLimitsConfig,
35
36    /// Linux-specific seccomp configuration
37    #[serde(default)]
38    pub seccomp: SeccompConfig,
39
40    /// External sandbox configuration (Docker, MicroVM, etc.)
41    #[serde(default)]
42    pub external: ExternalSandboxConfig,
43}
44
45impl Default for SandboxConfig {
46    fn default() -> Self {
47        Self {
48            enabled: default_false(),
49            default_mode: SandboxMode::default(),
50            network: NetworkConfig::default(),
51            sensitive_paths: SensitivePathsConfig::default(),
52            resource_limits: ResourceLimitsConfig::default(),
53            seccomp: SeccompConfig::default(),
54            external: ExternalSandboxConfig::default(),
55        }
56    }
57}
58
59/// Sandbox mode following the Codex model
60#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
61#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
62#[serde(rename_all = "snake_case")]
63pub enum SandboxMode {
64    /// Read-only access - safest mode
65    #[default]
66    ReadOnly,
67    /// Write access within workspace only
68    WorkspaceWrite,
69    /// Full access - dangerous, requires explicit approval
70    DangerFullAccess,
71    /// External sandbox (Docker, MicroVM)
72    External,
73}
74
75/// Network egress configuration
76#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
77#[derive(Debug, Clone, Default, Deserialize, Serialize)]
78pub struct NetworkConfig {
79    /// Allow any network access (legacy mode)
80    #[serde(default)]
81    pub allow_all: bool,
82
83    /// Domain allowlist for network egress
84    /// Following field guide: "Default-deny outbound network, then allowlist."
85    #[serde(default)]
86    pub allowlist: Vec<NetworkAllowlistEntryConfig>,
87
88    /// Block all network access (overrides allowlist)
89    #[serde(default)]
90    pub block_all: bool,
91}
92
93/// Network allowlist entry
94#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
95#[derive(Debug, Clone, Deserialize, Serialize)]
96pub struct NetworkAllowlistEntryConfig {
97    /// Domain pattern (e.g., "api.github.com", "*.npmjs.org")
98    pub domain: String,
99    /// Port (defaults to 443)
100    #[serde(default = "default_https_port")]
101    pub port: u16,
102}
103
104fn default_https_port() -> u16 {
105    443
106}
107
108/// Sensitive paths configuration
109#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
110#[derive(Debug, Clone, Deserialize, Serialize)]
111pub struct SensitivePathsConfig {
112    /// Use default sensitive paths (SSH, AWS, etc.)
113    #[serde(default = "default_true")]
114    pub use_defaults: bool,
115
116    /// Additional paths to block
117    #[serde(default)]
118    pub additional: Vec<String>,
119
120    /// Paths to explicitly allow (overrides defaults)
121    #[serde(default)]
122    pub exceptions: Vec<String>,
123}
124
125impl Default for SensitivePathsConfig {
126    fn default() -> Self {
127        Self {
128            use_defaults: default_true(),
129            additional: Vec::new(),
130            exceptions: Vec::new(),
131        }
132    }
133}
134
135/// Resource limits configuration
136#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
137#[derive(Debug, Clone, Default, Deserialize, Serialize)]
138pub struct ResourceLimitsConfig {
139    /// Preset resource limits profile
140    #[serde(default)]
141    pub preset: ResourceLimitsPreset,
142
143    /// Custom memory limit in MB (0 = use preset)
144    #[serde(default)]
145    pub max_memory_mb: u64,
146
147    /// Custom max processes (0 = use preset)
148    #[serde(default)]
149    pub max_pids: u32,
150
151    /// Custom disk write limit in MB (0 = use preset)
152    #[serde(default)]
153    pub max_disk_mb: u64,
154
155    /// Custom CPU time limit in seconds (0 = use preset)
156    #[serde(default)]
157    pub cpu_time_secs: u64,
158
159    /// Custom wall clock timeout in seconds (0 = use preset)
160    #[serde(default)]
161    pub timeout_secs: u64,
162}
163
164/// Resource limits preset
165#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
166#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
167#[serde(rename_all = "snake_case")]
168pub enum ResourceLimitsPreset {
169    /// No limits
170    Unlimited,
171    /// Conservative limits for untrusted code
172    Conservative,
173    /// Moderate limits for semi-trusted code
174    #[default]
175    Moderate,
176    /// Generous limits for trusted code
177    Generous,
178    /// Custom limits (use individual settings)
179    Custom,
180}
181
182/// Linux seccomp configuration
183#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
184#[derive(Debug, Clone, Deserialize, Serialize)]
185pub struct SeccompConfig {
186    /// Enable seccomp filtering (Linux only)
187    #[serde(default = "default_true")]
188    pub enabled: bool,
189
190    /// Seccomp profile preset
191    #[serde(default)]
192    pub profile: SeccompProfilePreset,
193
194    /// Additional syscalls to block
195    #[serde(default)]
196    pub additional_blocked: Vec<String>,
197
198    /// Log blocked syscalls instead of killing process (for debugging)
199    #[serde(default)]
200    pub log_only: bool,
201}
202
203impl Default for SeccompConfig {
204    fn default() -> Self {
205        Self {
206            enabled: default_true(),
207            profile: SeccompProfilePreset::default(),
208            additional_blocked: Vec::new(),
209            log_only: false,
210        }
211    }
212}
213
214/// Seccomp profile preset
215#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
216#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
217#[serde(rename_all = "snake_case")]
218pub enum SeccompProfilePreset {
219    /// Strict profile - blocks most dangerous syscalls
220    #[default]
221    Strict,
222    /// Permissive profile - only blocks critical syscalls
223    Permissive,
224    /// Disabled - no syscall filtering
225    Disabled,
226}
227
228/// External sandbox configuration (Docker, MicroVM)
229#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
230#[derive(Debug, Clone, Default, Deserialize, Serialize)]
231pub struct ExternalSandboxConfig {
232    /// Type of external sandbox
233    #[serde(default)]
234    pub sandbox_type: ExternalSandboxType,
235
236    /// Docker-specific settings
237    #[serde(default)]
238    pub docker: DockerSandboxConfig,
239
240    /// MicroVM-specific settings
241    #[serde(default)]
242    pub microvm: MicroVMSandboxConfig,
243}
244
245/// External sandbox type
246#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
247#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
248#[serde(rename_all = "snake_case")]
249pub enum ExternalSandboxType {
250    /// No external sandbox
251    #[default]
252    None,
253    /// Docker container
254    Docker,
255    /// MicroVM (Firecracker, cloud-hypervisor)
256    MicroVM,
257    /// gVisor container runtime
258    GVisor,
259}
260
261/// Docker sandbox configuration
262#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
263#[derive(Debug, Clone, Deserialize, Serialize)]
264pub struct DockerSandboxConfig {
265    /// Docker image to use
266    #[serde(default = "default_docker_image")]
267    pub image: String,
268
269    /// Memory limit for container
270    #[serde(default)]
271    pub memory_limit: String,
272
273    /// CPU limit for container
274    #[serde(default)]
275    pub cpu_limit: String,
276
277    /// Network mode
278    #[serde(default = "default_network_mode")]
279    pub network_mode: String,
280}
281
282fn default_docker_image() -> String {
283    "ubuntu:22.04".to_string()
284}
285
286fn default_network_mode() -> String {
287    "none".to_string()
288}
289
290impl Default for DockerSandboxConfig {
291    fn default() -> Self {
292        Self {
293            image: default_docker_image(),
294            memory_limit: String::new(),
295            cpu_limit: String::new(),
296            network_mode: default_network_mode(),
297        }
298    }
299}
300
301/// MicroVM sandbox configuration
302#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
303#[derive(Debug, Clone, Deserialize, Serialize)]
304pub struct MicroVMSandboxConfig {
305    /// VMM to use (firecracker, cloud-hypervisor)
306    #[serde(default)]
307    pub vmm: String,
308
309    /// Kernel image path
310    #[serde(default)]
311    pub kernel_path: String,
312
313    /// Root filesystem path
314    #[serde(default)]
315    pub rootfs_path: String,
316
317    /// Memory size in MB
318    #[serde(default = "default_microvm_memory")]
319    pub memory_mb: u64,
320
321    /// Number of vCPUs
322    #[serde(default = "default_vcpus")]
323    pub vcpus: u32,
324}
325
326fn default_microvm_memory() -> u64 {
327    512
328}
329
330fn default_vcpus() -> u32 {
331    1
332}
333
334impl Default for MicroVMSandboxConfig {
335    fn default() -> Self {
336        Self {
337            vmm: String::new(),
338            kernel_path: String::new(),
339            rootfs_path: String::new(),
340            memory_mb: default_microvm_memory(),
341            vcpus: default_vcpus(),
342        }
343    }
344}
345
346#[inline]
347const fn default_false() -> bool {
348    false
349}
350
351#[cfg(test)]
352mod tests {
353    use super::*;
354
355    #[test]
356    fn test_sandbox_config_default() {
357        let config = SandboxConfig::default();
358        assert!(!config.enabled);
359        assert_eq!(config.default_mode, SandboxMode::ReadOnly);
360    }
361
362    #[test]
363    fn test_network_config_default() {
364        let config = NetworkConfig::default();
365        assert!(!config.allow_all);
366        assert!(!config.block_all);
367        assert!(config.allowlist.is_empty());
368    }
369
370    #[test]
371    fn test_resource_limits_config_default() {
372        let config = ResourceLimitsConfig::default();
373        assert_eq!(config.preset, ResourceLimitsPreset::Moderate);
374    }
375
376    #[test]
377    fn test_seccomp_config_default() {
378        let config = SeccompConfig::default();
379        assert!(config.enabled);
380        assert_eq!(config.profile, SeccompProfilePreset::Strict);
381    }
382}