diff --git a/crates/buzz-acp/README.md b/crates/buzz-acp/README.md index d9cd362cb8..abb1136167 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/agentclientprotocol/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**, **grok** (`grok agent stdio`), **codex** (via [codex-acp](https://github.com/agentclientprotocol/codex-acp)), and **claude code** (via [claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp)). ## Prerequisites @@ -89,6 +89,27 @@ 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 Grok Build + +Grok Build speaks ACP natively over stdio — no separate `*-acp` adapter. + +```bash +# Install: https://x.ai/cli (binary often lands in ~/.grok/bin) +grok login # or export XAI_API_KEY=... + +export BUZZ_ACP_AGENT_COMMAND="grok" +export BUZZ_ACP_AGENT_ARGS="agent --always-approve stdio" + +> **Maintainer note:** Grok installs may also put a bare `agent` shim on PATH. +> Buzz registers **`grok` + args**, never a second runtime keyed as nested `agent`. +> Cursor uses `cursor-agent` for the same reason (shared shim name). + +buzz-acp +``` + +`--always-approve` is recommended for managed agents so tool permission prompts +do not block headless turns. + ## Configuration All configuration is via environment variables (or CLI flags — every env var has a matching flag). @@ -169,7 +190,24 @@ buzz-acp --respond-to anyone buzz-acp --respond-to nobody --heartbeat-interval 300 ``` -### Configuration Examples +### Running with Grok Build + +Grok Build speaks ACP natively over stdio — no separate `*-acp` adapter. + +```bash +# Install: https://x.ai/cli (binary often lands in ~/.grok/bin) +grok login # or export XAI_API_KEY=... + +export BUZZ_ACP_AGENT_COMMAND="grok" +export BUZZ_ACP_AGENT_ARGS="agent --always-approve stdio" + +buzz-acp +``` + +`--always-approve` is recommended for managed agents so tool permission prompts +do not block headless turns. + +## Configuration Examples **Single agent, no heartbeat (default):** ```bash diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index 50206567ad..7a3be5d8bd 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -13,10 +13,11 @@ mod runtime_metadata; 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 = +pub(crate) const GOOSE_AVATAR_URL: &str = "https://goose-docs.ai/img/logo_dark.png"; +pub(crate) const CLAUDE_CODE_AVATAR_URL: &str = "https://anthropic.gallerycdn.vsassets.io/extensions/anthropic/claude-code/2.1.77/1773707456892/Microsoft.VisualStudio.Services.Icons.Default"; +pub(crate) const GROK_AVATAR_URL: &str = "https://x.ai/favicon.ico"; +pub(crate) 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 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] { @@ -38,6 +39,7 @@ fn common_binary_paths() -> &'static [PathBuf] { paths.extend([ home.join(".local/share/mise/shims"), home.join(".local/bin"), + home.join(".grok/bin"), home.join(".volta/bin"), home.join(".asdf/shims"), ]); @@ -157,6 +159,50 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[ // Verified: `codex login status` exits 0 when logged in, non-zero otherwise. auth_probe_args: Some(&["codex", "login", "status"]), }, + + // Grok Build speaks ACP natively over stdio (`grok agent stdio`) — + // no separate *-acp npm adapter (same class as Goose / Cursor). + // Auth: `grok login` or XAI_API_KEY. For managed/headless agents we default + // `--always-approve` so tool calls do not block on a TTY prompt. + // + // PATH collision note for maintainers: the install may also drop an + // `agent` shim under ~/.local/bin. Registry discovery intentionally uses + // ONLY the `grok` binary (+ `agent` / `stdio` ARGS). Bare `agent` must + // never appear in `commands` here — Cursor's entry owns that name and + // disambiguates via path heuristics (see cursor-acp PR / is_cursor_agent_binary). + KnownAcpRuntime { + id: "grok", + label: "Grok Build", + commands: &["grok"], + aliases: &["grok-build", "grokbuild"], + avatar_url: GROK_AVATAR_URL, + mcp_command: Some("buzz-dev-mcp"), + mcp_hooks: false, + underlying_cli: Some("grok"), + cli_install_commands: &["curl -fsSL https://x.ai/cli/install.sh | bash"], + cli_install_commands_windows: &[], + adapter_install_commands: &[], + install_instructions_url: "https://docs.x.ai/developers/guides/grok-cli", + cli_install_hint: "Install Grok Build via the official xAI install script.", + adapter_install_hint: "", + skill_dir: Some(".grok/skills"), + supports_acp_model_switching: false, + model_env_var: Some("XAI_MODEL"), + provider_env_var: None, + provider_locked: false, + default_env: &[], + config_file_path: Some("~/.grok/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: &[], + // No stable `grok auth status` probe today — login is interactive / + // env-key based (same posture as Goose for auth_probe_args). + login_hint: Some("Run `grok login` to authenticate (or set XAI_API_KEY)."), + auth_probe_args: None, + }, KnownAcpRuntime { id: "buzz-agent", label: "Buzz Agent", @@ -342,7 +388,15 @@ pub use overrides::{apply_agent_command_update, create_time_agent_command_overri fn default_agent_args(command: &str) -> Option> { match normalize_command_identity(command).as_str() { + // goose speaks ACP as a subcommand (`goose acp`). "goose" => Some(vec!["acp".to_string()]), + // Grok Build ACP server over stdio. `--always-approve` keeps managed + // agents from blocking on interactive tool-permission prompts. + "grok" | "grok-build" | "grokbuild" => Some(vec![ + "agent".to_string(), + "--always-approve".to_string(), + "stdio".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 0ed4fe0f6a..7bfe261406 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/tests.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/tests.rs @@ -7,7 +7,7 @@ use super::{ effective_agent_command, find_nvm_default_bin, find_via_login_shell, is_login_shell_path_uninit, is_safe_nvm_tag, managed_agent_avatar_url, normalize_agent_args, parse_semver_tag, probe_codex_acp_major_version, record_agent_command, - refresh_login_shell_path, BUZZ_AGENT_AVATAR_URL, CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, + refresh_login_shell_path, BUZZ_AGENT_AVATAR_URL, CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, GROK_AVATAR_URL, GOOSE_AVATAR_URL, }; use crate::managed_agents::AcpAvailabilityStatus; @@ -72,6 +72,64 @@ fn normalizes_claude_and_codex_args_to_empty() { ); } + +#[test] +fn normalizes_grok_args_to_agent_stdio() { + // Grok Build ACP: `grok agent --always-approve stdio` + let expected = vec![ + "agent".to_string(), + "--always-approve".to_string(), + "stdio".to_string(), + ]; + assert_eq!(normalize_agent_args("grok", Vec::new()), expected); + assert_eq!(normalize_agent_args("grok-build", Vec::new()), expected); + assert_eq!( + normalize_agent_args( + "grok", + vec![ + "agent".into(), + "--always-approve".into(), + "stdio".into() + ] + ), + expected + ); +} + +#[test] +fn grok_does_not_claim_bare_agent_path_shim() { + // Differentiation vs Cursor: Grok registry is `grok` + args only. + // Bare `agent` must NOT resolve to the grok catalog entry. + let grok = super::known_acp_runtime_exact("grok").expect("grok runtime"); + assert!( + !grok.commands.iter().any(|c| *c == "agent"), + "grok.commands must not include bare agent (Cursor owns that disambiguation)" + ); + assert!( + super::known_acp_runtime("agent").map(|r| r.id) != Some("grok"), + "basename agent must not bind Grok Build" + ); + assert_eq!(super::known_acp_runtime("grok").map(|r| r.id), Some("grok")); +} + + +#[test] +fn resolves_grok_avatar() { + assert_eq!( + managed_agent_avatar_url("grok"), + Some(GROK_AVATAR_URL.to_string()) + ); + assert_eq!( + managed_agent_avatar_url("/Users/me/.grok/bin/grok"), + Some(GROK_AVATAR_URL.to_string()) + ); + assert_eq!( + managed_agent_avatar_url("grok-build"), + Some(GROK_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 87ee6241ee..e91897297d 100644 --- a/desktop/src-tauri/src/managed_agents/readiness.rs +++ b/desktop/src-tauri/src/managed_agents/readiness.rs @@ -247,6 +247,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`). +/// * **grok**: no stable CLI auth-status probe yet — use `grok login` / `XAI_API_KEY` +/// (auth_probe_args is None; status reports not_applicable when installed). /// * **unknown / custom command**: always `Ready` (no requirements known). /// /// Databricks note: `DATABRICKS_TOKEN` is `.unwrap_or_default()` in diff --git a/desktop/src-tauri/src/managed_agents/runtime.rs b/desktop/src-tauri/src/managed_agents/runtime.rs index cef669ab25..8a0ac88454 100644 --- a/desktop/src-tauri/src/managed_agents/runtime.rs +++ b/desktop/src-tauri/src/managed_agents/runtime.rs @@ -43,6 +43,8 @@ pub(crate) const KNOWN_AGENT_BINARIES: &[&str] = &[ "codex-acp", "codex_acp", "goose", + // Grok Build (native ACP via `grok agent stdio`). + "grok", // 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 d86b69938f..653ce01598 100644 --- a/desktop/src-tauri/src/managed_agents/runtime/tests.rs +++ b/desktop/src-tauri/src/managed_agents/runtime/tests.rs @@ -946,3 +946,26 @@ fn invalid_pubkey_resolves_no_pair_key() { // the summary must fall back to the stopped/legacy-pid path, not panic. assert!(super::resolve_workspace_pair_key("not-a-key", "", "wss://one.example").is_none()); } + +#[test] +fn grok_runtime_resolves_native_acp() { + use crate::managed_agents::discovery::{known_acp_runtime, normalize_agent_args}; + let p = known_acp_runtime("grok").expect("grok should resolve"); + assert_eq!(p.id, "grok"); + assert_eq!(p.label, "Grok Build"); + assert_eq!(p.skill_dir, Some(".grok/skills")); + assert_eq!(p.mcp_command, Some("buzz-dev-mcp")); + assert!(p.auth_probe_args.is_none()); + assert!( + known_acp_runtime("grok-build").is_some_and(|r| r.id == "grok"), + "grok-build alias should resolve" + ); + assert_eq!( + normalize_agent_args("grok", Vec::new()), + vec![ + "agent".to_string(), + "--always-approve".to_string(), + "stdio".to_string() + ] + ); +} diff --git a/desktop/src/features/onboarding/ui/agentReadiness.ts b/desktop/src/features/onboarding/ui/agentReadiness.ts index 86b9721af3..334ff39f90 100644 --- a/desktop/src/features/onboarding/ui/agentReadiness.ts +++ b/desktop/src/features/onboarding/ui/agentReadiness.ts @@ -47,7 +47,9 @@ export function resolveAgentReadiness( } if ( - (preferredRuntime.id === "claude" || preferredRuntime.id === "codex") && + (preferredRuntime.id === "claude" || + preferredRuntime.id === "codex" || + preferredRuntime.id === "grok") && (preferredRuntime.authStatus.status === "logged_in" || preferredRuntime.authStatus.status === "not_applicable") ) { diff --git a/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.test.mjs b/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.test.mjs index b10aa19154..83fd73067c 100644 --- a/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.test.mjs +++ b/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.test.mjs @@ -12,9 +12,10 @@ function runtime(id, availability, status) { return { id, availability, authStatus: { status } }; } -test("only Claude Code and Codex are visible in onboarding", () => { +test("Claude Code, Codex, and Grok Build are visible in onboarding", () => { assert.equal(runtimeIsVisibleInOnboarding("claude"), true); assert.equal(runtimeIsVisibleInOnboarding("codex"), true); + assert.equal(runtimeIsVisibleInOnboarding("grok"), true); assert.equal(runtimeIsVisibleInOnboarding("goose"), false); assert.equal(runtimeIsVisibleInOnboarding("buzz-agent"), false); assert.equal(runtimeIsVisibleInOnboarding("custom"), false); @@ -24,13 +25,14 @@ test("visible onboarding runtimes use the product order", () => { const runtimes = [ runtime("buzz-agent", "available", "not_applicable"), runtime("codex", "available", "logged_in"), + runtime("grok", "available", "not_applicable"), runtime("goose", "available", "not_applicable"), runtime("claude", "available", "logged_in"), ]; assert.deepEqual( getVisibleOnboardingRuntimes(runtimes).map(({ id }) => id), - ["claude", "codex"], + ["claude", "codex", "grok"], ); }); diff --git a/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.ts b/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.ts index 51339e2afe..40f4da3a47 100644 --- a/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.ts +++ b/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.ts @@ -1,6 +1,6 @@ import type { AcpRuntimeCatalogEntry } from "@/shared/api/types"; -export const ONBOARDING_RUNTIME_ORDER = ["claude", "codex"]; +export const ONBOARDING_RUNTIME_ORDER = ["claude", "codex", "grok"]; const VISIBLE_ONBOARDING_RUNTIME_IDS = new Set( ONBOARDING_RUNTIME_ORDER,