diff --git a/Cargo.lock b/Cargo.lock index eb360e9969b8..7796b6037962 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1265,7 +1265,7 @@ dependencies = [ "rustc-hash 1.1.0", "shlex", "syn 2.0.99", - "which", + "which 4.4.2", ] [[package]] @@ -3635,6 +3635,7 @@ dependencies = [ "urlencoding", "utoipa", "webbrowser 0.8.15", + "which 6.0.3", "xcap", ] @@ -9401,6 +9402,18 @@ dependencies = [ "rustix 0.38.44", ] +[[package]] +name = "which" +version = "6.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ee928febd44d98f2f459a4a79bd4d928591333a494a10a868418ac1b39cf1f" +dependencies = [ + "either", + "home", + "rustix 0.38.44", + "winsafe", +] + [[package]] name = "wild" version = "2.2.1" @@ -9821,6 +9834,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wiremock" version = "0.6.3" diff --git a/crates/goose-mcp/Cargo.toml b/crates/goose-mcp/Cargo.toml index 8bf405e7dfb3..83ab4ef36516 100644 --- a/crates/goose-mcp/Cargo.toml +++ b/crates/goose-mcp/Cargo.toml @@ -58,6 +58,7 @@ oauth2 = { version = "5.0.0", features = ["reqwest"] } utoipa = { version = "4.1", optional = true } hyper = "1" serde_with = "3" +which = "6.0" [dev-dependencies] diff --git a/crates/goose-mcp/src/computercontroller/mod.rs b/crates/goose-mcp/src/computercontroller/mod.rs index 7bd647e220f1..bdc9651eac90 100644 --- a/crates/goose-mcp/src/computercontroller/mod.rs +++ b/crates/goose-mcp/src/computercontroller/mod.rs @@ -736,10 +736,7 @@ impl ComputerControllerRouter { ToolError::ExecutionError(format!("Failed to write script: {}", e)) })?; - format!( - "powershell -NoProfile -NonInteractive -File {}", - script_path.display() - ) + script_path.display().to_string() } _ => { return Err( ToolError::InvalidParameters( @@ -749,12 +746,27 @@ impl ComputerControllerRouter { }; // Run the script - let output = Command::new(shell) - .arg(shell_arg) - .arg(&command) - .output() - .await - .map_err(|e| ToolError::ExecutionError(format!("Failed to run script: {}", e)))?; + let output = match language { + "powershell" => { + // For PowerShell, we need to use -File instead of -Command + Command::new("powershell") + .arg("-NoProfile") + .arg("-NonInteractive") + .arg("-File") + .arg(&command) + .output() + .await + .map_err(|e| { + ToolError::ExecutionError(format!("Failed to run script: {}", e)) + })? + } + _ => Command::new(shell) + .arg(shell_arg) + .arg(&command) + .output() + .await + .map_err(|e| ToolError::ExecutionError(format!("Failed to run script: {}", e)))?, + }; let output_str = String::from_utf8_lossy(&output.stdout).into_owned(); let error_str = String::from_utf8_lossy(&output.stderr).into_owned(); diff --git a/crates/goose-mcp/src/developer/mod.rs b/crates/goose-mcp/src/developer/mod.rs index 98d85b08c4fb..9eea65ce289b 100644 --- a/crates/goose-mcp/src/developer/mod.rs +++ b/crates/goose-mcp/src/developer/mod.rs @@ -39,10 +39,7 @@ use mcp_server::Router; use mcp_core::role::Role; use self::editor_models::{create_editor_model, EditorModel}; -use self::shell::{ - expand_path, format_command_for_platform, get_shell_config, is_absolute_path, - normalize_line_endings, -}; +use self::shell::{expand_path, get_shell_config, is_absolute_path, normalize_line_endings}; use indoc::indoc; use std::process::Stdio; use std::sync::{Arc, Mutex}; @@ -540,7 +537,6 @@ impl DeveloperRouter { // Get platform-specific shell configuration let shell_config = get_shell_config(); - let cmd_str = format_command_for_platform(command); // Execute the command using platform-specific shell let mut child = Command::new(&shell_config.executable) @@ -548,8 +544,8 @@ impl DeveloperRouter { .stderr(Stdio::piped()) .stdin(Stdio::null()) .kill_on_drop(true) - .arg(&shell_config.arg) - .arg(cmd_str) + .args(&shell_config.args) + .arg(command) .spawn() .map_err(|e| ToolError::ExecutionError(e.to_string()))?; diff --git a/crates/goose-mcp/src/developer/shell.rs b/crates/goose-mcp/src/developer/shell.rs index cb60f9babfdf..b45ec577a3d2 100644 --- a/crates/goose-mcp/src/developer/shell.rs +++ b/crates/goose-mcp/src/developer/shell.rs @@ -3,38 +3,72 @@ use std::env; #[derive(Debug, Clone)] pub struct ShellConfig { pub executable: String, - pub arg: String, + pub args: Vec, } impl Default for ShellConfig { fn default() -> Self { if cfg!(windows) { - // Execute PowerShell commands directly - Self { - executable: "powershell.exe".to_string(), - arg: "-NoProfile -NonInteractive -Command".to_string(), + // Detect the default shell on Windows + #[cfg(windows)] + { + Self::detect_windows_shell() + } + #[cfg(not(windows))] + { + // This branch should never be taken on non-Windows + // but we need it for compilation + Self { + executable: "cmd".to_string(), + args: vec!["/c".to_string()], + } } } else { + // Use bash on Unix/macOS (keep existing behavior) Self { executable: "bash".to_string(), - arg: "-c".to_string(), + args: vec!["-c".to_string()], } } } } -pub fn get_shell_config() -> ShellConfig { - ShellConfig::default() +impl ShellConfig { + #[cfg(windows)] + fn detect_windows_shell() -> Self { + // Check for PowerShell first (more modern) + if let Ok(ps_path) = which::which("pwsh") { + // PowerShell 7+ (cross-platform PowerShell) + Self { + executable: ps_path.to_string_lossy().to_string(), + args: vec![ + "-NoProfile".to_string(), + "-NonInteractive".to_string(), + "-Command".to_string(), + ], + } + } else if let Ok(ps_path) = which::which("powershell") { + // Windows PowerShell 5.1 + Self { + executable: ps_path.to_string_lossy().to_string(), + args: vec![ + "-NoProfile".to_string(), + "-NonInteractive".to_string(), + "-Command".to_string(), + ], + } + } else { + // Fall back to cmd.exe + Self { + executable: "cmd".to_string(), + args: vec!["/c".to_string()], + } + } + } } -pub fn format_command_for_platform(command: &str) -> String { - if cfg!(windows) { - // For PowerShell, wrap the command in braces to handle special characters - format!("{{ {} }}", command) - } else { - // For other shells, no braces needed - command.to_string() - } +pub fn get_shell_config() -> ShellConfig { + ShellConfig::default() } pub fn expand_path(path_str: &str) -> String {