vtcode-config 0.125.1

Config loader components shared across VT Code and downstream adopters
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
//! Sandbox configuration for VT Code
//!
//! Implements configuration for the sandbox system following the AI sandbox field guide's
//! three-question model:
//! - **Boundary**: What is shared between code and host
//! - **Policy**: What can code touch (files, network, devices, syscalls)
//! - **Lifecycle**: What survives between runs

use crate::env_helpers::default_true;
use serde::{Deserialize, Serialize};

/// Sandbox configuration
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
pub struct SandboxConfig {
    /// Enable sandboxing for command execution
    #[serde(default = "default_false")]
    pub enabled: bool,

    /// Default sandbox policy
    #[serde(default)]
    pub default_policy: SandboxPolicy,

    /// Network egress configuration
    #[serde(default)]
    pub network: NetworkConfig,

    /// Sensitive path blocking configuration
    #[serde(default)]
    pub sensitive_paths: SensitivePathsConfig,

    /// Resource limits configuration
    #[serde(default)]
    pub resource_limits: ResourceLimitsConfig,

    /// Linux-specific seccomp configuration
    #[serde(default)]
    pub seccomp: SeccompConfig,

    /// External sandbox configuration (Docker, MicroVM, etc.)
    #[serde(default)]
    pub external: ExternalSandboxConfig,
}

impl Default for SandboxConfig {
    fn default() -> Self {
        Self {
            enabled: default_false(),
            default_policy: SandboxPolicy::default(),
            network: NetworkConfig::default(),
            sensitive_paths: SensitivePathsConfig::default(),
            resource_limits: ResourceLimitsConfig::default(),
            seccomp: SeccompConfig::default(),
            external: ExternalSandboxConfig::default(),
        }
    }
}

/// Sandbox policy following the Codex model
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SandboxPolicy {
    /// Read-only access - safest policy
    #[default]
    ReadOnly,
    /// Write access within workspace only
    WorkspaceWrite,
    /// Full access - dangerous, requires explicit approval
    DangerFullAccess,
    /// External sandbox (Docker, MicroVM)
    External,
}

/// Network egress configuration
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct NetworkConfig {
    /// Allow any network access (legacy mode)
    #[serde(default)]
    pub allow_all: bool,

    /// Domain allowlist for network egress
    /// Following field guide: "Default-deny outbound network, then allowlist."
    #[serde(default)]
    pub allowlist: Vec<NetworkAllowlistEntryConfig>,

    /// Block all network access (overrides allowlist)
    #[serde(default)]
    pub block_all: bool,
}

/// Network allowlist entry
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct NetworkAllowlistEntryConfig {
    /// Domain pattern (e.g., "api.github.com", "*.npmjs.org")
    pub domain: String,
    /// Port (defaults to 443)
    #[serde(default = "default_https_port")]
    pub port: u16,
}

fn default_https_port() -> u16 {
    443
}

/// Sensitive paths configuration
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SensitivePathsConfig {
    /// Use default sensitive paths (SSH, AWS, etc.)
    #[serde(default = "default_true")]
    pub use_defaults: bool,

    /// Additional paths to block
    #[serde(default)]
    pub additional: Vec<String>,

    /// Paths to explicitly allow (overrides defaults)
    #[serde(default)]
    pub exceptions: Vec<String>,
}

impl Default for SensitivePathsConfig {
    fn default() -> Self {
        Self {
            use_defaults: default_true(),
            additional: Vec::new(),
            exceptions: Vec::new(),
        }
    }
}

/// Resource limits configuration
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ResourceLimitsConfig {
    /// Preset resource limits profile
    #[serde(default)]
    pub preset: ResourceLimitsPreset,

    /// Custom memory limit in MB (0 = use preset)
    #[serde(default)]
    pub max_memory_mb: u64,

    /// Custom max processes (0 = use preset)
    #[serde(default)]
    pub max_pids: u32,

    /// Custom disk write limit in MB (0 = use preset)
    #[serde(default)]
    pub max_disk_mb: u64,

    /// Custom CPU time limit in seconds (0 = use preset)
    #[serde(default)]
    pub cpu_time_secs: u64,

    /// Custom wall clock timeout in seconds (0 = use preset)
    #[serde(default)]
    pub timeout_secs: u64,
}

/// Resource limits preset
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ResourceLimitsPreset {
    /// No limits
    Unlimited,
    /// Conservative limits for untrusted code
    Conservative,
    /// Moderate limits for semi-trusted code
    #[default]
    Moderate,
    /// Generous limits for trusted code
    Generous,
    /// Custom limits (use individual settings)
    Custom,
}

/// Linux seccomp configuration
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SeccompConfig {
    /// Enable seccomp filtering (Linux only)
    #[serde(default = "default_true")]
    pub enabled: bool,

    /// Seccomp profile preset
    #[serde(default)]
    pub profile: SeccompProfilePreset,

    /// Additional syscalls to block
    #[serde(default)]
    pub additional_blocked: Vec<String>,

    /// Log blocked syscalls instead of killing process (for debugging)
    #[serde(default)]
    pub log_only: bool,
}

