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
4 changes: 2 additions & 2 deletions crates/buzz-acp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,9 @@ Buzz Desktop supports registering any ACP-speaking agent tool as a selectable ru

### How it works

**Tier-1 — compiled-in runtimes** (Goose, Claude Code, Codex, Buzz Agent): have auto-installers, auth probes, and first-class onboarding. Their IDs (`goose`, `claude`, `codex`, `buzz-agent`) are reserved and cannot be overridden.
**Tier-1 — compiled-in runtimes** (Goose, Claude Code, Codex, Buzz Agent, Hermes Agent): have auth probes and first-class runtime metadata, including credential-scoped MCP publication where required. Their IDs (`goose`, `claude`, `codex`, `buzz-agent`, `hermes`) are reserved and cannot be overridden.

**Tier-2 — preset catalog** (Cursor, Oh My Pi, Grok Build, OpenCode, Kimi Code, Amp, Hermes Agent, OpenClaw): static `HarnessDefinition` entries in `desktop/src-tauri/src/managed_agents/discovery.rs` (`PRESET_HARNESSES`). They are always present in the runtime catalog, PATH-probed for availability, not editable or deletable by the user. Displayed with bundled logos; if not installed, a docs link appears instead.
**Tier-2 — preset catalog** (Cursor, Oh My Pi, Grok Build, OpenCode, Kimi Code, Amp, OpenClaw): static `HarnessDefinition` entries in `desktop/src-tauri/src/managed_agents/discovery.rs` (`PRESET_HARNESSES`). They are always present in the runtime catalog, PATH-probed for availability, not editable or deletable by the user. Displayed with bundled logos; if not installed, a docs link appears instead. Hermes Agent is tier 1 because Buzz must attach its credential-scoped MCP publication tool at spawn time.

> **Note — OpenClaw:** `openclaw acp` is a Gateway-backed bridge; PATH availability shows "Available" even when the OpenClaw Gateway daemon is not running. This is expected tier-2 semantics (same class as a preset with unconfigured auth). The Gateway URL is configured via `OPENCLAW_GATEWAY_URL` (or the equivalent env var from OpenClaw's docs) — set it in the agent's **env vars** in Edit Agent, not in the definition env (the preset definition carries no env entries). Note that `openclaw acp` executes tools inside the Gateway daemon, not the Desktop process, so Desktop-injected `BUZZ_*` env vars do NOT reach the execution locus unless you also set them on the Gateway's own environment.

Expand Down
4 changes: 2 additions & 2 deletions desktop/src-tauri/src/managed_agents/custom_harnesses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,11 @@ pub(crate) fn validate_harness_definition_pub(def: &HarnessDefinition) -> Result
/// collides with a built-in or preset is rejected to prevent shadowing (e.g. a
/// file called `cursor.json` hiding the pre-existing tier-2 preset).
///
/// Derived at compile time from `PRESET_HARNESSES` (tier-2) plus the four
/// Derived at compile time from `PRESET_HARNESSES` (tier-2) plus the five
/// tier-1 runtimes — no hand-maintained copy. Adding a preset to
/// `PRESET_HARNESSES` automatically reserves its ID without a separate edit.
fn builtin_ids() -> impl Iterator<Item = &'static str> {
const TIER1: &[&str] = &["goose", "claude", "codex", "buzz-agent"];
const TIER1: &[&str] = &["goose", "claude", "codex", "buzz-agent", "hermes"];
let tier2 = crate::managed_agents::discovery::preset_harness_ids();
TIER1.iter().copied().chain(tier2.iter().copied())
}
Expand Down
37 changes: 5 additions & 32 deletions desktop/src-tauri/src/managed_agents/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,18 @@ use crate::managed_agents::{
AcpAvailabilityStatus, AcpRuntimeCatalogEntry, AuthStatus, CommandAvailabilityInfo,
HarnessSource,
};
mod args;
mod hermes;
mod presets;
mod runtime_metadata;
#[macro_use]
mod windows_install;
pub use args::normalize_agent_args;
pub(crate) use presets::{
canonical_harness_command, command_for_runtime_id, preset_harness_definitions,
preset_harness_ids,
};
use hermes::HERMES_RUNTIME;
use presets::{preset_catalog_entry, PRESET_HARNESSES};
pub(crate) use runtime_metadata::KnownAcpRuntime;

Expand Down Expand Up @@ -211,6 +215,7 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
login_hint: None,
auth_probe_args: None,
},
HERMES_RUNTIME,
];

/// Skill discovery directories declared by known runtimes.
Expand Down Expand Up @@ -440,38 +445,6 @@ pub fn try_record_agent_command(
Ok(default_agent_command())
}

fn default_agent_args(command: &str) -> Option<Vec<String>> {
match normalize_command_identity(command).as_str() {
"goose" => Some(vec!["acp".to_string()]),
"codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code"
| "claudecode" | "buzz-agent" => Some(Vec::new()),
_ => None,
}
}

