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
22 changes: 21 additions & 1 deletion crates/buzz-acp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Buzz Relay ──WS──→ buzz-acp ──stdio──→ Your Agent
(send_message, etc.)
```

Supports any agent that speaks [ACP](https://agentclientprotocol.com/) over stdio: **goose**, **codex** (via [codex-acp](https://github.com/zed-industries/codex-acp)), and **claude code** (via [claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp)).
Supports any agent that speaks [ACP](https://agentclientprotocol.com/) over stdio: **goose**, **codex** (via [codex-acp](https://github.com/zed-industries/codex-acp)), **claude code** (via [claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp)), and **cursor** (native `agent acp` — no separate adapter).

## Prerequisites

Expand Down Expand Up @@ -91,6 +91,26 @@ buzz-acp
Older installs that still expose `claude-code-acp` are also supported. `buzz-acp`
treats both Claude ACP command names as the same zero-arg runtime.

## Running with Cursor

Cursor's Agent CLI speaks ACP natively — no separate `*-acp` adapter.

```bash
# Install: https://cursor.com/docs/cli (binary lands as `agent` / `cursor-agent`)
agent login # or export CURSOR_API_KEY=...

export BUZZ_ACP_AGENT_COMMAND="agent"
export BUZZ_ACP_AGENT_ARGS="acp"

