Read paths from an interactive & login shell (#5774)

This commit is contained in:
Jack Amadeo
2025-11-18 19:24:49 -05:00
committed by GitHub
parent 662d79b97e
commit 5ba636f45f
10 changed files with 249 additions and 71 deletions
Generated
-1
View File
@@ -2787,7 +2787,6 @@ dependencies = [
"docx-rs", "docx-rs",
"etcetera", "etcetera",
"glob", "glob",
"goose",
"http-body-util", "http-body-util",
"hyper 1.6.0", "hyper 1.6.0",
"ignore", "ignore",
+19 -9
View File
@@ -2,6 +2,10 @@ use anyhow::Result;
use clap::{Args, Parser, Subcommand}; use clap::{Args, Parser, Subcommand};
use goose::config::{Config, ExtensionConfig}; use goose::config::{Config, ExtensionConfig};
use goose_mcp::mcp_server_runner::{serve, McpCommand};
use goose_mcp::{
AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer,
};
use crate::commands::acp::run_acp_agent; use crate::commands::acp::run_acp_agent;
use crate::commands::bench::agent_generator; use crate::commands::bench::agent_generator;
@@ -420,7 +424,10 @@ enum Command {
/// Manage system prompts and behaviors /// Manage system prompts and behaviors
#[command(about = "Run one of the mcp servers bundled with goose")] #[command(about = "Run one of the mcp servers bundled with goose")]
Mcp { name: String }, Mcp {
#[arg(value_parser = clap::value_parser!(McpCommand))]
server: McpCommand,
},
/// Run goose as an ACP (Agent Client Protocol) agent /// Run goose as an ACP (Agent Client Protocol) agent
#[command(about = "Run goose as an ACP agent server on stdio")] #[command(about = "Run goose as an ACP agent server on stdio")]
@@ -877,15 +884,18 @@ pub async fn cli() -> anyhow::Result<()> {
); );
match cli.command { match cli.command {
Some(Command::Configure {}) => { Some(Command::Configure {}) => handle_configure().await?,
handle_configure().await?; Some(Command::Info { verbose }) => handle_info(verbose)?,
} Some(Command::Mcp { server }) => {
Some(Command::Info { verbose }) => { let name = server.name();
handle_info(verbose)?;
}
Some(Command::Mcp { name }) => {
crate::logging::setup_logging(Some(&format!("mcp-{name}")), None)?; crate::logging::setup_logging(Some(&format!("mcp-{name}")), None)?;
goose_mcp::mcp_server_runner::run_mcp_server(&name).await?; match server {
McpCommand::AutoVisualiser => serve(AutoVisualiserRouter::new()).await?,
McpCommand::ComputerController => serve(ComputerControllerServer::new()).await?,
McpCommand::Memory => serve(MemoryServer::new()).await?,
McpCommand::Tutorial => serve(TutorialServer::new()).await?,
McpCommand::Developer => serve(DeveloperServer::new()).await?,
}
} }
Some(Command::Acp {}) => { Some(Command::Acp {}) => {
run_acp_agent().await?; run_acp_agent().await?;
+1 -2
View File
@@ -11,7 +11,6 @@ description.workspace = true
workspace = true workspace = true
[dependencies] [dependencies]
goose = { path = "../goose" }
rmcp = { version = "0.8.1", features = ["server", "client", "transport-io", "macros"] } rmcp = { version = "0.8.1", features = ["server", "client", "transport-io", "macros"] }
anyhow = "1.0.94" anyhow = "1.0.94"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["full"] }
@@ -77,13 +76,13 @@ libc = "0.2"
# ~1000 downloads). Pinned to exact version to prevent supply chain attacks. # ~1000 downloads). Pinned to exact version to prevent supply chain attacks.
mpatch = "=0.2.0" mpatch = "=0.2.0"
tokio-util = "0.7.16" tokio-util = "0.7.16"
clap = { version = "4", features = ["derive"] }
[dev-dependencies] [dev-dependencies]
serial_test = "3.0.0" serial_test = "3.0.0"
sysinfo = "0.32.1" sysinfo = "0.32.1"
temp-env = "0.3.6" temp-env = "0.3.6"
clap = { version = "4", features = ["derive"] }
colored = "2" colored = "2"
[features] [features]
@@ -2,11 +2,19 @@ use crate::developer::analyze::types::{
AnalysisMode, AnalysisResult, CallChain, EntryType, FocusedAnalysisData, AnalysisMode, AnalysisResult, CallChain, EntryType, FocusedAnalysisData,
}; };
use crate::developer::lang; use crate::developer::lang;
use goose::utils::safe_truncate;
use rmcp::model::{Content, Role}; use rmcp::model::{Content, Role};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
fn safe_truncate(s: &str, max_chars: usize) -> String {
if s.chars().count() <= max_chars {
s.to_string()
} else {
let truncated: String = s.chars().take(max_chars.saturating_sub(3)).collect();
format!("{}...", truncated)
}
}
pub struct Formatter; pub struct Formatter;
impl Formatter { impl Formatter {
+1
View File
@@ -1,6 +1,7 @@
pub mod analyze; pub mod analyze;
mod editor_models; mod editor_models;
mod lang; mod lang;
pub mod paths;
mod shell; mod shell;
mod text_editor; mod text_editor;
+112
View File
@@ -0,0 +1,112 @@
use anyhow::Result;
use std::env;
use std::path::PathBuf;
use tokio::process::Command;
use tokio::sync::OnceCell;
static SHELL_PATH_DIRS: OnceCell<Result<Vec<PathBuf>, anyhow::Error>> = OnceCell::const_new();
pub async fn get_shell_path_dirs() -> Result<&'static Vec<PathBuf>> {
let result = SHELL_PATH_DIRS
.get_or_init(|| async {
get_shell_path_async()
.await
.map(|path| env::split_paths(&path).collect())
})
.await;
match result {
Ok(dirs) => Ok(dirs),
Err(e) => Err(anyhow::anyhow!(
"Failed to get shell PATH directories: {}",
e
)),
}
}
async fn get_shell_path_async() -> Result<String> {
let shell = env::var("SHELL").unwrap_or_else(|_| {
if cfg!(windows) {
"cmd".to_string()
} else {
"/bin/bash".to_string()
}
});
if cfg!(windows) {
get_windows_path_async(&shell).await
} else {
get_unix_path_async(&shell).await
}
.or_else(|e| {
tracing::warn!(
"Failed to get PATH from shell ({}), falling back to current PATH",
e
);
env::var("PATH").map_err(|_| anyhow::anyhow!("No PATH variable available"))
})
}
async fn get_unix_path_async(shell: &str) -> Result<String> {
let output = Command::new(shell)
.args(["-l", "-i", "-c", "echo $PATH"])
.output()
.await
.map_err(|e| anyhow::anyhow!("Failed to execute shell command: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow::anyhow!("Shell command failed: {}", stderr));
}
let path = String::from_utf8(output.stdout)
.map_err(|e| anyhow::anyhow!("Invalid UTF-8 in shell output: {}", e))?
.trim()
.to_string();
if path.is_empty() {
return Err(anyhow::anyhow!("Shell returned empty PATH"));
}
Ok(path)
}
async fn get_windows_path_async(shell: &str) -> Result<String> {
let shell_name = std::path::Path::new(shell)
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("cmd");
let output = match shell_name {
"pwsh" | "powershell" => {
Command::new(shell)
.args(["-NoLogo", "-Command", "$env:PATH"])
.output()
.await
}
_ => {
Command::new(shell)
.args(["/c", "echo %PATH%"])
.output()
.await
}
};
let output = output.map_err(|e| anyhow::anyhow!("Failed to execute shell command: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(anyhow::anyhow!("Shell command failed: {}", stderr));
}
let path = String::from_utf8(output.stdout)
.map_err(|e| anyhow::anyhow!("Invalid UTF-8 in shell output: {}", e))?
.trim()
.to_string();
if path.is_empty() {
return Err(anyhow::anyhow!("Shell returned empty PATH"));
}
Ok(path)
}
@@ -1,3 +1,4 @@
use anyhow::anyhow;
use base64::Engine; use base64::Engine;
use ignore::gitignore::{Gitignore, GitignoreBuilder}; use ignore::gitignore::{Gitignore, GitignoreBuilder};
use include_dir::{include_dir, Dir}; use include_dir::{include_dir, Dir};
@@ -17,6 +18,8 @@ use rmcp::{
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{ use std::{
collections::HashMap, collections::HashMap,
env::join_paths,
ffi::OsString,
future::Future, future::Future,
io::Cursor, io::Cursor,
path::{Path, PathBuf}, path::{Path, PathBuf},
@@ -31,11 +34,11 @@ use tokio::{
use tokio_stream::{wrappers::SplitStream, StreamExt as _}; use tokio_stream::{wrappers::SplitStream, StreamExt as _};
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use crate::developer::{paths::get_shell_path_dirs, shell::ShellConfig};
use super::analyze::{types::AnalyzeParams, CodeAnalyzer}; use super::analyze::{types::AnalyzeParams, CodeAnalyzer};
use super::editor_models::{create_editor_model, EditorModel}; use super::editor_models::{create_editor_model, EditorModel};
use super::shell::{ use super::shell::{configure_shell_command, expand_path, is_absolute_path, kill_process_group};
configure_shell_command, expand_path, get_shell_config, is_absolute_path, kill_process_group,
};
use super::text_editor::{ use super::text_editor::{
text_editor_insert, text_editor_replace, text_editor_undo, text_editor_view, text_editor_write, text_editor_insert, text_editor_replace, text_editor_undo, text_editor_view, text_editor_write,
}; };
@@ -179,6 +182,8 @@ pub struct DeveloperServer {
pub running_processes: Arc<RwLock<HashMap<String, CancellationToken>>>, pub running_processes: Arc<RwLock<HashMap<String, CancellationToken>>>,
#[cfg(not(test))] #[cfg(not(test))]
running_processes: Arc<RwLock<HashMap<String, CancellationToken>>>, running_processes: Arc<RwLock<HashMap<String, CancellationToken>>>,
bash_env_file: Option<PathBuf>,
extend_path_with_shell: bool,
} }
#[tool_handler(router = self.tool_router)] #[tool_handler(router = self.tool_router)]
@@ -549,9 +554,21 @@ impl DeveloperServer {
prompts: load_prompt_files(), prompts: load_prompt_files(),
code_analyzer: CodeAnalyzer::new(), code_analyzer: CodeAnalyzer::new(),
running_processes: Arc::new(RwLock::new(HashMap::new())), running_processes: Arc::new(RwLock::new(HashMap::new())),
extend_path_with_shell: false,
bash_env_file: None,
} }
} }
pub fn extend_path_with_shell(mut self, value: bool) -> Self {
self.extend_path_with_shell = value;
self
}
pub fn bash_env_file(mut self, value: Option<PathBuf>) -> Self {
self.bash_env_file = value;
self
}
/// List all available windows that can be used with screen_capture. /// List all available windows that can be used with screen_capture.
/// Returns a list of window titles that can be used with the window_title parameter /// Returns a list of window titles that can be used with the window_title parameter
/// of the screen_capture tool. /// of the screen_capture tool.
@@ -942,10 +959,34 @@ impl DeveloperServer {
peer: &rmcp::service::Peer<RoleServer>, peer: &rmcp::service::Peer<RoleServer>,
cancellation_token: CancellationToken, cancellation_token: CancellationToken,
) -> Result<String, ErrorData> { ) -> Result<String, ErrorData> {
// Get platform-specific shell configuration let mut shell_config = ShellConfig::default();
let shell_config = get_shell_config(); let shell_name = std::path::Path::new(&shell_config.executable)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("bash");
let mut child = configure_shell_command(&shell_config, command) if let Some(ref env_file) = self.bash_env_file {
if shell_name == "bash" {
shell_config.envs.push((
OsString::from("BASH_ENV"),
env_file.clone().into_os_string(),
))
}
}
let mut command = configure_shell_command(&shell_config, command);
if self.extend_path_with_shell {
if let Err(e) = get_shell_path_dirs()
.await
.and_then(|dirs| join_paths(dirs).map_err(|e| anyhow!(e)))
.map(|path| command.env("PATH", path))
{
tracing::error!("Failed to extend PATH with shell directories: {}", e)
}
}
let mut child = command
.spawn() .spawn()
.map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?; .map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))?;
+1 -20
View File
@@ -1,6 +1,5 @@
use std::{env, ffi::OsString, process::Stdio}; use std::{env, ffi::OsString, process::Stdio};
use goose::config::paths::Paths;
#[cfg(unix)] #[cfg(unix)]
#[allow(unused_imports)] // False positive: trait is used for process_group method #[allow(unused_imports)] // False positive: trait is used for process_group method
use std::os::unix::process::CommandExt; use std::os::unix::process::CommandExt;
@@ -22,24 +21,10 @@ impl Default for ShellConfig {
#[cfg(not(windows))] #[cfg(not(windows))]
{ {
let shell = env::var("SHELL").unwrap_or_else(|_| "bash".to_string()); let shell = env::var("SHELL").unwrap_or_else(|_| "bash".to_string());
// Get just the shell name from the path (e.g., /bin/zsh -> zsh)
let shell_name = std::path::Path::new(&shell)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("bash");
// Configure environment based on shell type
let envs = if shell_name == "bash" {
let bash_env = Paths::config_dir().join(".bash_env").into_os_string();
vec![(OsString::from("BASH_ENV"), bash_env)]
} else {
vec![]
};
Self { Self {
executable: shell, executable: shell,
args: vec!["-c".to_string()], // -c is standard across bash/zsh/fish args: vec!["-c".to_string()], // -c is standard across bash/zsh/fish
envs, envs: vec![],
} }
} }
} }
@@ -82,10 +67,6 @@ impl ShellConfig {
} }
} }
pub fn get_shell_config() -> ShellConfig {
ShellConfig::default()
}
pub fn expand_path(path_str: &str) -> String { pub fn expand_path(path_str: &str) -> String {
if cfg!(windows) { if cfg!(windows) {
// Expand Windows environment variables (%VAR%) // Expand Windows environment variables (%VAR%)
+34 -26
View File
@@ -1,37 +1,45 @@
use crate::{ use std::str::FromStr;
AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer,
}; use anyhow::Result;
use anyhow::{anyhow, Result};
use rmcp::{transport::stdio, ServiceExt}; use rmcp::{transport::stdio, ServiceExt};
/// Run an MCP server by name #[derive(Clone, Debug)]
/// pub enum McpCommand {
/// This function handles the common logic for starting MCP servers. AutoVisualiser,
/// The caller is responsible for setting up logging before calling this function. ComputerController,
pub async fn run_mcp_server(name: &str) -> Result<()> { Developer,
if name == "googledrive" || name == "google_drive" { Memory,
return Err(anyhow!( Tutorial,
"the built-in Google Drive extension has been removed" }
));
}
tracing::info!("Starting MCP server"); impl FromStr for McpCommand {
type Err = String;
match name.to_lowercase().replace(' ', "").as_str() { fn from_str(s: &str) -> Result<Self, Self::Err> {
"autovisualiser" => serve_and_wait(AutoVisualiserRouter::new()).await, match s.to_lowercase().replace(' ', "").as_str() {
"computercontroller" => serve_and_wait(ComputerControllerServer::new()).await, "autovisualiser" => Ok(McpCommand::AutoVisualiser),
"developer" => serve_and_wait(DeveloperServer::new()).await, "computercontroller" => Ok(McpCommand::ComputerController),
"memory" => serve_and_wait(MemoryServer::new()).await, "developer" => Ok(McpCommand::Developer),
"tutorial" => serve_and_wait(TutorialServer::new()).await, "memory" => Ok(McpCommand::Memory),
_ => { "tutorial" => Ok(McpCommand::Tutorial),
tracing::warn!("Unknown MCP server name: {}", name); _ => Err(format!("Invalid command: {}", s)),
Err(anyhow!("Unknown MCP server name: {}", name))
} }
} }
} }
/// Helper function to run any MCP server with common error handling impl McpCommand {
async fn serve_and_wait<S>(server: S) -> Result<()> pub fn name(&self) -> &str {
match self {
McpCommand::AutoVisualiser => "autovisualiser",
McpCommand::ComputerController => "computercontroller",
McpCommand::Developer => "developer",
McpCommand::Memory => "memory",
McpCommand::Tutorial => "tutorial",
}
}
}
pub async fn serve<S>(server: S) -> Result<()>
where where
S: rmcp::ServerHandler, S: rmcp::ServerHandler,
{ {
+25 -6
View File
@@ -7,6 +7,11 @@ mod routes;
mod state; mod state;
use clap::{Parser, Subcommand}; use clap::{Parser, Subcommand};
use goose::config::paths::Paths;
use goose_mcp::{
mcp_server_runner::{serve, McpCommand},
AutoVisualiserRouter, ComputerControllerServer, DeveloperServer, MemoryServer, TutorialServer,
};
#[derive(Parser)] #[derive(Parser)]
#[command(author, version, about, long_about = None)] #[command(author, version, about, long_about = None)]
@@ -22,8 +27,8 @@ enum Commands {
Agent, Agent,
/// Run the MCP server /// Run the MCP server
Mcp { Mcp {
/// Name of the MCP server type #[arg(value_parser = clap::value_parser!(McpCommand))]
name: String, server: McpCommand,
}, },
} }
@@ -31,13 +36,27 @@ enum Commands {
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
match &cli.command { match cli.command {
Commands::Agent => { Commands::Agent => {
commands::agent::run().await?; commands::agent::run().await?;
} }
Commands::Mcp { name } => { Commands::Mcp { server } => {
logging::setup_logging(Some(&format!("mcp-{name}")))?; logging::setup_logging(Some(&format!("mcp-{}", server.name())))?;
goose_mcp::mcp_server_runner::run_mcp_server(name).await?; match server {
McpCommand::AutoVisualiser => serve(AutoVisualiserRouter::new()).await?,
McpCommand::ComputerController => serve(ComputerControllerServer::new()).await?,
McpCommand::Memory => serve(MemoryServer::new()).await?,
McpCommand::Tutorial => serve(TutorialServer::new()).await?,
McpCommand::Developer => {
let bash_env = Paths::config_dir().join(".bash_env");
serve(
DeveloperServer::new()
.extend_path_with_shell(true)
.bash_env_file(Some(bash_env)),
)
.await?
}
}
} }
} }