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
281#[derive(Debug, Clone, Serialize, Deserialize)]
283pub struct BoxConfig {
284 #[serde(default)]
286 pub isolation: ExecutionIsolation,
287
288 #[serde(default)]
290 pub image: String,
291
292 pub workspace: PathBuf,
294
295 pub resources: ResourceConfig,
297
298 pub log_level: LogLevel,
300
301 pub debug_grpc: bool,
303
304 #[serde(default)]
306 pub tee: TeeConfig,
307
308 #[serde(default)]
310 pub cmd: Vec<String>,
311
312 #[serde(default)]
316 pub stdin_open: bool,
317
318 #[serde(default)]
320 pub entrypoint_override: Option<Vec<String>>,
321
322 #[serde(default)]
326 pub user: Option<String>,
327
328 #[serde(default)]
330 pub workdir: Option<String>,
331
332 #[serde(default)]
334 pub hostname: Option<String>,
335
336 #[serde(default)]
338 pub volumes: Vec<String>,
339
340 #[serde(default)]
343 pub virtiofs_cache: Option<String>,
344
345 #[serde(default)]
347 pub extra_env: Vec<(String, String)>,
348
349 #[serde(default)]
351 pub cache: CacheConfig,
352
353 #[serde(default)]
355 pub pool: PoolConfig,
356
357 #[serde(default)]
362 pub deferred_main: bool,
363
364 #[serde(default)]
368 pub ksm: bool,
369
370 #[serde(default)]
374 pub snapshot_mem_file: Option<String>,
375
376 #[serde(default)]
379 pub snapshot_sock: Option<String>,
380
381 #[serde(default)]
385 pub restore_from: Option<String>,
386
387 #[serde(default)]
390 pub port_map: Vec<String>,
391
392 #[serde(default)]
395 pub dns: Vec<String>,
396
397 #[serde(default)]
399 pub add_hosts: Vec<String>,
400
401 #[serde(default)]
403 pub network: NetworkMode,
404
405 #[serde(default)]
408 pub tmpfs: Vec<String>,
409
410 #[serde(default)]
412 pub resource_limits: ResourceLimits,
413
414 #[serde(default)]
416 pub cap_add: Vec<String>,
417
418 #[serde(default)]
420 pub cap_drop: Vec<String>,
421
422 #[serde(default)]
424 pub security_opt: Vec<String>,
425
426 #[serde(default)]
431 pub sysctls: Vec<(String, String)>,
432
433 #[serde(default)]
435 pub privileged: bool,
436
437 #[serde(default)]
442 pub read_only: bool,
443
444 #[serde(default)]
450 pub sidecar: Option<SidecarConfig>,
451
452 #[serde(default)]
461 pub persistent: bool,
462}
463
464impl Default for BoxConfig {
465 fn default() -> Self {
466 Self {
467 isolation: ExecutionIsolation::default(),
468 image: String::new(),
469 workspace: PathBuf::new(),
472 resources: ResourceConfig::default(),
473 log_level: LogLevel::Info,
474 debug_grpc: false,
475 tee: TeeConfig::default(),
476 cmd: vec![],
477 stdin_open: false,
478 entrypoint_override: None,
479 user: None,
480 workdir: None,
481 hostname: None,
482 volumes: vec![],
483 virtiofs_cache: None,
484 extra_env: vec![],
485 cache: CacheConfig::default(),
486 pool: PoolConfig::default(),
487 deferred_main: false,
488 ksm: false,
489 snapshot_mem_file: None,
490 snapshot_sock: None,
491 restore_from: None,
492 port_map: vec![],
493 dns: vec![],
494 add_hosts: vec![],
495 network: NetworkMode::default(),
496 tmpfs: vec![],
497 resource_limits: ResourceLimits::default(),
498 cap_add: vec![],
499 cap_drop: vec![],
500 security_opt: vec![],
501 sysctls: vec![],
502 privileged: false,
503 read_only: false,
504 sidecar: None,
505 persistent: false,
506 }
507 }
508}
509
510#[derive(Debug, Clone, Serialize, Deserialize)]
525pub struct SidecarConfig {
526 pub image: String,
528
529 #[serde(default = "default_sidecar_vsock_port")]
531 pub vsock_port: u32,
532
533 #[serde(default)]
535 pub env: Vec<(String, String)>,
536}
537
538fn default_sidecar_vsock_port() -> u32 {
539 4092
540}
541
542impl Default for SidecarConfig {
543 fn default() -> Self {
544 Self {
545 image: String::new(),
546 vsock_port: default_sidecar_vsock_port(),
547 env: vec![],
548 }
549 }
550}
551
552#[derive(Debug, Clone, Serialize, Deserialize)]
554pub struct ResourceConfig {
555 pub vcpus: u32,
557
558 pub memory_mb: u32,
560
561 pub disk_mb: u32,
563
564 pub timeout: u64,
566}
567
568impl Default for ResourceConfig {
569 fn default() -> Self {
570 Self {
571 vcpus: 2,
572 memory_mb: 1024,
573 disk_mb: 4096,
574 timeout: 3600, }
576 }
577}
578
579#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
581pub enum LogLevel {
582 Debug,
583 Info,
584 Warn,
585 Error,
586}
587
588impl From<LogLevel> for tracing::Level {
589 fn from(level: LogLevel) -> Self {
590 match level {
591 LogLevel::Debug => tracing::Level::DEBUG,
592 LogLevel::Info => tracing::Level::INFO,
593 LogLevel::Warn => tracing::Level::WARN,
594 LogLevel::Error => tracing::Level::ERROR,
595 }
596 }
597}
598
599#[cfg(test)]
600mod tests {
601 use super::*;
602
603 #[test]
604 fn test_box_config_default() {
605 let config = BoxConfig::default();
606
607 assert!(config.image.is_empty());
608 assert!(config.workspace.as_os_str().is_empty());
610 assert_eq!(config.resources.vcpus, 2);
611 assert!(!config.debug_grpc);
612 assert!(!config.read_only);
613 assert!(config.user.is_none());
614 assert!(config.workdir.is_none());
615 assert!(config.hostname.is_none());
616 assert!(config.add_hosts.is_empty());
617 }
618
619 #[test]
620 fn test_box_config_read_only_default_false() {
621 let config = BoxConfig::default();
622 assert!(!config.read_only);
623 }
624
625 #[test]
626 fn test_box_config_read_only_serde() {
627 let json = r#"{"image":"test","workspace":"","resources":{"vcpus":2,"memory_mb":512,"disk_mb":4096,"timeout":3600},"log_level":"Info","debug_grpc":false}"#;
629 let config: BoxConfig = serde_json::from_str(json).unwrap();
630 assert!(!config.read_only);
631
632 let config = BoxConfig {
634 read_only: true,
635 ..Default::default()
636 };
637 let json = serde_json::to_string(&config).unwrap();
638 let deserialized: BoxConfig = serde_json::from_str(&json).unwrap();
639 assert!(deserialized.read_only);
640 }
641
642 #[test]
643 fn test_box_config_user_workdir_serde() {
644 let config = BoxConfig {
645 user: Some("1000:1000".to_string()),
646 workdir: Some("/app".to_string()),
647 ..Default::default()
648 };
649
650 let json = serde_json::to_string(&config).unwrap();
651 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
652
653 assert_eq!(parsed.user.as_deref(), Some("1000:1000"));
654 assert_eq!(parsed.workdir.as_deref(), Some("/app"));
655 }
656
657 #[test]
658 fn test_box_config_hostname_add_hosts_serde() {
659 let config = BoxConfig {
660 hostname: Some("web".to_string()),
661 add_hosts: vec!["db.local:10.88.0.10".to_string()],
662 ..Default::default()
663 };
664
665 let json = serde_json::to_string(&config).unwrap();
666 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
667
668 assert_eq!(parsed.hostname.as_deref(), Some("web"));
669 assert_eq!(parsed.add_hosts, vec!["db.local:10.88.0.10"]);
670 }
671
672 #[test]
673 fn test_resource_config_default() {
674 let config = ResourceConfig::default();
675
676 assert_eq!(config.vcpus, 2);
677 assert_eq!(config.memory_mb, 1024);
678 assert_eq!(config.disk_mb, 4096);
679 assert_eq!(config.timeout, 3600);
680 }
681
682 #[test]
683 fn test_resource_config_custom() {
684 let config = ResourceConfig {
685 vcpus: 4,
686 memory_mb: 2048,
687 disk_mb: 8192,
688 timeout: 7200,
689 };
690
691 assert_eq!(config.vcpus, 4);
692 assert_eq!(config.memory_mb, 2048);
693 assert_eq!(config.disk_mb, 8192);
694 assert_eq!(config.timeout, 7200);
695 }
696
697 #[test]
698 fn test_log_level_conversion() {
699 assert_eq!(tracing::Level::from(LogLevel::Debug), tracing::Level::DEBUG);
700 assert_eq!(tracing::Level::from(LogLevel::Info), tracing::Level::INFO);
701 assert_eq!(tracing::Level::from(LogLevel::Warn), tracing::Level::WARN);
702 assert_eq!(tracing::Level::from(LogLevel::Error), tracing::Level::ERROR);
703 }
704
705 #[test]
706 fn test_box_config_serialization() {
707 let config = BoxConfig::default();
708 let json = serde_json::to_string(&config).unwrap();
709
710 assert!(json.contains("workspace"));
711 assert!(json.contains("resources"));
712 }
713
714 #[test]
715 fn test_box_config_deserialization() {
716 let json = r#"{
717 "image": "nginx:alpine",
718 "workspace": "/tmp/workspace",
719 "resources": {
720 "vcpus": 4,
721 "memory_mb": 2048,
722 "disk_mb": 8192,
723 "timeout": 1800
724 },
725 "log_level": "Debug",
726 "debug_grpc": true
727 }"#;
728
729 let config: BoxConfig = serde_json::from_str(json).unwrap();
730 assert_eq!(config.image, "nginx:alpine");
731 assert_eq!(config.workspace.to_str().unwrap(), "/tmp/workspace");
732 assert_eq!(config.resources.vcpus, 4);
733 assert!(config.debug_grpc);
734 }
735
736 #[test]
737 fn test_resource_config_serialization() {
738 let config = ResourceConfig {
739 vcpus: 8,
740 memory_mb: 4096,
741 disk_mb: 16384,
742 timeout: 0,
743 };
744
745 let json = serde_json::to_string(&config).unwrap();
746 let parsed: ResourceConfig = serde_json::from_str(&json).unwrap();
747
748 assert_eq!(parsed.vcpus, 8);
749 assert_eq!(parsed.memory_mb, 4096);
750 assert_eq!(parsed.timeout, 0); }
752
753 #[test]
754 fn test_log_level_serialization() {
755 let levels = vec![
756 LogLevel::Debug,
757 LogLevel::Info,
758 LogLevel::Warn,
759 LogLevel::Error,
760 ];
761
762 for level in levels {
763 let json = serde_json::to_string(&level).unwrap();
764 let parsed: LogLevel = serde_json::from_str(&json).unwrap();
765 assert_eq!(tracing::Level::from(parsed), tracing::Level::from(level));
766 }
767 }
768
769 #[test]
770 fn test_config_clone() {
771 let config = BoxConfig::default();
772 let cloned = config.clone();
773
774 assert_eq!(config.workspace, cloned.workspace);
775 assert_eq!(config.resources.vcpus, cloned.resources.vcpus);
776 }
777
778 #[test]
779 fn test_config_debug() {
780 let config = BoxConfig::default();
781 let debug_str = format!("{:?}", config);
782
783 assert!(debug_str.contains("BoxConfig"));
784 assert!(debug_str.contains("workspace"));
785 }
786
787 #[test]
788 fn test_tee_config_default() {
789 let tee = TeeConfig::default();
790 assert_eq!(tee, TeeConfig::None);
791 }
792
793 #[test]
794 fn test_tee_config_sev_snp() {
795 let tee = TeeConfig::SevSnp {
796 workload_id: "test-agent".to_string(),
797 generation: SevSnpGeneration::Milan,
798 simulate: false,
799 };
800
801 match tee {
802 TeeConfig::SevSnp {
803 workload_id,
804 generation,
805 simulate,
806 } => {
807 assert_eq!(workload_id, "test-agent");
808 assert_eq!(generation, SevSnpGeneration::Milan);
809 assert!(!simulate);
810 }
811 _ => panic!("Expected SevSnp variant"),
812 }
813 }
814
815 #[test]
816 fn test_sev_snp_generation_as_str() {
817 assert_eq!(SevSnpGeneration::Milan.as_str(), "milan");
818 assert_eq!(SevSnpGeneration::Genoa.as_str(), "genoa");
819 }
820
821 #[test]
822 fn test_sev_snp_generation_default() {
823 let gen = SevSnpGeneration::default();
824 assert_eq!(gen, SevSnpGeneration::Milan);
825 }
826
827 #[test]
828 fn test_tee_config_serialization() {
829 let tee = TeeConfig::SevSnp {
830 workload_id: "my-workload".to_string(),
831 generation: SevSnpGeneration::Genoa,
832 simulate: false,
833 };
834
835 let json = serde_json::to_string(&tee).unwrap();
836 let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
837
838 assert_eq!(parsed, tee);
839 }
840
841 #[test]
842 fn test_tee_config_none_serialization() {
843 let tee = TeeConfig::None;
844 let json = serde_json::to_string(&tee).unwrap();
845 let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
846
847 assert_eq!(parsed, TeeConfig::None);
848 }
849
850 #[test]
851 fn test_tee_config_tdx() {
852 let tee = TeeConfig::Tdx {
853 workload_id: "tdx-workload".to_string(),
854 simulate: false,
855 };
856 let json = serde_json::to_string(&tee).unwrap();
857 let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
858 match parsed {
859 TeeConfig::Tdx {
860 workload_id,
861 simulate,
862 } => {
863 assert_eq!(workload_id, "tdx-workload");
864 assert!(!simulate);
865 }
866 _ => panic!("Expected Tdx variant"),
867 }
868 }
869
870 #[test]
871 fn test_tee_config_tdx_simulate() {
872 let tee = TeeConfig::Tdx {
873 workload_id: "test".to_string(),
874 simulate: true,
875 };
876 let json = serde_json::to_string(&tee).unwrap();
877 let parsed: TeeConfig = serde_json::from_str(&json).unwrap();
878 match parsed {
879 TeeConfig::Tdx { simulate, .. } => assert!(simulate),
880 _ => panic!("Expected Tdx variant"),
881 }
882 }
883
884 #[test]
885 fn test_box_config_with_tee() {
886 let config = BoxConfig {
887 tee: TeeConfig::SevSnp {
888 workload_id: "secure-agent".to_string(),
889 generation: SevSnpGeneration::Milan,
890 simulate: false,
891 },
892 ..Default::default()
893 };
894
895 let json = serde_json::to_string(&config).unwrap();
896 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
897
898 match parsed.tee {
899 TeeConfig::SevSnp {
900 workload_id,
901 generation,
902 simulate,
903 } => {
904 assert_eq!(workload_id, "secure-agent");
905 assert_eq!(generation, SevSnpGeneration::Milan);
906 assert!(!simulate);
907 }
908 _ => panic!("Expected SevSnp TEE config"),
909 }
910 }
911
912 #[test]
913 fn test_box_config_default_has_no_tee() {
914 let config = BoxConfig::default();
915 assert_eq!(config.tee, TeeConfig::None);
916 }
917
918 #[test]
921 fn test_cache_config_default() {
922 let config = CacheConfig::default();
923 assert!(config.enabled);
924 assert!(config.cache_dir.is_none());
925 assert_eq!(config.max_rootfs_entries, 10);
926 assert_eq!(config.max_cache_bytes, 10 * 1024 * 1024 * 1024);
927 }
928
929 #[test]
930 fn test_cache_config_serialization() {
931 let config = CacheConfig {
932 enabled: false,
933 cache_dir: Some(PathBuf::from("/tmp/cache")),
934 max_rootfs_entries: 5,
935 max_cache_bytes: 1024 * 1024 * 1024,
936 };
937
938 let json = serde_json::to_string(&config).unwrap();
939 let parsed: CacheConfig = serde_json::from_str(&json).unwrap();
940
941 assert!(!parsed.enabled);
942 assert_eq!(parsed.cache_dir, Some(PathBuf::from("/tmp/cache")));
943 assert_eq!(parsed.max_rootfs_entries, 5);
944 assert_eq!(parsed.max_cache_bytes, 1024 * 1024 * 1024);
945 }
946
947 #[test]
948 fn test_cache_config_deserialization_defaults() {
949 let json = "{}";
950 let config: CacheConfig = serde_json::from_str(json).unwrap();
951
952 assert!(config.enabled);
953 assert!(config.cache_dir.is_none());
954 assert_eq!(config.max_rootfs_entries, 10);
955 assert_eq!(config.max_cache_bytes, 10 * 1024 * 1024 * 1024);
956 }
957
958 #[test]
961 fn test_pool_config_default() {
962 let config = PoolConfig::default();
963 assert!(!config.enabled);
964 assert_eq!(config.min_idle, 1);
965 assert_eq!(config.max_size, 5);
966 assert_eq!(config.idle_ttl_secs, 300);
967 }
968
969 #[test]
970 fn test_pool_config_serialization() {
971 let config = PoolConfig {
972 enabled: true,
973 min_idle: 3,
974 max_size: 10,
975 idle_ttl_secs: 600,
976 ..Default::default()
977 };
978
979 let json = serde_json::to_string(&config).unwrap();
980 let parsed: PoolConfig = serde_json::from_str(&json).unwrap();
981
982 assert!(parsed.enabled);
983 assert_eq!(parsed.min_idle, 3);
984 assert_eq!(parsed.max_size, 10);
985 assert_eq!(parsed.idle_ttl_secs, 600);
986 }
987
988 #[test]
989 fn test_pool_config_deserialization_defaults() {
990 let json = "{}";
991 let config: PoolConfig = serde_json::from_str(json).unwrap();
992
993 assert!(!config.enabled);
994 assert_eq!(config.min_idle, 1);
995 assert_eq!(config.max_size, 5);
996 assert_eq!(config.idle_ttl_secs, 300);
997 }
998
999 #[test]
1002 fn test_box_config_default_has_cache_and_pool() {
1003 let config = BoxConfig::default();
1004 assert!(config.cache.enabled);
1005 assert!(!config.pool.enabled);
1006 }
1007
1008 #[test]
1009 fn test_box_config_with_cache_serialization() {
1010 let config = BoxConfig {
1011 cache: CacheConfig {
1012 enabled: false,
1013 cache_dir: Some(PathBuf::from("/custom/cache")),
1014 max_rootfs_entries: 20,
1015 max_cache_bytes: 5 * 1024 * 1024 * 1024,
1016 },
1017 ..Default::default()
1018 };
1019
1020 let json = serde_json::to_string(&config).unwrap();
1021 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1022
1023 assert!(!parsed.cache.enabled);
1024 assert_eq!(parsed.cache.cache_dir, Some(PathBuf::from("/custom/cache")));
1025 assert_eq!(parsed.cache.max_rootfs_entries, 20);
1026 }
1027
1028 #[test]
1029 fn test_box_config_with_pool_serialization() {
1030 let config = BoxConfig {
1031 pool: PoolConfig {
1032 enabled: true,
1033 min_idle: 2,
1034 max_size: 8,
1035 idle_ttl_secs: 120,
1036 ..Default::default()
1037 },
1038 ..Default::default()
1039 };
1040
1041 let json = serde_json::to_string(&config).unwrap();
1042 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1043
1044 assert!(parsed.pool.enabled);
1045 assert_eq!(parsed.pool.min_idle, 2);
1046 assert_eq!(parsed.pool.max_size, 8);
1047 assert_eq!(parsed.pool.idle_ttl_secs, 120);
1048 }
1049
1050 #[test]
1051 fn test_box_config_backward_compatible_deserialization() {
1052 let json = r#"{
1054 "workspace": "/tmp/workspace",
1055 "resources": {
1056 "vcpus": 2,
1057 "memory_mb": 1024,
1058 "disk_mb": 4096,
1059 "timeout": 3600
1060 },
1061 "log_level": "Info",
1062 "debug_grpc": false
1063 }"#;
1064
1065 let config: BoxConfig = serde_json::from_str(json).unwrap();
1066 assert!(config.cache.enabled);
1067 assert!(!config.pool.enabled);
1068 }
1069
1070 #[test]
1073 fn test_resource_limits_default() {
1074 let limits = ResourceLimits::default();
1075 assert!(limits.pids_limit.is_none());
1076 assert!(limits.cpuset_cpus.is_none());
1077 assert!(limits.ulimits.is_empty());
1078 assert!(limits.cpu_shares.is_none());
1079 assert!(limits.cpu_quota.is_none());
1080 assert!(limits.cpu_period.is_none());
1081 assert!(limits.memory_reservation.is_none());
1082 assert!(limits.memory_swap.is_none());
1083 }
1084
1085 #[test]
1086 fn test_resource_limits_serialization() {
1087 let limits = ResourceLimits {
1088 pids_limit: Some(100),
1089 cpuset_cpus: Some("0,1".to_string()),
1090 ulimits: vec!["nofile=1024:4096".to_string()],
1091 cpu_shares: Some(512),
1092 cpu_quota: Some(50000),
1093 cpu_period: Some(100000),
1094 memory_reservation: Some(256 * 1024 * 1024),
1095 memory_swap: Some(1024 * 1024 * 1024),
1096 };
1097
1098 let json = serde_json::to_string(&limits).unwrap();
1099 let parsed: ResourceLimits = serde_json::from_str(&json).unwrap();
1100
1101 assert_eq!(parsed.pids_limit, Some(100));
1102 assert_eq!(parsed.cpuset_cpus, Some("0,1".to_string()));
1103 assert_eq!(parsed.ulimits, vec!["nofile=1024:4096"]);
1104 assert_eq!(parsed.cpu_shares, Some(512));
1105 assert_eq!(parsed.cpu_quota, Some(50000));
1106 assert_eq!(parsed.cpu_period, Some(100000));
1107 assert_eq!(parsed.memory_reservation, Some(256 * 1024 * 1024));
1108 assert_eq!(parsed.memory_swap, Some(1024 * 1024 * 1024));
1109 }
1110
1111 #[test]
1112 fn test_resource_limits_deserialization_defaults() {
1113 let json = "{}";
1114 let limits: ResourceLimits = serde_json::from_str(json).unwrap();
1115 assert!(limits.pids_limit.is_none());
1116 assert!(limits.ulimits.is_empty());
1117 }
1118
1119 #[test]
1120 fn test_resource_limits_memory_swap_unlimited() {
1121 let limits = ResourceLimits {
1122 memory_swap: Some(-1),
1123 ..Default::default()
1124 };
1125
1126 let json = serde_json::to_string(&limits).unwrap();
1127 let parsed: ResourceLimits = serde_json::from_str(&json).unwrap();
1128 assert_eq!(parsed.memory_swap, Some(-1));
1129 }
1130
1131 #[test]
1132 fn test_box_config_with_resource_limits() {
1133 let config = BoxConfig {
1134 resource_limits: ResourceLimits {
1135 pids_limit: Some(256),
1136 cpu_shares: Some(1024),
1137 ..Default::default()
1138 },
1139 ..Default::default()
1140 };
1141
1142 let json = serde_json::to_string(&config).unwrap();
1143 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1144
1145 assert_eq!(parsed.resource_limits.pids_limit, Some(256));
1146 assert_eq!(parsed.resource_limits.cpu_shares, Some(1024));
1147 }
1148
1149 #[test]
1150 fn test_box_config_backward_compat_no_resource_limits() {
1151 let json = r#"{
1153 "workspace": "/tmp/workspace",
1154 "resources": {
1155 "vcpus": 2,
1156 "memory_mb": 1024,
1157 "disk_mb": 4096,
1158 "timeout": 3600
1159 },
1160 "log_level": "Info",
1161 "debug_grpc": false
1162 }"#;
1163
1164 let config: BoxConfig = serde_json::from_str(json).unwrap();
1165 assert!(config.resource_limits.pids_limit.is_none());
1166 assert!(config.resource_limits.ulimits.is_empty());
1167 }
1168
1169 #[test]
1172 fn test_sidecar_config_default() {
1173 let s = SidecarConfig::default();
1174 assert!(s.image.is_empty());
1175 assert_eq!(s.vsock_port, 4092);
1176 assert!(s.env.is_empty());
1177 }
1178
1179 #[test]
1180 fn test_sidecar_config_roundtrip() {
1181 let s = SidecarConfig {
1182 image: "ghcr.io/a3s-lab/safeclaw:latest".to_string(),
1183 vsock_port: 4092,
1184 env: vec![
1185 ("LOG_LEVEL".to_string(), "debug".to_string()),
1186 ("MODE".to_string(), "proxy".to_string()),
1187 ],
1188 };
1189 let json = serde_json::to_string(&s).unwrap();
1190 let parsed: SidecarConfig = serde_json::from_str(&json).unwrap();
1191 assert_eq!(parsed.image, "ghcr.io/a3s-lab/safeclaw:latest");
1192 assert_eq!(parsed.vsock_port, 4092);
1193 assert_eq!(parsed.env.len(), 2);
1194 assert_eq!(
1195 parsed.env[0],
1196 ("LOG_LEVEL".to_string(), "debug".to_string())
1197 );
1198 }
1199
1200 #[test]
1201 fn test_sidecar_config_default_vsock_port_from_json() {
1202 let json = r#"{"image":"safeclaw:latest"}"#;
1203 let s: SidecarConfig = serde_json::from_str(json).unwrap();
1204 assert_eq!(s.vsock_port, 4092);
1205 assert!(s.env.is_empty());
1206 }
1207
1208 #[test]
1209 fn test_box_config_default_has_no_sidecar() {
1210 let config = BoxConfig::default();
1211 assert!(config.sidecar.is_none());
1212 }
1213
1214 #[test]
1215 fn test_box_config_with_sidecar_roundtrip() {
1216 let config = BoxConfig {
1217 sidecar: Some(SidecarConfig {
1218 image: "safeclaw:latest".to_string(),
1219 vsock_port: 4092,
1220 env: vec![],
1221 }),
1222 ..Default::default()
1223 };
1224 let json = serde_json::to_string(&config).unwrap();
1225 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1226 let sidecar = parsed.sidecar.unwrap();
1227 assert_eq!(sidecar.image, "safeclaw:latest");
1228 assert_eq!(sidecar.vsock_port, 4092);
1229 }
1230
1231 #[test]
1232 fn test_box_config_without_sidecar_deserializes_as_none() {
1233 let config = BoxConfig::default();
1235 let json = serde_json::to_string(&config).unwrap();
1236 let parsed: BoxConfig = serde_json::from_str(&json).unwrap();
1237 assert!(parsed.sidecar.is_none());
1238 }
1239}