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
2 changes: 2 additions & 0 deletions desktop/src-tauri/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ mod project_terminal;
mod qr_download;
mod relay_members;
mod relay_reconnect;
mod remote_agent_discovery;
mod social;
mod team_snapshot;
mod teams;
Expand Down Expand Up @@ -103,6 +104,7 @@ pub use project_terminal::*;
pub use qr_download::*;
pub use relay_members::*;
pub use relay_reconnect::*;
pub use remote_agent_discovery::*;
pub use social::*;
pub use team_snapshot::*;
pub use teams::*;
Expand Down
57 changes: 57 additions & 0 deletions desktop/src-tauri/src/commands/remote_agent_discovery.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//! Tauri commands for host-aware harness discovery.
//!
//! These answer "which machines can I reach, and what agent harnesses are on
//! them?" so an agent that already runs on another host can be found instead of
//! described by hand.
//!
//! All three commands are read-only. Nothing here installs software, writes to
//! a remote host, or collects a credential — the probe runs `command -v` and
//! `--version` and nothing else.

use crate::managed_agents::remote_probe::{probe_localhost, probe_ssh_host, HostProbeResult};
use crate::managed_agents::ssh_config::{parse_ssh_config, SshHost};

/// Enumerate the user's `~/.ssh/config` host aliases.
///
/// No connection is attempted. An absent config yields an empty list, which
/// means "no remote hosts configured", not a failure.
#[tauri::command]
pub async fn list_ssh_hosts() -> Result<Vec<SshHost>, String> {
tokio::task::spawn_blocking(parse_ssh_config)
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))
}

/// Probe one host for agent harnesses and the `buzz` CLI.
///
/// `host` must name an alias present in `~/.ssh/config`. Resolving it through
/// the parsed config rather than trusting the argument is what keeps an
/// arbitrary string — including anything shaped like an ssh option — from
/// reaching the `ssh` argv.
///
/// A host-side problem (unreachable, password-only, unknown host key) comes back
/// as `Ok` with `ok: false` and a classified `errorKind`: the UI shows one row
/// per host and needs a renderable status, not an exception.
#[tauri::command]
pub async fn probe_agent_host(host: String) -> Result<HostProbeResult, String> {
tokio::task::spawn_blocking(move || {
let hosts = parse_ssh_config();
let Some(entry) = hosts.into_iter().find(|candidate| candidate.host == host) else {
return Err(format!(
"'{host}' is not a Host alias in ~/.ssh/config; only configured hosts can be probed"
));
};
Ok(probe_ssh_host(&entry))
})
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))?
}