impl Default for SeccompConfig {
    fn default() -> Self {
        Self {
            enabled: default_true(),
            profile: SeccompProfilePreset::default(),
            additional_blocked: Vec::new(),
            log_only: false,
        }
    }
}

/// Seccomp profile preset
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum SeccompProfilePreset {
    /// Strict profile - blocks most dangerous syscalls
    #[default]
    Strict,
    /// Permissive profile - only blocks critical syscalls
    Permissive,
    /// Disabled - no syscall filtering
    Disabled,
}

/// External sandbox configuration (Docker, MicroVM)
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct ExternalSandboxConfig {
    /// Type of external sandbox
    #[serde(default)]
    pub sandbox_type: ExternalSandboxType,

    /// Docker-specific settings
    #[serde(default)]
    pub docker: DockerSandboxConfig,

    /// MicroVM-specific settings
    #[serde(default)]
    pub microvm: MicroVMSandboxConfig,
}

/// External sandbox type
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ExternalSandboxType {
    /// No external sandbox
    #[default]
    None,
    /// Docker container
    Docker,
    /// MicroVM (Firecracker, cloud-hypervisor)
    MicroVM,
    /// gVisor container runtime
    GVisor,
}

/// Docker sandbox configuration
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct DockerSandboxConfig {
    /// Docker image to use
    #[serde(default = "default_docker_image")]
    pub image: String,

    /// Memory limit for container
    #[serde(default)]
    pub memory_limit: String,

    /// CPU limit for container
    #[serde(default)]
    pub cpu_limit: String,

    /// Network mode
    #[serde(default = "default_network_mode")]
    pub network_mode: String,
}

fn default_docker_image() -> String {
    "ubuntu:22.04".to_string()
}

fn default_network_mode() -> String {
    "none".to_string()
}

impl Default for DockerSandboxConfig {
    fn default() -> Self {
        Self {
            image: default_docker_image(),
            memory_limit: String::new(),
            cpu_limit: String::new(),
            network_mode: default_network_mode(),
        }
    }
}

/// MicroVM sandbox configuration
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct MicroVMSandboxConfig {
    /// VMM to use (firecracker, cloud-hypervisor)
    #[serde(default)]
    pub vmm: String,

    /// Kernel image path
    #[serde(default)]
    pub kernel_path: String,

    /// Root filesystem path
    #[serde(default)]
    pub rootfs_path: String,

    /// Memory size in MB
    #[serde(default = "default_microvm_memory")]
    pub memory_mb: u64,

    /// Number of vCPUs
    #[serde(default = "default_vcpus")]
    pub vcpus: u32,
}

fn default_microvm_memory() -> u64 {
    512
}

fn default_vcpus() -> u32 {
    1
}

impl Default for MicroVMSandboxConfig {
    fn default() -> Self {
        Self {
            vmm: String::new(),
            kernel_path: String::new(),
            rootfs_path: String::new(),
            memory_mb: default_microvm_memory(),
            vcpus: default_vcpus(),
        }
    }
}

#[inline]
const fn default_false() -> bool {
    false
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_sandbox_config_default() {
        let config = SandboxConfig::default();
        assert!(!config.enabled);
        assert_eq!(config.default_policy, SandboxPolicy::ReadOnly);
    }

    #[test]
    fn test_sandbox_config_parses_default_policy() {
        let config: SandboxConfig = toml::from_str(
            r#"
enabled = true
default_policy = "workspace_write"
"#,
        )
        .expect("sandbox config with default_policy should parse");

        assert!(config.enabled);
        assert_eq!(config.default_policy, SandboxPolicy::WorkspaceWrite);
    }

    #[test]
    fn test_sandbox_config_serializes_default_policy() {
        let config = SandboxConfig {
            default_policy: SandboxPolicy::DangerFullAccess,
            ..SandboxConfig::default()
        };

        let toml = toml::to_string(&config).expect("sandbox config should serialize");

        assert!(toml.contains("default_policy = \"danger_full_access\""));
        let removed_field = format!("default_{}", "mode");
        assert!(!toml.contains(&removed_field));
    }

    #[test]
    fn test_sandbox_config_rejects_removed_default_field() {
        let removed_field = format!("default_{}", "mode");
        let input = format!(
            r#"
enabled = true
{removed_field} = "workspace_write"
"#,
        );
        let err = toml::from_str::<SandboxConfig>(&input)
            .expect_err("sandbox config should reject removed default field");

        assert!(
            err.to_string()
                .contains(&format!("unknown field `{removed_field}`"))
        );
    }

    #[test]
    fn test_network_config_default() {
        let config = NetworkConfig::default();
        assert!(!config.allow_all);
        assert!(!config.block_all);
        assert!(config.allowlist.is_empty());
    }

    #[test]
    fn test_resource_limits_config_default() {
        let config = ResourceLimitsConfig::default();
        assert_eq!(config.preset, ResourceLimitsPreset::Moderate);
    }

    #[test]
    fn test_seccomp_config_default() {
        let config = SeccompConfig::default();
        assert!(config.enabled);
        assert_eq!(config.profile, SeccompProfilePreset::Strict);
    }
}