pub fn normalize_agent_args(command: &str, agent_args: Vec<String>) -> Vec<String> {
let normalized = agent_args
.into_iter()
.map(|arg| arg.trim().to_string())
.filter(|arg| !arg.is_empty())
.collect::<Vec<_>>();

let Some(default_args) = default_agent_args(command) else {
return normalized;
};

if normalized.is_empty() {
return default_args;
}

if normalized.len() == 1 && normalized[0].eq_ignore_ascii_case("acp") && default_args.is_empty()
{
return default_args;
}

normalized
}

fn profile_target_dirs(root: &Path) -> [PathBuf; 2] {
if cfg!(debug_assertions) {
// `just dev` builds fresh debug sidecars; never prefer stale release output.
Expand Down
35 changes: 35 additions & 0 deletions desktop/src-tauri/src/managed_agents/discovery/args.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
use super::normalize_command_identity;

fn default_agent_args(command: &str) -> Option<Vec<String>> {
match normalize_command_identity(command).as_str() {
"goose" => Some(vec!["acp".to_string()]),
"codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code"
| "claudecode" | "buzz-agent" | "hermes" | "hermes-acp" | "hermes-agent" => {
Some(Vec::new())
}
_ => None,
}
}

pub fn normalize_agent_args(command: &str, agent_args: Vec<String>) -> Vec<String> {
let normalized = agent_args
.into_iter()
.map(|arg| arg.trim().to_string())
.filter(|arg| !arg.is_empty())
.collect::<Vec<_>>();

let Some(default_args) = default_agent_args(command) else {
return normalized;
};

if normalized.is_empty() {
return default_args;
}

if normalized.len() == 1 && normalized[0].eq_ignore_ascii_case("acp") && default_args.is_empty()
{
return default_args;
}

normalized
}
57 changes: 57 additions & 0 deletions desktop/src-tauri/src/managed_agents/discovery/hermes.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use super::KnownAcpRuntime;

pub(super) const HERMES_RUNTIME: KnownAcpRuntime = KnownAcpRuntime {
id: "hermes",
label: "Hermes Agent",
commands: &["hermes-acp"],
aliases: &["hermes-agent"],
avatar_url: "",
// Hermes terminal subprocesses intentionally sanitize signing secrets.
// Give Hermes the credential-scoped Buzz MCP shell so message sends can
// emit recipient p-tags without exposing the key to generic tools.
mcp_command: Some("buzz-dev-mcp"),
mcp_hooks: true,
underlying_cli: Some("hermes"),
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
cli_install_instructions_url: "https://hermes-agent.nousresearch.com/docs",
adapter_install_instructions_url: "https://hermes-agent.nousresearch.com/docs",
cli_install_hint: "Install and configure Hermes Agent before using this runtime.",
adapter_install_hint: "Hermes Agent provides the hermes-acp adapter.",
skill_dir: Some(".hermes/skills"),
supports_acp_model_switching: true,
model_env_var: None,
provider_env_var: None,
provider_locked: false,
default_env: &[],
config_file_path: None,
config_file_format: None,
supports_acp_native_config: false,
thinking_env_var: None,
max_tokens_env_var: None,
context_limit_env_var: None,
max_rounds_env_var: None,
required_normalized_fields: &[],
login_hint: Some("Run `hermes setup` to configure a provider."),
auth_probe_args: None,
};

#[cfg(test)]
mod tests {
use super::super::{known_acp_runtime, normalize_agent_args};

#[test]
fn runtime_uses_credential_scoped_buzz_mcp() {
let runtime = known_acp_runtime("/Users/test/.local/bin/hermes-acp")
.expect("hermes-acp should resolve as a known runtime");

assert_eq!(runtime.id, "hermes");
assert_eq!(runtime.mcp_command, Some("buzz-dev-mcp"));
assert!(runtime.mcp_hooks);
assert_eq!(
normalize_agent_args("hermes-acp", vec!["acp".into()]),
Vec::<String>::new()
);
}
}
9 changes: 0 additions & 9 deletions desktop/src-tauri/src/managed_agents/discovery/presets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,15 +153,6 @@ pub(super) const PRESET_HARNESSES: &[PresetHarness] = &[
install_hint: "Buzz talks to the Amp CLI through the amp-acp adapter. Follow the setup guide to install the adapter so the amp-acp command is on your PATH.",
underlying_cli: Some("amp"),
},
PresetHarness {
id: "hermes",
label: "Hermes Agent",
command: "hermes-acp",
args: &[],
install_instructions_url: "https://hermes-agent.nousresearch.com",
install_hint: "Buzz talks to Hermes Agent through its hermes-acp command.",
underlying_cli: None,
},
PresetHarness {
id: "openclaw",
label: "OpenClaw",
Expand Down