Skip to main content

smith_config/
shell.rs

1//! Shell execution configuration
2
3use anyhow::Result;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::time::Duration;
8
9/// Shell execution configuration
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct ShellConfig {
12    /// Default shell to use when none specified
13    pub default_shell: String,
14
15    /// Default timeout for commands
16    #[serde(with = "duration_serde")]
17    pub default_timeout: Duration,
18
19    /// Maximum output buffer size before truncation
20    pub max_output_size: usize,
21
22    /// PTY configuration
23    pub pty: PtyConfig,
24
25    /// ANSI escape sequence handling
26    pub strip_ansi_codes: bool,
27
28    /// Environment variable handling
29    pub environment: EnvironmentConfig,
30
31    /// Security settings
32    pub security: ShellSecurityConfig,
33
34    /// Shell-specific configurations
35    pub shell_specific: HashMap<String, ShellSpecificConfig>,
36
37    /// Working directory settings
38    pub working_directory: WorkingDirectoryConfig,
39
40    /// Logging and debugging
41    pub logging: ShellLoggingConfig,
42}
43
44/// PTY (pseudo-terminal) configuration
45#[derive(Debug, Clone, Serialize, Deserialize)]
46pub struct PtyConfig {
47    /// Number of rows
48    pub rows: Option<u16>,
49
50    /// Number of columns
51    pub cols: Option<u16>,
52
53    /// Terminal type (e.g., "xterm-256color")
54    pub terminal_type: Option<String>,
55}
56
57/// Environment variable handling configuration
58#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct EnvironmentConfig {
60    /// Capture environment changes after command execution
61    pub capture_changes: bool,
62
63    /// Maximum number of environment variables to capture
64    pub snapshot_size_limit: usize,
65
66    /// Environment variables to always preserve
67    pub preserve_vars: Vec<String>,
68
69    /// Environment variables to always remove
70    pub remove_vars: Vec<String>,
71}
72
73/// Shell security configuration
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct ShellSecurityConfig {
76    /// Allow interactive commands (with stdin)
77    pub allow_interactive_commands: bool,
78
79    /// Enable sandbox mode (restricted command set)
80    pub sandbox_mode: bool,
81
82    /// Allowed commands in sandbox mode (None = allow all)
83    pub allowed_commands: Option<Vec<String>>,
84
85    /// Always blocked commands
86    pub blocked_commands: Vec<String>,
87
88    /// Maximum command line length
89    pub max_command_length: usize,
90
91    /// Block commands with dangerous patterns
92    pub block_dangerous_patterns: bool,
93}
94
95/// Shell-specific configuration
96#[derive(Debug, Clone, Serialize, Deserialize, Default)]
97pub struct ShellSpecificConfig {
98    /// Shell-specific timeout override
99    #[serde(with = "duration_serde_opt")]
100    pub timeout: Option<Duration>,
101
102    /// Shell-specific environment variables
103    pub environment: HashMap<String, String>,
104
105    /// Shell initialization commands
106    pub init_commands: Vec<String>,
107
108    /// Login shell arguments
109    pub login_args: Option<Vec<String>>,
110
111    /// Non-login shell arguments
112    pub non_login_args: Option<Vec<String>>,
113
114    /// Feature support flags
115    pub features: ShellFeatures,
116}
117
118/// Shell feature support configuration
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct ShellFeatures {
121    /// Supports job control
122    pub job_control: bool,
123
124    /// Supports command history
125    pub history: bool,
126
127    /// Supports tab completion
128    pub completion: bool,
129
130    /// Supports colored output
131    pub color: bool,
132}
133
134/// Working directory configuration
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct WorkingDirectoryConfig {
137    /// Preserve current working directory across commands
138    pub preserve_cwd: bool,
139
140    /// Default working directory if none specified
141    pub default_cwd: Option<PathBuf>,
142
143    /// Allowed working directories (None = allow all)
144    pub allowed_directories: Option<Vec<PathBuf>>,
145}
146
147/// Shell logging configuration
148#[derive(Debug, Clone, Serialize, Deserialize)]
149pub struct ShellLoggingConfig {
150    /// Log executed commands
151    pub log_commands: bool,
152
153    /// Log command output
154    pub log_output: bool,
155
156    /// Log environment changes
157    pub log_environment_changes: bool,
158
159    /// Enable PTY debugging
160    pub debug_pty: bool,
161
162    /// Maximum log line length before truncation
163    pub max_log_line_length: usize,
164}
165
166impl Default for ShellConfig {
167    fn default() -> Self {
168        Self {
169            default_shell: "/bin/sh".to_string(), // Will be detected at runtime
170            default_timeout: Duration::from_secs(120), // 2 minutes
171            max_output_size: 2 * 1024 * 1024,     // 2MB
172            pty: PtyConfig::default(),
173            strip_ansi_codes: false,
174            environment: EnvironmentConfig::default(),
175            security: ShellSecurityConfig::default(),
176            shell_specific: HashMap::new(),
177            working_directory: WorkingDirectoryConfig::default(),
178            logging: ShellLoggingConfig::default(),
179        }
180    }
181}
182
183impl Default for PtyConfig {
184    fn default() -> Self {
185        Self {
186            rows: Some(24),
187            cols: Some(80),
188            terminal_type: Some("xterm-256color".to_string()),
189        }
190    }
191}
192
193impl Default for EnvironmentConfig {
194    fn default() -> Self {
195        Self {
196            capture_changes: true,
197            snapshot_size_limit: 1000,
198            preserve_vars: vec![
199                "PATH".to_string(),
200                "HOME".to_string(),
201                "USER".to_string(),
202                "SHELL".to_string(),
203                "TERM".to_string(),
204                "LANG".to_string(),
205                "LC_ALL".to_string(),
206            ],
207            remove_vars: vec![
208                "SSH_AUTH_SOCK".to_string(),
209                "SSH_AGENT_PID".to_string(),
210                "GPG_AGENT_INFO".to_string(),
211            ],
212        }
213    }
214}
215
216impl Default for ShellSecurityConfig {
217    fn default() -> Self {
218        Self {
219            allow_interactive_commands: true,
220            sandbox_mode: false,
221            allowed_commands: None,
222            blocked_commands: vec![
223                // Fork bombs and resource exhaustion
224                ":(){ :|:& };:".to_string(),
225                "fork() { fork|fork& }; fork".to_string(),
226                // Dangerous deletions
227                "rm -rf /".to_string(),
228                "sudo rm -rf".to_string(),
229                "> /dev/sda".to_string(),
230                // System manipulation
231                "mkfs".to_string(),
232                "fdisk".to_string(),
233                "dd if=/dev/zero".to_string(),
234                "dd if=/dev/urandom".to_string(),
235                // Network attacks
236                "ping -f".to_string(),
237                "hping".to_string(),
238                "nmap".to_string(),
239                // Process manipulation
240                "kill -9 -1".to_string(),
241                "killall -9".to_string(),
242                "pkill -9".to_string(),
243            ],
244            max_command_length: 8192, // 8KB
245            block_dangerous_patterns: true,
246        }
247    }
248}
249
250impl Default for ShellFeatures {
251    fn default() -> Self {
252        Self {
253            job_control: true,
254            history: true,
255            completion: true,
256            color: true,
257        }
258    }
259}
260
261impl Default for WorkingDirectoryConfig {
262    fn default() -> Self {
263        Self {
264            preserve_cwd: true,
265            default_cwd: None,
266            allowed_directories: None, // Allow all by default
267        }
268    }
269}
270
271impl Default for ShellLoggingConfig {
272    fn default() -> Self {
273        Self {
274            log_commands: true,
275            log_output: false, // Can be very verbose
276            log_environment_changes: false,
277            debug_pty: false,
278            max_log_line_length: 1024,
279        }
280    }
281}
282
283impl ShellConfig {
284    pub fn validate(&self) -> Result<()> {
285        // Validate shell exists (simple check)
286        if self.default_shell.is_empty() {
287            return Err(anyhow::anyhow!("Default shell cannot be empty"));
288        }
289
290        // Validate timeout
291        if self.default_timeout.as_millis() == 0 {
292            return Err(anyhow::anyhow!("Default timeout cannot be zero"));
293        }
294
295        if self.default_timeout.as_secs() > 10 * 60 {
296            tracing::warn!("Default timeout > 10 minutes may cause resource issues");
297        }
298
299        // Validate output size
300        if self.max_output_size == 0 {
301            return Err(anyhow::anyhow!("Max output size cannot be zero"));
302        }
303
304        if self.max_output_size > 100 * 1024 * 1024 {
305            return Err(anyhow::anyhow!("Max output size too large (max 100MB)"));
306        }
307
308        // Validate sub-configurations
309        self.pty.validate()?;
310        self.environment.validate()?;
311        self.security.validate()?;
312        self.working_directory.validate()?;
313        self.logging.validate()?;
314
315        // Validate shell-specific configurations
316        for (shell_name, shell_config) in &self.shell_specific {
317            shell_config.validate().map_err(|e| {
318                anyhow::anyhow!(
319                    "Shell-specific config for '{}' validation failed: {}",
320                    shell_name,
321                    e
322                )
323            })?;
324        }
325
326        Ok(())
327    }
328
329    pub fn development() -> Self {
330        Self {
331            default_timeout: Duration::from_secs(300), // 5 minutes for development
332            security: ShellSecurityConfig {
333                sandbox_mode: false,
334                allow_interactive_commands: true,
335                block_dangerous_patterns: false, // More permissive
336                ..Default::default()
337            },
338            logging: ShellLoggingConfig {
339                log_commands: true,
340                log_output: true, // Verbose for development
341                debug_pty: true,
342                ..Default::default()
343            },
344            ..Default::default()
345        }
346    }
347
348    pub fn production() -> Self {
349        Self {
350            default_timeout: Duration::from_secs(30), // Strict timeout for production
351            strip_ansi_codes: true,                   // Clean output for logging
352            security: ShellSecurityConfig {
353                sandbox_mode: true,
354                allow_interactive_commands: false,
355                allowed_commands: Some(vec![
356                    // Core utilities
357                    "ls".to_string(),
358                    "cat".to_string(),
359                    "grep".to_string(),
360                    "awk".to_string(),
361                    "sed".to_string(),
362                    "sort".to_string(),
363                    "uniq".to_string(),
364                    "cut".to_string(),
365                    "head".to_string(),
366                    "tail".to_string(),
367                    "wc".to_string(),
368                    // File operations
369                    "find".to_string(),
370                    "mkdir".to_string(),
371                    "touch".to_string(),
372                    "cp".to_string(),
373                    "mv".to_string(),
374                    // System info
375                    "ps".to_string(),
376                    "top".to_string(),
377                    "df".to_string(),
378                    "du".to_string(),
379                    "free".to_string(),
380                    "uptime".to_string(),
381                    "uname".to_string(),
382                    // Network utilities (limited)
383                    "ping".to_string(),
384                    "dig".to_string(),
385                    "host".to_string(),
386                    "nslookup".to_string(),
387                    // Text processing
388                    "tr".to_string(),
389                    "expand".to_string(),
390                    "unexpand".to_string(),
391                    "fold".to_string(),
392                    // Archiving
393                    "tar".to_string(),
394                    "gzip".to_string(),
395                    "gunzip".to_string(),
396                    "zip".to_string(),
397                    "unzip".to_string(),
398                ]),
399                block_dangerous_patterns: true,
400                max_command_length: 2048, // Stricter limit
401                ..Default::default()
402            },
403            environment: EnvironmentConfig {
404                capture_changes: false,   // Don't capture in production
405                snapshot_size_limit: 100, // Small snapshot
406                ..Default::default()
407            },
408            logging: ShellLoggingConfig {
409                log_commands: true,
410                log_output: false, // Too verbose for production
411                log_environment_changes: false,
412                debug_pty: false,
413                max_log_line_length: 512, // Shorter lines
414            },
415            ..Default::default()
416        }
417    }
418
419    pub fn testing() -> Self {
420        Self {
421            default_timeout: Duration::from_secs(5), // Quick timeout for tests
422            max_output_size: 64 * 1024,              // 64KB for tests
423            security: ShellSecurityConfig {
424                sandbox_mode: false,               // Permissive for tests
425                allow_interactive_commands: false, // No interaction in tests
426                block_dangerous_patterns: false,
427                ..Default::default()
428            },
429            environment: EnvironmentConfig {
430                capture_changes: false,
431                snapshot_size_limit: 10,
432                ..Default::default()
433            },
434            logging: ShellLoggingConfig {
435                log_commands: false,
436                log_output: false,
437                log_environment_changes: false,
438                debug_pty: false,
439                max_log_line_length: 256,
440            },
441            ..Default::default()
442        }
443    }
444
445    /// Check if a command is allowed based on security configuration
446    pub fn is_command_allowed(&self, command: &str) -> bool {
447        if command.len() > self.security.max_command_length {
448            return false;
449        }
450
451        // Check blocked commands first
452        for blocked in &self.security.blocked_commands {
453            if command.contains(blocked) {
454                return false;
455            }
456        }
457
458        // Check dangerous patterns if enabled
459        if self.security.block_dangerous_patterns && self.contains_dangerous_pattern(command) {
460            return false;
461        }
462
463        // If allow list is specified, check it
464        if let Some(ref allowed) = self.security.allowed_commands {
465            let command_name = command.split_whitespace().next().unwrap_or(command);
466            return allowed.iter().any(|allowed_cmd| {
467                command_name == allowed_cmd || command.starts_with(&format!("{} ", allowed_cmd))
468            });
469        }
470
471        true
472    }
473
474    fn contains_dangerous_pattern(&self, command: &str) -> bool {
475        let dangerous_patterns = [
476            // Redirection to devices
477            ">/dev/",
478            ">>/dev/",
479            // Recursive operations on root
480            "rm -rf /",
481            "chmod -R /",
482            "chown -R /",
483            // Pipe to shell
484            "| sh",
485            "| bash",
486            "| zsh",
487            // Dangerous combinations
488            "sudo rm",
489            "sudo dd",
490            "sudo mkfs",
491            // Network flood attacks
492            "ping -f",
493            "ping -i 0",
494            // Resource exhaustion
495            "while true",
496            "for i in $(seq 1 1000000)",
497            "> /dev/null &",
498        ];
499
500        dangerous_patterns
501            .iter()
502            .any(|pattern| command.contains(pattern))
503    }
504}
505
506impl PtyConfig {
507    pub fn validate(&self) -> Result<()> {
508        if let Some(rows) = self.rows {
509            if rows == 0 || rows > 1000 {
510                return Err(anyhow::anyhow!(
511                    "Invalid PTY rows: {}. Must be between 1 and 1000",
512                    rows
513                ));
514            }
515        }
516
517        if let Some(cols) = self.cols {
518            if cols == 0 || cols > 1000 {
519                return Err(anyhow::anyhow!(
520                    "Invalid PTY cols: {}. Must be between 1 and 1000",
521                    cols
522                ));
523            }
524        }
525
526        Ok(())
527    }
528}
529
530impl EnvironmentConfig {
531    pub fn validate(&self) -> Result<()> {
532        if self.snapshot_size_limit == 0 {
533            return Err(anyhow::anyhow!(
534                "Environment snapshot size limit must be > 0"
535            ));
536        }
537
538        if self.snapshot_size_limit > 10_000 {
539            return Err(anyhow::anyhow!(
540                "Environment snapshot size limit too large (max 10,000)"
541            ));
542        }
543
544        Ok(())
545    }
546}
547
548impl ShellSecurityConfig {
549    pub fn validate(&self) -> Result<()> {
550        if self.max_command_length == 0 {
551            return Err(anyhow::anyhow!("Max command length must be > 0"));
552        }
553
554        if self.max_command_length > 1024 * 1024 {
555            return Err(anyhow::anyhow!("Max command length too large (max 1MB)"));
556        }
557
558        Ok(())
559    }
560}
561
562impl ShellSpecificConfig {
563    pub fn validate(&self) -> Result<()> {
564        if let Some(timeout) = self.timeout {
565            if timeout.as_millis() == 0 {
566                return Err(anyhow::anyhow!("Shell-specific timeout cannot be zero"));
567            }
568        }
569
570        Ok(())
571    }
572}
573
574impl WorkingDirectoryConfig {
575    pub fn validate(&self) -> Result<()> {
576        if let Some(ref default_cwd) = self.default_cwd {
577            if !default_cwd.is_absolute() {
578                return Err(anyhow::anyhow!("Default CWD must be an absolute path"));
579            }
580        }
581
582        if let Some(ref allowed_dirs) = self.allowed_directories {
583            for dir in allowed_dirs {
584                if !dir.is_absolute() {
585                    return Err(anyhow::anyhow!(
586                        "Allowed directories must be absolute paths"
587                    ));
588                }
589            }
590        }
591
592        Ok(())
593    }
594}
595
596impl ShellLoggingConfig {
597    pub fn validate(&self) -> Result<()> {
598        if self.max_log_line_length == 0 {
599            return Err(anyhow::anyhow!("Max log line length must be > 0"));
600        }
601
602        if self.max_log_line_length > 64 * 1024 {
603            return Err(anyhow::anyhow!("Max log line length too large (max 64KB)"));
604        }
605
606        Ok(())
607    }
608}
609
610// Helper modules for Duration serialization
611mod duration_serde {
612    use serde::{Deserialize, Deserializer, Serializer};
613    use std::time::Duration;
614
615    pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
616    where
617        S: Serializer,
618    {
619        serializer.serialize_u64(duration.as_millis() as u64)
620    }
621
622    pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
623    where
624        D: Deserializer<'de>,
625    {
626        let millis = u64::deserialize(deserializer)?;
627        Ok(Duration::from_millis(millis))
628    }
629}
630
631mod duration_serde_opt {
632    use serde::{Deserialize, Deserializer, Serializer};
633    use std::time::Duration;
634
635    pub fn serialize<S>(duration: &Option<Duration>, serializer: S) -> Result<S::Ok, S::Error>
636    where
637        S: Serializer,
638    {
639        match duration {
640            Some(d) => serializer.serialize_some(&(d.as_millis() as u64)),
641            None => serializer.serialize_none(),
642        }
643    }
644
645    pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<Duration>, D::Error>
646    where
647        D: Deserializer<'de>,
648    {
649        match Option::<u64>::deserialize(deserializer)? {
650            Some(millis) => Ok(Some(Duration::from_millis(millis))),
651            None => Ok(None),
652        }
653    }
654}
655
656#[cfg(test)]
657mod tests {
658    use super::*;
659    use std::collections::HashMap;
660    use std::time::Duration;
661
662    #[test]
663    fn test_shell_config_default() {
664        let config = ShellConfig::default();
665
666        assert_eq!(config.default_shell, "/bin/sh");
667        assert_eq!(config.default_timeout, Duration::from_secs(120));
668        assert_eq!(config.max_output_size, 2 * 1024 * 1024);
669        assert!(!config.strip_ansi_codes);
670        assert!(config.shell_specific.is_empty());
671    }
672
673    #[test]
674    fn test_shell_config_validation() {
675        let mut config = ShellConfig::default();
676        assert!(config.validate().is_ok());
677
678        // Test empty shell path
679        config.default_shell = "".to_string();
680        assert!(config.validate().is_err());
681
682        config.default_shell = "/bin/bash".to_string();
683        assert!(config.validate().is_ok());
684    }
685
686    #[test]
687    fn test_shell_security_config() {
688        let security = ShellSecurityConfig::default();
689        assert!(security.allow_interactive_commands);
690        assert!(!security.sandbox_mode);
691        assert!(security.allowed_commands.is_none());
692        assert_eq!(security.max_command_length, 8192);
693        assert!(security.block_dangerous_patterns); // Default is true
694
695        assert!(security.validate().is_ok());
696    }
697
698    #[test]
699    fn test_shell_security_config_validation() {
700        let invalid_security = ShellSecurityConfig {
701            max_command_length: 0,
702            ..ShellSecurityConfig::default()
703        };
704        assert!(invalid_security.validate().is_err());
705
706        let valid_security = ShellSecurityConfig {
707            max_command_length: 1024,
708            ..ShellSecurityConfig::default()
709        };
710        assert!(valid_security.validate().is_ok());
711    }
712
713    #[test]
714    fn test_pty_config() {
715        let pty = PtyConfig::default();
716        assert_eq!(pty.rows, Some(24));
717        assert_eq!(pty.cols, Some(80));
718        assert_eq!(pty.terminal_type, Some("xterm-256color".to_string())); // Fixed field name
719    }
720
721    #[test]
722    fn test_environment_config() {
723        let env = EnvironmentConfig::default();
724        assert!(env.capture_changes); // Fixed field name
725        assert_eq!(env.snapshot_size_limit, 1000); // Fixed field name
726        assert!(env.preserve_vars.contains(&"PATH".to_string())); // Fixed field name
727        assert!(env.preserve_vars.contains(&"HOME".to_string()));
728        assert!(env.preserve_vars.contains(&"USER".to_string()));
729        assert!(env.remove_vars.contains(&"SSH_AUTH_SOCK".to_string())); // Fixed field name
730    }
731
732    #[test]
733    fn test_working_directory_config() {
734        let wd = WorkingDirectoryConfig::default();
735        assert!(wd.preserve_cwd); // Fixed field name
736        assert!(wd.default_cwd.is_none()); // Fixed field name
737        assert!(wd.allowed_directories.is_none()); // Fixed field name
738    }
739
740    #[test]
741    fn test_shell_features() {
742        let features = ShellFeatures::default();
743        assert!(features.job_control);
744        assert!(features.history);
745        assert!(features.completion);
746        assert!(features.color); // Only these 4 fields exist
747    }
748
749    #[test]
750    fn test_shell_specific_config_validation() {
751        let mut shell_config = ShellSpecificConfig {
752            timeout: Some(Duration::from_secs(60)),
753            environment: HashMap::new(),           // Fixed field name
754            init_commands: vec!["-i".to_string()], // Fixed field name
755            login_args: Some(vec!["--login".to_string()]),
756            non_login_args: Some(vec!["--norc".to_string()]),
757            features: ShellFeatures::default(), // Not optional
758        };
759
760        assert!(shell_config.validate().is_ok());
761
762        // Test zero timeout
763        shell_config.timeout = Some(Duration::from_millis(0));
764        assert!(shell_config.validate().is_err());
765    }
766
767    #[test]
768    fn test_shell_logging_config() {
769        let logging = ShellLoggingConfig::default();
770        assert!(logging.log_commands);
771        assert!(!logging.log_output);
772        assert!(!logging.log_environment_changes);
773        assert!(!logging.debug_pty); // Fixed field name
774        assert_eq!(logging.max_log_line_length, 1024);
775
776        assert!(logging.validate().is_ok());
777    }
778
779    #[test]
780    fn test_shell_logging_config_validation() {
781        let zero_len_logging = ShellLoggingConfig {
782            max_log_line_length: 0,
783            ..ShellLoggingConfig::default()
784        };
785        assert!(zero_len_logging.validate().is_err());
786
787        let too_large_logging = ShellLoggingConfig {
788            max_log_line_length: 128 * 1024, // > 64KB
789            ..ShellLoggingConfig::default()
790        };
791        assert!(too_large_logging.validate().is_err());
792
793        let valid_logging = ShellLoggingConfig {
794            max_log_line_length: 2048,
795            ..ShellLoggingConfig::default()
796        };
797        assert!(valid_logging.validate().is_ok());
798    }
799
800    #[test]
801    fn test_duration_serialization() {
802        let config = ShellConfig {
803            default_timeout: Duration::from_millis(5000),
804            ..Default::default()
805        };
806
807        let serialized = serde_json::to_string(&config).unwrap();
808        assert!(serialized.contains("5000"));
809
810        let deserialized: ShellConfig = serde_json::from_str(&serialized).unwrap();
811        assert_eq!(deserialized.default_timeout, Duration::from_millis(5000));
812    }
813
814    #[test]
815    fn test_shell_config_serialization_roundtrip() {
816        let original_config = ShellConfig::default();
817
818        let json = serde_json::to_string(&original_config).unwrap();
819        let deserialized_config: ShellConfig = serde_json::from_str(&json).unwrap();
820
821        assert_eq!(
822            original_config.default_shell,
823            deserialized_config.default_shell
824        );
825        assert_eq!(
826            original_config.default_timeout,
827            deserialized_config.default_timeout
828        );
829        assert_eq!(
830            original_config.max_output_size,
831            deserialized_config.max_output_size
832        );
833        assert_eq!(
834            original_config.strip_ansi_codes,
835            deserialized_config.strip_ansi_codes
836        );
837    }
838
839    #[test]
840    fn test_security_config_with_allowed_commands() {
841        let security = ShellSecurityConfig {
842            allow_interactive_commands: false,
843            sandbox_mode: true,
844            allowed_commands: Some(vec![
845                "ls".to_string(),
846                "cat".to_string(),
847                "grep".to_string(),
848                "find".to_string(),
849            ]),
850            blocked_commands: vec!["rm".to_string(), "sudo".to_string()], // Fixed field name
851            max_command_length: 2048,
852            block_dangerous_patterns: true, // Fixed field name
853        };
854
855        assert!(security.validate().is_ok());
856        assert!(security.allowed_commands.is_some());
857        assert_eq!(security.allowed_commands.as_ref().unwrap().len(), 4);
858        assert!(!security.allow_interactive_commands);
859        assert!(security.sandbox_mode);
860        assert_eq!(security.blocked_commands.len(), 2);
861    }
862
863    #[test]
864    fn test_working_directory_config_with_restrictions() {
865        let wd_config = WorkingDirectoryConfig {
866            preserve_cwd: false, // Fixed field names
867            default_cwd: Some(PathBuf::from("/home/user")),
868            allowed_directories: Some(vec![
869                PathBuf::from("/home"),
870                PathBuf::from("/tmp"),
871                PathBuf::from("/var/log"),
872            ]),
873        };
874
875        assert_eq!(wd_config.default_cwd, Some(PathBuf::from("/home/user")));
876        assert!(!wd_config.preserve_cwd);
877        assert!(wd_config.allowed_directories.is_some());
878        assert_eq!(wd_config.allowed_directories.as_ref().unwrap().len(), 3);
879    }
880
881    #[test]
882    fn test_pty_config_custom() {
883        let pty = PtyConfig {
884            rows: Some(50),
885            cols: Some(150),
886            terminal_type: Some("screen-256color".to_string()), // Fixed field name
887        };
888
889        assert_eq!(pty.rows, Some(50));
890        assert_eq!(pty.cols, Some(150));
891        assert_eq!(pty.terminal_type, Some("screen-256color".to_string()));
892    }
893
894    #[test]
895    fn test_environment_config_with_custom_settings() {
896        let env_config = EnvironmentConfig {
897            capture_changes: true, // Fixed field names
898            snapshot_size_limit: 2000,
899            preserve_vars: vec![
900                "PATH".to_string(),
901                "HOME".to_string(),
902                "USER".to_string(),
903                "SHELL".to_string(),
904            ],
905            remove_vars: vec!["TEMP_VAR".to_string(), "OLD_VAR".to_string()],
906        };
907
908        assert!(env_config.capture_changes);
909        assert_eq!(env_config.snapshot_size_limit, 2000);
910        assert_eq!(env_config.preserve_vars.len(), 4);
911        assert_eq!(env_config.remove_vars.len(), 2);
912        assert!(env_config.preserve_vars.contains(&"PATH".to_string()));
913        assert!(env_config.remove_vars.contains(&"TEMP_VAR".to_string()));
914    }
915}