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
14 changes: 8 additions & 6 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ header fallback. There is no REST API for fetching message threads — use

## ACP Harness (optional, end-to-end with a real agent)

`buzz-acp` connects an ACP-speaking agent (goose, codex, claude code,
buzz-agent) to the relay. The harness listens for events, drives the
`buzz-acp` connects an ACP-speaking agent (goose, grok build, codex,
claude code, buzz-agent) to the relay. The harness listens for events, drives the
agent over stdio, and the agent replies through MCP tools.

Minimum recipe — assumes the relay from step 3 is running and the channel
Expand Down Expand Up @@ -221,10 +221,12 @@ buzz-acp # foreground; logs to stdout (run in
```

> **Using a different ACP agent?** The default recipe assumes `goose` is on
> `$PATH` and configured (`goose --version` should print). For codex / claude
> code / buzz-agent, set `BUZZ_ACP_AGENT_COMMAND` and `BUZZ_ACP_AGENT_ARGS`
> accordingly — see `crates/buzz-acp/README.md`. Without these, buzz-acp
> will fail to spawn the agent subprocess on startup.
> `$PATH` and configured (`goose --version` should print). For grok build /
> codex / claude code / buzz-agent, set `BUZZ_ACP_AGENT_COMMAND` and
> `BUZZ_ACP_AGENT_ARGS` accordingly — see `crates/buzz-acp/README.md`
> (grok build: `BUZZ_ACP_AGENT_COMMAND=grok` with args
> `agent --always-approve stdio`). Without these, buzz-acp will fail to
> spawn the agent subprocess on startup.

If you started the agent before adding it to the channel, just run the
`add-member` afterwards — it picks up the membership notification live and
Expand Down
2 changes: 1 addition & 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/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 build** (native, via `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

Expand Down
21 changes: 21 additions & 0 deletions crates/buzz-acp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,6 +617,14 @@ 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()]),
// `--always-approve` is intentional: Buzz owns the approval layer for
// managed agents, so Grok-side tool prompts would deadlock a headless
// stdio session.
"grok" | "grok-build" => 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,
Expand Down Expand Up @@ -1440,6 +1448,19 @@ mod tests {
assert_eq!(normalize_agent_args("goose", vec!["".into()]), vec!["acp"]);
}

#[test]
fn normalizes_grok_args_to_stdio_default() {
let expected = vec!["agent", "--always-approve", "stdio"];
assert_eq!(normalize_agent_args("grok", Vec::new()), expected);
assert_eq!(normalize_agent_args("grok", vec!["".into()]), expected);
assert_eq!(normalize_agent_args("grok-build", Vec::new()), expected);
// Explicit args are preserved verbatim — no default-arg injection.
assert_eq!(
normalize_agent_args("grok", vec!["agent".into(), "stdio".into()]),
vec!["agent", "stdio"]
);
}

