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