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
6 changes: 4 additions & 2 deletions crates/buzz-acp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -267,9 +267,11 @@ 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, OpenCode, Buzz Agent): have auto-installers, auth probes, and first-class onboarding. Their IDs (`goose`, `claude`, `codex`, `opencode`, `buzz-agent`) 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, 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.

> **Note — default engine:** new agents default to the bundled `buzz-agent`. Set `BUZZ_DEFAULT_RUNTIME=<runtime-id>` (e.g. `opencode`, `goose`, or any tier-2/tier-3 id) in the Desktop process environment to make another harness the site default for newly-created agents; unresolvable ids fall back to the bundled default with a warning.

> **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
9 changes: 8 additions & 1 deletion crates/buzz-acp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -730,7 +730,7 @@ pub(crate) fn normalize_agent_command_identity(command: &str) -> String {

fn default_agent_args(command: &str) -> Option<Vec<String>> {
match normalize_agent_command_identity(command).as_str() {
"goose" => Some(vec!["acp".to_string()]),
"goose" | "opencode" => Some(vec!["acp".to_string()]),
"codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code"
| "claudecode" | "buzz-agent" => Some(Vec::new()),
_ => None,
Expand Down Expand Up @@ -1582,6 +1582,13 @@ mod tests {
fn normalizes_goose_args_to_acp() {
assert_eq!(normalize_agent_args("goose", Vec::new()), vec!["acp"]);
assert_eq!(normalize_agent_args("goose", vec!["".into()]), vec!["acp"]);
// opencode is native ACP like goose: empty args must mean `acp`, or a
// bare `opencode` spawn would start the TUI instead of the harness.
assert_eq!(normalize_agent_args("opencode", Vec::new()), vec!["acp"]);
assert_eq!(
normalize_agent_args("opencode", vec!["acp".into()]),
vec!["acp"]
);
}

#[test]
Expand Down
20 changes: 10 additions & 10 deletions desktop/src-tauri/src/managed_agents/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ pub(crate) use presets::{
preset_harness_ids,
};
use presets::{preset_catalog_entry, PRESET_HARNESSES};
pub(crate) use runtime_metadata::KnownAcpRuntime;
pub(crate) use runtime_metadata::{KnownAcpRuntime, OPENCODE_RUNTIME};

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";
Expand Down Expand Up @@ -55,6 +55,8 @@ fn common_binary_paths() -> &'static [PathBuf] {
home.join(".volta/bin"),
home.join(".asdf/shims"),
home.join(".bun/bin"),
// opencode's native installer target; it only edits rc files.
home.join(".opencode/bin"),
]);
}
// Windows well-known dirs for npm global shims and standalone installer targets.
Expand Down Expand Up @@ -186,6 +188,7 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
// Verified: `codex login status` exits 0 when logged in, non-zero otherwise.
auth_probe_args: Some(&["codex", "login", "status"]),
},
OPENCODE_RUNTIME,
KnownAcpRuntime {
id: "buzz-agent",
label: "Buzz Agent",
Expand Down Expand Up @@ -291,15 +294,12 @@ pub(crate) fn known_acp_runtime_exact(id: &str) -> Option<&'static KnownAcpRunti
}

/// The agent command a freshly-created agent defaults to when the create
/// request supplies none. Resolves the bundled `buzz-agent` from the catalog so
/// the default cannot drift from the provider definition. Falls back to the id
/// if the catalog entry is missing. (Previous default was bare `goose`, which
/// is not on PATH on a stock Windows install; buzz-agent ships with the app.)
/// request supplies none: the bundled `buzz-agent`, unless the operator set
/// `BUZZ_DEFAULT_RUNTIME` to a resolvable harness id (three-tier lookup —
/// builtins, presets, custom registry; unresolvable ids warn and fall back).
pub fn default_agent_command() -> String {
known_acp_runtime_exact("buzz-agent")
.and_then(|p| p.commands.first().copied())
.unwrap_or("buzz-agent")
.to_string()
runtime_metadata::default_runtime_override()
.unwrap_or_else(runtime_metadata::bundled_default_agent_command)
}