#[test]
fn normalizes_codex_and_claude_args_to_empty() {
assert_eq!(
Expand Down
7 changes: 6 additions & 1 deletion crates/buzz-acp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,12 @@ fn is_subcommand(name: &str) -> bool {
}

/// Timeout for lightweight helper subcommands (spawn + initialize + model/method probes).
const MODELS_TIMEOUT: Duration = Duration::from_secs(10);
///
/// 30s rather than 10s: some agents boot every user-configured MCP server
/// during `session/new` (observed with Grok Build, where a handful of stdio
/// MCP servers pushed session creation past 10s on a real machine). The
/// happy path is unaffected — fast agents still return in ~2-5s.
const MODELS_TIMEOUT: Duration = Duration::from_secs(30);

/// Timeout for `buzz-acp authenticate`. Browser-based vendor auth can require
/// human interaction, so it must not share the short probe timeout.
Expand Down
47 changes: 47 additions & 0 deletions desktop/src-tauri/src/managed_agents/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ 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 GROK_AVATAR_URL: &str = "https://x.ai/icon.png";
const BUZZ_AGENT_AVATAR_URL: &str =
"https://raw.githubusercontent.com/block/buzz/refs/heads/main/crates/buzz-agent/buzz-agent.png";

Expand All @@ -41,6 +42,7 @@ fn common_binary_paths() -> &'static [PathBuf] {
home.join(".local/bin"),
home.join(".volta/bin"),
home.join(".asdf/shims"),
home.join(".grok/bin"),
]);
}
// Windows well-known dirs for npm global shims and standalone installer targets.
Expand Down Expand Up @@ -163,6 +165,43 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
// Verified: `codex login status` exits 0 when logged in, non-zero otherwise.
auth_probe_args: Some(&["codex", "login", "status"]),
},
KnownAcpRuntime {
id: "grok",
label: "Grok Build",
commands: &["grok"],
aliases: &["grok-build"],
avatar_url: GROK_AVATAR_URL,
mcp_command: None,
mcp_hooks: false,
underlying_cli: Some("grok"),
cli_install_commands: &["curl -fsSL https://x.ai/cli/install.sh | bash"],
// No official PowerShell installer documented yet; Windows users get
// the Unix command as a hint alongside the instructions URL.
cli_install_commands_windows: &[],
adapter_install_commands: &[],
cli_install_instructions_url: "https://x.ai/cli",
adapter_install_instructions_url: "",
cli_install_hint: "Buzz requires the Grok CLI; `grok agent stdio` provides the ACP server.",
adapter_install_hint: "",
skill_dir: Some(".grok/skills"),
supports_acp_model_switching: false,
model_env_var: None,
provider_env_var: None,
provider_locked: true,
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,
required_normalized_fields: &[],
login_hint: Some("Run `grok login` (or set XAI_API_KEY) to authenticate."),
// Grok 0.2.x has no verified non-interactive auth-status command
// (`grok models` exits 0 when logged in, but the logged-out exit code
// is unverified), so the probe is intentionally omitted.
auth_probe_args: None,
},
KnownAcpRuntime {
id: "buzz-agent",
label: "Buzz Agent",
Expand Down Expand Up @@ -457,6 +496,14 @@ 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()]),
// `--always-approve` is intentional: Buzz owns the approval layer for
// managed agents, so Grok-side tool prompts would deadlock a headless
// stdio session.
"grok" | "grok-build" => 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,
Expand Down
40 changes: 39 additions & 1 deletion desktop/src-tauri/src/managed_agents/discovery/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use super::{
is_login_shell_path_uninit, is_safe_nvm_tag, managed_agent_avatar_url, normalize_agent_args,
parse_semver_tag, preset_catalog_entry, probe_codex_acp_major_version, record_agent_command,
refresh_login_shell_path, try_record_agent_command, PresetHarness, BUZZ_AGENT_AVATAR_URL,
CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, GOOSE_AVATAR_URL,
CLAUDE_CODE_AVATAR_URL, CODEX_AVATAR_URL, GOOSE_AVATAR_URL, GROK_AVATAR_URL,
};
use crate::managed_agents::AcpAvailabilityStatus;

Expand Down Expand Up @@ -72,6 +72,44 @@ fn normalizes_claude_and_codex_args_to_empty() {
);
}

#[test]
fn normalizes_grok_args_to_stdio_default() {
let expected = vec![
"agent".to_string(),
"--always-approve".to_string(),
"stdio".to_string(),
];
// Empty args resolve to the full agent-mode invocation.
assert_eq!(normalize_agent_args("grok", Vec::new()), expected);
// Path and alias forms normalize to the same identity.
assert_eq!(
normalize_agent_args("/home/dev/.grok/bin/grok", Vec::new()),
expected
);
assert_eq!(normalize_agent_args("grok-build", Vec::new()), expected);
// Explicit args are preserved verbatim — no default-arg injection.
assert_eq!(
normalize_agent_args("grok", vec!["agent".into(), "stdio".into()]),
vec!["agent".to_string(), "stdio".to_string()]
);
}

#[test]
fn resolves_grok_avatar() {
assert_eq!(
managed_agent_avatar_url("grok"),
Some(GROK_AVATAR_URL.to_string())
);
assert_eq!(
managed_agent_avatar_url("/home/dev/.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!(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ test("resolveRuntimeProviderCapability classifies known CLI-login runtimes as lo
// The core fix: a not-yet-loaded catalog must not force these to "unknown".
assert.equal(resolveRuntimeProviderCapability("claude", false), "locked");
assert.equal(resolveRuntimeProviderCapability("codex", false), "locked");
assert.equal(resolveRuntimeProviderCapability("grok", false), "locked");
assert.equal(resolveRuntimeProviderCapability(" claude ", false), "locked");
});

Expand Down
5 changes: 3 additions & 2 deletions desktop/src/features/agents/ui/personaRuntimeModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ export type ProviderRuntimeCapability = "capable" | "locked" | "unknown";
* provider. To avoid that, we resolve capability STATICALLY for known ids:
*
* - buzz-agent / goose → "capable" (`isProviderCapable`, id-based).
* - claude / codex → "locked" (CLI-login runtimes; no LLM provider selection).
* - claude / codex / grok → "locked" (CLI-login runtimes; no LLM provider
* selection).
* - anything else (custom, empty, genuinely unknown) → "unknown".
*
* `isProviderCapable` is the caller-supplied {@link
Expand All @@ -33,7 +34,7 @@ export function resolveRuntimeProviderCapability(
return "capable";
}
const id = runtimeId.trim();
if (id === "claude" || id === "codex") {
if (id === "claude" || id === "codex" || id === "grok") {
return "locked";
}
return "unknown";
Expand Down