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_idle_ttl")]
136 pub idle_ttl_secs: u64,
137
138 #[serde(default)]
140 pub scaling: ScalingPolicy,
141
142 #[serde(default)]
147 pub snapshot_fork: bool,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct ScalingPolicy {
156 #[serde(default)]
158 pub enabled: bool,
159
160 #[serde(default = "default_scale_up_threshold")]
162 pub scale_up_threshold: f64,
163
164 #[serde(default = "default_scale_down_threshold")]
166 pub scale_down_threshold: f64,
167
168 #[serde(default)]
170 pub max_min_idle: usize,
171
172 #[serde(default = "default_cooldown_secs")]
174 pub cooldown_secs: u64,
175
176 #[serde(default = "default_window_secs")]
178 pub window_secs: u64,
179}
180
181fn default_scale_up_threshold() -> f64 {
182 0.3
183}
184
185fn default_scale_down_threshold() -> f64 {
186 0.05
187}
188
189fn default_cooldown_secs() -> u64 {
190 60
191}
192
193fn default_window_secs() -> u64 {
194 120
195}
196
197impl Default for ScalingPolicy {
198 fn default() -> Self {
199 Self {
200 enabled: false,
201 scale_up_threshold: 0.3,
202 scale_down_threshold: 0.05,
203 max_min_idle: 0,
204 cooldown_secs: 60,
205 window_secs: 120,
206 }
207 }
208}
209
210fn default_min_idle() -> usize {
211 1
212}
213
214fn default_max_pool_size() -> usize {
215 5
216}
217
218fn default_idle_ttl() -> u64 {
219 300 }
221
222impl Default for PoolConfig {
223 fn default() -> Self {
224 Self {
225 enabled: false,
226 min_idle: 1,
227 max_size: 5,
228 idle_ttl_secs: 300,
229 scaling: ScalingPolicy::default(),
230 snapshot_fork: false,
231 }
232 }
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize, Default)]
240pub struct ResourceLimits {
241 #[serde(default)]
244 pub pids_limit: Option<u64>,
245
246 #[serde(default)]
249 pub cpuset_cpus: Option<String>,
250
251 #[serde(default)]
253 pub ulimits: Vec<String>,
254
255 #[serde(default)]
258 pub cpu_shares: Option<u64>,
259
260 #[serde(default)]
263 pub cpu_quota: Option<i64>,
264
265 #[serde(default)]
268 pub cpu_period: Option<u64>,
269
270 #[serde(default)]
273 pub memory_reservation: Option<u64>,
274
275 #[serde(default)]
278 pub memory_swap: Option<i64>,
279
280 #[serde(default)]
286 pub sandbox_memory_limit_bytes: Option<u64>,
287}
288
289#[derive(Debug, Clone, Serialize, Deserialize)]
291pub struct BoxConfig {
292 #[serde(default)]
294 pub isolation: ExecutionIsolation,
295
296 #[serde(default)]
298 pub image: String,
299
300 pub workspace: PathBuf,
302
303 pub resources: ResourceConfig,
305
306 pub log_level: LogLevel,
308
309 pub debug_grpc: bool,
311
312 #[serde(default)]
314 pub tee: TeeConfig,
315
316 #[serde(default)]
318 pub cmd: Vec<String>,
319
320 #[serde(default)]
324 pub stdin_open: bool,
325
326 #[serde(default)]
328 pub entrypoint_override: Option<Vec<String>>,
329
330 #[serde(default)]
334 pub user: Option<String>,
335
336 #[serde(default)]
338 pub workdir: Option<String>,
339
340 #[serde(default)]
342 pub hostname: Option<String>,
343
344 #[serde(default)]
346 pub volumes: Vec<String>,
347
348 #[serde(default)]
351 pub virtiofs_cache: Option<String>,
352
353 #[serde(default)]
355 pub extra_env: Vec<(String, String)>,
356
357 #[serde(default)]
359 pub cache: CacheConfig,
360
361 #[serde(default)]
363 pub pool: PoolConfig,
364
365 #[serde(default)]
370 pub deferred_main: bool,
371
372 #[serde(default)]
376 pub ksm: bool,
377
378 #[serde(default)]
382 pub snapshot_mem_file: Option<String>,
383
384 #[serde(default)]
387 pub snapshot_sock: Option<String>,
388
389 #[serde(default)]
393 pub restore_from: Option<String>,
394
395 #[serde(default)]
398 pub port_map: Vec<String>,
399
400 #[serde(default)]
403 pub dns: Vec<String>,
404
405 #[serde(default)]
407 pub add_hosts: Vec<String>,
408
409 #[serde(default)]
411 pub network: NetworkMode,
412
413 #[serde(default)]
416 pub tmpfs: Vec<String>,
417
418 #[serde(default)]
420 pub resource_limits: ResourceLimits,
421
422 #[serde(default)]
424 pub cap_add: Vec<String>,
425
426 #[serde(default)]
428 pub cap_drop: Vec<String>,
429
430 #[serde(default)]
432 pub security_opt: Vec<String>,
433
434 #[serde(default)]
439 pub sysctls: Vec<(String, String)>,
440
441 #[serde(default)]
443 pub privileged: bool,
444
445 #[serde(default)]
450 pub read_only: bool,
451
452 #[serde(default)]
458 pub sidecar: Option<SidecarConfig>,
459
460 #[serde(default)]
469 pub persistent: bool,
470}
471
472impl Default for BoxConfig {
473 fn default() -> Self {
474 Self {
475 isolation: ExecutionIsolation::default(),
476 image: String::new(),
477 workspace: PathBuf::new(),
480 resources: ResourceConfig::default(),
481 log_level: LogLevel::Info,
482 debug_grpc: false,
483 tee: TeeConfig::default(),
484 cmd: vec![],
485 stdin_open: false,
486 entrypoint_override: None,
487 user: None,
488 workdir: None,
489 hostname: None,
490 volumes: vec![],
491 virtiofs_cache: None,
492 extra_env: vec![],
493 cache: CacheConfig::default(),
494 pool: PoolConfig::default(),
495 deferred_main: false,
496 ksm: false,
497 snapshot_mem_file: None,
498 snapshot_sock: None,
499 restore_from: None,
500 port_map: vec![],
501 dns: vec![],
502 add_hosts: vec![],
503 network: NetworkMode::default(),
504 tmpfs: vec![],
505 resource_limits: ResourceLimits::default(),
506 cap_add: vec![],
507 cap_drop: vec![],
508 security_opt: vec![],
509 sysctls: vec![],
510 privileged: false,
511 read_only: false,
512 sidecar: None,
513 persistent: false,
514 }
515 }
516}
517
518#[derive(Debug, Clone, Serialize, Deserialize)]
533pub struct SidecarConfig {
534 pub image: String,
536
537 #[serde(default = "default_sidecar_vsock_port")]
539 pub vsock_port: u32,
540
541 #[serde(default)]
543 pub env: Vec<(String, String)>,
544}
545
546fn default_sidecar_vsock_port() -> u32 {
547 4092
548}
549
550pub const DEFAULT_VCPUS: u32 = if cfg!(target_os = "windows") { 1 } else { 2 };
555
556pub const MAX_VCPUS: u32 = u8::MAX as u32;
558
559pub fn validate_vcpu_count(vcpus: u32) -> std::result::Result<(), String> {
561 if vcpus == 0 {
562 return Err("virtual CPU count must be at least 1".to_string());
563 }
564 if vcpus > MAX_VCPUS {
565 return Err(format!(
566 "virtual CPU count {vcpus} exceeds the maximum of {MAX_VCPUS}"
567 ));
568 }
569
570 #[cfg(target_os = "windows")]
571 if vcpus != 1 {
572 return Err(format!(
573 "the Windows WHPX backend currently supports exactly 1 virtual CPU; requested {vcpus}"
574 ));
575 }
576
577 Ok(())
578}
579
580impl Default for SidecarConfig {
581 fn default() -> Self {
582 Self {
583 image: String::new(),
584 vsock_port: default_sidecar_vsock_port(),
585 env: vec![],
586 }
587 }
588}
589
590#[derive(Debug, Clone, Serialize, Deserialize)]
592pub struct ResourceConfig {
593 pub vcpus: u32,
595
596 pub memory_mb: u32,
598
599 pub disk_mb: u32,
601
602 pub timeout: u64,
604}
605
606impl Default for ResourceConfig {
607 fn default() -> Self {
608 Self {
609 vcpus: DEFAULT_VCPUS,
610 memory_mb: 1024,
611 disk_mb: 4096,
612 timeout: 3600, }
614 }
615}
616
617#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
619pub enum LogLevel {
620 Debug,
621 Info,
622 Warn,
623 Error,
624}
625
626impl From<LogLevel> for tracing::Level {
627 fn from(level: LogLevel) -> Self {
628 match level {
629 LogLevel::Debug => tracing::Level::DEBUG,
630 LogLevel::Info => tracing::Level::INFO,
631 LogLevel::Warn => tracing::Level::WARN,
632 LogLevel::Error => tracing::Level::ERROR,
633 }
634 }
635}
636
637#[cfg(test)]
638mod tests {
639 use super::*;
640
641 #[test]
642 fn test_box_config_default() {
643 let config = BoxConfig::default();
644
645 assert!(config.image.is_empty());
646 assert!(config.workspace.as_os_str().is_empty());
648 assert_eq!(config.resources.vcpus, DEFAULT_VCPUS);
649 assert!(!config.debug_grpc);
650 assert!(!config.read_only);
651 assert!(config.user.is_none());
652 assert!(config.workdir.is_none());
653 assert!(config.hostname.is_none());
654 assert!(config.add_hosts.is_empty());
655 }
656
657 #[test]
658 fn test_box_config_read_only_default_false() {
659 let config = BoxConfig::default();
660 assert!(!config.read_only);
661 }
662
663 #[test]
664 fn test_box_config_read_only_serde() {
665 let json = r#"{"image":"test","workspace":"","resources":{"vcpus":2,"memory_mb":512,"disk_mb":4096,"timeout":3600},"log_level":"Info","debug_grpc":false}"#;
667 let config: BoxConfig = serde_json::from_str(json).unwrap();
668 assert!(!config.read_only);
669
670 let config = BoxConfig {
672 read_only: true,
673 ..Default::default()
674 };
675 let json = serde_json::to_string(&config).unwrap();
676 let deserialized: BoxConfig = serde_json::from_str(&json).unwrap();
677 assert!(deserialized.read_only);
678 }
679
680 #[test]
681 fn test_box_config_user_workdir_serde() {
682 let config = BoxConfig {
683 user: Some("1000:1000".to_string()),
684 workdir: Some("/app".to_string()),
685 ..Default::default()
686 };
687
688 let json = serde_json::to_string(&config).unwrap();
689 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
690
691 assert_eq!(parsed.user.as_deref(), Some("1000:1000"));
692 assert_eq!(parsed.workdir.as_deref(), Some("/app"));
693 }
694
695 #[test]
696 fn test_box_config_hostname_add_hosts_serde() {
697 let config = BoxConfig {
698 hostname: Some("web".to_string()),
699 add_hosts: vec!["db.local:10.88.0.10".to_string()],
700 ..Default::default()
701 };
702
703 let json = serde_json::to_string(&config).unwrap();
704 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
705
706 assert_eq!(parsed.hostname.as_deref(), Some("web"));
707 assert_eq!(parsed.add_hosts, vec!["db.local:10.88.0.10"]);
708 }
709
710 #[test]
711 fn test_resource_config_default() {
712 let config = ResourceConfig::default();
713
714 assert_eq!(config.vcpus, DEFAULT_VCPUS);
715 assert_eq!(config.memory_mb, 1024);
716 assert_eq!(config.disk_mb, 4096);
717 assert_eq!(config.timeout, 3600);
718 }
719
720 #[test]
721 fn test_validate_vcpu_count() {
722 assert!(validate_vcpu_count(DEFAULT_VCPUS).is_ok());
723 assert!(validate_vcpu_count(0).unwrap_err().contains("at least 1"));
724 assert!(validate_vcpu_count(MAX_VCPUS + 1)
725 .unwrap_err()
726 .contains("maximum"));
727
728 #[cfg(target_os = "windows")]
729 assert!(validate_vcpu_count(2).unwrap_err().contains("WHPX"));
730 #[cfg(not(target_os = "windows"))]
731 assert!(validate_vcpu_count(2).is_ok());
732 }
733
734 #[test]
735 fn test_resource_config_custom() {
736 let config = ResourceConfig {
737 vcpus: 4,
738 memory_mb: 2048,
739 disk_mb: 8192,
740 timeout: 7200,
741 };
742
743 assert_eq!(config.vcpus, 4);
744 assert_eq!(config.memory_mb, 2048);
745 assert_eq!(config.disk_mb, 8192);
746 assert_eq!(config.timeout, 7200);
747 }
748
749 #[test]
750 fn test_log_level_conversion() {
751 assert_eq!(tracing::Level::from(LogLevel::Debug), tracing::Level::DEBUG);
752 assert_eq!(tracing::Level::from(LogLevel::Info), tracing::Level::INFO);
753 assert_eq!(tracing::Level::from(LogLevel::Warn), tracing::Level::WARN);
754 assert_eq!(tracing::Level::from(LogLevel::Error), tracing::Level::ERROR);
755 }
756
757 #[test]
758 fn test_box_config_serialization() {
759 let config = BoxConfig::default();
760 let json = serde_json::to_string(&config).unwrap();
761
762 assert!(json.contains("workspace"));
763 assert!(json.contains("resources"));
764 }
765
766 #[test]
767 fn test_box_config_deserialization() {
768 let json = r#"{
769 "image": "nginx:alpine",
770 "workspace": "/tmp/workspace",
771 "resources": {
772 "vcpus": 4,
773 "memory_mb": 2048,
774 "disk_mb": 8192,
775 "timeout": 1800
776 },
777 "log_level": "Debug",
778 "debug_grpc": true
779 }"#;
780
781 let config: BoxConfig = serde_json::from_str(json).unwrap();
782 assert_eq!(config.image, "nginx:alpine");
783 assert_eq!(config.workspace.to_str().unwrap(), "/tmp/workspace");
784 assert_eq!(config.resources.vcpus, 4);
785 assert!(config.debug_grpc);
786 }
787
788 #[test]
789 fn test_resource_config_serialization() {
790 let config = ResourceConfig {
791 vcpus: 8,
792 memory_mb: 4096,
793 disk_mb: 16384,
794 timeout: 0,
795 };
796
797 let json = serde_json::to_string(&config).unwrap();
798 let parsed: ResourceConfig = serde_json::from_str(&json).unwrap();
799
800 assert_eq!(parsed.vcpus, 8);
801 assert_eq!(parsed.memory_mb, 4096);
802 assert_eq!(parsed.timeout, 0); }
804
805 #[test]
806 fn test_log_level_serialization() {
807 let levels = vec![
808 LogLevel::Debug,
809 LogLevel::Info,
810 LogLevel::Warn,
811 LogLevel::Error,
812 ];
813
814 for level in levels {
815 let json = serde_json::to_string(&level).unwrap();
816 let parsed: LogLevel = serde_json::from_str(&json).unwrap();
817 assert_eq!(tracing::Level::from(parsed), tracing::Level::from(level));
818 }
819 }
820
821 #[test]
822 fn test_config_clone() {
823 let config = BoxConfig::default();
824 let cloned = config.clone();
825
826 assert_eq!(config.workspace, cloned.workspace);
827 assert_eq!(config.resources.vcpus, cloned.resources.vcpus);
828 }
829
830 #[test]
831 fn test_config_debug() {
832 let config = BoxConfig::default();
833 let debug_str = format!("{:?}", config);
834
835 assert!(debug_str.contains("BoxConfig"));
836 assert!(debug_str.contains("workspace"));
837 }
838
839 #[test]
840 fn test_tee_config_default() {
841 let tee = TeeConfig::default();
842 assert_eq!(tee, TeeConfig::None);
843 }
844
845 #[test]
846 fn test_tee_config_sev_snp() {
847 let tee = TeeConfig::SevSnp {
848 workload_id: "test-agent".to_string(),
849 generation: SevSnpGeneration::Milan,
850 simulate: false,
851 };
852
853 match tee {
854 TeeConfig::SevSnp {
855 workload_id,
856 generation,
857 simulate,
858 } => {
859 assert_eq!(workload_id, "test-agent");
860 assert_eq!(generation, SevSnpGeneration::Milan);
861 assert!(!simulate);
862 }
863 _ => panic!("Expected SevSnp variant"),
864 }
865 }
866
867 #[test]
868 fn test_sev_snp_generation_as_str() {
869 assert_eq!(SevSnpGeneration::Milan.as_str(), "milan");
870 assert_eq!(SevSnpGeneration::Genoa.as_str(), "genoa");
871 }
872
873 #[test]
874 fn test_sev_snp_generation_default() {
875 let gen = SevSnpGeneration::default();
876 assert_eq!(gen, SevSnpGeneration::Milan);
877 }
878
879 #[test]
880 fn test_tee_config_serialization() {
881 let tee = TeeConfig::SevSnp {
882 workload_id: "my-workload".to_string(),
883 generation: SevSnpGeneration::Genoa,
884 simulate: false,
885 };
886
887 let json = serde_json::to_string(&tee).unwrap();
888 let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
889
890 assert_eq!(parsed, tee);
891 }
892
893 #[test]
894 fn test_tee_config_none_serialization() {
895 let tee = TeeConfig::None;
896 let json = serde_json::to_string(&tee).unwrap();
897 let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
898
899 assert_eq!(parsed, TeeConfig::None);
900 }
901
902 #[test]
903 fn test_tee_config_tdx() {
904 let tee = TeeConfig::Tdx {
905 workload_id: "tdx-workload".to_string(),
906 simulate: false,
907 };
908 let json = serde_json::to_string(&tee).unwrap();
909 let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
910 match parsed {
911 TeeConfig::Tdx {
912 workload_id,
913 simulate,
914 } => {
915 assert_eq!(workload_id, "tdx-workload");
916 assert!(!simulate);
917 }
918 _ => panic!("Expected Tdx variant"),
919 }
920 }
921
922 #[test]
923 fn test_tee_config_tdx_simulate() {
924 let tee = TeeConfig::Tdx {
925 workload_id: "test".to_string(),
926 simulate: true,
927 };
928 let json = serde_json::to_string(&tee).unwrap();
929 let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
930 match parsed {
931 TeeConfig::Tdx { simulate, .. } => assert!(simulate),
932 _ => panic!("Expected Tdx variant"),
933 }
934 }
935
936 #[test]
937 fn test_box_config_with_tee() {
938 let config = BoxConfig {
939 tee: TeeConfig::SevSnp {
940 workload_id: "secure-agent".to_string(),
941 generation: SevSnpGeneration::Milan,
942 simulate: false,
943 },
944 ..Default::default()
945 };
946
947 let json = serde_json::to_string(&config).unwrap();
948 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
949
950 match parsed.tee {
951 TeeConfig::SevSnp {
952 workload_id,
953 generation,
954 simulate,
955 } => {
956 assert_eq!(workload_id, "secure-agent");
957 assert_eq!(generation, SevSnpGeneration::Milan);
958 assert!(!simulate);
959 }
960 _ => panic!("Expected SevSnp TEE config"),
961 }
962 }
963
964 #[test]
965 fn test_box_config_default_has_no_tee() {
966 let config = BoxConfig::default();
967 assert_eq!(config.tee, TeeConfig::None);
968 }
969
970 #[test]
973 fn test_cache_config_default() {
974 let config = CacheConfig::default();
975 assert!(config.enabled);
976 assert!(config.cache_dir.is_none());
977 assert_eq!(config.max_rootfs_entries, 10);
978 assert_eq!(config.max_cache_bytes, 10 * 1024 * 1024 * 1024);
979 }
980
981 #[test]
982 fn test_cache_config_serialization() {
983 let config = CacheConfig {
984 enabled: false,
985 cache_dir: Some(PathBuf::from("/tmp/cache")),
986 max_rootfs_entries: 5,
987 max_cache_bytes: 1024 * 1024 * 1024,
988 };
989
990 let json = serde_json::to_string(&config).unwrap();
991 let parsed: CacheConfig = serde_json::from_str(&json).unwrap();
992
993 assert!(!parsed.enabled);
994 assert_eq!(parsed.cache_dir, Some(PathBuf::from("/tmp/cache")));
995 assert_eq!(parsed.max_rootfs_entries, 5);
996 assert_eq!(parsed.max_cache_bytes, 1024 * 1024 * 1024);
997 }
998
999 #[test]
1000 fn test_cache_config_deserialization_defaults() {
1001 let json = "{}";
1002 let config: CacheConfig = serde_json::from_str(json).unwrap();
1003
1004 assert!(config.enabled);
1005 assert!(config.cache_dir.is_none());
1006 assert_eq!(config.max_rootfs_entries, 10);
1007 assert_eq!(config.max_cache_bytes, 10 * 1024 * 1024 * 1024);
1008 }
1009
1010 #[test]
1013 fn test_pool_config_default() {
1014 let config = PoolConfig::default();
1015 assert!(!config.enabled);
1016 assert_eq!(config.min_idle, 1);
1017 assert_eq!(config.max_size, 5);
1018 assert_eq!(config.idle_ttl_secs, 300);
1019 }
1020
1021 #[test]
1022 fn test_pool_config_serialization() {
1023 let config = PoolConfig {
1024 enabled: true,
1025 min_idle: 3,
1026 max_size: 10,
1027 idle_ttl_secs: 600,
1028 ..Default::default()
1029 };
1030
1031 let json = serde_json::to_string(&config).unwrap();
1032 let parsed: PoolConfig = serde_json::from_str(&json).unwrap();
1033
1034 assert!(parsed.enabled);
1035 assert_eq!(parsed.min_idle, 3);
1036 assert_eq!(parsed.max_size, 10);
1037 assert_eq!(parsed.idle_ttl_secs, 600);
1038 }
1039
1040 #[test]
1041 fn test_pool_config_deserialization_defaults() {
1042 let json = "{}";
1043 let config: PoolConfig = serde_json::from_str(json).unwrap();
1044
1045 assert!(!config.enabled);
1046 assert_eq!(config.min_idle, 1);
1047 assert_eq!(config.max_size, 5);
1048 assert_eq!(config.idle_ttl_secs, 300);
1049 }
1050
1051 #[test]
1054 fn test_box_config_default_has_cache_and_pool() {
1055 let config = BoxConfig::default();
1056 assert!(config.cache.enabled);
1057 assert!(!config.pool.enabled);
1058 }
1059
1060 #[test]
1061 fn test_box_config_with_cache_serialization() {
1062 let config = BoxConfig {
1063 cache: CacheConfig {
1064 enabled: false,
1065 cache_dir: Some(PathBuf::from("/custom/cache")),
1066 max_rootfs_entries: 20,
1067 max_cache_bytes: 5 * 1024 * 1024 * 1024,
1068 },
1069 ..Default::default()
1070 };
1071
1072 let json = serde_json::to_string(&config).unwrap();
1073 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1074
1075 assert!(!parsed.cache.enabled);
1076 assert_eq!(parsed.cache.cache_dir, Some(PathBuf::from("/custom/cache")));
1077 assert_eq!(parsed.cache.max_rootfs_entries, 20);
1078 }
1079
1080 #[test]
1081 fn test_box_config_with_pool_serialization() {
1082 let config = BoxConfig {
1083 pool: PoolConfig {
1084 enabled: true,
1085 min_idle: 2,
1086 max_size: 8,
1087 idle_ttl_secs: 120,
1088 ..Default::default()
1089 },
1090 ..Default::default()
1091 };
1092
1093 let json = serde_json::to_string(&config).unwrap();
1094 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1095
1096 assert!(parsed.pool.enabled);
1097 assert_eq!(parsed.pool.min_idle, 2);
1098 assert_eq!(parsed.pool.max_size, 8);
1099 assert_eq!(parsed.pool.idle_ttl_secs, 120);
1100 }
1101
1102 #[test]
1103 fn test_box_config_backward_compatible_deserialization() {
1104 let json = r#"{
1106 "workspace": "/tmp/workspace",
1107 "resources": {
1108 "vcpus": 2,
1109 "memory_mb": 1024,
1110 "disk_mb": 4096,
1111 "timeout": 3600
1112 },
1113 "log_level": "Info",
1114 "debug_grpc": false
1115 }"#;
1116
1117 let config: BoxConfig = serde_json::from_str(json).unwrap();
1118 assert!(config.cache.enabled);
1119 assert!(!config.pool.enabled);
1120 }
1121
1122 #[test]
1125 fn test_resource_limits_default() {
1126 let limits = ResourceLimits::default();
1127 assert!(limits.pids_limit.is_none());
1128 assert!(limits.cpuset_cpus.is_none());
1129 assert!(limits.ulimits.is_empty());
1130 assert!(limits.cpu_shares.is_none());
1131 assert!(limits.cpu_quota.is_none());
1132 assert!(limits.cpu_period.is_none());
1133 assert!(limits.memory_reservation.is_none());
1134 assert!(limits.memory_swap.is_none());
1135 assert!(limits.sandbox_memory_limit_bytes.is_none());
1136 }
1137
1138 #[test]
1139 fn test_resource_limits_serialization() {
1140 let limits = ResourceLimits {
1141 pids_limit: Some(100),
1142 cpuset_cpus: Some("0,1".to_string()),
1143 ulimits: vec!["nofile=1024:4096".to_string()],
1144 cpu_shares: Some(512),
1145 cpu_quota: Some(50000),
1146 cpu_period: Some(100000),
1147 memory_reservation: Some(256 * 1024 * 1024),
1148 memory_swap: Some(1024 * 1024 * 1024),
1149 sandbox_memory_limit_bytes: Some(256 * 1024 * 1024),
1150 };
1151
1152 let json = serde_json::to_string(&limits).unwrap();
1153 let parsed: ResourceLimits = serde_json::from_str(&json).unwrap();
1154
1155 assert_eq!(parsed.pids_limit, Some(100));
1156 assert_eq!(parsed.cpuset_cpus, Some("0,1".to_string()));
1157 assert_eq!(parsed.ulimits, vec!["nofile=1024:4096"]);
1158 assert_eq!(parsed.cpu_shares, Some(512));
1159 assert_eq!(parsed.cpu_quota, Some(50000));
1160 assert_eq!(parsed.cpu_period, Some(100000));
1161 assert_eq!(parsed.memory_reservation, Some(256 * 1024 * 1024));
1162 assert_eq!(parsed.memory_swap, Some(1024 * 1024 * 1024));
1163 assert_eq!(parsed.sandbox_memory_limit_bytes, Some(256 * 1024 * 1024));
1164 }
1165
1166 #[test]
1167 fn test_resource_limits_deserialization_defaults() {
1168 let json = "{}";
1169 let limits: ResourceLimits = serde_json::from_str(json).unwrap();
1170 assert!(limits.pids_limit.is_none());
1171 assert!(limits.ulimits.is_empty());
1172 }
1173
1174 #[test]
1175 fn test_resource_limits_memory_swap_unlimited() {
1176 let limits = ResourceLimits {
1177 memory_swap: Some(-1),
1178 ..Default::default()
1179 };
1180
1181 let json = serde_json::to_string(&limits).unwrap();
1182 let parsed: ResourceLimits = serde_json::from_str(&json).unwrap();
1183 assert_eq!(parsed.memory_swap, Some(-1));
1184 }
1185
1186 #[test]
1187 fn test_box_config_with_resource_limits() {
1188 let config = BoxConfig {
1189 resource_limits: ResourceLimits {
1190 pids_limit: Some(256),
1191 cpu_shares: Some(1024),
1192 ..Default::default()
1193 },
1194 ..Default::default()
1195 };
1196
1197 let json = serde_json::to_string(&config).unwrap();
1198 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1199
1200 assert_eq!(parsed.resource_limits.pids_limit, Some(256));
1201 assert_eq!(parsed.resource_limits.cpu_shares, Some(1024));
1202 }
1203
1204 #[test]
1205 fn test_box_config_backward_compat_no_resource_limits() {
1206 let json = r#"{
1208 "workspace": "/tmp/workspace",
1209 "resources": {
1210 "vcpus": 2,
1211 "memory_mb": 1024,
1212 "disk_mb": 4096,
1213 "timeout": 3600
1214 },
1215 "log_level": "Info",
1216 "debug_grpc": false
1217 }"#;
1218
1219 let config: BoxConfig = serde_json::from_str(json).unwrap();
1220 assert!(config.resource_limits.pids_limit.is_none());
1221 assert!(config.resource_limits.ulimits.is_empty());
1222 }
1223
1224 #[test]
1227 fn test_sidecar_config_default() {
1228 let s = SidecarConfig::default();
1229 assert!(s.image.is_empty());
1230 assert_eq!(s.vsock_port, 4092);
1231 assert!(s.env.is_empty());
1232 }
1233
1234 #[test]
1235 fn test_sidecar_config_roundtrip() {
1236 let s = SidecarConfig {
1237 image: "ghcr.io/a3s-lab/safeclaw:latest".to_string(),
1238 vsock_port: 4092,
1239 env: vec![
1240 ("LOG_LEVEL".to_string(), "debug".to_string()),
1241 ("MODE".to_string(), "proxy".to_string()),
1242 ],
1243 };
1244 let json = serde_json::to_string(&s).unwrap();
1245 let parsed: SidecarConfig = serde_json::from_str(&json).unwrap();
1246 assert_eq!(parsed.image, "ghcr.io/a3s-lab/safeclaw:latest");
1247 assert_eq!(parsed.vsock_port, 4092);
1248 assert_eq!(parsed.env.len(), 2);
1249 assert_eq!(
1250 parsed.env[0],
1251 ("LOG_LEVEL".to_string(), "debug".to_string())
1252 );
1253 }
1254
1255 #[test]
1256 fn test_sidecar_config_default_vsock_port_from_json() {
1257 let json = r#"{"image":"safeclaw:latest"}"#;
1258 let s: SidecarConfig = serde_json::from_str(json).unwrap();
1259 assert_eq!(s.vsock_port, 4092);
1260 assert!(s.env.is_empty());
1261 }
1262
1263 #[test]
1264 fn test_box_config_default_has_no_sidecar() {
1265 let config = BoxConfig::default();
1266 assert!(config.sidecar.is_none());
1267 }
1268
1269 #[test]
1270 fn test_box_config_with_sidecar_roundtrip() {
1271 let config = BoxConfig {
1272 sidecar: Some(SidecarConfig {
1273 image: "safeclaw:latest".to_string(),
1274 vsock_port: 4092,
1275 env: vec![],
1276 }),
1277 ..Default::default()
1278 };
1279 let json = serde_json::to_string(&config).unwrap();
1280 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1281 let sidecar = parsed.sidecar.unwrap();
1282 assert_eq!(sidecar.image, "safeclaw:latest");
1283 assert_eq!(sidecar.vsock_port, 4092);
1284 }
1285
1286 #[test]
1287 fn test_box_config_without_sidecar_deserializes_as_none() {
1288 let config = BoxConfig::default();
1290 let json = serde_json::to_string(&config).unwrap();
1291 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1292 assert!(parsed.sidecar.is_none());
1293 }
1294}