/// Record-first harness resolution (unified agent model, Phase 1A).
Expand Down Expand Up @@ -450,7 +450,7 @@ pub fn try_record_agent_command(

fn default_agent_args(command: &str) -> Option<Vec<String>> {
match normalize_command_identity(command).as_str() {
"goose" => Some(vec!["acp".to_string()]),
"goose" | "opencode" => Some(vec!["acp".to_string()]),
"codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code"
| "claudecode" | "buzz-agent" => Some(Vec::new()),
_ => None,
Expand Down
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 @@ -126,15 +126,6 @@ pub(super) const PRESET_HARNESSES: &[PresetHarness] = &[
install_hint: "Buzz talks to Grok Build through its CLI's agent stdio mode.",
underlying_cli: None,
},
PresetHarness {
id: "opencode",
label: "OpenCode",
command: "opencode",
args: &["acp"],
install_instructions_url: "https://opencode.ai/docs",
install_hint: "Buzz talks to OpenCode through its CLI's ACP mode (opencode acp).",
underlying_cli: None,
},
PresetHarness {
id: "kimi",
label: "Kimi Code",
Expand Down
118 changes: 118 additions & 0 deletions desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,95 @@ impl KnownAcpRuntime {
}
}

const OPENCODE_AVATAR_URL: &str =
"https://raw.githubusercontent.com/block/buzz/refs/heads/main/desktop/public/harness-logos/opencode.svg";

/// Compiled-in opencode runtime (tier-1). Native ACP: the CLI is the agent, so
/// `underlying_cli` doubles as the Phase-1 auto-install marker. Two deliberate
/// `None`s, verified against opencode source: `auth_probe_args` (`opencode
/// auth list` exits 0 even with zero credentials — no faithful probe) and
/// `model_env_var` (opencode core reads no model env var; pinning is
/// config-side via `~/.config/opencode/opencode.json`).
pub(crate) const OPENCODE_RUNTIME: KnownAcpRuntime = KnownAcpRuntime {
id: "opencode",
label: "OpenCode",
commands: &["opencode"],
aliases: &[],
avatar_url: OPENCODE_AVATAR_URL,
mcp_command: None,
mcp_hooks: false,
underlying_cli: Some("opencode"),
// The vendor installer is one cross-platform bash script (Git Bash on
// Windows); opencode publishes no PowerShell installer to route through
// the Defender-safe two-step form, so both OSes use the pipe.
cli_install_commands: &["curl -fsSL https://opencode.ai/install | bash"],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
cli_install_instructions_url: "https://opencode.ai/docs",
adapter_install_instructions_url: "",
cli_install_hint: "Buzz talks to OpenCode through the OpenCode CLI's ACP mode (opencode acp).",
adapter_install_hint: "",
skill_dir: Some(".opencode/skills"),
supports_acp_model_switching: false,
model_env_var: None,
provider_env_var: None,
provider_locked: false,
default_env: &[],
config_file_path: Some("~/.config/opencode/opencode.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,
max_rounds_env_var: None,
required_normalized_fields: &[],
login_hint: None,
auth_probe_args: None,
};

/// Resolve a `BUZZ_DEFAULT_RUNTIME` value to a harness command via the
/// authoritative three-tier lookup (builtins → presets → loaded registry).
/// Empty or unresolvable values yield `None` so callers keep the bundled
/// default; unknown ids log a warning rather than pinning agents to a
/// dangling command.
pub(super) fn resolve_default_runtime(id: &str) -> Option<String> {
let trimmed = id.trim();
if trimmed.is_empty() {
return None;
}
match super::presets::command_for_runtime_id(trimmed) {
Some(cmd) => Some(cmd),
None => {
tracing::warn!(
runtime = trimmed,
"BUZZ_DEFAULT_RUNTIME does not resolve to a known harness — using bundled buzz-agent"
);
None
}
}
}

/// `BUZZ_DEFAULT_RUNTIME` engine override for newly-created agents, read live
/// from the process environment. Documented in crates/buzz-acp/README.md.
pub(super) fn default_runtime_override() -> Option<String> {
resolve_default_runtime(&std::env::var("BUZZ_DEFAULT_RUNTIME").ok()?)
}

/// The bundled default engine: buzz-agent ships with the app, so it is safe
/// on a stock install where no third-party CLI is on PATH.
pub(super) fn bundled_default_agent_command() -> String {
super::known_acp_runtime_exact("buzz-agent")
.and_then(|p| p.commands.first().copied())
.unwrap_or("buzz-agent")
.to_string()
}

/// opencode runtime tests live in a sibling file (file-size ratchet keeps
/// discovery.rs/tests.rs at their merge-base sizes; this module has headroom).
#[cfg(test)]
#[path = "tests/opencode.rs"]
mod opencode_tests;

#[cfg(test)]
mod tests {
use super::super::known_acp_runtime_exact;
Expand Down Expand Up @@ -123,4 +212,33 @@ mod tests {
assert!(codex.adapter_install_instructions_url.contains("codex-acp"));
assert!(codex.cli_install_hint.contains("Codex CLI"));
}

#[test]
fn opencode_metadata_pins_native_acp_shape() {
let opencode = known_acp_runtime_exact("opencode").unwrap();

// Native ACP: the CLI is the agent — no adapter tier, and the CLI
// itself doubles as the underlying-CLI marker so Phase-1 install runs.
assert_eq!(opencode.commands, &["opencode"]);
assert_eq!(opencode.underlying_cli, Some("opencode"));
assert!(opencode.adapter_install_commands.is_empty());
assert!(opencode
.cli_install_commands
.iter()
.any(|cmd| cmd.contains("https://opencode.ai/install")));

// Skills and config follow opencode's documented project layout.
assert_eq!(opencode.skill_dir, Some(".opencode/skills"));
assert_eq!(
opencode.config_file_path,
Some("~/.config/opencode/opencode.json")
);

// No faithful exit-code auth probe exists (`opencode auth list` exits
// 0 with zero credentials) and no native model env var is read by
// opencode core — both stay None until those become probeable.
assert_eq!(opencode.auth_probe_args, None);
assert_eq!(opencode.model_env_var, None);
assert_eq!(opencode.provider_env_var, None);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
//! opencode runtime behavior: ACP-mode default args and the
//! `BUZZ_DEFAULT_RUNTIME` engine override resolution.

use super::resolve_default_runtime;
use crate::managed_agents::discovery::normalize_agent_args;

#[test]
fn opencode_defaults_to_acp_mode_args() {
// Bare `opencode` starts the TUI; ACP needs the `acp` subcommand, so the
// empty-args default must be `["acp"]` (goose pattern).
assert_eq!(
normalize_agent_args("opencode", Vec::new()),
vec!["acp".to_string()]
);
}

#[test]
fn resolve_default_runtime_accepts_known_harness_ids() {
// Tier-1 builtin id and a raw command form both resolve; whitespace is
// tolerated so operators can quote the value loosely.
assert_eq!(resolve_default_runtime("opencode"), Some("opencode".into()));
assert_eq!(resolve_default_runtime(" goose "), Some("goose".into()));
}

#[test]
fn resolve_default_runtime_rejects_empty_and_unknown_ids() {
// Empty/blank keeps the bundled default silently; unknown ids do too but
// are worth a warning — pinned agents must never dangle.
assert_eq!(resolve_default_runtime(""), None);
assert_eq!(resolve_default_runtime(" "), None);
assert_eq!(resolve_default_runtime("definitely-not-a-harness"), None);
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 5 additions & 3 deletions desktop/src/features/onboarding/ui/RuntimeIcon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,24 @@ import type { AcpRuntimeCatalogEntry } from "@/shared/api/types";
import { cn } from "@/shared/lib/cn";
import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark";
import claudeLogoUrl from "../assets/harness-logos/claude.png?inline";
import opencodeLogoUrl from "../assets/harness-logos/opencode.svg?inline";
import { RUNTIME_MARKS } from "./HarnessMarks";

// Bundled logos for compiled-in runtimes (inline base64, no network fetch).
// Monochrome marks live in RUNTIME_MARKS instead — inline SVGs that follow
// `currentColor`, so they adapt to dark/light without bitmap filters.
const RUNTIME_LOGOS: Record<string, string> = {
claude: claudeLogoUrl,
opencode: opencodeLogoUrl,
};

// Public-path logos for bundled presets. Served from /harness-logos/ at runtime.
// Keys match the preset `id` values emitted by the backend PRESET_HARNESSES.
// Public-path logos for bundled presets. Served from /harness-logos/ at
// runtime. Keys match the preset `id` values emitted by the backend
// PRESET_HARNESSES; builtin runtimes belong in RUNTIME_LOGOS above.
export const PRESET_LOGOS: Record<string, string> = {
devin: "/harness-logos/devin.svg",
omp: "/harness-logos/omp.svg",
grok: "/harness-logos/grok.svg",
opencode: "/harness-logos/opencode.svg",
kimi: "/harness-logos/kimi.png",
amp: "/harness-logos/amp.png",
hermes: "/harness-logos/hermes.png",
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/features/settings/ui/harnessCatalogCopy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ const HARNESS_DESCRIPTIONS: Record<string, string> = {
// Source: https://block.github.io/goose/ — "an open source, extensible AI
// agent".
goose: "Block's open-source, extensible AI agent.",
// Source: https://github.com/anomalyco/opencode
opencode: "An open-source coding agent.",

// Bundled presets — sources per RESEARCH/BYOH_CATALOG_IA.md.
// Source: https://cursor.com/docs/cli/acp
Expand All @@ -30,8 +32,6 @@ const HARNESS_DESCRIPTIONS: Record<string, string> = {
// Source: https://build.x.ai (docs unavailable during research; kept
// deliberately conservative).
grok: "xAI's coding agent, connected to Buzz through its ACP entrypoint.",
// Source: https://github.com/anomalyco/opencode
opencode: "An open-source coding agent.",
// Sources: https://github.com/MoonshotAI/kimi-cli,
// https://moonshotai.github.io/kimi-cli/en/
kimi: "A terminal coding agent for software development and command-line tasks.",
Expand Down
35 changes: 17 additions & 18 deletions desktop/tests/e2e/harness-catalog-screenshots.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,23 @@ const CATALOG = [
node_required: false,
auth_status: { status: "unknown" },
},
{
id: "opencode",
label: "OpenCode",
avatar_url: "",
availability: "not_installed",
command: null,
binary_path: null,
default_args: ["acp"],
mcp_command: null,
install_hint:
"Buzz talks to OpenCode through the OpenCode CLI's ACP mode (opencode acp).",
install_instructions_url: "https://opencode.ai/docs",
can_auto_install: true,
underlying_cli_path: null,
node_required: false,
auth_status: { status: "not_applicable" },
},
{
id: "cursor",
label: "Cursor",
Expand Down Expand Up @@ -97,24 +114,6 @@ const CATALOG = [
auth_status: { status: "not_applicable" },
source: "preset",
},
{
id: "opencode",
label: "OpenCode",
avatar_url: "",
availability: "not_installed",
command: null,
binary_path: null,
default_args: ["acp"],
mcp_command: null,
install_hint:
"Buzz talks to OpenCode through its CLI's ACP mode (opencode acp).",
install_instructions_url: "https://opencode.ai/docs",
can_auto_install: false,
underlying_cli_path: null,
node_required: false,
auth_status: { status: "not_applicable" },
source: "preset",
},
{
id: "my-custom",
label: "My Custom Harness",
Expand Down
7 changes: 4 additions & 3 deletions docs/remote-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ The defining constraint, stated as a design axiom:
An agent's identity is a Nostr keypair. The **agent record** on `D` carries:
`name`, `relay_url`, the nsec (keyring-hydrated), the NIP-OA `auth` tag
attesting owner authorization, `agent_command`/`agent_args` (the ACP agent the
harness spawns — `goose`, `claude-agent-acp`, `codex-acp`, `buzz-agent`, or
harness spawns — `goose`, `claude-agent-acp`, `codex-acp`, `opencode`, `buzz-agent`, or
any user-supplied command: this is the **configurable harness** requirement),
effective `system_prompt`/`model`/`provider`, timeout and parallelism knobs,
the `respond_to` gate, merged `env_vars`, and a `backend` discriminator:
Expand Down Expand Up @@ -1028,8 +1028,9 @@ the local spawn's `credential.<relay-url>/git.helper` scoping — never a
global `credential.helper`: a global nostr helper would answer for every
remote, including github.com. ~15–25MB;
not FROM-scratch (bash and git preclude it). Sprig-only: alternate-harness
dependencies (node for Claude Code / Codex) come via the `image` override
field, not a fatter default. Tagging follows the relay image's matrix —
dependencies (node for Claude Code / Codex, the OpenCode binary) come via the
`image` override field, not a fatter default — see
`examples/sprig-opencode/` for the reference derived image. Tagging follows the relay image's matrix —
`sha-<short>` on main, semver on `sprig-v*` tags (the sprig tarball's
`+git.<sha>` version string is not a legal Docker tag). **The default image
reference MUST be pinned by digest, not tag**: the provider bakes, at
Expand Down
Loading