1use crate::network::NetworkMode;
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum ExecutionIsolation {
12 #[default]
14 Microvm,
15 Sandbox,
17}
18
19impl ExecutionIsolation {
20 pub fn is_sandbox(self) -> bool {
22 matches!(self, Self::Sandbox)
23 }
24}
25
26#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
28#[serde(tag = "kind", rename_all = "snake_case")]
29pub enum TeeConfig {
30 #[default]
32 None,
33
34 SevSnp {
36 workload_id: String,
38 #[serde(default)]
40 generation: SevSnpGeneration,
41 #[serde(default)]
43 simulate: bool,
44 },
45
46 Tdx {
48 workload_id: String,
50 #[serde(default)]
52 simulate: bool,
53 },
54}
55
56#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Default)]
58#[serde(rename_all = "lowercase")]
59pub enum SevSnpGeneration {
60 #[default]
62 Milan,
63 Genoa,
65}
66
67impl SevSnpGeneration {
68 pub fn as_str(&self) -> &'static str {
70 match self {
71 SevSnpGeneration::Milan => "milan",
72 SevSnpGeneration::Genoa => "genoa",
73 }
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct CacheConfig {
80 #[serde(default = "default_true")]
82 pub enabled: bool,
83
84 pub cache_dir: Option<PathBuf>,
86
87 #[serde(default = "default_max_rootfs_entries")]
89 pub max_rootfs_entries: usize,
90
91 #[serde(default = "default_max_cache_bytes")]
93 pub max_cache_bytes: u64,
94}
95
96fn default_true() -> bool {
97 true
98}
99
100fn default_max_rootfs_entries() -> usize {
101 10
102}
103
104fn default_max_cache_bytes() -> u64 {
105 10 * 1024 * 1024 * 1024 }
107
108impl Default for CacheConfig {
109 fn default() -> Self {
110 Self {
111 enabled: true,
112 cache_dir: None,
113 max_rootfs_entries: 10,
114 max_cache_bytes: 10 * 1024 * 1024 * 1024,
115 }
116 }
117}
118
119#[derive(Debug, Clone, Serialize, Deserialize)]
121pub struct PoolConfig {
122 #[serde(default)]
124 pub enabled: bool,
125
126 #[serde(default = "default_min_idle")]
128 pub min_idle: usize,
129
130 #[serde(default = "default_max_pool_size")]
132 pub max_size: usize,
133
134 #[serde(default = "default_max_concurrent_boots")]
138 pub max_concurrent_boots: usize,
139
140 #[serde(default = "default_idle_ttl")]
142 pub idle_ttl_secs: u64,
143
144 #[serde(default)]
146 pub scaling: ScalingPolicy,
147
148 #[serde(default)]
153 pub snapshot_fork: bool,
154}
155
156#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct ScalingPolicy {
162 #[serde(default)]
164 pub enabled: bool,
165
166 #[serde(default = "default_scale_up_threshold")]
168 pub scale_up_threshold: f64,
169
170 #[serde(default = "default_scale_down_threshold")]
172 pub scale_down_threshold: f64,
173
174 #[serde(default)]
176 pub max_min_idle: usize,
177
178 #[serde(default = "default_cooldown_secs")]
180 pub cooldown_secs: u64,
181
182 #[serde(default = "default_window_secs")]
184 pub window_secs: u64,
185}
186
187fn default_scale_up_threshold() -> f64 {
188 0.3
189}
190
191fn default_scale_down_threshold() -> f64 {
192 0.05
193}
194
195fn default_cooldown_secs() -> u64 {
196 60
197}
198
199fn default_window_secs() -> u64 {
200 120
201}
202
203impl Default for ScalingPolicy {
204 fn default() -> Self {
205 Self {
206 enabled: false,
207 scale_up_threshold: 0.3,
208 scale_down_threshold: 0.05,
209 max_min_idle: 0,
210 cooldown_secs: 60,
211 window_secs: 120,
212 }
213 }
214}
215
216fn default_min_idle() -> usize {
217 1
218}
219
220fn default_max_pool_size() -> usize {
221 5
222}
223
224fn default_max_concurrent_boots() -> usize {
225 2
226}
227
228fn default_idle_ttl() -> u64 {
229 300 }
231
232impl Default for PoolConfig {
233 fn default() -> Self {
234 Self {
235 enabled: false,
236 min_idle: 1,
237 max_size: 5,
238 max_concurrent_boots: 2,
239 idle_ttl_secs: 300,
240 scaling: ScalingPolicy::default(),
241 snapshot_fork: false,
242 }
243 }
244}
245
246#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
251pub struct ResourceLimits {
252 #[serde(default)]
255 pub pids_limit: Option<u64>,
256
257 #[serde(default)]
260 pub cpuset_cpus: Option<String>,
261
262 #[serde(default)]
264 pub ulimits: Vec<String>,
265
266 #[serde(default)]
269 pub cpu_shares: Option<u64>,
270
271 #[serde(default)]
274 pub cpu_quota: Option<i64>,
275
276 #[serde(default)]
279 pub cpu_period: Option<u64>,
280
281 #[serde(default)]
284 pub memory_reservation: Option<u64>,
285
286 #[serde(default)]
289 pub memory_swap: Option<i64>,
290
291 #[serde(default)]
297 pub sandbox_memory_limit_bytes: Option<u64>,
298}
299
300#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct BoxConfig {
303 #[serde(default)]
305 pub isolation: ExecutionIsolation,
306
307 #[serde(default)]
309 pub image: String,
310
311 pub workspace: PathBuf,
313
314 pub resources: ResourceConfig,
316
317 pub log_level: LogLevel,
319
320 pub debug_grpc: bool,
322
323 #[serde(default)]
325 pub tee: TeeConfig,
326
327 #[serde(default)]
329 pub cmd: Vec<String>,
330
331 #[serde(default)]
335 pub stdin_open: bool,
336
337 #[serde(default)]
339 pub entrypoint_override: Option<Vec<String>>,
340
341 #[serde(default)]
345 pub user: Option<String>,
346
347 #[serde(default)]
349 pub workdir: Option<String>,
350
351 #[serde(default)]
353 pub hostname: Option<String>,
354
355 #[serde(default)]
357 pub volumes: Vec<String>,
358
359 #[serde(default)]
362 pub virtiofs_cache: Option<String>,
363
364 #[serde(default)]
366 pub extra_env: Vec<(String, String)>,
367
368 #[serde(default)]
370 pub cache: CacheConfig,
371
372 #[serde(default)]
374 pub pool: PoolConfig,
375
376 #[serde(default)]
381 pub deferred_main: bool,
382
383 #[serde(default)]
387 pub ksm: bool,
388
389 #[serde(default)]
393 pub snapshot_mem_file: Option<String>,
394
395 #[serde(default)]
398 pub snapshot_sock: Option<String>,
399
400 #[serde(default)]
404 pub restore_from: Option<String>,
405
406 #[serde(default)]
409 pub port_map: Vec<String>,
410
411 #[serde(default)]
414 pub dns: Vec<String>,
415
416 #[serde(default)]
418 pub add_hosts: Vec<String>,
419
420 #[serde(default)]
422 pub network: NetworkMode,
423
424 #[serde(default)]
427 pub tmpfs: Vec<String>,
428
429 #[serde(default)]
431 pub resource_limits: ResourceLimits,
432
433 #[serde(default)]
435 pub cap_add: Vec<String>,
436
437 #[serde(default)]
439 pub cap_drop: Vec<String>,
440
441 #[serde(default)]
443 pub security_opt: Vec<String>,
444
445 #[serde(default)]
450 pub sysctls: Vec<(String, String)>,
451
452 #[serde(default)]
454 pub privileged: bool,
455
456 #[serde(default)]
461 pub read_only: bool,
462
463 #[serde(default)]
469 pub sidecar: Option<SidecarConfig>,
470
471 #[serde(default)]
480 pub persistent: bool,
481}
482
483impl Default for BoxConfig {
484 fn default() -> Self {
485 Self {
486 isolation: ExecutionIsolation::default(),
487 image: String::new(),
488 workspace: PathBuf::new(),
491 resources: ResourceConfig::default(),
492 log_level: LogLevel::Info,
493 debug_grpc: false,
494 tee: TeeConfig::default(),
495 cmd: vec![],
496 stdin_open: false,
497 entrypoint_override: None,
498 user: None,
499 workdir: None,
500 hostname: None,
501 volumes: vec![],
502 virtiofs_cache: None,
503 extra_env: vec![],
504 cache: CacheConfig::default(),
505 pool: PoolConfig::default(),
506 deferred_main: false,
507 ksm: false,
508 snapshot_mem_file: None,
509 snapshot_sock: None,
510 restore_from: None,
511 port_map: vec![],
512 dns: vec![],
513 add_hosts: vec![],
514 network: NetworkMode::default(),
515 tmpfs: vec![],
516 resource_limits: ResourceLimits::default(),
517 cap_add: vec![],
518 cap_drop: vec![],
519 security_opt: vec![],
520 sysctls: vec![],
521 privileged: false,
522 read_only: false,
523 sidecar: None,
524 persistent: false,
525 }
526 }
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize)]
544pub struct SidecarConfig {
545 pub image: String,
547
548 #[serde(default = "default_sidecar_vsock_port")]
550 pub vsock_port: u32,
551
552 #[serde(default)]
554 pub env: Vec<(String, String)>,
555}
556
557fn default_sidecar_vsock_port() -> u32 {
558 4092
559}
560
561pub const DEFAULT_VCPUS: u32 = if cfg!(target_os = "windows") { 1 } else { 2 };
566
567pub const MAX_VCPUS: u32 = u8::MAX as u32;
569
570pub fn validate_vcpu_count(vcpus: u32) -> std::result::Result<(), String> {
572 if vcpus == 0 {
573 return Err("virtual CPU count must be at least 1".to_string());
574 }
575 if vcpus > MAX_VCPUS {
576 return Err(format!(
577 "virtual CPU count {vcpus} exceeds the maximum of {MAX_VCPUS}"
578 ));
579 }
580
581 #[cfg(target_os = "windows")]
582 if vcpus != 1 {
583 return Err(format!(
584 "the Windows WHPX backend currently supports exactly 1 virtual CPU; requested {vcpus}"
585 ));
586 }
587
588 Ok(())
589}
590
591impl Default for SidecarConfig {
592 fn default() -> Self {
593 Self {
594 image: String::new(),
595 vsock_port: default_sidecar_vsock_port(),
596 env: vec![],
597 }
598 }
599}
600
601#[derive(Debug, Clone, Serialize, Deserialize)]
603pub struct ResourceConfig {
604 pub vcpus: u32,
606
607 pub memory_mb: u32,
609
610 pub disk_mb: u32,
612
613 #[serde(default, skip_serializing_if = "Option::is_none")]
621 pub ephemeral_storage_bytes: Option<u64>,
622
623 pub timeout: u64,
625}
626
627impl Default for ResourceConfig {
628 fn default() -> Self {
629 Self {
630 vcpus: DEFAULT_VCPUS,
631 memory_mb: 1024,
632 disk_mb: 4096,
633 ephemeral_storage_bytes: None,
634 timeout: 3600, }
636 }
637}
638
639#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
641pub enum LogLevel {
642 Debug,
643 Info,
644 Warn,
645 Error,
646}
647
648impl From<LogLevel> for tracing::Level {
649 fn from(level: LogLevel) -> Self {
650 match level {
651 LogLevel::Debug => tracing::Level::DEBUG,
652 LogLevel::Info => tracing::Level::INFO,
653 LogLevel::Warn => tracing::Level::WARN,
654 LogLevel::Error => tracing::Level::ERROR,
655 }
656 }
657}
658
659#[cfg(test)]
660mod tests {
661 use super::*;
662
663 #[test]
664 fn test_box_config_default() {
665 let config = BoxConfig::default();
666
667 assert!(config.image.is_empty());
668 assert!(config.workspace.as_os_str().is_empty());
670 assert_eq!(config.resources.vcpus, DEFAULT_VCPUS);
671 assert!(!config.debug_grpc);
672 assert!(!config.read_only);
673 assert!(config.user.is_none());
674 assert!(config.workdir.is_none());
675 assert!(config.hostname.is_none());
676 assert!(config.add_hosts.is_empty());
677 }
678
679 #[test]
680 fn test_box_config_read_only_default_false() {
681 let config = BoxConfig::default();
682 assert!(!config.read_only);
683 }
684
685 #[test]
686 fn test_box_config_read_only_serde() {
687 let json = r#"{"image":"test","workspace":"","resources":{"vcpus":2,"memory_mb":512,"disk_mb":4096,"timeout":3600},"log_level":"Info","debug_grpc":false}"#;
689 let config: BoxConfig = serde_json::from_str(json).unwrap();
690 assert!(!config.read_only);
691
692 let config = BoxConfig {
694 read_only: true,
695 ..Default::default()
696 };
697 let json = serde_json::to_string(&config).unwrap();
698 let deserialized: BoxConfig = serde_json::from_str(&json).unwrap();
699 assert!(deserialized.read_only);
700 }
701
702 #[test]
703 fn test_box_config_user_workdir_serde() {
704 let config = BoxConfig {
705 user: Some("1000:1000".to_string()),
706 workdir: Some("/app".to_string()),
707 ..Default::default()
708 };
709
710 let json = serde_json::to_string(&config).unwrap();
711 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
712
713 assert_eq!(parsed.user.as_deref(), Some("1000:1000"));
714 assert_eq!(parsed.workdir.as_deref(), Some("/app"));
715 }
716
717 #[test]
718 fn test_box_config_hostname_add_hosts_serde() {
719 let config = BoxConfig {
720 hostname: Some("web".to_string()),
721 add_hosts: vec!["db.local:10.88.0.10".to_string()],
722 ..Default::default()
723 };
724
725 let json = serde_json::to_string(&config).unwrap();
726 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
727
728 assert_eq!(parsed.hostname.as_deref(), Some("web"));
729 assert_eq!(parsed.add_hosts, vec!["db.local:10.88.0.10"]);
730 }
731
732 #[test]
733 fn test_resource_config_default() {
734 let config = ResourceConfig::default();
735
736 assert_eq!(config.vcpus, DEFAULT_VCPUS);
737 assert_eq!(config.memory_mb, 1024);
738 assert_eq!(config.disk_mb, 4096);
739 assert_eq!(config.ephemeral_storage_bytes, None);
740 assert_eq!(config.timeout, 3600);
741 }
742
743 #[test]
744 fn test_validate_vcpu_count() {
745 assert!(validate_vcpu_count(DEFAULT_VCPUS).is_ok());
746 assert!(validate_vcpu_count(0).unwrap_err().contains("at least 1"));
747 assert!(validate_vcpu_count(MAX_VCPUS + 1)
748 .unwrap_err()
749 .contains("maximum"));
750
751 #[cfg(target_os = "windows")]
752 assert!(validate_vcpu_count(2).unwrap_err().contains("WHPX"));
753 #[cfg(not(target_os = "windows"))]
754 assert!(validate_vcpu_count(2).is_ok());
755 }
756
757 #[test]
758 fn test_resource_config_custom() {
759 let config = ResourceConfig {
760 vcpus: 4,
761 memory_mb: 2048,
762 disk_mb: 8192,
763 ephemeral_storage_bytes: None,
764 timeout: 7200,
765 };
766
767 assert_eq!(config.vcpus, 4);
768 assert_eq!(config.memory_mb, 2048);
769 assert_eq!(config.disk_mb, 8192);
770 assert_eq!(config.ephemeral_storage_bytes, None);
771 assert_eq!(config.timeout, 7200);
772 }
773
774 #[test]
775 fn test_log_level_conversion() {
776 assert_eq!(tracing::Level::from(LogLevel::Debug), tracing::Level::DEBUG);
777 assert_eq!(tracing::Level::from(LogLevel::Info), tracing::Level::INFO);
778 assert_eq!(tracing::Level::from(LogLevel::Warn), tracing::Level::WARN);
779 assert_eq!(tracing::Level::from(LogLevel::Error), tracing::Level::ERROR);
780 }
781
782 #[test]
783 fn test_box_config_serialization() {
784 let config = BoxConfig::default();
785 let json = serde_json::to_string(&config).unwrap();
786
787 assert!(json.contains("workspace"));
788 assert!(json.contains("resources"));
789 }
790
791 #[test]
792 fn test_box_config_deserialization() {
793 let json = r#"{
794 "image": "nginx:alpine",
795 "workspace": "/tmp/workspace",
796 "resources": {
797 "vcpus": 4,
798 "memory_mb": 2048,
799 "disk_mb": 8192,
800 "timeout": 1800
801 },
802 "log_level": "Debug",
803 "debug_grpc": true
804 }"#;
805
806 let config: BoxConfig = serde_json::from_str(json).unwrap();
807 assert_eq!(config.image, "nginx:alpine");
808 assert_eq!(config.workspace.to_str().unwrap(), "/tmp/workspace");
809 assert_eq!(config.resources.vcpus, 4);
810 assert!(config.debug_grpc);
811 }
812
813 #[test]
814 fn test_resource_config_serialization() {
815 let config = ResourceConfig {
816 vcpus: 8,
817 memory_mb: 4096,
818 disk_mb: 16384,
819 ephemeral_storage_bytes: None,
820 timeout: 0,
821 };
822
823 let json = serde_json::to_string(&config).unwrap();
824 let parsed: ResourceConfig = serde_json::from_str(&json).unwrap();
825
826 assert_eq!(parsed.vcpus, 8);
827 assert_eq!(parsed.memory_mb, 4096);
828 assert_eq!(parsed.ephemeral_storage_bytes, None);
829 assert_eq!(parsed.timeout, 0); }
831
832 #[test]
833 fn test_resource_config_ephemeral_storage_roundtrip_and_legacy_shape() {
834 let config = ResourceConfig {
835 ephemeral_storage_bytes: Some(64 * 1024 * 1024),
836 ..ResourceConfig::default()
837 };
838 let json = serde_json::to_string(&config).unwrap();
839 assert!(json.contains("ephemeral_storage_bytes"));
840 let parsed: ResourceConfig = serde_json::from_str(&json).unwrap();
841 assert_eq!(
842 parsed.ephemeral_storage_bytes,
843 config.ephemeral_storage_bytes
844 );
845
846 let legacy = r#"{"vcpus":1,"memory_mb":128,"disk_mb":512,"timeout":0}"#;
847 let parsed: ResourceConfig = serde_json::from_str(legacy).unwrap();
848 assert_eq!(parsed.ephemeral_storage_bytes, None);
849 assert!(!serde_json::to_string(&parsed)
850 .unwrap()
851 .contains("ephemeral_storage_bytes"));
852 }
853
854 #[test]
855 fn test_log_level_serialization() {
856 let levels = vec![
857 LogLevel::Debug,
858 LogLevel::Info,
859 LogLevel::Warn,
860 LogLevel::Error,
861 ];
862
863 for level in levels {
864 let json = serde_json::to_string(&level).unwrap();
865 let parsed: LogLevel = serde_json::from_str(&json).unwrap();
866 assert_eq!(tracing::Level::from(parsed), tracing::Level::from(level));
867 }
868 }
869
870 #[test]
871 fn test_config_clone() {
872 let config = BoxConfig::default();
873 let cloned = config.clone();
874
875 assert_eq!(config.workspace, cloned.workspace);
876 assert_eq!(config.resources.vcpus, cloned.resources.vcpus);
877 }
878
879 #[test]
880 fn test_config_debug() {
881 let config = BoxConfig::default();
882 let debug_str = format!("{:?}", config);
883
884 assert!(debug_str.contains("BoxConfig"));
885 assert!(debug_str.contains("workspace"));
886 }
887
888 #[test]
889 fn test_tee_config_default() {
890 let tee = TeeConfig::default();
891 assert_eq!(tee, TeeConfig::None);
892 }
893
894 #[test]
895 fn test_tee_config_sev_snp() {
896 let tee = TeeConfig::SevSnp {
897 workload_id: "test-agent".to_string(),
898 generation: SevSnpGeneration::Milan,
899 simulate: false,
900 };
901
902 match tee {
903 TeeConfig::SevSnp {
904 workload_id,
905 generation,
906 simulate,
907 } => {
908 assert_eq!(workload_id, "test-agent");
909 assert_eq!(generation, SevSnpGeneration::Milan);
910 assert!(!simulate);
911 }
912 _ => panic!("Expected SevSnp variant"),
913 }
914 }
915
916 #[test]
917 fn test_sev_snp_generation_as_str() {
918 assert_eq!(SevSnpGeneration::Milan.as_str(), "milan");
919 assert_eq!(SevSnpGeneration::Genoa.as_str(), "genoa");
920 }
921
922 #[test]
923 fn test_sev_snp_generation_default() {
924 let gen = SevSnpGeneration::default();
925 assert_eq!(gen, SevSnpGeneration::Milan);
926 }
927
928 #[test]
929 fn test_tee_config_serialization() {
930 let tee = TeeConfig::SevSnp {
931 workload_id: "my-workload".to_string(),
932 generation: SevSnpGeneration::Genoa,
933 simulate: false,
934 };
935
936 let json = serde_json::to_string(&tee).unwrap();
937 let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
938
939 assert_eq!(parsed, tee);
940 }
941
942 #[test]
943 fn test_tee_config_none_serialization() {
944 let tee = TeeConfig::None;
945 let json = serde_json::to_string(&tee).unwrap();
946 let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
947
948 assert_eq!(parsed, TeeConfig::None);
949 }
950
951 #[test]
952 fn test_tee_config_tdx() {
953 let tee = TeeConfig::Tdx {
954 workload_id: "tdx-workload".to_string(),
955 simulate: false,
956 };
957 let json = serde_json::to_string(&tee).unwrap();
958 let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
959 match parsed {
960 TeeConfig::Tdx {
961 workload_id,
962 simulate,
963 } => {
964 assert_eq!(workload_id, "tdx-workload");
965 assert!(!simulate);
966 }
967 _ => panic!("Expected Tdx variant"),
968 }
969 }
970
971 #[test]
972 fn test_tee_config_tdx_simulate() {
973 let tee = TeeConfig::Tdx {
974 workload_id: "test".to_string(),
975 simulate: true,
976 };
977 let json = serde_json::to_string(&tee).unwrap();
978 let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
979 match parsed {
980 TeeConfig::Tdx { simulate, .. } => assert!(simulate),
981 _ => panic!("Expected Tdx variant"),
982 }
983 }
984
985 #[test]
986 fn test_box_config_with_tee() {
987 let config = BoxConfig {
988 tee: TeeConfig::SevSnp {
989 workload_id: "secure-agent".to_string(),
990 generation: SevSnpGeneration::Milan,
991 simulate: false,
992 },
993 ..Default::default()
994 };
995
996 let json = serde_json::to_string(&config).unwrap();
997 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
998
999 match parsed.tee {
1000 TeeConfig::SevSnp {
1001 workload_id,
1002 generation,
1003 simulate,
1004 } => {
1005 assert_eq!(workload_id, "secure-agent");
1006 assert_eq!(generation, SevSnpGeneration::Milan);
1007 assert!(!simulate);
1008 }
1009 _ => panic!("Expected SevSnp TEE config"),
1010 }
1011 }
1012
1013 #[test]
1014 fn test_box_config_default_has_no_tee() {
1015 let config = BoxConfig::default();
1016 assert_eq!(config.tee, TeeConfig::None);
1017 }
1018
1019 #[test]
1022 fn test_cache_config_default() {
1023 let config = CacheConfig::default();
1024 assert!(config.enabled);
1025 assert!(config.cache_dir.is_none());
1026 assert_eq!(config.max_rootfs_entries, 10);
1027 assert_eq!(config.max_cache_bytes, 10 * 1024 * 1024 * 1024);
1028 }
1029
1030 #[test]
1031 fn test_cache_config_serialization() {
1032 let config = CacheConfig {
1033 enabled: false,
1034 cache_dir: Some(PathBuf::from("/tmp/cache")),
1035 max_rootfs_entries: 5,
1036 max_cache_bytes: 1024 * 1024 * 1024,
1037 };
1038
1039 let json = serde_json::to_string(&config).unwrap();
1040 let parsed: CacheConfig = serde_json::from_str(&json).unwrap();
1041
1042 assert!(!parsed.enabled);
1043 assert_eq!(parsed.cache_dir, Some(PathBuf::from("/tmp/cache")));
1044 assert_eq!(parsed.max_rootfs_entries, 5);
1045 assert_eq!(parsed.max_cache_bytes, 1024 * 1024 * 1024);
1046 }
1047
1048 #[test]
1049 fn test_cache_config_deserialization_defaults() {
1050 let json = "{}";
1051 let config: CacheConfig = serde_json::from_str(json).unwrap();
1052
1053 assert!(config.enabled);
1054 assert!(config.cache_dir.is_none());
1055 assert_eq!(config.max_rootfs_entries, 10);
1056 assert_eq!(config.max_cache_bytes, 10 * 1024 * 1024 * 1024);
1057 }
1058
1059 #[test]
1062 fn test_pool_config_default() {
1063 let config = PoolConfig::default();
1064 assert!(!config.enabled);
1065 assert_eq!(config.min_idle, 1);
1066 assert_eq!(config.max_size, 5);
1067 assert_eq!(config.max_concurrent_boots, 2);
1068 assert_eq!(config.idle_ttl_secs, 300);
1069 }
1070
1071 #[test]
1072 fn test_pool_config_serialization() {
1073 let config = PoolConfig {
1074 enabled: true,
1075 min_idle: 3,
1076 max_size: 10,
1077 max_concurrent_boots: 4,
1078 idle_ttl_secs: 600,
1079 ..Default::default()
1080 };
1081
1082 let json = serde_json::to_string(&config).unwrap();
1083 let parsed: PoolConfig = serde_json::from_str(&json).unwrap();
1084
1085 assert!(parsed.enabled);
1086 assert_eq!(parsed.min_idle, 3);
1087 assert_eq!(parsed.max_size, 10);
1088 assert_eq!(parsed.max_concurrent_boots, 4);
1089 assert_eq!(parsed.idle_ttl_secs, 600);
1090 }
1091
1092 #[test]
1093 fn test_pool_config_deserialization_defaults() {
1094 let json = "{}";
1095 let config: PoolConfig = serde_json::from_str(json).unwrap();
1096
1097 assert!(!config.enabled);
1098 assert_eq!(config.min_idle, 1);
1099 assert_eq!(config.max_size, 5);
1100 assert_eq!(config.max_concurrent_boots, 2);
1101 assert_eq!(config.idle_ttl_secs, 300);
1102 }
1103
1104 #[test]
1107 fn test_box_config_default_has_cache_and_pool() {
1108 let config = BoxConfig::default();
1109 assert!(config.cache.enabled);
1110 assert!(!config.pool.enabled);
1111 }
1112
1113 #[test]
1114 fn test_box_config_with_cache_serialization() {
1115 let config = BoxConfig {
1116 cache: CacheConfig {
1117 enabled: false,
1118 cache_dir: Some(PathBuf::from("/custom/cache")),
1119 max_rootfs_entries: 20,
1120 max_cache_bytes: 5 * 1024 * 1024 * 1024,
1121 },
1122 ..Default::default()
1123 };
1124
1125 let json = serde_json::to_string(&config).unwrap();
1126 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1127
1128 assert!(!parsed.cache.enabled);
1129 assert_eq!(parsed.cache.cache_dir, Some(PathBuf::from("/custom/cache")));
1130 assert_eq!(parsed.cache.max_rootfs_entries, 20);
1131 }
1132
1133 #[test]
1134 fn test_box_config_with_pool_serialization() {
1135 let config = BoxConfig {
1136 pool: PoolConfig {
1137 enabled: true,
1138 min_idle: 2,
1139 max_size: 8,
1140 idle_ttl_secs: 120,
1141 ..Default::default()
1142 },
1143 ..Default::default()
1144 };
1145
1146 let json = serde_json::to_string(&config).unwrap();
1147 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1148
1149 assert!(parsed.pool.enabled);
1150 assert_eq!(parsed.pool.min_idle, 2);
1151 assert_eq!(parsed.pool.max_size, 8);
1152 assert_eq!(parsed.pool.max_concurrent_boots, 2);
1153 assert_eq!(parsed.pool.idle_ttl_secs, 120);
1154 }
1155
1156 #[test]
1157 fn test_box_config_backward_compatible_deserialization() {
1158 let json = r#"{
1160 "workspace": "/tmp/workspace",
1161 "resources": {
1162 "vcpus": 2,
1163 "memory_mb": 1024,
1164 "disk_mb": 4096,
1165 "timeout": 3600
1166 },
1167 "log_level": "Info",
1168 "debug_grpc": false
1169 }"#;
1170
1171 let config: BoxConfig = serde_json::from_str(json).unwrap();
1172 assert!(config.cache.enabled);
1173 assert!(!config.pool.enabled);
1174 }
1175
1176 #[test]
1179 fn test_resource_limits_default() {
1180 let limits = ResourceLimits::default();
1181 assert!(limits.pids_limit.is_none());
1182 assert!(limits.cpuset_cpus.is_none());
1183 assert!(limits.ulimits.is_empty());
1184 assert!(limits.cpu_shares.is_none());
1185 assert!(limits.cpu_quota.is_none());
1186 assert!(limits.cpu_period.is_none());
1187 assert!(limits.memory_reservation.is_none());
1188 assert!(limits.memory_swap.is_none());
1189 assert!(limits.sandbox_memory_limit_bytes.is_none());
1190 }
1191
1192 #[test]
1193 fn test_resource_limits_serialization() {
1194 let limits = ResourceLimits {
1195 pids_limit: Some(100),
1196 cpuset_cpus: Some("0,1".to_string()),
1197 ulimits: vec!["nofile=1024:4096".to_string()],
1198 cpu_shares: Some(512),
1199 cpu_quota: Some(50000),
1200 cpu_period: Some(100000),
1201 memory_reservation: Some(256 * 1024 * 1024),
1202 memory_swap: Some(1024 * 1024 * 1024),
1203 sandbox_memory_limit_bytes: Some(256 * 1024 * 1024),
1204 };
1205
1206 let json = serde_json::to_string(&limits).unwrap();
1207 let parsed: ResourceLimits = serde_json::from_str(&json).unwrap();
1208
1209 assert_eq!(parsed.pids_limit, Some(100));
1210 assert_eq!(parsed.cpuset_cpus, Some("0,1".to_string()));
1211 assert_eq!(parsed.ulimits, vec!["nofile=1024:4096"]);
1212 assert_eq!(parsed.cpu_shares, Some(512));
1213 assert_eq!(parsed.cpu_quota, Some(50000));
1214 assert_eq!(parsed.cpu_period, Some(100000));
1215 assert_eq!(parsed.memory_reservation, Some(256 * 1024 * 1024));
1216 assert_eq!(parsed.memory_swap, Some(1024 * 1024 * 1024));
1217 assert_eq!(parsed.sandbox_memory_limit_bytes, Some(256 * 1024 * 1024));
1218 }
1219
1220 #[test]
1221 fn test_resource_limits_deserialization_defaults() {
1222 let json = "{}";
1223 let limits: ResourceLimits = serde_json::from_str(json).unwrap();
1224 assert!(limits.pids_limit.is_none());
1225 assert!(limits.ulimits.is_empty());
1226 }
1227
1228 #[test]
1229 fn test_resource_limits_memory_swap_unlimited() {
1230 let limits = ResourceLimits {
1231 memory_swap: Some(-1),
1232 ..Default::default()
1233 };
1234
1235 let json = serde_json::to_string(&limits).unwrap();
1236 let parsed: ResourceLimits = serde_json::from_str(&json).unwrap();
1237 assert_eq!(parsed.memory_swap, Some(-1));
1238 }
1239
1240 #[test]
1241 fn test_box_config_with_resource_limits() {
1242 let config = BoxConfig {
1243 resource_limits: ResourceLimits {
1244 pids_limit: Some(256),
1245 cpu_shares: Some(1024),
1246 ..Default::default()
1247 },
1248 ..Default::default()
1249 };
1250
1251 let json = serde_json::to_string(&config).unwrap();
1252 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1253
1254 assert_eq!(parsed.resource_limits.pids_limit, Some(256));
1255 assert_eq!(parsed.resource_limits.cpu_shares, Some(1024));
1256 }
1257
1258 #[test]
1259 fn test_box_config_backward_compat_no_resource_limits() {
1260 let json = r#"{
1262 "workspace": "/tmp/workspace",
1263 "resources": {
1264 "vcpus": 2,
1265 "memory_mb": 1024,
1266 "disk_mb": 4096,
1267 "timeout": 3600
1268 },
1269 "log_level": "Info",
1270 "debug_grpc": false
1271 }"#;
1272
1273 let config: BoxConfig = serde_json::from_str(json).unwrap();
1274 assert!(config.resource_limits.pids_limit.is_none());
1275 assert!(config.resource_limits.ulimits.is_empty());
1276 }
1277
1278 #[test]
1281 fn test_sidecar_config_default() {
1282 let s = SidecarConfig::default();
1283 assert!(s.image.is_empty());
1284 assert_eq!(s.vsock_port, 4092);
1285 assert!(s.env.is_empty());
1286 }
1287
1288 #[test]
1289 fn test_sidecar_config_roundtrip() {
1290 let s = SidecarConfig {
1291 image: "ghcr.io/a3s-lab/safeclaw:latest".to_string(),
1292 vsock_port: 4092,
1293 env: vec![
1294 ("LOG_LEVEL".to_string(), "debug".to_string()),
1295 ("MODE".to_string(), "proxy".to_string()),
1296 ],
1297 };
1298 let json = serde_json::to_string(&s).unwrap();
1299 let parsed: SidecarConfig = serde_json::from_str(&json).unwrap();
1300 assert_eq!(parsed.image, "ghcr.io/a3s-lab/safeclaw:latest");
1301 assert_eq!(parsed.vsock_port, 4092);
1302 assert_eq!(parsed.env.len(), 2);
1303 assert_eq!(
1304 parsed.env[0],
1305 ("LOG_LEVEL".to_string(), "debug".to_string())
1306 );
1307 }
1308
1309 #[test]
1310 fn test_sidecar_config_default_vsock_port_from_json() {
1311 let json = r#"{"image":"safeclaw:latest"}"#;
1312 let s: SidecarConfig = serde_json::from_str(json).unwrap();
1313 assert_eq!(s.vsock_port, 4092);
1314 assert!(s.env.is_empty());
1315 }
1316
1317 #[test]
1318 fn test_box_config_default_has_no_sidecar() {
1319 let config = BoxConfig::default();
1320 assert!(config.sidecar.is_none());
1321 }
1322
1323 #[test]
1324 fn test_box_config_with_sidecar_roundtrip() {
1325 let config = BoxConfig {
1326 sidecar: Some(SidecarConfig {
1327 image: "safeclaw:latest".to_string(),
1328 vsock_port: 4092,
1329 env: vec![],
1330 }),
1331 ..Default::default()
1332 };
1333 let json = serde_json::to_string(&config).unwrap();
1334 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1335 let sidecar = parsed.sidecar.unwrap();
1336 assert_eq!(sidecar.image, "safeclaw:latest");
1337 assert_eq!(sidecar.vsock_port, 4092);
1338 }
1339
1340 #[test]
1341 fn test_box_config_without_sidecar_deserializes_as_none() {
1342 let config = BoxConfig::default();
1344 let json = serde_json::to_string(&config).unwrap();
1345 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1346 assert!(parsed.sidecar.is_none());
1347 }
1348}