Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions desktop/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ export default defineConfig({
"**/observer-archive-policy.spec.ts",
"**/harness-management.spec.ts",
"**/harness-catalog-screenshots.spec.ts",
"**/goose-update-screenshots.spec.ts",
"**/inline-custom-harness.spec.ts",
],
use: {
Expand Down
1 change: 1 addition & 0 deletions desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ getrandom = "0.2"
zeroize = "1"
reqwest = { version = "0.13", features = ["json", "query", "stream", "blocking"] }
rustls = { version = "0.23", default-features = false, features = ["aws_lc_rs", "std"] }
semver = "1"
url = "2"
buzz_core_pkg = { package = "buzz-core", path = "../../crates/buzz-core" }
buzz_persona_pkg = { package = "buzz-persona", path = "../../crates/buzz-persona" }
Expand Down
17 changes: 7 additions & 10 deletions desktop/src-tauri/src/commands/agent_discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use crate::{
relay::query_relay,
};

mod cli_setup;
mod post_install_verification;

fn active_installs() -> &'static std::sync::Mutex<std::collections::HashSet<String>> {
Expand Down Expand Up @@ -316,16 +317,12 @@ fn install_acp_runtime_blocking(
// Today every entry in `cli_install_commands` is a curl-pipe; npm-backed
// adapter installs live in Phase 2 below where they are rewritten to a
// Buzz-private prefix before execution.
if let Some(cli) = runtime.underlying_cli {
if crate::managed_agents::resolve_command(cli).is_none() {
for cmd in runtime.cli_install_commands_for_os() {
let result = run_install_command_with_retry("cli", cmd, &reporter);
let success = result.success;
steps.push(result);
if !success {
return Ok(reporter.failed(steps));
}
}
for (step, command) in cli_setup::plan(runtime) {
let result = run_install_command_with_retry(step, &command, &reporter);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking: this sends goose update through installer retry semantics. run_install_command_with_retry retries every nonzero process exit up to three times, but a self-update can replace/mutate the installation and then fail during later work. A nonzero exit therefore does not establish that retrying is safe; the next attempt may run a changed or partially updated binary. Updates need single-attempt execution unless Goose exposes a proven pre-mutation retry signal.

— Carl, reviewing on Wes's behalf

let success = result.success;
steps.push(result);
if !success {
return Ok(reporter.failed(steps));
}
}

Expand Down
99 changes: 99 additions & 0 deletions desktop/src-tauri/src/commands/agent_discovery/cli_setup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
use std::path::Path;

use crate::managed_agents::KnownAcpRuntime;

/// Plan the vendor CLI portion of an explicit runtime setup request.
///
/// Missing CLIs use the catalog installer. An already-installed Goose uses its
/// resolved executable for an optional user-requested update. Other installed
/// CLIs need no setup command.
pub(super) fn plan(runtime: &KnownAcpRuntime) -> Vec<(&'static str, String)> {
let Some(cli) = runtime.underlying_cli else {
return Vec::new();
};
let cli_path = crate::managed_agents::resolve_command(cli);
plan_commands(
runtime.id,
cli_path.as_deref(),
runtime.cli_install_commands_for_os(),
)
}

fn plan_commands(
runtime_id: &str,
cli_path: Option<&Path>,
install_commands: &[&str],
) -> Vec<(&'static str, String)> {
match cli_path {
Some(path) if runtime_id == "goose" => {
vec![("update", goose_update_command(path))]
}
Some(_) => Vec::new(),
None => install_commands
.iter()
.map(|command| ("cli", (*command).to_string()))
.collect(),
}
}

#[cfg(not(windows))]
fn goose_update_command(path: &Path) -> String {
let path = path.display().to_string();
format!("'{}' update", path.replace('\'', "'\"'\"'"))
}

#[cfg(windows)]
fn goose_update_command(path: &Path) -> String {
let path = path.display().to_string();
let quoted_path = format!("'{}'", path.replace('\'', "''"));
format!("powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"& {quoted_path} update\"")
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn missing_goose_uses_catalog_installer() {
assert_eq!(
plan_commands("goose", None, &["install goose"]),
vec![("cli", "install goose".to_string())]
);
}

#[cfg(not(windows))]
#[test]
fn installed_goose_updates_resolved_binary() {
let path = Path::new("/tmp/Buzz's Goose/goose");

assert_eq!(
plan_commands("goose", Some(path), &["install goose"]),
vec![(
"update",
"'/tmp/Buzz'\"'\"'s Goose/goose' update".to_string()
)]
);
}

#[cfg(windows)]
#[test]
fn installed_goose_uses_native_powershell() {
let path = Path::new(r"C:\Users\lifei\Buzz's Goose\goose.exe");

assert_eq!(
plan_commands("goose", Some(path), &["install goose"]),
vec![(
"update",
r#"powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& 'C:\Users\lifei\Buzz''s Goose\goose.exe' update""#
.to_string()
)]
);
}

#[test]
fn installed_non_goose_is_unchanged() {
let path = Path::new("/usr/local/bin/claude");

assert!(plan_commands("claude", Some(path), &["install claude"]).is_empty());
}
}
Loading
Loading