/// Probe the machine Buzz is running on, using the identical probe script so
/// the result is shape-compatible with [`probe_agent_host`].
#[tauri::command]
pub async fn probe_local_agent_host() -> Result<HostProbeResult, String> {
tokio::task::spawn_blocking(probe_localhost)
.await
.map_err(|e| format!("spawn_blocking failed: {e}"))
}
3 changes: 3 additions & 0 deletions desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,9 @@ pub fn run() {
get_relay_self,
resolve_oa_owner,
list_relay_agents,
list_ssh_hosts,
probe_agent_host,
probe_local_agent_host,
list_managed_agents,
list_managed_agent_runtimes,
start_managed_agent_runtime,
Expand Down
151 changes: 12 additions & 139 deletions desktop/src-tauri/src/managed_agents/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,25 @@ use crate::managed_agents::{
HarnessSource,
};

mod known_runtimes;
mod presets;
mod probe_targets;
mod runtime_metadata;

pub(crate) use known_runtimes::KNOWN_ACP_RUNTIMES;
use presets::{preset_catalog_entry, PRESET_HARNESSES};
pub(crate) use presets::{preset_harness_definitions, preset_harness_ids};
pub use probe_targets::{harness_probe_targets, HarnessProbeTarget};
// The avatar URLs are only named directly by `tests.rs`, which asserts each
// runtime resolves to its own icon; production code reaches them through
// `KNOWN_ACP_RUNTIMES`. Re-exported here so the move out of this file stays
// invisible to the test module.
#[cfg(test)]
pub(crate) use known_runtimes::{
BUZZ_AGENT_AVATAR_URL, CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, GOOSE_AVATAR_URL,
};
pub(crate) use runtime_metadata::KnownAcpRuntime;

const GOOSE_AVATAR_URL: &str = "https://goose-docs.ai/img/logo_dark.png";
const CLAUDE_CODE_AVATAR_URL: &str = "https://anthropic.gallerycdn.vsassets.io/extensions/anthropic/claude-code/2.1.77/1773707456892/Microsoft.VisualStudio.Services.Icons.Default";
const CODEX_AVATAR_URL: &str = "https://openai.gallerycdn.vsassets.io/extensions/openai/chatgpt/26.5313.41514/1773706730621/Microsoft.VisualStudio.Services.Icons.Default";
const BUZZ_AGENT_AVATAR_URL: &str =
"https://raw.githubusercontent.com/block/buzz/refs/heads/main/crates/buzz-agent/buzz-agent.png";
fn common_binary_paths() -> &'static [PathBuf] {
static PATHS: OnceLock<Vec<PathBuf>> = OnceLock::new();
PATHS.get_or_init(|| {
Expand Down Expand Up @@ -72,140 +79,6 @@ fn common_binary_paths() -> &'static [PathBuf] {
})
}

