feat: goose windows (#880)

Co-authored-by: Ryan Versaw <ryan@versaw.com>
This commit is contained in:
Max Novich
2025-02-10 15:05:13 -08:00
committed by GitHub
parent 98aecbef23
commit cfd3ee8fd9
43 changed files with 1327 additions and 456 deletions
+4
View File
@@ -46,6 +46,10 @@ tracing = "0.1"
chrono = "0.4"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt", "json", "time"] }
tracing-appender = "0.2"
winapi = { version = "0.3", features = ["wincred"], optional = true }
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["wincred"] }
[dev-dependencies]
tempfile = "3"
@@ -62,12 +62,29 @@ pub async fn handle_configure() -> Result<(), Box<dyn Error>> {
);
}
Some(ConfigError::KeyringError(msg)) => {
#[cfg(target_os = "macos")]
println!(
"\n {} Failed to access secure storage (keyring): {} \n Please check your system keychain and run '{}' again. \n If your system is unable to use the keyring, please try setting secret key(s) via environment variables.",
style("Error").red().italic(),
msg,
style("goose configure").cyan()
);
#[cfg(target_os = "windows")]
println!(
"\n {} Failed to access Windows Credential Manager: {} \n Please check Windows Credential Manager and run '{}' again. \n If your system is unable to use the Credential Manager, please try setting secret key(s) via environment variables.",
style("Error").red().italic(),
msg,
style("goose configure").cyan()
);
#[cfg(not(any(target_os = "macos", target_os = "windows")))]
println!(
"\n {} Failed to access secure storage: {} \n Please check your system's secure storage and run '{}' again. \n If your system is unable to use secure storage, please try setting secret key(s) via environment variables.",
style("Error").red().italic(),
msg,
style("goose configure").cyan()
);
}
Some(ConfigError::DeserializeError(msg)) => {
println!(
+11 -2
View File
@@ -12,7 +12,12 @@ use goose::tracing::langfuse_layer;
/// Returns the directory where log files should be stored.
/// Creates the directory structure if it doesn't exist.
fn get_log_directory() -> Result<PathBuf> {
let home = std::env::var("HOME").context("HOME environment variable not set")?;
let home = if cfg!(windows) {
std::env::var("USERPROFILE").context("USERPROFILE environment variable not set")?
} else {
std::env::var("HOME").context("HOME environment variable not set")?
};
let base_log_dir = PathBuf::from(home)
.join(".config")
.join("goose")
@@ -114,7 +119,11 @@ mod tests {
fn setup_temp_home() -> TempDir {
let temp_dir = TempDir::new().unwrap();
env::set_var("HOME", temp_dir.path());
if cfg!(windows) {
env::set_var("USERPROFILE", temp_dir.path());
} else {
env::set_var("HOME", temp_dir.path());
}
temp_dir
}
+119 -99
View File
@@ -3,8 +3,7 @@ use indoc::{formatdoc, indoc};
use reqwest::{Client, Url};
use serde_json::{json, Value};
use std::{
collections::HashMap, fs, future::Future, os::unix::fs::PermissionsExt, path::PathBuf,
pin::Pin, sync::Arc, sync::Mutex,
collections::HashMap, fs, future::Future, path::PathBuf, pin::Pin, sync::Arc, sync::Mutex,
};
use tokio::process::Command;
@@ -18,6 +17,9 @@ use mcp_core::{
use mcp_server::router::CapabilitiesBuilder;
use mcp_server::Router;
mod platform;
use platform::{create_system_automation, SystemAutomation};
/// An extension designed for non-developers to help them with common tasks like
/// web scraping, data processing, and automation.
#[derive(Clone)]
@@ -27,6 +29,7 @@ pub struct ComputerControllerRouter {
active_resources: Arc<Mutex<HashMap<String, Resource>>>,
http_client: Client,
instructions: String,
system_automation: Arc<Box<dyn SystemAutomation + Send + Sync>>,
}
impl Default for ComputerControllerRouter {
@@ -86,9 +89,19 @@ impl ComputerControllerRouter {
}),
);
let computer_control_tool = Tool::new(
"computer_control",
indoc! {r#"
let computer_control_desc = match std::env::consts::OS {
"windows" => indoc! {r#"
Control the computer using Windows system automation.
Features available:
- PowerShell automation for system control
- UI automation through PowerShell
- File and system management
- Windows-specific features and settings
Can be combined with screenshot tool for visual task assistance.
"#},
_ => indoc! {r#"
Control the computer using AppleScript (macOS only). Automate applications and system features.
Key capabilities:
@@ -104,14 +117,19 @@ impl ComputerControllerRouter {
- Data: Interact with spreadsheets and documents
Can be combined with screenshot tool for visual task assistance.
"#},
"#},
};
let computer_control_tool = Tool::new(
"computer_control",
computer_control_desc.to_string(),
json!({
"type": "object",
"required": ["script"],
"properties": {
"script": {
"type": "string",
"description": "The AppleScript content to execute"
"description": "The automation script content (PowerShell for Windows, AppleScript for macOS)"
},
"save_output": {
"type": "boolean",
@@ -122,9 +140,18 @@ impl ComputerControllerRouter {
}),
);
let quick_script_tool = Tool::new(
"automation_script",
indoc! {r#"
let quick_script_desc = match std::env::consts::OS {
"windows" => indoc! {r#"
Create and run small PowerShell or Batch scripts for automation tasks.
PowerShell is recommended for most tasks.
The script is saved to a temporary file and executed.
Some examples:
- Sort unique lines: Get-Content file.txt | Sort-Object -Unique
- Extract CSV column: Import-Csv file.csv | Select-Object -ExpandProperty Column2
- Find text: Select-String -Pattern "pattern" -Path file.txt
"#},
_ => indoc! {r#"
Create and run small scripts for automation tasks.
Supports Shell and Ruby (on macOS).
@@ -135,14 +162,19 @@ impl ComputerControllerRouter {
- create a sorted list of unique lines: sort file.txt | uniq
- extract 2nd column in csv: awk -F "," '{ print $2}'
- pattern matching: grep pattern file.txt
"#},
"#},
};
let quick_script_tool = Tool::new(
"automation_script",
quick_script_desc.to_string(),
json!({
"type": "object",
"required": ["language", "script"],
"properties": {
"language": {
"type": "string",
"enum": ["shell", "ruby"],
"enum": ["shell", "ruby", "powershell", "batch"],
"description": "The scripting language to use"
},
"script": {
@@ -186,9 +218,10 @@ impl ComputerControllerRouter {
// Create cache directory in user's home directory
let cache_dir = dirs::cache_dir()
.unwrap_or_else(|| PathBuf::from("/tmp"))
.unwrap_or_else(|| create_system_automation().get_temp_path())
.join("goose")
.join("computer_controller");
fs::create_dir_all(&cache_dir).unwrap_or_else(|_| {
println!(
"Warning: Failed to create cache directory at {:?}",
@@ -196,8 +229,41 @@ impl ComputerControllerRouter {
)
});
let macos_browser_instructions = if std::env::consts::OS == "macos" {
indoc! {r#"
let system_automation: Arc<Box<dyn SystemAutomation + Send + Sync>> =
Arc::new(create_system_automation());
let os_specific_instructions = match std::env::consts::OS {
"windows" => indoc! {r#"
Here are some extra tools:
automation_script
- Create and run PowerShell or Batch scripts
- PowerShell is recommended for most tasks
- Scripts can save their output to files
- Windows-specific features:
- PowerShell for system automation and UI control
- Windows Management Instrumentation (WMI)
- Registry access and system settings
- Use the screenshot tool if needed to help with tasks
computer_control
- System automation using PowerShell
- Consider the screenshot tool to work out what is on screen and what to do to help with the control task.
"#},
_ => indoc! {r#"
Here are some extra tools:
automation_script
- Create and run Shell and Ruby scripts
- Shell (bash) is recommended for most tasks
- Scripts can save their output to files
- macOS-specific features:
- AppleScript for system and UI control
- Integration with macOS apps and services
- Use the screenshot tool if needed to help with tasks
computer_control
- System automation using AppleScript
- Consider the screenshot tool to work out what is on screen and what to do to help with the control task.
When you need to interact with websites or web applications, consider using the computer_control tool with AppleScript, which can automate Safari or other browsers to:
- Open specific URLs
- Fill in forms
@@ -205,47 +271,25 @@ impl ComputerControllerRouter {
- Extract content
- Handle web-based workflows
This is often more reliable than web scraping for modern web applications.
"#}
} else {
""
"#},
};
let instructions = formatdoc! {r#"
You are a helpful assistant to a power user who is not a professional developer, but you may use devleopment tools to help assist them.
You are a helpful assistant to a power user who is not a professional developer, but you may use development tools to help assist them.
The user may not know how to break down tasks, so you will need to ensure that you do, and run things in batches as needed.
The ComputerControllerExtension helps you with common tasks like web scraping,
data processing, and automation and computer control without requiring programming expertise,
supplementing the Developer Extension.
data processing, and automation without requiring programming expertise.
You can use scripting as needed to work with text files of data, such as csvs, json, or text files etc.
Using the developer extension is allowed for more sophisticated tasks or instructed to (js or py can be helpful for more complex tasks if tools are available).
{macos_instructions}
Accessing web sites, even apis, may be common (you can use bash scripting to do this) without troubling them too much (they won't know what limits are).
Try to do your best to find ways to complete a task without too many quesitons or offering options unless it is really unclear, find a way if you can.
Accessing web sites, even apis, may be common (you can use scripting to do this) without troubling them too much (they won't know what limits are).
Try to do your best to find ways to complete a task without too many questions or offering options unless it is really unclear, find a way if you can.
You can also guide them steps if they can help out as you go along.
There is already a screenshot tool available you can use if needed to see what is on screen.
Here are some extra tools:
automation_script
- Create and run simple automation scripts
- Supports Shell (such as bash), AppleScript (on macos), Ruby (on macos)
- Scripts can save their output to files
- on macos, can use applescript to interact with the desktop, eg calendars, notes and more, anything apple script can do for apps that support it:
AppleScript is a powerful scripting language designed for automating tasks on macOS such as: Integration with Other Scripts
Execute shell scripts, Ruby scripts, or other automation scripts.
Combine workflows across scripting languages.
Complex Workflows
Automate multi-step tasks involving multiple apps or system features.
Create scheduled tasks using Calendar or other scheduling apps.
- use the screenshot tool if needed to help with tasks
computer_control
- Control the computer using AppleScript (macOS only)
- Consider the screenshot tool to work out what is on screen and what to do to help with the control task.
{os_instructions}
web_search
- Search the web using DuckDuckGo's API for general topics or keywords
@@ -262,7 +306,7 @@ impl ComputerControllerRouter {
- Cache directory: {cache_dir}
- File organization and cleanup
"#,
macos_instructions = macos_browser_instructions,
os_instructions = os_specific_instructions,
cache_dir = cache_dir.display()
};
@@ -278,6 +322,7 @@ impl ComputerControllerRouter {
active_resources: Arc::new(Mutex::new(HashMap::new())),
http_client: Client::builder().user_agent("Goose/1.0").build().unwrap(),
instructions: instructions.clone(),
system_automation,
}
}
@@ -318,7 +363,7 @@ impl ComputerControllerRouter {
Ok(())
}
// Implement web_scrape tool functionality
// Implement web_search tool functionality
async fn web_search(&self, params: Value) -> Result<Vec<Content>, ToolError> {
let query = params
.get("query")
@@ -452,22 +497,18 @@ impl ComputerControllerRouter {
ToolError::ExecutionError(format!("Failed to create temporary directory: {}", e))
})?;
let (shell, shell_arg) = self.system_automation.get_shell_command();
let command = match language {
"shell" => {
let script_path = script_dir.path().join("script.sh");
"shell" | "batch" => {
let script_path = script_dir.path().join(format!(
"script.{}",
if cfg!(windows) { "bat" } else { "sh" }
));
fs::write(&script_path, script).map_err(|e| {
ToolError::ExecutionError(format!("Failed to write script: {}", e))
})?;
fs::set_permissions(&script_path, fs::Permissions::from_mode(0o755)).map_err(
|e| {
ToolError::ExecutionError(format!(
"Failed to set script permissions: {}",
e
))
},
)?;
script_path.display().to_string()
}
"ruby" => {
@@ -478,12 +519,23 @@ impl ComputerControllerRouter {
format!("ruby {}", script_path.display())
}
"powershell" => {
let script_path = script_dir.path().join("script.ps1");
fs::write(&script_path, script).map_err(|e| {
ToolError::ExecutionError(format!("Failed to write script: {}", e))
})?;
format!(
"powershell -NoProfile -NonInteractive -File {}",
script_path.display()
)
}
_ => unreachable!(), // Prevented by enum in tool definition
};
// Run the script
let output = Command::new("bash")
.arg("-c")
let output = Command::new(shell)
.arg(shell_arg)
.arg(&command)
.output()
.await
@@ -515,14 +567,8 @@ impl ComputerControllerRouter {
Ok(vec![Content::text(result)])
}
// Implement computer control (AppleScript) functionality
// Implement computer control functionality
async fn computer_control(&self, params: Value) -> Result<Vec<Content>, ToolError> {
if std::env::consts::OS != "macos" {
return Err(ToolError::ExecutionError(
"Computer control (AppleScript) is only supported on macOS".into(),
));
}
let script = params
.get("script")
.and_then(|v| v.as_str())
@@ -533,44 +579,18 @@ impl ComputerControllerRouter {
.and_then(|v| v.as_bool())
.unwrap_or(false);
// Create a temporary directory for the script
let script_dir = tempfile::tempdir().map_err(|e| {
ToolError::ExecutionError(format!("Failed to create temporary directory: {}", e))
})?;
// Use platform-specific automation
let output = self
.system_automation
.execute_system_script(script)
.map_err(|e| ToolError::ExecutionError(format!("Failed to execute script: {}", e)))?;
let script_path = script_dir.path().join("script.scpt");
fs::write(&script_path, script)
.map_err(|e| ToolError::ExecutionError(format!("Failed to write script: {}", e)))?;
let command = format!("osascript {}", script_path.display());
// Run the script
let output = Command::new("bash")
.arg("-c")
.arg(&command)
.output()
.await
.map_err(|e| ToolError::ExecutionError(format!("Failed to run AppleScript: {}", e)))?;
let output_str = String::from_utf8_lossy(&output.stdout).into_owned();
let error_str = String::from_utf8_lossy(&output.stderr).into_owned();
let mut result = if output.status.success() {
format!(
"AppleScript completed successfully.\n\nOutput:\n{}",
output_str
)
} else {
format!(
"AppleScript failed with error code {}.\n\nError:\n{}\nOutput:\n{}",
output.status, error_str, output_str
)
};
let mut result = format!("Script completed successfully.\n\nOutput:\n{}", output);
// Save output if requested
if save_output && !output_str.is_empty() {
if save_output && !output.is_empty() {
let cache_path = self
.save_to_cache(output_str.as_bytes(), "applescript_output", "txt")
.save_to_cache(output.as_bytes(), "automation_output", "txt")
.await?;
result.push_str(&format!("\n\nOutput saved to: {}", cache_path.display()));
@@ -0,0 +1,25 @@
use super::SystemAutomation;
use std::path::PathBuf;
use std::process::Command;
pub struct MacOSAutomation;
// MacOSAutomation is Send + Sync because it contains no shared state
unsafe impl Send for MacOSAutomation {}
unsafe impl Sync for MacOSAutomation {}
impl SystemAutomation for MacOSAutomation {
fn execute_system_script(&self, script: &str) -> std::io::Result<String> {
let output = Command::new("osascript").arg("-e").arg(script).output()?;
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
fn get_shell_command(&self) -> (&'static str, &'static str) {
("bash", "-c")
}
fn get_temp_path(&self) -> PathBuf {
PathBuf::from("/tmp")
}
}
@@ -0,0 +1,29 @@
mod macos;
mod windows;
#[cfg(target_os = "windows")]
pub use self::windows::WindowsAutomation;
#[cfg(target_os = "macos")]
pub use self::macos::MacOSAutomation;
pub trait SystemAutomation: Send + Sync {
fn execute_system_script(&self, script: &str) -> std::io::Result<String>;
fn get_shell_command(&self) -> (&'static str, &'static str); // (shell, arg)
fn get_temp_path(&self) -> std::path::PathBuf;
}
pub fn create_system_automation() -> Box<dyn SystemAutomation + Send + Sync> {
#[cfg(target_os = "windows")]
{
Box::new(WindowsAutomation)
}
#[cfg(target_os = "macos")]
{
Box::new(MacOSAutomation)
}
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
{
unimplemented!("Unsupported operating system")
}
}
@@ -0,0 +1,32 @@
use super::SystemAutomation;
use std::path::PathBuf;
use std::process::Command;
pub struct WindowsAutomation;
// WindowsAutomation is Send + Sync because it contains no shared state
unsafe impl Send for WindowsAutomation {}
unsafe impl Sync for WindowsAutomation {}
impl SystemAutomation for WindowsAutomation {
fn execute_system_script(&self, script: &str) -> std::io::Result<String> {
let output = Command::new("powershell")
.arg("-NoProfile")
.arg("-NonInteractive")
.arg("-Command")
.arg(script)
.output()?;
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
fn get_shell_command(&self) -> (&'static str, &'static str) {
("powershell", "-Command")
}
fn get_temp_path(&self) -> PathBuf {
std::env::var("TEMP")
.map(PathBuf::from)
.unwrap_or_else(|_| PathBuf::from(r"C:\Windows\Temp"))
}
}
+3
View File
@@ -11,6 +11,9 @@ pub fn get_language_identifier(path: &Path) -> &'static str {
Some("toml") => "toml",
Some("yaml") | Some("yml") => "yaml",
Some("sh") => "bash",
Some("ps1") => "powershell",
Some("bat") | Some("cmd") => "batch",
Some("vbs") => "vbscript",
Some("go") => "go",
Some("md") => "markdown",
Some("html") => "html",
+106 -86
View File
@@ -1,4 +1,5 @@
mod lang;
mod shell;
use anyhow::Result;
use base64::Engine;
@@ -31,6 +32,11 @@ use std::process::Stdio;
use std::sync::{Arc, Mutex};
use xcap::{Monitor, Window};
use self::shell::{
expand_path, format_command_for_platform, get_shell_config, is_absolute_path,
normalize_line_endings,
};
pub struct DeveloperRouter {
tools: Vec<Tool>,
file_history: Arc<Mutex<HashMap<PathBuf, Vec<String>>>>,
@@ -48,9 +54,30 @@ impl DeveloperRouter {
// TODO consider rust native search tools, we could use
// https://docs.rs/ignore/latest/ignore/
let bash_tool = Tool::new(
"shell".to_string(),
indoc! {r#"
// Get OS-specific shell tool description
let shell_tool_desc = match std::env::consts::OS {
"windows" => indoc! {r#"
Execute a command in the shell.
This will return the output and error concatenated into a single string, as
you would see from running on the command line. There will also be an indication
of if the command succeeded or failed.
Avoid commands that produce a large amount of output, and consider piping those outputs to files.
**Important**: For searching files and code:
Preferred: Use ripgrep (`rg`) when available - it respects .gitignore and is fast:
- To locate a file by name: `rg --files | rg example.py`
- To locate content inside files: `rg 'class Example'`
Alternative Windows commands (if ripgrep is not installed):
- To locate a file by name: `dir /s /b example.py`
- To locate content inside files: `findstr /s /i "class Example" *.py`
Note: Alternative commands may show ignored/hidden files that should be excluded.
"#},
_ => indoc! {r#"
Execute a command in the shell.
This will return the output and error concatenated into a single string, as
@@ -64,8 +91,13 @@ impl DeveloperRouter {
**Important**: Use ripgrep - `rg` - when you need to locate a file or a code reference, other solutions
may show ignored or hidden files. For example *do not* use `find` or `ls -r`
- To locate a file by name: `rg --files | rg example.py`
- To locate consent inside files: `rg 'class Example'`
"#}.to_string(),
- To locate content inside files: `rg 'class Example'`
"#},
};
let bash_tool = Tool::new(
"shell".to_string(),
shell_tool_desc.to_string(),
json!({
"type": "object",
"required": ["command"],
@@ -157,9 +189,31 @@ impl DeveloperRouter {
// Get base instructions and working directory
let cwd = std::env::current_dir().expect("should have a current working dir");
let base_instructions = formatdoc! {r#"
The developer extension gives you the capabilities to edit code files and run shell commands,
and can be used to solve a wide range of problems.
let os = std::env::consts::OS;
let base_instructions = match os {
"windows" => formatdoc! {r#"
The developer extension gives you the capabilities to edit code files and run shell commands,
and can be used to solve a wide range of problems.
You can use the shell tool to run Windows commands (PowerShell or CMD).
When using paths, you can use either backslashes or forward slashes.
Use the shell tool as needed to locate files or interact with the project.
Your windows/screen tools can be used for visual debugging. You should not use these tools unless
prompted to, but you can mention they are available if they are relevant.
operating system: {os}
current directory: {cwd}
"#,
os=os,
cwd=cwd.to_string_lossy(),
},
_ => formatdoc! {r#"
The developer extension gives you the capabilities to edit code files and run shell commands,
and can be used to solve a wide range of problems.
You can use the shell tool to run any command that would work on the relevant operating system.
Use the shell tool as needed to locate files or interact with the project.
@@ -170,9 +224,10 @@ impl DeveloperRouter {
operating system: {os}
current directory: {cwd}
"#,
os=std::env::consts::OS,
cwd=cwd.to_string_lossy(),
"#,
os=os,
cwd=cwd.to_string_lossy(),
},
};
// Check for global hints in ~/.config/goose/.goosehints
@@ -223,15 +278,15 @@ impl DeveloperRouter {
}
}
// Helper method to resolve a path relative to cwd
// Helper method to resolve a path relative to cwd with platform-specific handling
fn resolve_path(&self, path_str: &str) -> Result<PathBuf, ToolError> {
let cwd = std::env::current_dir().expect("should have a current working dir");
let expanded = shellexpand::tilde(path_str);
let path = Path::new(expanded.as_ref());
let expanded = expand_path(path_str);
let path = Path::new(&expanded);
let suggestion = cwd.join(path);
match path.is_absolute() {
match is_absolute_path(&expanded) {
true => Ok(path.to_path_buf()),
false => Err(ToolError::InvalidParameters(format!(
"The path {} is not an absolute path, did you possibly mean {}?",
@@ -241,7 +296,7 @@ impl DeveloperRouter {
}
}
// Implement bash tool functionality
// Shell command execution with platform-specific handling
async fn bash(&self, params: Value) -> Result<Vec<Content>, ToolError> {
let command =
params
@@ -251,19 +306,17 @@ impl DeveloperRouter {
"The command string is required".to_string(),
))?;
// TODO consider command suggestions and safety rails
// Get platform-specific shell configuration
let shell_config = get_shell_config();
let cmd_with_redirect = format_command_for_platform(command);
// TODO be more careful about backgrounding, revisit interleave
// Redirect stderr to stdout to interleave outputs
let cmd_with_redirect = format!("{} 2>&1", command);
// Execute the command
let child = Command::new("bash")
.stdout(Stdio::piped()) // These two pipes required to capture output later.
// Execute the command using platform-specific shell
let child = Command::new(&shell_config.executable)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.stdin(Stdio::null())
.kill_on_drop(true) // Critical so that the command is killed when the agent.reply stream is interrupted.
.arg("-c")
.kill_on_drop(true)
.arg(&shell_config.arg)
.arg(cmd_with_redirect)
.spawn()
.map_err(|e| ToolError::ExecutionError(e.to_string()))?;
@@ -417,12 +470,15 @@ impl DeveloperRouter {
path: &PathBuf,
file_text: &str,
) -> Result<Vec<Content>, ToolError> {
// Normalize line endings based on platform
let normalized_text = normalize_line_endings(file_text);
// Write to the file
std::fs::write(path, file_text)
std::fs::write(path, normalized_text)
.map_err(|e| ToolError::ExecutionError(format!("Failed to write file: {}", e)))?;
// Try to detect the language from the file extension
let language = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
let language = lang::get_language_identifier(path);
// The assistant output does not show the file again because the content is already in the tool request
// but we do show it to the user here
@@ -478,13 +534,14 @@ impl DeveloperRouter {
// Save history for undo
self.save_file_history(path)?;
// Replace and write back
// Replace and write back with platform-specific line endings
let new_content = content.replace(old_str, new_str);
std::fs::write(path, &new_content)
let normalized_content = normalize_line_endings(&new_content);
std::fs::write(path, &normalized_content)
.map_err(|e| ToolError::ExecutionError(format!("Failed to write file: {}", e)))?;
// Try to detect the language from the file extension
let language = path.extension().and_then(|ext| ext.to_str()).unwrap_or("");
let language = lang::get_language_identifier(path);
// Show a snippet of the changed content with context
const SNIPPET_LINES: usize = 4;
@@ -811,65 +868,28 @@ mod tests {
#[tokio::test]
#[serial]
async fn test_text_editor_size_limits() {
// Create temp directory first so it stays in scope for the whole test
let temp_dir = tempfile::tempdir().unwrap();
std::env::set_current_dir(&temp_dir).unwrap();
// Get router after setting current directory
#[cfg(windows)]
async fn test_windows_specific_commands() {
let router = get_router().await;
// Test file size limit
{
let large_file_path = temp_dir.path().join("large.txt");
let large_file_str = large_file_path.to_str().unwrap();
// Test PowerShell command
let result = router
.call_tool(
"shell",
json!({
"command": "Get-ChildItem"
}),
)
.await;
assert!(result.is_ok());
// Create a file larger than 2MB
let content = "x".repeat(3 * 1024 * 1024); // 3MB
std::fs::write(&large_file_path, content).unwrap();
// Test Windows path handling
let result = router.resolve_path("C:\\Windows\\System32");
assert!(result.is_ok());
let result = router
.call_tool(
"text_editor",
json!({
"command": "view",
"path": large_file_str
}),
)
.await;
assert!(result.is_err());
let err = result.err().unwrap();
assert!(matches!(err, ToolError::ExecutionError(_)));
assert!(err.to_string().contains("too large"));
}
// Test character count limit
{
let many_chars_path = temp_dir.path().join("many_chars.txt");
let many_chars_str = many_chars_path.to_str().unwrap();
// Create a file with more than 400K characters but less than 400KB
let content = "x".repeat(405_000);
std::fs::write(&many_chars_path, content).unwrap();
let result = router
.call_tool(
"text_editor",
json!({
"command": "view",
"path": many_chars_str
}),
)
.await;
assert!(result.is_err());
let err = result.err().unwrap();
assert!(matches!(err, ToolError::ExecutionError(_)));
assert!(err.to_string().contains("too many characters"));
}
// Let temp_dir drop naturally at end of scope
// Test UNC path handling
let result = router.resolve_path("\\\\server\\share");
assert!(result.is_ok());
}
#[tokio::test]
+72
View File
@@ -0,0 +1,72 @@
use std::env;
#[derive(Debug, Clone)]
pub struct ShellConfig {
pub executable: String,
pub arg: String,
pub redirect_syntax: String,
}
impl Default for ShellConfig {
fn default() -> Self {
if cfg!(windows) {
// Use cmd.exe for simpler command execution
Self {
executable: "cmd.exe".to_string(),
arg: "/C".to_string(),
redirect_syntax: "2>&1".to_string(), // cmd.exe also supports this syntax
}
} else {
Self {
executable: "bash".to_string(),
arg: "-c".to_string(),
redirect_syntax: "2>&1".to_string(),
}
}
}
}
pub fn get_shell_config() -> ShellConfig {
ShellConfig::default()
}
pub fn format_command_for_platform(command: &str) -> String {
let config = get_shell_config();
// For all shells, no braces needed
format!("{} {}", command, config.redirect_syntax)
}
pub fn expand_path(path_str: &str) -> String {
if cfg!(windows) {
// Expand Windows environment variables (%VAR%)
let with_userprofile = path_str.replace(
"%USERPROFILE%",
&env::var("USERPROFILE").unwrap_or_default(),
);
// Add more Windows environment variables as needed
with_userprofile.replace("%APPDATA%", &env::var("APPDATA").unwrap_or_default())
} else {
// Unix-style expansion
shellexpand::tilde(path_str).into_owned()
}
}
pub fn is_absolute_path(path_str: &str) -> bool {
if cfg!(windows) {
// Check for Windows absolute paths (drive letters and UNC)
path_str.contains(":\\") || path_str.starts_with("\\\\")
} else {
// Unix absolute paths start with /
path_str.starts_with('/')
}
}
pub fn normalize_line_endings(text: &str) -> String {
if cfg!(windows) {
// Ensure CRLF line endings on Windows
text.replace("\r\n", "\n").replace("\n", "\r\n")
} else {
// Ensure LF line endings on Unix
text.replace("\r\n", "\n")
}
}
+6 -1
View File
@@ -12,7 +12,12 @@ use goose::tracing::langfuse_layer;
/// Returns the directory where log files should be stored.
/// Creates the directory structure if it doesn't exist.
fn get_log_directory() -> Result<PathBuf> {
let home = std::env::var("HOME").context("HOME environment variable not set")?;
let home = if cfg!(windows) {
std::env::var("USERPROFILE").context("USERPROFILE environment variable not set")?
} else {
std::env::var("HOME").context("HOME environment variable not set")?
};
let base_log_dir = PathBuf::from(home)
.join(".config")
.join("goose")
+3
View File
@@ -66,6 +66,9 @@ aws-config = { version = "1.1.7", features = ["behavior-version-latest"] }
aws-smithy-types = "1.2.12"
aws-sdk-bedrockruntime = "1.72.0"
[target.'cfg(target_os = "windows")'.dependencies]
winapi = { version = "0.3", features = ["wincred"] }
[dev-dependencies]
criterion = "0.5"
tempfile = "3.15.0"
+5 -1
View File
@@ -32,7 +32,11 @@ struct TokenCache {
fn get_base_path() -> PathBuf {
const BASE_PATH: &str = ".config/goose/databricks/oauth";
let home_dir = std::env::var("HOME").expect("HOME environment variable not set");
let home_dir = if cfg!(windows) {
std::env::var("USERPROFILE").expect("USERPROFILE environment variable not set")
} else {
std::env::var("HOME").expect("HOME environment variable not set")
};
PathBuf::from(home_dir).join(BASE_PATH)
}
+13 -4
View File
@@ -196,15 +196,24 @@ impl StdioTransport {
}
async fn spawn_process(&self) -> Result<(Child, ChildStdin, ChildStdout, ChildStderr), Error> {
let mut process = Command::new(&self.command)
let mut command = Command::new(&self.command);
command
.envs(&self.env)
.args(&self.args)
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.kill_on_drop(true)
// 0 sets the process group ID equal to the process ID
.process_group(0) // don't inherit signal handling from parent process
.kill_on_drop(true);
// Set process group only on Unix systems
#[cfg(unix)]
command.process_group(0); // don't inherit signal handling from parent process
// Hide console window on Windows
#[cfg(windows)]
command.creation_flags(0x08000000); // CREATE_NO_WINDOW flag
let mut process = command
.spawn()
.map_err(|e| Error::StdioProcessError(e.to_string()))?;