Skip to main content

smith_config/
executor.rs

1//! Executor service configuration
2
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::env;
7use std::fs;
8use std::path::PathBuf;
9
10/// Executor service configuration
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct ExecutorConfig {
13    /// Executor node name
14    pub node_name: String,
15
16    /// Work directory root
17    pub work_root: PathBuf,
18
19    /// State directory for persistence
20    pub state_dir: PathBuf,
21
22    /// Audit directory for audit logs
23    pub audit_dir: PathBuf,
24
25    /// User UID to run intents as
26    pub user_uid: u32,
27
28    /// User GID to run intents as
29    pub user_gid: u32,
30
31    /// Enable Landlock LSM sandboxing
32    pub landlock_enabled: bool,
33
34    /// Egress proxy socket path
35    pub egress_proxy_socket: PathBuf,
36
37    /// Metrics port for Prometheus
38    pub metrics_port: Option<u16>,
39
40    /// Intent stream configurations by capability
41    pub intent_streams: HashMap<String, IntentStreamConfig>,
42
43    /// Results configuration
44    pub results: ResultsConfig,
45
46    /// Resource limits
47    pub limits: LimitsConfig,
48
49    /// Security configuration
50    pub security: SecurityConfig,
51
52    /// Capability bundle configuration
53    pub capabilities: CapabilityConfig,
54
55    /// OPA policy update configuration
56    pub policy: PolicyConfig,
57
58    /// NATS configuration specific to executor
59    pub nats_config: ExecutorNatsConfig,
60
61    /// Supply chain attestation configuration
62    pub attestation: AttestationConfig,
63
64    /// Micro-VM pool configuration for persistent shell environments
65    #[serde(default)]
66    pub vm_pool: VmPoolConfig,
67}
68
69/// Intent stream configuration for a capability
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct IntentStreamConfig {
72    /// NATS subject pattern
73    pub subject: String,
74
75    /// Maximum age for messages
76    pub max_age: String,
77
78    /// Maximum bytes in stream
79    pub max_bytes: String,
80
81    /// Number of worker instances
82    pub workers: u32,
83}
84
85/// Results stream configuration
86#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct ResultsConfig {
88    /// Subject prefix for results
89    pub subject_prefix: String,
90
91    /// Maximum age for result messages
92    pub max_age: String,
93}
94
95/// Resource limits configuration
96#[derive(Debug, Clone, Serialize, Deserialize, Default)]
97pub struct LimitsConfig {
98    /// Default resource limits
99    pub defaults: DefaultLimits,
100
101    /// Per-capability limit overrides
102    pub overrides: HashMap<String, DefaultLimits>,
103}
104
105/// Default resource limits for intent execution
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct DefaultLimits {
108    /// CPU time per 100ms window
109    pub cpu_ms_per_100ms: u32,
110
111    /// Memory limit in bytes
112    pub mem_bytes: u64,
113
114    /// I/O bytes limit
115    pub io_bytes: u64,
116
117    /// Maximum number of processes/threads
118    pub pids_max: u32,
119
120    /// Temporary filesystem size in MB
121    pub tmpfs_mb: u32,
122
123    /// Maximum intent payload size
124    pub intent_max_bytes: u64,
125}
126
127/// Security configuration
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct SecurityConfig {
130    /// Public keys directory for signature verification
131    pub pubkeys_dir: PathBuf,
132
133    /// JWT token issuers (URLs)
134    pub jwt_issuers: Vec<String>,
135
136    /// Enable strict sandbox mode
137    pub strict_sandbox: bool,
138
139    /// Enable network isolation
140    pub network_isolation: bool,
141
142    /// Allowed outbound network destinations
143    pub allowed_destinations: Vec<String>,
144}
145
146/// Capability enforcement configuration
147#[derive(Debug, Clone, Serialize, Deserialize)]
148pub struct CapabilityConfig {
149    /// Path to capability derivations file
150    pub derivations_path: PathBuf,
151
152    /// Enable capability enforcement
153    pub enforcement_enabled: bool,
154}
155
156/// OPA policy update configuration
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct PolicyConfig {
159    /// Policy update check interval in seconds
160    pub update_interval_seconds: u64,
161
162    /// NATS subject for streaming policy updates
163    #[serde(default = "PolicyConfig::default_updates_subject")]
164    pub updates_subject: String,
165
166    /// Optional queue group for load-balanced policy updates
167    #[serde(default)]
168    pub updates_queue: Option<String>,
169}
170
171/// NATS configuration specific to executor service
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct ExecutorNatsConfig {
174    /// NATS server URLs
175    pub servers: Vec<String>,
176
177    /// JetStream domain
178    pub jetstream_domain: String,
179
180    /// TLS certificate file path
181    pub tls_cert: Option<PathBuf>,
182
183    /// TLS key file path
184    pub tls_key: Option<PathBuf>,
185
186    /// TLS CA file path
187    pub tls_ca: Option<PathBuf>,
188}
189
190/// Supply chain attestation configuration
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct AttestationConfig {
193    /// Enable capability bundle signing and verification
194    pub enable_capability_signing: bool,
195
196    /// Enable container image verification
197    pub enable_image_verification: bool,
198
199    /// Enable SLSA provenance generation and verification
200    pub enable_slsa_provenance: bool,
201
202    /// Fail execution if attestation verification fails
203    pub fail_on_signature_error: bool,
204
205    /// Cosign public key for verification (optional for keyless)
206    pub cosign_public_key: Option<String>,
207
208    /// SLSA provenance output directory
209    pub provenance_output_dir: PathBuf,
210
211    /// Attestation verification cache TTL in seconds
212    pub verification_cache_ttl: u64,
213
214    /// Periodic attestation verification interval in seconds
215    pub periodic_verification_interval: u64,
216}
217
218/// Persistent micro-VM pool configuration
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct VmPoolConfig {
221    /// Enable the pool; when disabled the executor falls back to per-intent sandboxes
222    #[serde(default)]
223    pub enabled: bool,
224
225    /// Root directory where per-session volumes are stored
226    pub volume_root: PathBuf,
227
228    /// Optional Nix profile or flake reference used to hydrate VM environments
229    pub nix_profile: Option<String>,
230
231    /// Base shell binary used to execute commands inside a VM
232    pub shell: PathBuf,
233
234    /// Additional shell arguments (e.g., ["-lc"] or nix develop flags)
235    #[serde(default)]
236    pub shell_args: Vec<String>,
237
238    /// Static environment variables injected into every VM command
239    #[serde(default)]
240    pub env: HashMap<String, String>,
241
242    /// Maximum number of concurrently active VMs
243    pub max_vms: usize,
244
245    /// Idle duration (seconds) before a VM is automatically shut down
246    pub idle_shutdown_seconds: u64,
247
248    /// Additional grace period after shutdown before pruning persistent volumes
249    pub prune_after_seconds: u64,
250
251    /// Optional delay before triggering a backup of the user volume once stopped
252    pub backup_after_seconds: Option<u64>,
253
254    /// Destination directory for backups (if enabled)
255    pub backup_destination: Option<PathBuf>,
256
257    /// Optional bootstrap command invoked once when the VM volume is created
258    #[serde(default)]
259    pub bootstrap_command: Option<Vec<String>>,
260}
261
262impl Default for VmPoolConfig {
263    fn default() -> Self {
264        Self {
265            enabled: false,
266            volume_root: PathBuf::from("/var/lib/smith/executor/vm-pool"),
267            nix_profile: None,
268            shell: PathBuf::from("/bin/bash"),
269            shell_args: vec!["-lc".to_string()],
270            env: HashMap::new(),
271            max_vms: 32,
272            idle_shutdown_seconds: 900,
273            prune_after_seconds: 3_600,
274            backup_after_seconds: None,
275            backup_destination: None,
276            bootstrap_command: None,
277        }
278    }
279}
280
281impl VmPoolConfig {
282    pub fn validate(&self) -> Result<()> {
283        if !self.enabled {
284            return Ok(());
285        }
286
287        if self.max_vms == 0 {
288            return Err(anyhow::anyhow!(
289                "vm_pool.max_vms must be greater than zero when the pool is enabled"
290            ));
291        }
292
293        if self.idle_shutdown_seconds == 0 {
294            return Err(anyhow::anyhow!(
295                "vm_pool.idle_shutdown_seconds must be greater than zero"
296            ));
297        }
298
299        if self.prune_after_seconds == 0 {
300            return Err(anyhow::anyhow!(
301                "vm_pool.prune_after_seconds must be greater than zero"
302            ));
303        }
304
305        if let Some(backup_after) = self.backup_after_seconds {
306            if backup_after == 0 {
307                return Err(anyhow::anyhow!(
308                    "vm_pool.backup_after_seconds must be greater than zero"
309                ));
310            }
311            if self.backup_destination.is_none() {
312                return Err(anyhow::anyhow!(
313                    "vm_pool.backup_destination must be set when backup_after_seconds is provided"
314                ));
315            }
316        }
317
318        if !self.volume_root.exists() {
319            std::fs::create_dir_all(&self.volume_root).with_context(|| {
320                format!(
321                    "Failed to create vm_pool.volume_root directory: {}",
322                    self.volume_root.display()
323                )
324            })?;
325        }
326
327        if let Some(dest) = &self.backup_destination {
328            if !dest.exists() {
329                std::fs::create_dir_all(dest).with_context(|| {
330                    format!(
331                        "Failed to create vm_pool.backup_destination directory: {}",
332                        dest.display()
333                    )
334                })?;
335            }
336        }
337
338        Ok(())
339    }
340}
341
342impl Default for ExecutorConfig {
343    fn default() -> Self {
344        let mut intent_streams = HashMap::new();
345
346        intent_streams.insert(
347            "fs.read.v1".to_string(),
348            IntentStreamConfig {
349                subject: "smith.intents.fs.read.v1".to_string(),
350                max_age: "10m".to_string(),
351                max_bytes: "1GB".to_string(),
352                workers: 4,
353            },
354        );
355
356        intent_streams.insert(
357            "http.fetch.v1".to_string(),
358            IntentStreamConfig {
359                subject: "smith.intents.http.fetch.v1".to_string(),
360                max_age: "10m".to_string(),
361                max_bytes: "1GB".to_string(),
362                workers: 4,
363            },
364        );
365
366        Self {
367            node_name: "exec-01".to_string(),
368            work_root: PathBuf::from("/var/lib/smith/executor/work"),
369            state_dir: PathBuf::from("/var/lib/smith/executor/state"),
370            audit_dir: PathBuf::from("/var/lib/smith/executor/audit"),
371            user_uid: 65534, // nobody
372            user_gid: 65534, // nobody
373            landlock_enabled: true,
374            egress_proxy_socket: PathBuf::from("/run/smith/egress-proxy.sock"),
375            metrics_port: Some(9090),
376            intent_streams,
377            results: ResultsConfig::default(),
378            limits: LimitsConfig::default(),
379            security: SecurityConfig::default(),
380            capabilities: CapabilityConfig::default(),
381            policy: PolicyConfig::default(),
382            nats_config: ExecutorNatsConfig::default(),
383            attestation: AttestationConfig::default(),
384            vm_pool: VmPoolConfig::default(),
385        }
386    }
387}
388
389impl Default for ResultsConfig {
390    fn default() -> Self {
391        Self {
392            subject_prefix: "smith.results.".to_string(),
393            max_age: "5m".to_string(),
394        }
395    }
396}
397
398impl Default for DefaultLimits {
399    fn default() -> Self {
400        Self {
401            cpu_ms_per_100ms: 50,
402            mem_bytes: 256 * 1024 * 1024, // 256MB
403            io_bytes: 10 * 1024 * 1024,   // 10MB
404            pids_max: 32,
405            tmpfs_mb: 64,
406            intent_max_bytes: 64 * 1024, // 64KB
407        }
408    }
409}
410
411impl Default for SecurityConfig {
412    fn default() -> Self {
413        Self {
414            pubkeys_dir: PathBuf::from("/etc/smith/executor/pubkeys"),
415            jwt_issuers: vec!["https://auth.smith.example.com/".to_string()],
416            strict_sandbox: false,
417            network_isolation: true,
418            allowed_destinations: vec![],
419        }
420    }
421}
422
423impl Default for CapabilityConfig {
424    fn default() -> Self {
425        Self {
426            derivations_path: PathBuf::from("build/capability/sandbox_profiles/derivations.json"),
427            enforcement_enabled: true,
428        }
429    }
430}
431
432impl Default for PolicyConfig {
433    fn default() -> Self {
434        Self {
435            update_interval_seconds: 300, // 5 minutes
436            updates_subject: "smith.policies.updates".to_string(),
437            updates_queue: None,
438        }
439    }
440}
441
442impl Default for ExecutorNatsConfig {
443    fn default() -> Self {
444        Self {
445            servers: vec!["nats://127.0.0.1:4222".to_string()],
446            jetstream_domain: "JS".to_string(),
447            tls_cert: Some(PathBuf::from("/etc/smith/executor/nats.crt")),
448            tls_key: Some(PathBuf::from("/etc/smith/executor/nats.key")),
449            tls_ca: Some(PathBuf::from("/etc/smith/executor/ca.crt")),
450        }
451    }
452}
453
454impl ExecutorConfig {
455    pub fn validate(&self) -> Result<()> {
456        // Validate node name
457        if self.node_name.is_empty() {
458            return Err(anyhow::anyhow!("Node name cannot be empty"));
459        }
460
461        if self.node_name.len() > 63 {
462            return Err(anyhow::anyhow!("Node name too long (max 63 chars)"));
463        }
464
465        // Validate directories exist or can be created
466        for (name, path) in [
467            ("work_root", &self.work_root),
468            ("state_dir", &self.state_dir),
469            ("audit_dir", &self.audit_dir),
470        ] {
471            if let Some(parent) = path.parent() {
472                if !parent.exists() {
473                    fs::create_dir_all(parent).with_context(|| {
474                        format!(
475                            "Failed to create {} parent directory: {}",
476                            name,
477                            parent.display()
478                        )
479                    })?;
480                }
481            }
482        }
483
484        // Validate UIDs/GIDs
485        if self.user_uid == 0 {
486            tracing::warn!("⚠️  Running as root (UID 0) is not recommended for security");
487        }
488
489        if self.user_gid == 0 {
490            tracing::warn!("⚠️  Running as root group (GID 0) is not recommended for security");
491        }
492
493        // Validate metrics port
494        if let Some(port) = self.metrics_port {
495            if port < 1024 {
496                return Err(anyhow::anyhow!(
497                    "Invalid metrics port: {}. Must be between 1024 and 65535",
498                    port
499                ));
500            }
501        }
502
503        // Validate intent stream configurations
504        if self.intent_streams.is_empty() {
505            return Err(anyhow::anyhow!("No intent streams configured"));
506        }
507
508        for (capability, stream_config) in &self.intent_streams {
509            stream_config.validate().map_err(|e| {
510                anyhow::anyhow!("Intent stream '{}' validation failed: {}", capability, e)
511            })?;
512        }
513
514        // Validate sub-configurations
515        self.results
516            .validate()
517            .context("Results configuration validation failed")?;
518
519        self.limits
520            .validate()
521            .context("Limits configuration validation failed")?;
522
523        self.security
524            .validate()
525            .context("Security configuration validation failed")?;
526
527        self.capabilities
528            .validate()
529            .context("Capability configuration validation failed")?;
530
531        self.policy
532            .validate()
533            .context("Policy configuration validation failed")?;
534
535        self.nats_config
536            .validate()
537            .context("NATS configuration validation failed")?;
538
539        self.vm_pool
540            .validate()
541            .context("VM pool configuration validation failed")?;
542
543        Ok(())
544    }
545
546    pub fn development() -> Self {
547        Self {
548            work_root: PathBuf::from("/tmp/smith/executor/work"),
549            state_dir: PathBuf::from("/tmp/smith/executor/state"),
550            audit_dir: PathBuf::from("/tmp/smith/executor/audit"),
551            landlock_enabled: false, // May not be available in all dev environments
552            security: SecurityConfig {
553                strict_sandbox: false,
554                network_isolation: false,
555                ..Default::default()
556            },
557            limits: LimitsConfig {
558                defaults: DefaultLimits {
559                    cpu_ms_per_100ms: 80,         // More generous for development
560                    mem_bytes: 512 * 1024 * 1024, // 512MB
561                    io_bytes: 50 * 1024 * 1024,   // 50MB
562                    ..Default::default()
563                },
564                overrides: HashMap::new(),
565            },
566            nats_config: ExecutorNatsConfig::default(),
567            ..Default::default()
568        }
569    }
570
571    pub fn production() -> Self {
572        Self {
573            landlock_enabled: true,
574            security: SecurityConfig {
575                strict_sandbox: true,
576                network_isolation: true,
577                allowed_destinations: vec!["127.0.0.1".to_string(), "::1".to_string()],
578                ..Default::default()
579            },
580            limits: LimitsConfig {
581                defaults: DefaultLimits {
582                    cpu_ms_per_100ms: 30,         // Strict limits for production
583                    mem_bytes: 128 * 1024 * 1024, // 128MB
584                    io_bytes: 5 * 1024 * 1024,    // 5MB
585                    pids_max: 16,
586                    tmpfs_mb: 32,
587                    intent_max_bytes: 32 * 1024, // 32KB
588                },
589                overrides: {
590                    let mut overrides = HashMap::new();
591
592                    // HTTP fetch needs more network I/O
593                    overrides.insert(
594                        "http.fetch.v1".to_string(),
595                        DefaultLimits {
596                            io_bytes: 20 * 1024 * 1024,   // 20MB
597                            intent_max_bytes: 128 * 1024, // 128KB
598                            ..DefaultLimits::default()
599                        },
600                    );
601
602                    overrides
603                },
604            },
605            capabilities: CapabilityConfig {
606                enforcement_enabled: true,
607                ..Default::default()
608            },
609            policy: PolicyConfig {
610                update_interval_seconds: 60, // More frequent in production
611                ..Default::default()
612            },
613            nats_config: ExecutorNatsConfig::default(),
614            ..Default::default()
615        }
616    }
617
618    pub fn testing() -> Self {
619        Self {
620            work_root: PathBuf::from("/tmp/smith-test/work"),
621            state_dir: PathBuf::from("/tmp/smith-test/state"),
622            audit_dir: PathBuf::from("/tmp/smith-test/audit"),
623            landlock_enabled: false,        // Disable for test simplicity
624            metrics_port: None,             // Disable metrics in tests
625            intent_streams: HashMap::new(), // Tests define their own
626            security: SecurityConfig {
627                strict_sandbox: false,
628                network_isolation: false,
629                jwt_issuers: vec![], // No JWT validation in tests
630                ..Default::default()
631            },
632            limits: LimitsConfig {
633                defaults: DefaultLimits {
634                    cpu_ms_per_100ms: 100,         // Generous for test timing
635                    mem_bytes: 1024 * 1024 * 1024, // 1GB
636                    io_bytes: 100 * 1024 * 1024,   // 100MB
637                    pids_max: 64,
638                    tmpfs_mb: 128,
639                    intent_max_bytes: 1024 * 1024, // 1MB
640                },
641                overrides: HashMap::new(),
642            },
643            capabilities: CapabilityConfig {
644                enforcement_enabled: false, // Disable capability enforcement in tests
645                ..Default::default()
646            },
647            nats_config: ExecutorNatsConfig::default(),
648            ..Default::default()
649        }
650    }
651}
652
653impl IntentStreamConfig {
654    pub fn validate(&self) -> Result<()> {
655        if self.subject.is_empty() {
656            return Err(anyhow::anyhow!("Subject cannot be empty"));
657        }
658
659        if self.workers == 0 {
660            return Err(anyhow::anyhow!("Worker count must be > 0"));
661        }
662
663        if self.workers > 64 {
664            return Err(anyhow::anyhow!("Worker count too high (max 64)"));
665        }
666
667        // Validate duration format
668        self.validate_duration(&self.max_age)
669            .context("Invalid max_age format")?;
670
671        // Validate byte size format
672        self.validate_byte_size(&self.max_bytes)
673            .context("Invalid max_bytes format")?;
674
675        Ok(())
676    }
677
678    fn validate_duration(&self, duration_str: &str) -> Result<()> {
679        if duration_str.is_empty() {
680            return Err(anyhow::anyhow!("Duration cannot be empty"));
681        }
682
683        let valid_suffixes = ["s", "m", "h", "d"];
684        let has_valid_suffix = valid_suffixes
685            .iter()
686            .any(|&suffix| duration_str.ends_with(suffix));
687
688        if !has_valid_suffix {
689            return Err(anyhow::anyhow!(
690                "Duration must end with valid time unit (s, m, h, d): {}",
691                duration_str
692            ));
693        }
694
695        let numeric_part = &duration_str[..duration_str.len() - 1];
696        numeric_part
697            .parse::<u64>()
698            .with_context(|| format!("Invalid numeric part in duration: {}", duration_str))?;
699
700        Ok(())
701    }
702
703    fn validate_byte_size(&self, size_str: &str) -> Result<()> {
704        if size_str.is_empty() {
705            return Err(anyhow::anyhow!("Byte size cannot be empty"));
706        }
707
708        let valid_suffixes = ["TB", "GB", "MB", "KB", "B"]; // Longest first
709        let suffix = valid_suffixes
710            .iter()
711            .find(|&&suffix| size_str.ends_with(suffix))
712            .ok_or_else(|| {
713                anyhow::anyhow!(
714                    "Byte size must end with valid unit (B, KB, MB, GB, TB): {}",
715                    size_str
716                )
717            })?;
718
719        if let Some(numeric_part) = size_str.strip_suffix(suffix) {
720            numeric_part
721                .parse::<u64>()
722                .with_context(|| format!("Invalid numeric part in byte size: {}", size_str))?;
723        } else {
724            return Err(anyhow::anyhow!("Failed to parse byte size: {}", size_str));
725        }
726
727        Ok(())
728    }
729}
730
731impl ResultsConfig {
732    pub fn validate(&self) -> Result<()> {
733        if self.subject_prefix.is_empty() {
734            return Err(anyhow::anyhow!("Results subject prefix cannot be empty"));
735        }
736
737        // Simple duration validation
738        if !self.max_age.ends_with(['s', 'm', 'h', 'd']) {
739            return Err(anyhow::anyhow!(
740                "Results max_age must end with valid time unit (s, m, h, d): {}",
741                self.max_age
742            ));
743        }
744
745        Ok(())
746    }
747}
748
749impl LimitsConfig {
750    pub fn validate(&self) -> Result<()> {
751        self.defaults
752            .validate()
753            .context("Default limits validation failed")?;
754
755        for (capability, limits) in &self.overrides {
756            limits.validate().map_err(|e| {
757                anyhow::anyhow!(
758                    "Limits override for '{}' validation failed: {}",
759                    capability,
760                    e
761                )
762            })?;
763        }
764
765        Ok(())
766    }
767}
768
769impl DefaultLimits {
770    pub fn validate(&self) -> Result<()> {
771        if self.cpu_ms_per_100ms > 100 {
772            return Err(anyhow::anyhow!("CPU limit cannot exceed 100ms per 100ms"));
773        }
774
775        if self.mem_bytes == 0 {
776            return Err(anyhow::anyhow!("Memory limit cannot be zero"));
777        }
778
779        if self.mem_bytes > 8 * 1024 * 1024 * 1024 {
780            tracing::warn!("Memory limit > 8GB may be excessive");
781        }
782
783        if self.pids_max == 0 || self.pids_max > 1024 {
784            return Err(anyhow::anyhow!("PID limit must be between 1 and 1024"));
785        }
786
787        if self.tmpfs_mb > 1024 {
788            tracing::warn!("tmpfs size > 1GB may consume excessive memory");
789        }
790
791        if self.intent_max_bytes > 10 * 1024 * 1024 {
792            tracing::warn!("Intent max bytes > 10MB may cause memory issues");
793        }
794
795        Ok(())
796    }
797}
798
799impl SecurityConfig {
800    pub fn validate(&self) -> Result<()> {
801        // Validate JWT issuers are valid URLs
802        for issuer in &self.jwt_issuers {
803            url::Url::parse(issuer)
804                .with_context(|| format!("Invalid JWT issuer URL: {}", issuer))?;
805        }
806
807        // Validate allowed destinations
808        for dest in &self.allowed_destinations {
809            if dest.parse::<std::net::IpAddr>().is_err() && !dest.contains(':') {
810                // Simple hostname validation
811                if dest.is_empty() || dest.len() > 255 {
812                    return Err(anyhow::anyhow!("Invalid destination: {}", dest));
813                }
814            }
815        }
816
817        Ok(())
818    }
819}
820
821impl CapabilityConfig {
822    pub fn validate(&self) -> Result<()> {
823        if self.derivations_path.as_os_str().is_empty() {
824            return Err(anyhow::anyhow!(
825                "Capability derivations path cannot be empty"
826            ));
827        }
828
829        Ok(())
830    }
831}
832
833impl PolicyConfig {
834    fn default_updates_subject() -> String {
835        "smith.policies.updates".to_string()
836    }
837
838    pub fn validate(&self) -> Result<()> {
839        if self.update_interval_seconds == 0 {
840            return Err(anyhow::anyhow!("Policy update interval must be > 0"));
841        }
842
843        if self.update_interval_seconds < 60 {
844            tracing::warn!("Policy update interval < 60s may cause excessive load");
845        }
846
847        if self.updates_subject.trim().is_empty() {
848            return Err(anyhow::anyhow!("Policy updates subject cannot be empty"));
849        }
850
851        if let Some(queue) = &self.updates_queue {
852            if queue.trim().is_empty() {
853                return Err(anyhow::anyhow!(
854                    "Policy updates queue group cannot be blank"
855                ));
856            }
857        }
858
859        Ok(())
860    }
861}
862
863impl ExecutorNatsConfig {
864    pub fn validate(&self) -> Result<()> {
865        // Validate NATS servers format
866        for server in &self.servers {
867            if !server.starts_with("nats://") && !server.starts_with("tls://") {
868                return Err(anyhow::anyhow!("Invalid NATS server URL: {}", server));
869            }
870        }
871
872        // Validate TLS configuration consistency
873        if let (Some(cert), Some(key), Some(ca)) = (&self.tls_cert, &self.tls_key, &self.tls_ca) {
874            // All TLS files specified - validate they exist
875            if !cert.exists() {
876                return Err(anyhow::anyhow!(
877                    "TLS cert file not found: {}",
878                    cert.display()
879                ));
880            }
881            if !key.exists() {
882                return Err(anyhow::anyhow!("TLS key file not found: {}", key.display()));
883            }
884            if !ca.exists() {
885                return Err(anyhow::anyhow!("TLS CA file not found: {}", ca.display()));
886            }
887        }
888
889        Ok(())
890    }
891}
892
893/// Policy derivations loaded from derivations.json
894#[derive(Debug, Clone, Serialize, Deserialize)]
895pub struct PolicyDerivations {
896    pub seccomp_allow: HashMap<String, Vec<String>>,
897    pub landlock_paths: HashMap<String, LandlockProfile>,
898    pub cgroups: HashMap<String, CgroupLimits>,
899}
900
901/// Landlock access profile for a capability
902#[derive(Debug, Clone, Serialize, Deserialize)]
903pub struct LandlockProfile {
904    /// Paths with read access
905    pub read: Vec<String>,
906    /// Paths with write access  
907    pub write: Vec<String>,
908}
909
910/// Cgroup resource limits for a capability
911#[derive(Debug, Clone, Serialize, Deserialize)]
912pub struct CgroupLimits {
913    /// CPU percentage limit
914    pub cpu_pct: u32,
915    /// Memory limit in MB
916    pub mem_mb: u64,
917}
918
919impl ExecutorConfig {
920    /// Convert byte size string to actual bytes
921    pub fn parse_byte_size(size_str: &str) -> Result<u64> {
922        let multipliers = [
923            ("TB", 1024_u64.pow(4)),
924            ("GB", 1024_u64.pow(3)),
925            ("MB", 1024_u64.pow(2)),
926            ("KB", 1024),
927            ("B", 1),
928        ];
929
930        for (suffix, multiplier) in &multipliers {
931            if let Some(numeric_part) = size_str.strip_suffix(suffix) {
932                let number: u64 = numeric_part
933                    .parse()
934                    .with_context(|| format!("Invalid numeric part in byte size: {}", size_str))?;
935                return Ok(number * multiplier);
936            }
937        }
938
939        Err(anyhow::anyhow!("Invalid byte size format: {}", size_str))
940    }
941
942    /// Convert duration string to seconds
943    pub fn parse_duration_seconds(duration_str: &str) -> Result<u64> {
944        let multipliers = [
945            ("d", 86400), // days
946            ("h", 3600),  // hours
947            ("m", 60),    // minutes
948            ("s", 1),     // seconds
949        ];
950
951        for (suffix, multiplier) in &multipliers {
952            if let Some(numeric_part) = duration_str.strip_suffix(suffix) {
953                let number: u64 = numeric_part.parse().with_context(|| {
954                    format!("Invalid numeric part in duration: {}", duration_str)
955                })?;
956                return Ok(number * multiplier);
957            }
958        }
959
960        Err(anyhow::anyhow!("Invalid duration format: {}", duration_str))
961    }
962
963    /// Load configuration from TOML file
964    pub fn load(path: &std::path::Path) -> Result<Self> {
965        let content = std::fs::read_to_string(path)
966            .with_context(|| format!("Failed to read config file: {}", path.display()))?;
967
968        let raw_value: toml::Value = toml::from_str(&content)
969            .with_context(|| format!("Failed to parse TOML config: {}", path.display()))?;
970
971        let mut config: ExecutorConfig = if let Some(executor_table) = raw_value.get("executor") {
972            executor_table
973                .clone()
974                .try_into()
975                .map_err(anyhow::Error::from)
976                .with_context(|| {
977                    format!(
978                        "Failed to parse TOML config `[executor]` section: {}",
979                        path.display()
980                    )
981                })?
982        } else {
983            toml::from_str(&content)
984                .map_err(anyhow::Error::from)
985                .with_context(|| {
986                    format!(
987                        "Failed to parse TOML config: {} (expected top-level executor fields \
988                         or an `[executor]` table)",
989                        path.display()
990                    )
991                })?
992        };
993
994        config.apply_env_overrides()?;
995        config.validate()?;
996        Ok(config)
997    }
998
999    fn apply_env_overrides(&mut self) -> Result<()> {
1000        if let Ok(raw_servers) = env::var("SMITH_EXECUTOR_NATS_SERVERS") {
1001            let servers = Self::parse_env_server_list(&raw_servers);
1002            if !servers.is_empty() {
1003                self.nats_config.servers = servers;
1004            }
1005        } else if let Ok(single) = env::var("SMITH_EXECUTOR_NATS_URL") {
1006            let trimmed = single.trim();
1007            if !trimmed.is_empty() {
1008                self.nats_config.servers = vec![trimmed.to_string()];
1009            }
1010        } else if let Ok(single) = env::var("SMITH_NATS_URL") {
1011            let trimmed = single.trim();
1012            if !trimmed.is_empty() {
1013                self.nats_config.servers = vec![trimmed.to_string()];
1014            }
1015        }
1016
1017        if let Ok(domain) = env::var("SMITH_EXECUTOR_JETSTREAM_DOMAIN")
1018            .or_else(|_| env::var("SMITH_NATS_JETSTREAM_DOMAIN"))
1019            .or_else(|_| env::var("SMITH_JETSTREAM_DOMAIN"))
1020        {
1021            let trimmed = domain.trim();
1022            if !trimmed.is_empty() {
1023                self.nats_config.jetstream_domain = trimmed.to_string();
1024            }
1025        }
1026
1027        Ok(())
1028    }
1029
1030    fn parse_env_server_list(raw: &str) -> Vec<String> {
1031        raw.split(|c| c == ',' || c == ';')
1032            .map(|part| part.trim())
1033            .filter(|part| !part.is_empty())
1034            .map(|part| part.to_string())
1035            .collect()
1036    }
1037}
1038
1039impl PolicyDerivations {
1040    /// Load policy derivations from JSON file
1041    pub fn load(path: &std::path::Path) -> Result<Self> {
1042        let content = std::fs::read_to_string(path)
1043            .with_context(|| format!("Failed to read derivations file: {}", path.display()))?;
1044
1045        let derivations: PolicyDerivations = serde_json::from_str(&content)
1046            .with_context(|| format!("Failed to parse derivations JSON: {}", path.display()))?;
1047
1048        Ok(derivations)
1049    }
1050
1051    /// Get seccomp syscall allowlist for a capability
1052    pub fn get_seccomp_allowlist(&self, capability: &str) -> Option<&Vec<String>> {
1053        self.seccomp_allow.get(capability)
1054    }
1055
1056    /// Get landlock paths configuration for a capability
1057    pub fn get_landlock_profile(&self, capability: &str) -> Option<&LandlockProfile> {
1058        self.landlock_paths.get(capability)
1059    }
1060
1061    /// Get cgroup limits for a capability
1062    pub fn get_cgroup_limits(&self, capability: &str) -> Option<&CgroupLimits> {
1063        self.cgroups.get(capability)
1064    }
1065}
1066
1067impl Default for AttestationConfig {
1068    fn default() -> Self {
1069        Self {
1070            enable_capability_signing: true,
1071            enable_image_verification: true,
1072            enable_slsa_provenance: true,
1073            fail_on_signature_error: std::env::var("SMITH_FAIL_ON_SIGNATURE_ERROR")
1074                .unwrap_or_else(|_| "true".to_string())
1075                .parse()
1076                .unwrap_or(true),
1077            cosign_public_key: std::env::var("SMITH_COSIGN_PUBLIC_KEY").ok(),
1078            provenance_output_dir: PathBuf::from(
1079                std::env::var("SMITH_PROVENANCE_OUTPUT_DIR")
1080                    .unwrap_or_else(|_| "build/attestation".to_string()),
1081            ),
1082            verification_cache_ttl: 3600,        // 1 hour
1083            periodic_verification_interval: 300, // 5 minutes
1084        }
1085    }
1086}
1087
1088#[cfg(test)]
1089mod tests {
1090    use super::*;
1091    use std::collections::HashMap;
1092    use std::path::PathBuf;
1093    use tempfile::tempdir;
1094
1095    #[test]
1096    fn test_executor_config_creation() {
1097        let config = ExecutorConfig {
1098            node_name: "test-executor".to_string(),
1099            work_root: PathBuf::from("/tmp/work"),
1100            state_dir: PathBuf::from("/tmp/state"),
1101            audit_dir: PathBuf::from("/tmp/audit"),
1102            user_uid: 1000,
1103            user_gid: 1000,
1104            landlock_enabled: true,
1105            egress_proxy_socket: PathBuf::from("/tmp/proxy.sock"),
1106            metrics_port: Some(9090),
1107            intent_streams: HashMap::new(),
1108            results: ResultsConfig::default(),
1109            limits: LimitsConfig::default(),
1110            security: SecurityConfig::default(),
1111            capabilities: CapabilityConfig::default(),
1112            policy: PolicyConfig::default(),
1113            nats_config: ExecutorNatsConfig::default(),
1114            attestation: AttestationConfig::default(),
1115            vm_pool: VmPoolConfig::default(),
1116        };
1117
1118        assert_eq!(config.node_name, "test-executor");
1119        assert_eq!(config.work_root, PathBuf::from("/tmp/work"));
1120        assert_eq!(config.user_uid, 1000);
1121        assert!(config.landlock_enabled);
1122        assert_eq!(config.metrics_port, Some(9090));
1123    }
1124
1125    #[test]
1126    fn test_intent_stream_config() {
1127        let stream_config = IntentStreamConfig {
1128            subject: "smith.intents.test".to_string(),
1129            max_age: "1h".to_string(),
1130            max_bytes: "10MB".to_string(),
1131            workers: 4,
1132        };
1133
1134        assert_eq!(stream_config.subject, "smith.intents.test");
1135        assert_eq!(stream_config.max_age, "1h");
1136        assert_eq!(stream_config.max_bytes, "10MB");
1137        assert_eq!(stream_config.workers, 4);
1138    }
1139
1140    #[test]
1141    fn test_intent_stream_config_validation() {
1142        let mut config = IntentStreamConfig {
1143            subject: "smith.intents.test".to_string(),
1144            max_age: "1h".to_string(),
1145            max_bytes: "1GB".to_string(), // Use GB as in the default config
1146            workers: 4,
1147        };
1148
1149        assert!(config.validate().is_ok());
1150
1151        // Test empty subject
1152        config.subject = "".to_string();
1153        assert!(config.validate().is_err());
1154        config.subject = "smith.intents.test".to_string(); // Fix it
1155
1156        // Test zero workers
1157        config.workers = 0;
1158        assert!(config.validate().is_err());
1159
1160        // Test too many workers
1161        config.workers = 100;
1162        assert!(config.validate().is_err());
1163
1164        // Test valid workers
1165        config.workers = 32;
1166        assert!(config.validate().is_ok());
1167    }
1168
1169    #[test]
1170    fn test_results_config_default() {
1171        let results_config = ResultsConfig::default();
1172
1173        assert_eq!(results_config.subject_prefix, "smith.results."); // Includes trailing dot
1174        assert_eq!(results_config.max_age, "5m"); // Correct default max_age
1175    }
1176
1177    #[test]
1178    fn test_limits_config_default() {
1179        let limits_config = LimitsConfig::default();
1180
1181        // Just verify the structure exists and has defaults
1182        assert_eq!(limits_config.overrides.len(), 0); // Empty by default
1183                                                      // The actual defaults are in the impl
1184    }
1185
1186    #[test]
1187    fn test_default_limits_validation() {
1188        let mut limits = DefaultLimits::default();
1189        assert!(limits.validate().is_ok());
1190
1191        // Test CPU limit validation
1192        limits.cpu_ms_per_100ms = 150; // > 100
1193        assert!(limits.validate().is_err());
1194        limits.cpu_ms_per_100ms = 50; // Valid
1195
1196        // Test memory limit validation
1197        limits.mem_bytes = 0; // Invalid
1198        assert!(limits.validate().is_err());
1199        limits.mem_bytes = 64 * 1024 * 1024; // Valid
1200
1201        // Test PID limit validation
1202        limits.pids_max = 0; // Invalid
1203        assert!(limits.validate().is_err());
1204        limits.pids_max = 2000; // > 1024, invalid
1205        assert!(limits.validate().is_err());
1206        limits.pids_max = 64; // Valid
1207
1208        assert!(limits.validate().is_ok());
1209    }
1210
1211    #[test]
1212    fn test_security_config_validation() {
1213        let mut security_config = SecurityConfig::default();
1214        assert!(security_config.validate().is_ok());
1215
1216        // Test invalid JWT issuer
1217        security_config.jwt_issuers = vec!["invalid-url".to_string()];
1218        assert!(security_config.validate().is_err());
1219
1220        // Test valid JWT issuer
1221        security_config.jwt_issuers = vec!["https://auth.example.com".to_string()];
1222        assert!(security_config.validate().is_ok());
1223
1224        // Test allowed destinations validation
1225        security_config.allowed_destinations =
1226            vec!["192.168.1.1".to_string(), "example.com".to_string()];
1227        assert!(security_config.validate().is_ok());
1228
1229        // Test invalid destination (empty string)
1230        security_config.allowed_destinations = vec!["".to_string()];
1231        assert!(security_config.validate().is_err());
1232
1233        // Test invalid destination (too long)
1234        security_config.allowed_destinations = vec!["a".repeat(256)];
1235        assert!(security_config.validate().is_err());
1236    }
1237
1238    #[test]
1239    fn test_policy_config_validation() {
1240        let mut policy_config = PolicyConfig::default();
1241        assert!(policy_config.validate().is_ok());
1242
1243        // Test zero update interval (invalid)
1244        policy_config.update_interval_seconds = 0;
1245        assert!(policy_config.validate().is_err());
1246
1247        // Test valid update interval
1248        policy_config.update_interval_seconds = 300;
1249        assert!(policy_config.validate().is_ok());
1250    }
1251
1252    #[test]
1253    fn test_executor_nats_config_validation() {
1254        let mut nats_config = ExecutorNatsConfig {
1255            servers: vec!["nats://127.0.0.1:4222".to_string()],
1256            jetstream_domain: "JS".to_string(),
1257            tls_cert: None, // No TLS files for testing
1258            tls_key: None,
1259            tls_ca: None,
1260        };
1261        assert!(nats_config.validate().is_ok());
1262
1263        // Test invalid server URL
1264        nats_config.servers = vec!["invalid-url".to_string()];
1265        assert!(nats_config.validate().is_err());
1266
1267        // Test valid server URLs
1268        nats_config.servers = vec![
1269            "nats://localhost:4222".to_string(),
1270            "tls://nats.example.com:4222".to_string(),
1271        ];
1272        assert!(nats_config.validate().is_ok());
1273    }
1274
1275    #[test]
1276    fn test_executor_nats_config_tls_validation() {
1277        let temp_dir = tempdir().unwrap();
1278        let cert_path = temp_dir.path().join("cert.pem");
1279        let key_path = temp_dir.path().join("key.pem");
1280        let ca_path = temp_dir.path().join("ca.pem");
1281
1282        // Create dummy files
1283        std::fs::write(&cert_path, "cert").unwrap();
1284        std::fs::write(&key_path, "key").unwrap();
1285        std::fs::write(&ca_path, "ca").unwrap();
1286
1287        let valid_config = ExecutorNatsConfig {
1288            tls_cert: Some(cert_path.clone()),
1289            tls_key: Some(key_path.clone()),
1290            tls_ca: Some(ca_path.clone()),
1291            ..ExecutorNatsConfig::default()
1292        };
1293        assert!(valid_config.validate().is_ok());
1294
1295        let missing_cert = ExecutorNatsConfig {
1296            tls_cert: Some(temp_dir.path().join("missing.pem")),
1297            tls_key: Some(key_path.clone()),
1298            tls_ca: Some(ca_path.clone()),
1299            ..ExecutorNatsConfig::default()
1300        };
1301        assert!(missing_cert.validate().is_err());
1302
1303        let missing_key = ExecutorNatsConfig {
1304            tls_cert: Some(cert_path),
1305            tls_key: Some(temp_dir.path().join("missing.pem")),
1306            tls_ca: Some(ca_path),
1307            ..ExecutorNatsConfig::default()
1308        };
1309        assert!(missing_key.validate().is_err());
1310    }
1311
1312    #[test]
1313    #[ignore] // requires infra/config/smith-executor.toml from deployment repo
1314    fn test_repo_executor_config_loads() {
1315        let path = PathBuf::from("../../infra/config/smith-executor.toml");
1316        let result = ExecutorConfig::load(&path);
1317        assert!(result.is_ok(), "error: {:?}", result.unwrap_err());
1318    }
1319
1320    #[test]
1321    fn test_executor_env_overrides_nats_servers() {
1322        let temp_dir = tempdir().unwrap();
1323        let config_path = temp_dir.path().join("executor.toml");
1324
1325        let mut config = ExecutorConfig::development();
1326        config.work_root = temp_dir.path().join("work");
1327        config.state_dir = temp_dir.path().join("state");
1328        config.audit_dir = temp_dir.path().join("audit");
1329        config.egress_proxy_socket = temp_dir.path().join("proxy.sock");
1330        config.security.pubkeys_dir = temp_dir.path().join("pubkeys");
1331        config.capabilities.derivations_path = temp_dir.path().join("capability.json");
1332        config.attestation.provenance_output_dir = temp_dir.path().join("attestation_outputs");
1333        config.nats_config.tls_cert = None;
1334        config.nats_config.tls_key = None;
1335        config.nats_config.tls_ca = None;
1336
1337        let toml = toml::to_string(&config).unwrap();
1338        std::fs::write(&config_path, toml).unwrap();
1339
1340        let prev_servers = env::var("SMITH_EXECUTOR_NATS_SERVERS").ok();
1341        let prev_exec_url = env::var("SMITH_EXECUTOR_NATS_URL").ok();
1342        let prev_nats_url = env::var("SMITH_NATS_URL").ok();
1343        let prev_domain = env::var("SMITH_NATS_JETSTREAM_DOMAIN").ok();
1344        let prev_exec_domain = env::var("SMITH_EXECUTOR_JETSTREAM_DOMAIN").ok();
1345
1346        env::remove_var("SMITH_EXECUTOR_NATS_SERVERS");
1347        env::remove_var("SMITH_EXECUTOR_NATS_URL");
1348        env::remove_var("SMITH_NATS_URL");
1349        env::remove_var("SMITH_NATS_JETSTREAM_DOMAIN");
1350        env::remove_var("SMITH_EXECUTOR_JETSTREAM_DOMAIN");
1351
1352        env::set_var(
1353            "SMITH_EXECUTOR_NATS_SERVERS",
1354            "nats://localhost:7222, nats://backup:7223",
1355        );
1356        env::set_var("SMITH_NATS_JETSTREAM_DOMAIN", "devtools");
1357
1358        let loaded = ExecutorConfig::load(&config_path).unwrap();
1359        assert_eq!(
1360            loaded.nats_config.servers,
1361            vec![
1362                "nats://localhost:7222".to_string(),
1363                "nats://backup:7223".to_string()
1364            ]
1365        );
1366        assert_eq!(loaded.nats_config.jetstream_domain, "devtools");
1367
1368        restore_env_var("SMITH_EXECUTOR_NATS_SERVERS", prev_servers);
1369        restore_env_var("SMITH_EXECUTOR_NATS_URL", prev_exec_url);
1370        restore_env_var("SMITH_NATS_URL", prev_nats_url);
1371        restore_env_var("SMITH_NATS_JETSTREAM_DOMAIN", prev_domain);
1372        restore_env_var("SMITH_EXECUTOR_JETSTREAM_DOMAIN", prev_exec_domain);
1373    }
1374
1375    #[test]
1376    fn test_attestation_config_default() {
1377        let attestation_config = AttestationConfig::default();
1378
1379        assert!(attestation_config.enable_capability_signing);
1380        assert!(attestation_config.enable_image_verification);
1381        assert!(attestation_config.enable_slsa_provenance);
1382        assert_eq!(attestation_config.verification_cache_ttl, 3600);
1383        assert_eq!(attestation_config.periodic_verification_interval, 300);
1384    }
1385
1386    #[test]
1387    fn test_cgroup_limits() {
1388        let cgroup_limits = CgroupLimits {
1389            cpu_pct: 50,
1390            mem_mb: 128,
1391        };
1392
1393        assert_eq!(cgroup_limits.cpu_pct, 50);
1394        assert_eq!(cgroup_limits.mem_mb, 128);
1395    }
1396
1397    #[test]
1398    fn test_executor_config_presets() {
1399        // Test development preset
1400        let dev_config = ExecutorConfig::development();
1401        assert_eq!(dev_config.node_name, "exec-01"); // Uses default node_name
1402        assert!(!dev_config.landlock_enabled);
1403        assert!(!dev_config.security.strict_sandbox);
1404
1405        // Test production preset
1406        let prod_config = ExecutorConfig::production();
1407        assert!(prod_config.landlock_enabled);
1408        assert!(prod_config.security.strict_sandbox);
1409        assert!(prod_config.security.network_isolation);
1410        assert!(prod_config.capabilities.enforcement_enabled);
1411
1412        // Test testing preset
1413        let test_config = ExecutorConfig::testing();
1414        assert!(!test_config.landlock_enabled);
1415        assert!(!test_config.security.strict_sandbox);
1416        assert!(!test_config.capabilities.enforcement_enabled);
1417        assert_eq!(test_config.metrics_port, None);
1418    }
1419
1420    #[test]
1421    fn test_parse_byte_size() {
1422        assert_eq!(ExecutorConfig::parse_byte_size("1024B").unwrap(), 1024);
1423        assert_eq!(ExecutorConfig::parse_byte_size("10KB").unwrap(), 10 * 1024);
1424        assert_eq!(
1425            ExecutorConfig::parse_byte_size("5MB").unwrap(),
1426            5 * 1024 * 1024
1427        );
1428        assert_eq!(
1429            ExecutorConfig::parse_byte_size("2GB").unwrap(),
1430            2 * 1024 * 1024 * 1024
1431        );
1432
1433        // Test invalid formats
1434        assert!(ExecutorConfig::parse_byte_size("invalid").is_err());
1435        assert!(ExecutorConfig::parse_byte_size("10XB").is_err());
1436        assert!(ExecutorConfig::parse_byte_size("").is_err());
1437    }
1438
1439    #[test]
1440    fn test_serialization_roundtrip() {
1441        let original = ExecutorConfig {
1442            node_name: "test-node".to_string(),
1443            work_root: PathBuf::from("/work"),
1444            state_dir: PathBuf::from("/state"),
1445            audit_dir: PathBuf::from("/audit"),
1446            user_uid: 1001,
1447            user_gid: 1001,
1448            landlock_enabled: false,
1449            egress_proxy_socket: PathBuf::from("/proxy.sock"),
1450            metrics_port: Some(8080),
1451            intent_streams: HashMap::new(),
1452            results: ResultsConfig::default(),
1453            limits: LimitsConfig::default(),
1454            security: SecurityConfig::default(),
1455            capabilities: CapabilityConfig::default(),
1456            policy: PolicyConfig::default(),
1457            nats_config: ExecutorNatsConfig::default(),
1458            attestation: AttestationConfig::default(),
1459            vm_pool: VmPoolConfig::default(),
1460        };
1461
1462        // Test JSON serialization roundtrip
1463        let json = serde_json::to_string(&original).unwrap();
1464        let deserialized: ExecutorConfig = serde_json::from_str(&json).unwrap();
1465
1466        assert_eq!(original.node_name, deserialized.node_name);
1467        assert_eq!(original.work_root, deserialized.work_root);
1468        assert_eq!(original.user_uid, deserialized.user_uid);
1469        assert_eq!(original.landlock_enabled, deserialized.landlock_enabled);
1470        assert_eq!(original.metrics_port, deserialized.metrics_port);
1471    }
1472
1473    #[test]
1474    fn test_debug_formatting() {
1475        let config = ExecutorConfig {
1476            node_name: "debug-test".to_string(),
1477            work_root: PathBuf::from("/work"),
1478            state_dir: PathBuf::from("/state"),
1479            audit_dir: PathBuf::from("/audit"),
1480            user_uid: 1000,
1481            user_gid: 1000,
1482            landlock_enabled: true,
1483            egress_proxy_socket: PathBuf::from("/proxy.sock"),
1484            metrics_port: Some(9090),
1485            intent_streams: HashMap::new(),
1486            results: ResultsConfig::default(),
1487            limits: LimitsConfig::default(),
1488            security: SecurityConfig::default(),
1489            capabilities: CapabilityConfig::default(),
1490            policy: PolicyConfig::default(),
1491            nats_config: ExecutorNatsConfig::default(),
1492            attestation: AttestationConfig::default(),
1493            vm_pool: VmPoolConfig::default(),
1494        };
1495
1496        let debug_output = format!("{:?}", config);
1497        assert!(debug_output.contains("debug-test"));
1498        assert!(debug_output.contains("/work"));
1499        assert!(debug_output.contains("1000"));
1500    }
1501
1502    fn restore_env_var(name: &str, value: Option<String>) {
1503        if let Some(value) = value {
1504            env::set_var(name, value);
1505        } else {
1506            env::remove_var(name);
1507        }
1508    }
1509}