1use crate::env_helpers::default_true;
10use serde::de::{self, MapAccess, Visitor};
11use serde::{Deserialize, Deserializer, Serialize};
12use std::fmt;
13
14#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
16#[derive(Debug, Clone, Deserialize, Serialize)]
17pub struct SandboxConfig {
18 #[serde(default = "default_false")]
20 pub enabled: bool,
21
22 #[serde(default)]
24 pub default_policy: SandboxPolicy,
25
26 #[serde(default)]
28 pub network: NetworkConfig,
29
30 #[serde(default)]
32 pub sensitive_paths: SensitivePathsConfig,
33
34 #[serde(default)]
36 pub resource_limits: ResourceLimitsConfig,
37
38 #[serde(default)]
40 pub seccomp: SeccompConfig,
41
42 #[serde(default)]
44 pub external: ExternalSandboxConfig,
45}
46
47impl Default for SandboxConfig {
48 fn default() -> Self {
49 Self {
50 enabled: default_false(),
51 default_policy: SandboxPolicy::default(),
52 network: NetworkConfig::default(),
53 sensitive_paths: SensitivePathsConfig::default(),
54 resource_limits: ResourceLimitsConfig::default(),
55 seccomp: SeccompConfig::default(),
56 external: ExternalSandboxConfig::default(),
57 }
58 }
59}
60
61#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
63#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
64#[serde(rename_all = "snake_case")]
65pub enum SandboxPolicy {
66 #[default]
68 ReadOnly,
69 WorkspaceWrite,
71 DangerFullAccess,
73 External,
75}
76
77#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
83#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
84#[serde(rename_all = "snake_case")]
85pub enum NetworkPolicy {
86 #[default]
88 AllowlistOnly,
89 AllowAll,
91 BlockAll,
93}
94
95#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
97#[derive(Debug, Clone, Serialize)]
98pub struct NetworkConfig {
99 pub policy: NetworkPolicy,
102
103 #[serde(default)]
106 pub allowlist: Vec<NetworkAllowlistEntryConfig>,
107}
108
109impl Default for NetworkConfig {
110 fn default() -> Self {
111 Self {
112 policy: NetworkPolicy::AllowlistOnly,
113 allowlist: Vec::new(),
114 }
115 }
116}
117
118impl<'de> Deserialize<'de> for NetworkConfig {
119 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
120 where
121 D: Deserializer<'de>,
122 {
123 #[derive(Deserialize)]
124 #[serde(field_identifier, rename_all = "snake_case")]
125 enum Field {
126 Policy,
127 Allowlist,
128 AllowAll,
129 BlockAll,
130 }
131
132 struct NetworkConfigVisitor;
133
134 impl<'de> Visitor<'de> for NetworkConfigVisitor {
135 type Value = NetworkConfig;
136
137 fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {
138 f.write_str("NetworkConfig struct")
139 }
140
141 fn visit_map<V>(self, mut map: V) -> Result<NetworkConfig, V::Error>
142 where
143 V: MapAccess<'de>,
144 {
145 let mut policy: Option<NetworkPolicy> = None;
146 let mut allowlist: Option<Vec<NetworkAllowlistEntryConfig>> = None;
147 let mut allow_all: Option<bool> = None;
148 let mut block_all: Option<bool> = None;
149
150 while let Some(key) = map.next_key()? {
151 match key {
152 Field::Policy => {
153 if policy.is_some() {
154 return Err(de::Error::duplicate_field("policy"));
155 }
156 policy = Some(map.next_value()?);
157 }
158 Field::Allowlist => {
159 if allowlist.is_some() {
160 return Err(de::Error::duplicate_field("allowlist"));
161 }
162 allowlist = Some(map.next_value()?);
163 }
164 Field::AllowAll => {
165 if allow_all.is_some() {
166 return Err(de::Error::duplicate_field("allow_all"));
167 }
168 allow_all = Some(map.next_value()?);
169 }
170 Field::BlockAll => {
171 if block_all.is_some() {
172 return Err(de::Error::duplicate_field("block_all"));
173 }
174 block_all = Some(map.next_value()?);
175 }
176 }
177 }
178
179 let resolved_policy = if block_all.unwrap_or(false) {
182 NetworkPolicy::BlockAll
183 } else if allow_all.unwrap_or(false) {
184 NetworkPolicy::AllowAll
185 } else {
186 policy.unwrap_or_default()
187 };
188
189 Ok(NetworkConfig {
190 policy: resolved_policy,
191 allowlist: allowlist.unwrap_or_default(),
192 })
193 }
194 }
195
196 deserializer.deserialize_map(NetworkConfigVisitor)
197 }
198}
199
200#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
202#[derive(Debug, Clone, Deserialize, Serialize)]
203pub struct NetworkAllowlistEntryConfig {
204 pub domain: String,
206 #[serde(default = "default_https_port")]
208 pub port: u16,
209}
210
211fn default_https_port() -> u16 {
212 443
213}
214
215#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
217#[derive(Debug, Clone, Deserialize, Serialize)]
218pub struct SensitivePathsConfig {
219 #[serde(default = "default_true")]
221 pub use_defaults: bool,
222
223 #[serde(default)]
225 pub additional: Vec<String>,
226
227 #[serde(default)]
229 pub exceptions: Vec<String>,
230}
231
232impl Default for SensitivePathsConfig {
233 fn default() -> Self {
234 Self {
235 use_defaults: default_true(),
236 additional: Vec::new(),
237 exceptions: Vec::new(),
238 }
239 }
240}
241
242#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
244#[derive(Debug, Clone, Default, Deserialize, Serialize)]
245pub struct ResourceLimitsConfig {
246 #[serde(default)]
248 pub preset: ResourceLimitsPreset,
249
250 #[serde(default)]
252 pub max_memory_mb: u64,
253
254 #[serde(default)]
256 pub max_pids: u32,
257
258 #[serde(default)]
260 pub max_disk_mb: u64,
261
262 #[serde(default)]
264 pub cpu_time_secs: u64,
265
266 #[serde(default)]
268 pub timeout_secs: u64,
269}
270
271#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
273#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
274#[serde(rename_all = "snake_case")]
275pub enum ResourceLimitsPreset {
276 Unlimited,
278 Conservative,
280 #[default]
282 Moderate,
283 Generous,
285 Custom,
287}
288
289#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
291#[derive(Debug, Clone, Deserialize, Serialize)]
292pub struct SeccompConfig {
293 #[serde(default = "default_true")]
295 pub enabled: bool,
296
297 #[serde(default)]
299 pub profile: SeccompProfilePreset,
300
301 #[serde(default)]
303 pub additional_blocked: Vec<String>,
304
305 #[serde(default)]
307 pub log_only: bool,
308}
309
310impl Default for SeccompConfig {
311 fn default() -> Self {
312 Self {
313 enabled: default_true(),
314 profile: SeccompProfilePreset::default(),
315 additional_blocked: Vec::new(),
316 log_only: false,
317 }
318 }
319}
320
321#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
323#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
324#[serde(rename_all = "snake_case")]
325pub enum SeccompProfilePreset {
326 #[default]
328 Strict,
329 Permissive,
331 Disabled,
333}
334
335#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
337#[derive(Debug, Clone, Default, Deserialize, Serialize)]
338pub struct ExternalSandboxConfig {
339 #[serde(default)]
341 pub sandbox_type: ExternalSandboxType,
342
343 #[serde(default)]
345 docker: DockerSandboxConfig,
346
347 #[serde(default)]
349 microvm: MicroVMSandboxConfig,
350}
351
352#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
354#[derive(Debug, Clone, Copy, Default, Deserialize, Serialize, PartialEq, Eq)]
355#[serde(rename_all = "snake_case")]
356pub enum ExternalSandboxType {
357 #[default]
359 None,
360 Docker,
362 MicroVM,
364 GVisor,
366}
367
368#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
370#[derive(Debug, Clone, Deserialize, Serialize)]
371pub struct DockerSandboxConfig {
372 #[serde(default = "default_docker_image")]
374 image: String,
375
376 #[serde(default)]
378 memory_limit: String,
379
380 #[serde(default)]
382 cpu_limit: String,
383}
384
385fn default_docker_image() -> String {
386 "ubuntu:22.04".to_string()
387}
388
389impl Default for DockerSandboxConfig {
390 fn default() -> Self {
391 Self {
392 image: default_docker_image(),
393 memory_limit: String::new(),
394 cpu_limit: String::new(),
395 }
396 }
397}
398
399#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
401#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
402#[serde(rename_all = "kebab-case")]
403pub enum MicroVmProvider {
404 #[default]
406 #[serde(rename = "")]
407 None,
408 Firecracker,
410 CloudHypervisor,
412 #[serde(other)]
414 Unknown,
415}
416
417impl MicroVmProvider {
418 fn as_str(&self) -> &str {
420 match self {
421 Self::None => "",
422 Self::Firecracker => "firecracker",
423 Self::CloudHypervisor => "cloud-hypervisor",
424 Self::Unknown => "unknown",
425 }
426 }
427}
428
429impl fmt::Display for MicroVmProvider {
430 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
431 f.write_str(self.as_str())
432 }
433}
434
435#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
437#[derive(Debug, Clone, Deserialize, Serialize)]
438pub struct MicroVMSandboxConfig {
439 #[serde(default)]
441 vmm: MicroVmProvider,
442
443 #[serde(default)]
445 kernel_path: String,
446
447 #[serde(default)]
449 rootfs_path: String,
450
451 #[serde(default = "default_microvm_memory")]
453 memory_mb: u64,
454
455 #[serde(default = "default_vcpus")]
457 vcpus: u32,
458}
459
460fn default_microvm_memory() -> u64 {
461 512
462}
463
464fn default_vcpus() -> u32 {
465 1
466}
467
468impl Default for MicroVMSandboxConfig {
469 fn default() -> Self {
470 Self {
471 vmm: MicroVmProvider::None,
472 kernel_path: String::new(),
473 rootfs_path: String::new(),
474 memory_mb: default_microvm_memory(),
475 vcpus: default_vcpus(),
476 }
477 }
478}
479
480#[inline]
481const fn default_false() -> bool {
482 false
483}
484
485#[cfg(test)]
486mod tests {
487 use super::*;
488
489 #[test]
490 fn test_sandbox_config_default() {
491 let config = SandboxConfig::default();
492 assert!(!config.enabled);
493 assert_eq!(config.default_policy, SandboxPolicy::ReadOnly);
494 }
495
496 #[test]
497 fn test_sandbox_config_parses_default_policy() {
498 let config: SandboxConfig = toml::from_str(
499 r#"
500enabled = true
501default_policy = "workspace_write"
502"#,
503 )
504 .expect("sandbox config with default_policy should parse");
505
506 assert!(config.enabled);
507 assert_eq!(config.default_policy, SandboxPolicy::WorkspaceWrite);
508 }
509
510 #[test]
511 fn test_sandbox_config_serializes_default_policy() {
512 let config = SandboxConfig {
513 default_policy: SandboxPolicy::DangerFullAccess,
514 ..SandboxConfig::default()
515 };
516
517 let toml = toml::to_string(&config).expect("sandbox config should serialize");
518
519 assert!(toml.contains("default_policy = \"danger_full_access\""));
520 let removed_field = format!("default_{}", "mode");
521 assert!(!toml.contains(&removed_field));
522 }
523
524 #[test]
525 fn test_sandbox_config_ignores_unknown_fields_for_forward_compatibility() {
526 let removed_field = format!("default_{}", "mode");
529 let input = format!(
530 r#"
531enabled = true
532{removed_field} = "workspace_write"
533"#,
534 );
535 let config: SandboxConfig =
536 toml::from_str(&input).expect("sandbox config should accept unknown fields for forward compatibility");
537 assert!(config.enabled);
538 }
539
540 #[test]
541 fn test_network_config_default() {
542 let config = NetworkConfig::default();
543 assert_eq!(config.policy, NetworkPolicy::AllowlistOnly);
544 assert!(config.allowlist.is_empty());
545 }
546
547 #[test]
548 fn test_network_config_policy_field() {
549 let config: NetworkConfig = toml::from_str(r#"policy = "allow_all""#).expect("policy field should parse");
550 assert_eq!(config.policy, NetworkPolicy::AllowAll);
551 }
552
553 #[test]
554 fn test_network_config_legacy_allow_all() {
555 let config: NetworkConfig = toml::from_str(r#"allow_all = true"#).expect("legacy allow_all should parse");
556 assert_eq!(config.policy, NetworkPolicy::AllowAll);
557 }
558
559 #[test]
560 fn test_network_config_legacy_block_all() {
561 let config: NetworkConfig = toml::from_str(r#"block_all = true"#).expect("legacy block_all should parse");
562 assert_eq!(config.policy, NetworkPolicy::BlockAll);
563 }
564
565 #[test]
566 fn test_network_config_legacy_block_all_overrides_allow_all() {
567 let config: NetworkConfig = toml::from_str(
568 r#"
569allow_all = true
570block_all = true
571"#,
572 )
573 .expect("legacy bool combination should parse");
574 assert_eq!(config.policy, NetworkPolicy::BlockAll);
575 }
576
577 #[test]
578 fn test_resource_limits_config_default() {
579 let config = ResourceLimitsConfig::default();
580 assert_eq!(config.preset, ResourceLimitsPreset::Moderate);
581 }
582
583 #[test]
584 fn test_seccomp_config_default() {
585 let config = SeccompConfig::default();
586 assert!(config.enabled);
587 assert_eq!(config.profile, SeccompProfilePreset::Strict);
588 }
589}