buzz-acp
```

> **Harness note:** Cursor ACP can emit blocking extension methods
> (`cursor/ask_question`, `cursor/create_plan`). Headless `buzz-acp` needs an
> explicit client policy for those (auto-allow vs reject-with-reason) so turns
> neither hang nor over-permit. That policy is a separate design item from
> registering the runtime in Desktop's `KNOWN_ACP_RUNTIMES`.

## Configuration

All configuration is via environment variables (or CLI flags — every env var has a matching flag).
Expand Down
42 changes: 39 additions & 3 deletions desktop/src-tauri/src/managed_agents/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ pub(crate) struct 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";
pub(crate) const CURSOR_AVATAR_URL: &str = "https://www.cursor.com/favicon.ico";
const BUZZ_AGENT_AVATAR_URL: &str =
"https://raw.githubusercontent.com/block/buzz/refs/heads/main/crates/buzz-agent/buzz-agent.png";

Expand Down Expand Up @@ -153,10 +154,10 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
mcp_hooks: false,
underlying_cli: Some("codex"),
cli_install_commands: &["curl -fsSL https://chatgpt.com/codex/install.sh | sh"],
adapter_install_commands: &["npm install -g @zed-industries/codex-acp"],
adapter_install_commands: &["nix profile install --impure --file ~/buzz/nix/codex-acp-latest.nix"],
install_instructions_url: "https://github.com/zed-industries/codex-acp",
cli_install_hint: "Install the Codex CLI via the official install script.",
adapter_install_hint: "Install the Codex ACP adapter via npm.",
adapter_install_hint: "Install the latest Codex ACP adapter via the Buzz Nix package.",
skill_dir: Some(".codex/skills"),
supports_acp_model_switching: false,
model_env_var: None,
Expand All @@ -171,6 +172,40 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
context_limit_env_var: None,
required_normalized_fields: &[],
},
// Cursor CLI speaks ACP natively (`agent acp`) — no separate *-acp adapter.
// Auth is `agent login` / CURSOR_API_KEY; see readiness probe.
// NOTE (sprout): Cursor ACP extension methods (cursor/ask_question,
// cursor/create_plan) are blocking client RPCs. Headless buzz-acp needs an
// explicit permission/policy decision so turns neither hang nor over-permit.
// That harness policy is intentionally NOT part of this registry entry.
KnownAcpRuntime {
id: "cursor",
label: "Cursor",
commands: &["agent", "cursor-agent"],
aliases: &["cursor"],
avatar_url: CURSOR_AVATAR_URL,
mcp_command: Some("buzz-dev-mcp"),
mcp_hooks: false,
underlying_cli: Some("agent"),
cli_install_commands: &["curl -fsSL https://cursor.com/install | bash"],
adapter_install_commands: &[],
install_instructions_url: "https://cursor.com/docs/cli/acp",
cli_install_hint: "Install the Cursor Agent CLI via the official install script.",
adapter_install_hint: "",
skill_dir: Some(".cursor/skills"),
supports_acp_model_switching: false,
model_env_var: None,
provider_env_var: None,
provider_locked: false,
default_env: &[],
config_file_path: Some("~/.cursor/cli-config.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: &[],
},
KnownAcpRuntime {
id: "buzz-agent",
label: "Buzz Agent",
Expand Down Expand Up @@ -470,7 +505,8 @@ pub fn create_time_agent_command_override(

fn default_agent_args(command: &str) -> Option<Vec<String>> {
match normalize_command_identity(command).as_str() {
"goose" => Some(vec!["acp".to_string()]),
// goose and Cursor both speak ACP as a subcommand (`goose acp`, `agent acp`).
"goose" | "agent" | "cursor-agent" | "cursor" => 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
44 changes: 43 additions & 1 deletion desktop/src-tauri/src/managed_agents/discovery/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use super::{
divergent_agent_command_override, effective_agent_command, find_via_login_shell,
managed_agent_avatar_url, normalize_agent_args, record_agent_command,
update_time_agent_command_override, BUZZ_AGENT_AVATAR_URL, CLAUDE_CODE_AVATAR_URL,
CODEX_AVATAR_URL, GOOSE_AVATAR_URL,
CODEX_AVATAR_URL, CURSOR_AVATAR_URL, GOOSE_AVATAR_URL,
};
use crate::managed_agents::AcpAvailabilityStatus;

Expand Down Expand Up @@ -69,6 +69,48 @@ fn normalizes_claude_and_codex_args_to_empty() {
);
}

#[test]
fn normalizes_cursor_args_to_acp_subcommand() {
// Cursor speaks ACP natively as `agent acp` (same shape as goose).
assert_eq!(
normalize_agent_args("agent", Vec::new()),
vec!["acp".to_string()]
);
assert_eq!(
normalize_agent_args("cursor-agent", Vec::new()),
vec!["acp".to_string()]
);
assert_eq!(
normalize_agent_args("cursor", Vec::new()),
vec!["acp".to_string()]
);
// Explicit `acp` is preserved (default is already `["acp"]`).
assert_eq!(
normalize_agent_args("agent", vec!["acp".into()]),
vec!["acp".to_string()]
);
}

#[test]
fn resolves_cursor_avatar() {
assert_eq!(
managed_agent_avatar_url("agent"),
Some(CURSOR_AVATAR_URL.to_string())
);
assert_eq!(
managed_agent_avatar_url("cursor-agent"),
Some(CURSOR_AVATAR_URL.to_string())
);
assert_eq!(
managed_agent_avatar_url("/Users/me/.local/bin/agent"),
Some(CURSOR_AVATAR_URL.to_string())
);
assert_eq!(
managed_agent_avatar_url("cursor"),
Some(CURSOR_AVATAR_URL.to_string())
);
}

#[test]
fn resolves_buzz_agent_avatar() {
assert_eq!(
Expand Down
5 changes: 5 additions & 0 deletions desktop/src-tauri/src/managed_agents/readiness.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,8 @@ impl AgentReadiness {
/// * **claude**: a successful `claude auth status` probe.
/// * **codex**: a successful `codex login status` probe (checks the codex
/// credential store — NOT `OPENAI_API_KEY`).
/// * **cursor**: a successful `agent status` probe (Cursor CLI auth —
/// `agent login` / `CURSOR_API_KEY`; native ACP, no separate adapter).
/// * **unknown / custom command**: always `Ready` (no requirements known).
///
/// Databricks note: `DATABRICKS_TOKEN` is `.unwrap_or_default()` in
Expand Down Expand Up @@ -252,6 +254,9 @@ fn collect_missing_requirements(
rt,
),
"codex" => cli_login_requirements(&["codex", "login", "status"], "run `codex login`", rt),
"cursor" => {
cli_login_requirements(&["agent", "status"], "run `agent login`", rt)
}
_ => vec![],
}
}
Expand Down
4 changes: 4 additions & 0 deletions desktop/src-tauri/src/managed_agents/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ pub(crate) const KNOWN_AGENT_BINARIES: &[&str] = &[
"codex-acp",
"codex_acp",
"goose",
// Cursor Agent CLI (native ACP via `agent acp` / `cursor-agent acp`).
"agent",
"cursor-agent",
"cursor_agent",
// buzz-dev-mcp's multicall personalities (rg, tree, buzz,
// git-credential-nostr, git-sign-nostr) are short-lived per-tool-call
// invocations — not listed here.
Expand Down
14 changes: 14 additions & 0 deletions desktop/src-tauri/src/managed_agents/runtime/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,20 @@ fn codex_has_mcp_command() {
let p = known_acp_runtime("codex-acp").expect("should resolve");
assert!(!p.mcp_hooks, "codex-acp does not handle MCP_HOOK_SERVERS");
assert_eq!(p.mcp_command, Some("buzz-dev-mcp"));

let p = known_acp_runtime("agent").expect("cursor agent should resolve");
assert_eq!(p.id, "cursor");
assert!(!p.mcp_hooks, "cursor does not handle MCP_HOOK_SERVERS");
assert_eq!(p.skill_dir, Some(".cursor/skills"));
assert_eq!(p.mcp_command, Some("buzz-dev-mcp"));
assert!(
known_acp_runtime("cursor-agent").is_some_and(|r| r.id == "cursor"),
"cursor-agent alias should resolve"
);
assert!(
known_acp_runtime("cursor").is_some_and(|r| r.id == "cursor"),
"cursor id/alias should resolve"
);
}

#[test]
Expand Down