Skip to main content

ai_agent/utils/shell/
bash_provider.rs

1//! Bash shell provider implementation.
2
3use crate::constants::env::{ai, system};
4use super::shell_provider::{ShellError, ShellExecCommand};
5use super::shell_tool_utils::ShellType;
6use std::collections::HashMap;
7
8/// Returns a shell command to disable extended glob patterns for security.
9/// Extended globs (bash extglob, zsh EXTENDED_GLOB) can be exploited via
10/// malicious filenames that expand after our security validation.
11fn get_disable_extglob_command(shell_path: &str) -> Option<String> {
12    // When AI_SHELL_PREFIX is set, the wrapper may use a different shell
13    // than shell_path, so we include both bash and zsh commands
14    if std::env::var(ai::SHELL_PREFIX).is_ok() {
15        // Redirect both stdout and stderr because zsh's command_not_found_handler
16        // writes to stdout instead of stderr
17        return Some(
18            "{ shopt -u extglob || setopt NO_EXTENDED_GLOB; } >/dev/null 2>&1 || true".to_string(),
19        );
20    }
21
22    // No shell prefix - use shell-specific command
23    if shell_path.contains("bash") {
24        Some("shopt -u extglob 2>/dev/null || true".to_string())
25    } else if shell_path.contains("zsh") {
26        Some("setopt NO_EXTENDED_GLOB 2>/dev/null || true".to_string())
27    } else {
28        // Unknown shell - do nothing
29        None
30    }
31}
32
33/// Bash shell provider implementation.
34/// Provides command building and environment overrides for bash/zsh shells.
35pub struct BashShellProvider {
36    shell_path: String,
37}
38
39impl BashShellProvider {
40    /// Create a new BashShellProvider
41    pub fn new(shell_path: &str) -> Self {
42        Self {
43            shell_path: shell_path.to_string(),
44        }
45    }
46
47    /// Get shell type
48    pub fn get_type(&self) -> ShellType {
49        ShellType::Bash
50    }
51
52    /// Get shell path
53    pub fn get_shell_path(&self) -> &str {
54        &self.shell_path
55    }
56
57    /// Whether the shell is detached
58    pub fn is_detached(&self) -> bool {
59        true
60    }
61
62    /// Build the full command string including all shell-specific setup.
63    /// Includes: source snapshot, session env, disable extglob, eval-wrap, pwd tracking.
64    pub async fn build_exec_command(
65        &self,
66        command: &str,
67        id: usize,
68        sandbox_tmp_dir: Option<&str>,
69        use_sandbox: bool,
70    ) -> Result<ShellExecCommand, ShellError> {
71        let tmpdir = std::env::temp_dir();
72
73        // shell_cwd_file_path: POSIX path used inside the bash command (pwd -P >| ...)
74        // cwd_file_path: native OS path used by Node.js for readFile/unlink
75        let (shell_cwd_file_path, cwd_file_path) = if use_sandbox {
76            let sandbox = sandbox_tmp_dir.ok_or_else(|| {
77                ShellError::BuildError(
78                    "sandbox_tmp_dir required when use_sandbox is true".to_string(),
79                )
80            })?;
81            (
82                format!("{}/cwd-{}", sandbox, id),
83                format!("{}/cwd-{}", sandbox, id),
84            )
85        } else {
86            let cwd_file = format!("ai-{}-cwd", id);
87            (
88                tmpdir.join(&cwd_file).to_string_lossy().to_string(),
89                tmpdir.join(&cwd_file).to_string_lossy().to_string(),
90            )
91        };
92
93        // For now, we don't implement the snapshot feature in Rust
94        // This would require integrating with bash initialization
95
96        let mut command_parts: Vec<String> = Vec::new();
97
98        // Source session environment variables captured from session start hooks
99        // This would be implemented with session environment integration
100        // let session_env_script = get_session_environment_script();
101        // if let Some(script) = session_env_script {
102        //     command_parts.push(script);
103        // }
104
105        // Disable extended glob patterns for security
106        if let Some(disable_extglob_cmd) = get_disable_extglob_command(&self.shell_path) {
107            command_parts.push(disable_extglob_cmd);
108        }
109
110        // Quote and wrap the command with eval
111        // Use `pwd -P` to get the physical path of the current working directory
112        command_parts.push(format!("eval {}", self.quote_command(command)));
113        command_parts.push(format!(
114            "pwd -P >| {}",
115            self.quote_path(&shell_cwd_file_path)
116        ));
117
118        let command_string = command_parts.join(" && ");
119
120        // Apply AI_SHELL_PREFIX if set
121        let command_string = if let Ok(prefix) = std::env::var(ai::SHELL_PREFIX) {
122            format!("{} -c '{}'", prefix, command_string)
123        } else {
124            command_string
125        };
126
127        Ok(ShellExecCommand {
128            command_string,
129            cwd_file_path,
130        })
131    }
132
133    /// Get shell args for spawn
134    pub fn get_spawn_args(&self, command_string: &str) -> Vec<String> {
135        // For now, we always use login shell
136        // In a full implementation, we'd check if snapshot file exists
137        vec![
138            "-c".to_string(),
139            "-l".to_string(),
140            command_string.to_string(),
141        ]
142    }
143
144    /// Get extra env vars for this shell type
145    pub async fn get_environment_overrides(&self, _command: &str) -> HashMap<String, String> {
146        let mut env = HashMap::new();
147
148        // Apply session env vars set via /env (child processes only, not the REPL)
149        // This would be implemented with session env vars integration
150        // for (key, value) in get_session_env_vars() {
151        //     env.insert(key, value);
152        // }
153
154        env
155    }
156
157    /// Quote a command for safe shell execution
158    fn quote_command(&self, command: &str) -> String {
159        // Simple single-quote escaping for now
160        // In production, would use proper shell quoting
161        format!("'{}'", command.replace('\'', "'\\''"))
162    }
163
164    /// Quote a path for safe shell execution
165    fn quote_path(&self, path: &str) -> String {
166        self.quote_command(path)
167    }
168}
169
170impl Default for BashShellProvider {
171    fn default() -> Self {
172        #[cfg(target_os = "windows")]
173        let shell = "bash".to_string();
174        #[cfg(not(target_os = "windows"))]
175        let shell = std::env::var(system::SHELL).unwrap_or_else(|_| "/bin/bash".to_string());
176
177        Self::new(&shell)
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184
185    #[tokio::test]
186    async fn test_build_exec_command() {
187        let provider = BashShellProvider::new("/bin/bash");
188        let result = provider
189            .build_exec_command("echo hello", 1, None, false)
190            .await;
191        assert!(result.is_ok());
192        let cmd = result.unwrap();
193        assert!(cmd.command_string.contains("echo hello"));
194    }
195
196    #[test]
197    fn test_get_spawn_args() {
198        let provider = BashShellProvider::new("/bin/bash");
199        let args = provider.get_spawn_args("echo hello");
200        assert_eq!(args[0], "-c");
201    }
202
203    #[test]
204    fn test_disable_extglob_command() {
205        let cmd = get_disable_extglob_command("/bin/bash");
206        assert!(cmd.is_some());
207
208        let cmd = get_disable_extglob_command("/bin/zsh");
209        assert!(cmd.is_some());
210
211        let cmd = get_disable_extglob_command("/bin/sh");
212        assert!(cmd.is_none());
213    }
214}