From 9aa3b97622dff2a1967c9b8c6c99d3cdbbefd944 Mon Sep 17 00:00:00 2001 From: gudnuf Date: Thu, 9 Jul 2026 10:11:18 -0700 Subject: [PATCH] feat(desktop): register Cursor as a native ACP runtime MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add `cursor` to KNOWN_ACP_RUNTIMES so Desktop can spawn `agent acp` (no separate *-acp adapter), with readiness via `agent status` and process-sweep coverage. Extension-method policy for headless buzz-acp is intentionally left for sprout — called out in comments/README. Co-authored-by: Cursor --- crates/buzz-acp/README.md | 22 +++++++++- .../src-tauri/src/managed_agents/discovery.rs | 42 ++++++++++++++++-- .../src/managed_agents/discovery/tests.rs | 44 ++++++++++++++++++- .../src-tauri/src/managed_agents/readiness.rs | 5 +++ .../src-tauri/src/managed_agents/runtime.rs | 4 ++ .../src/managed_agents/runtime/tests.rs | 14 ++++++ 6 files changed, 126 insertions(+), 5 deletions(-) diff --git a/crates/buzz-acp/README.md b/crates/buzz-acp/README.md index 9399766f17..5eb987bcc2 100644 --- a/crates/buzz-acp/README.md +++ b/crates/buzz-acp/README.md @@ -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 @@ -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). diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index 059ab988f1..03cc492248 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -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"; @@ -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, @@ -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", @@ -470,7 +505,8 @@ pub fn create_time_agent_command_override( fn default_agent_args(command: &str) -> Option> { 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, diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests.rs b/desktop/src-tauri/src/managed_agents/discovery/tests.rs index 260bc9e3b8..273aa9569e 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/tests.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/tests.rs @@ -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; @@ -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!( diff --git a/desktop/src-tauri/src/managed_agents/readiness.rs b/desktop/src-tauri/src/managed_agents/readiness.rs index aff4bf17ae..c7307515e2 100644 --- a/desktop/src-tauri/src/managed_agents/readiness.rs +++ b/desktop/src-tauri/src/managed_agents/readiness.rs @@ -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 @@ -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![], } } diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index 354e343070..72bfb5e36a 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -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. diff --git a/desktop/src-tauri/src/managed_agents/runtime/tests.rs b/desktop/src-tauri/src/managed_agents/runtime/tests.rs index bfb7e1cbd2..b0f3830c9b 100644 --- a/desktop/src-tauri/src/managed_agents/runtime/tests.rs +++ b/desktop/src-tauri/src/managed_agents/runtime/tests.rs @@ -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]