const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
KnownAcpRuntime {
id: "goose",
label: "Goose",
commands: &["goose"],
aliases: &[],
avatar_url: GOOSE_AVATAR_URL,
mcp_command: None,
mcp_hooks: false,
underlying_cli: Some("goose"),
cli_install_commands: &["curl -fsSL https://github.com/aaif-goose/goose/releases/download/stable/download_cli.sh | CONFIGURE=false bash"],
// Goose's stable release currently publishes only the Unix installer;
// its official Windows instructions intentionally point at this main-branch script.
cli_install_commands_windows: &["powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"$env:CONFIGURE='false'; irm https://raw.githubusercontent.com/aaif-goose/goose/main/download_cli.ps1 | iex\""],
adapter_install_commands: &[],
cli_install_instructions_url: "https://goose-docs.ai/docs/getting-started/installation/",
adapter_install_instructions_url: "",
cli_install_hint: "Buzz talks to Goose through the Goose CLI.",
adapter_install_hint: "",
skill_dir: Some(".goose/skills"),
supports_acp_model_switching: false,
model_env_var: Some("GOOSE_MODEL"),
provider_env_var: Some("GOOSE_PROVIDER"),
provider_locked: false,
default_env: &[("GOOSE_MODE", "auto")],
config_file_path: Some("~/.config/goose/config.yaml"),
config_file_format: Some("yaml"),
supports_acp_native_config: true,
thinking_env_var: Some("GOOSE_THINKING_EFFORT"),
max_tokens_env_var: Some("GOOSE_MAX_TOKENS"),
context_limit_env_var: Some("GOOSE_CONTEXT_LIMIT"),
required_normalized_fields: &["model", "provider"],
login_hint: None,
auth_probe_args: None,
},
KnownAcpRuntime {
id: "claude",
label: "Claude Code",
commands: &["claude-agent-acp", "claude-code-acp"],
aliases: &["claude-code", "claudecode"],
avatar_url: CLAUDE_CODE_AVATAR_URL,
mcp_command: None,
mcp_hooks: false,
underlying_cli: Some("claude"),
cli_install_commands: &["curl -fsSL https://claude.ai/install.sh | bash"],
cli_install_commands_windows: &["powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"irm https://claude.ai/install.ps1 | iex\""],
adapter_install_commands: &["npm install -g @agentclientprotocol/claude-agent-acp"],
cli_install_instructions_url: "https://code.claude.com/docs/en/getting-started",
adapter_install_instructions_url: "https://github.com/agentclientprotocol/claude-agent-acp",
cli_install_hint: "Buzz talks to Claude Code through the Claude Code CLI.",
adapter_install_hint: "Buzz talks to the Claude Code CLI through an ACP adapter. Install it with: npm install -g @agentclientprotocol/claude-agent-acp.",
skill_dir: Some(".claude/skills"),
supports_acp_model_switching: false,
model_env_var: None,
provider_env_var: None,
provider_locked: true,
default_env: &[],
config_file_path: Some("~/.claude/settings.json"),
config_file_format: Some("json"),
supports_acp_native_config: false,
thinking_env_var: None,
max_tokens_env_var: None,
context_limit_env_var: None,
required_normalized_fields: &[],
login_hint: Some("Run the Claude CLI to complete authentication."),
auth_probe_args: Some(&["claude", "auth", "status"]),
},
KnownAcpRuntime {
id: "codex",
label: "Codex",
commands: &["codex-acp"],
aliases: &[],
avatar_url: CODEX_AVATAR_URL,
mcp_command: Some("buzz-dev-mcp"),
mcp_hooks: false,
underlying_cli: Some("codex"),
cli_install_commands: &["curl -fsSL https://chatgpt.com/codex/install.sh | sh"],
cli_install_commands_windows: &["powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"irm https://chatgpt.com/codex/install.ps1 | iex\""],
adapter_install_commands: &["npm install -g @agentclientprotocol/codex-acp"],
cli_install_instructions_url: "https://developers.openai.com/codex/cli/",
adapter_install_instructions_url: "https://github.com/agentclientprotocol/codex-acp",
cli_install_hint: "Buzz talks to Codex through the Codex CLI.",
adapter_install_hint: "Buzz talks to the Codex CLI through an ACP adapter. Install it with: npm install -g @agentclientprotocol/codex-acp.",
skill_dir: Some(".codex/skills"),
supports_acp_model_switching: false,
model_env_var: None,
provider_env_var: None,
provider_locked: false,
default_env: &[],
config_file_path: Some("~/.codex/config.toml"),
config_file_format: Some("toml"),
supports_acp_native_config: false,
thinking_env_var: None,
max_tokens_env_var: None,
context_limit_env_var: None,
required_normalized_fields: &[],
login_hint: Some("Run `codex login` to authenticate."),
// Verified: `codex login status` exits 0 when logged in, non-zero otherwise.
auth_probe_args: Some(&["codex", "login", "status"]),
},
KnownAcpRuntime {
id: "buzz-agent",
label: "Buzz Agent",
commands: &["buzz-agent"],
aliases: &[],
avatar_url: BUZZ_AGENT_AVATAR_URL,
mcp_command: Some("buzz-dev-mcp"),
mcp_hooks: true,
underlying_cli: None,
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
cli_install_instructions_url: "https://github.com/block/buzz",
adapter_install_instructions_url: "https://github.com/block/buzz",
cli_install_hint: "Ships with the Buzz desktop app.",
adapter_install_hint: "",
skill_dir: None,
supports_acp_model_switching: true,
model_env_var: Some("BUZZ_AGENT_MODEL"),
provider_env_var: Some("BUZZ_AGENT_PROVIDER"),
provider_locked: false,
default_env: &[],
config_file_path: None,
config_file_format: None,
supports_acp_native_config: false,
thinking_env_var: Some("BUZZ_AGENT_THINKING_EFFORT"),
max_tokens_env_var: Some("BUZZ_AGENT_MAX_OUTPUT_TOKENS"),
context_limit_env_var: Some("BUZZ_AGENT_MAX_CONTEXT_TOKENS"),
required_normalized_fields: &["model", "provider"],
login_hint: None,
auth_probe_args: None,
},
];

/// Skill discovery directories declared by known runtimes.
pub(crate) fn known_skill_dirs() -> impl Iterator<Item = &'static str> {
KNOWN_ACP_RUNTIMES.iter().filter_map(|p| p.skill_dir)
Expand Down
Loading