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
25 changes: 22 additions & 3 deletions 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**, **codex** (via [codex-acp](https://github.com/agentclientprotocol/codex-acp)), **claude code** (via [claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp)), and **google antigravity** (via `agy` on `PATH`, PATH-probed preset — no bundled installer).

## Prerequisites

Expand Down Expand Up @@ -98,6 +98,25 @@ 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 Google Antigravity

Google Antigravity is exposed in Buzz as a PATH-probed preset (`agy` on `PATH`). Buzz does **not** bundle or download an `agy_acp_server` binary — install `agy` via Google's official Antigravity distribution and ensure `agy` is on `PATH`.

```bash
# 1. Verify agy is on PATH
agy --version

# 2. Run with buzz-acp (no BUZZ_ACP_AGENT_ARGS needed for agy)
export BUZZ_PRIVATE_KEY="nsec1..."
export BUZZ_ACP_AGENT_COMMAND="agy"

buzz-acp
```

Buzz Desktop shows the `agy` harness when `agy` is found on `PATH` (via `common_binary_paths` + login-shell `PATH`). When unavailable, the catalog entry shows `Not installed` with a link to `https://antigravity.google`.

> **Note:** `agy` currently exposes `agent`, `mcp`, `models`, `plugin`, `update` subcommands and `--print --output-format stream-json` (no ACP `agy_acp_server` / `session/new` mode was found on the tested `agy 1.1.17` binary). A future ACP shim (`agy -p --output-format stream-json`) or tier-2 preset with no installer is the honest integration path until Google publishes an ACP endpoint.

## Configuration

All configuration is via environment variables (or CLI flags — every env var has a matching flag).
Expand Down Expand Up @@ -267,7 +286,7 @@ 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, Buzz Agent): have auto-installers, auth probes, and first-class onboarding. Their IDs (`goose`, `claude`, `codex`, `buzz-agent`) are reserved and cannot be overridden. **Antigravity** (`antigravity` / `agy`) is currently a tier-1 ID reservation with PATH probing only — no managed download — to avoid alias collision and to allow a future managed or shim integration without ID breakage.

**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.

Expand Down Expand Up @@ -318,7 +337,7 @@ To add a new runtime to the tier-2 gallery:
4. **Add a bundled logo** (64×64 PNG or optimised SVG) to `desktop/public/harness-logos/<id>.png` and add a corresponding entry to `PRESET_LOGOS` in `desktop/src/features/onboarding/ui/RuntimeIcon.tsx`. Record the source and license in `desktop/public/harness-logos/CREDITS.md`. Only bundle a mark whose upstream license permits redistribution; skipping this step is caught by `presetLogos.test.mjs`, which asserts every `PRESET_HARNESSES` id has a mapped logo that exists on disk.
5. Run `cargo test --lib` and `just desktop-typecheck` to verify everything compiles.

The built-in `BUILTIN_IDS` set (`goose`, `claude`, `codex`, `buzz-agent`, and all current preset ids) is the reserved namespace; every other id is available for custom harnesses.
The built-in `BUILTIN_IDS` set (`goose`, `claude`, `codex`, `antigravity`, `buzz-agent`, and all current preset ids) is the reserved namespace; every other id is available for custom harnesses.

## Using Any ACP Agent

Expand Down
75 changes: 69 additions & 6 deletions crates/buzz-acp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -715,8 +715,8 @@ pub(crate) fn normalize_agent_command_identity(command: &str) -> String {
.expect("rsplit always yields at least one element");
let lower = basename.to_ascii_lowercase();
// Windows resolves commands through `.exe` binaries and npm's `.cmd`/`.bat`
// shims; all three name the same runtime identity.
let stem = [".exe", ".cmd", ".bat"]
// shims; Python/PEX artifacts use `.par` on Unix/macOS; all name the same runtime identity.
let stem = [".exe", ".cmd", ".bat", ".par"]
.iter()
.find_map(|extension| lower.strip_suffix(extension))
.unwrap_or(&lower);
Expand All @@ -728,9 +728,10 @@ pub(crate) fn normalize_agent_command_identity(command: &str) -> String {
.collect()
}

fn default_agent_args(command: &str) -> Option<Vec<String>> {
pub(crate) fn default_agent_args(command: &str) -> Option<Vec<String>> {
match normalize_agent_command_identity(command).as_str() {
"goose" => Some(vec!["acp".to_string()]),
"antigravity" | "google-antigravity" | "agy" | "agy-acp-server" => Some(Vec::new()),
"codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code"
| "claudecode" | "buzz-agent" => Some(Vec::new()),
_ => None,
Expand Down Expand Up @@ -825,9 +826,12 @@ pub fn normalize_agent_args(command: &str, agent_args: Vec<String>) -> Vec<Strin
}

// Older callers relied on the Goose-specific default even for runtimes like
// Codex and Claude. Treat that legacy fallback as "no args" for zero-arg
// providers so desktop- and env-based launches behave the same way.
if normalized.len() == 1 && normalized[0].eq_ignore_ascii_case("acp") && default_args.is_empty()
// Codex, Claude, and Antigravity. Treat that legacy fallback as the runtime's
// default args for non-Goose providers so desktop- and env-based launches
// behave the same way.
if normalized.len() == 1
&& normalized[0].eq_ignore_ascii_case("acp")
&& normalize_agent_command_identity(command) != "goose"
{
return default_args;
}
Expand Down Expand Up @@ -1666,6 +1670,28 @@ mod tests {
normalize_agent_command_identity(r"C:\Tools\Hermes\HERMES-AGENT.BAT"),
"hermes-agent"
);
// Antigravity .par and .exe artifacts.
assert_eq!(
normalize_agent_command_identity("agy_acp_server.par"),
"agy-acp-server"
);
assert_eq!(
normalize_agent_command_identity("AGY_ACP_SERVER.PAR"),
"agy-acp-server"
);
assert_eq!(
normalize_agent_command_identity("/opt/google/bin/agy_acp_server.par"),
"agy-acp-server"
);
assert_eq!(
normalize_agent_command_identity("antigravity"),
"antigravity"
);
assert_eq!(
normalize_agent_command_identity("google-antigravity"),
"google-antigravity"
);
assert_eq!(normalize_agent_command_identity("agy"), "agy");
// Non-ASCII must not panic.
assert_eq!(normalize_agent_command_identity("my-agënt"), "my-agënt");
// Edge cases: empty, whitespace-only, bare separators.
Expand All @@ -1675,6 +1701,43 @@ mod tests {
assert_eq!(normalize_agent_command_identity("///"), "");
}

#[test]
fn default_agent_args_empty_for_antigravity_on_all_platforms() {
for cmd in [
"antigravity",
"agy",
"google-antigravity",
"agy_acp_server",
"agy_acp_server.par",
"agy_acp_server.exe",
"/usr/local/bin/agy_acp_server.par",
r"C:\Tools\agy_acp_server.exe",
] {
assert_eq!(
default_agent_args(cmd),
Some(Vec::<String>::new()),
"expected empty args for {cmd}"
);
assert_eq!(normalize_agent_args(cmd, Vec::new()), Vec::<String>::new());
assert_eq!(
normalize_agent_args(cmd, vec!["acp".into()]),
Vec::<String>::new()
);
}
}

#[test]
fn normalize_agent_args_preserves_explicit_custom_antigravity_args() {
assert_eq!(
normalize_agent_args("antigravity", vec!["--flag".into(), "val".into()]),
vec!["--flag", "val"]
);
assert_eq!(
normalize_agent_args("agy", vec!["--model".into(), "gemini".into()]),
vec!["--model", "gemini"]
);
}

#[test]
fn default_agent_env_recognizes_hermes_identities() {
for command in [
Expand Down
2 changes: 2 additions & 0 deletions desktop/public/harness-logos/CREDITS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ license permits redistribution.
| `omp.svg` | [can1357/oh-my-pi](https://github.com/can1357/oh-my-pi) | `667111575ebba136dadfd6989379e7f67e0d40d9` | MIT © 2025 Mario Zechner; © 2025–2026 Can Bölük | `assets/icon.svg` | None |
| `kimi.png` | [MoonshotAI/kimi-cli](https://github.com/MoonshotAI/kimi-cli) | `4a550effdfcb29a25a5d325bf935296cc50cd417` | Apache-2.0; NOTICE: Kimi Code CLI © 2025 Moonshot AI | `web/public/logo.png` | None |
| `grok.svg` | [SpaceXAI brand guidelines](https://x.ai/legal/brand-guidelines) | Retrieved 2026-07-25 | xAI Brand Guidelines: marks may be used to accurately refer to xAI or its services; logos must be used exactly as provided | `SpaceXAI_Grok_Assets.zip` → `Grok_Logomark_Dark.svg` | None |
| `antigravity.svg` | Placeholder neutral mark for Antigravity harness | 2026-08-20 | Neutral geometric placeholder (not a Google brand asset); nominative use only | `desktop/src/features/onboarding/ui/HarnessMarks.tsx` four-point star path | Scaled to 24×24 viewBox, `currentColor` |

## Inline SVG marks (`RUNTIME_MARKS`)

Expand All @@ -26,6 +27,7 @@ Monochrome marks inlined as `currentColor` paths in
|---|---|---|---|---|---|
| Goose | [block/goose](https://github.com/block/goose) | `305849b71709b95b86ed9f11bd3bc939899c0aab` | Apache-2.0 © Block, Inc. | `documentation/static/img/goose.svg` | `fill="#101010"` → `currentColor`; dropped the redundant clipPath wrapper |
| Cursor | [simple-icons](https://github.com/simple-icons/simple-icons) | `16.27.1` (slug `cursor`) | CC0-1.0 (path data); nominative use of the Cursor mark to identify Cursor's harness | `icons/cursor.svg` | `fill` → `currentColor` |
| Antigravity | Neutral placeholder | 2026-08-20 | Neutral placeholder mark (not a Google brand asset) | `desktop/src/features/onboarding/ui/HarnessMarks.tsx` `AntigravityMark` | `fill` → `currentColor`; single four-point star path |

Codex deliberately has **no** bundled mark: the OpenAI blossom was removed
from simple-icons in v16 at the vendor's request, so we do not ship it —
Expand Down
3 changes: 3 additions & 0 deletions desktop/public/harness-logos/antigravity.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 40 additions & 2 deletions desktop/src-tauri/src/managed_agents/custom_harnesses.rs
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ pub(crate) fn validate_harness_definition_pub(def: &HarnessDefinition) -> Result
/// tier-1 runtimes — no hand-maintained copy. Adding a preset to
/// `PRESET_HARNESSES` automatically reserves its ID without a separate edit.
fn builtin_ids() -> impl Iterator<Item = &'static str> {
const TIER1: &[&str] = &["goose", "claude", "codex", "buzz-agent"];
const TIER1: &[&str] = &["goose", "claude", "codex", "buzz-agent", "antigravity"];
let tier2 = crate::managed_agents::discovery::preset_harness_ids();
TIER1.iter().copied().chain(tier2.iter().copied())
}
Expand Down Expand Up @@ -537,7 +537,7 @@ mod tests {
#[test]
fn builtin_ids_are_rejected() {
// Tier-1 hard-coded IDs must always be reserved.
for id in &["goose", "claude", "codex", "buzz-agent"] {
for id in &["goose", "claude", "codex", "buzz-agent", "antigravity"] {
assert!(check_id_collision(id).is_err(), "{id} should be rejected");
}
// Tier-2 preset IDs must also be reserved (derived from PRESET_HARNESSES).
Expand All @@ -546,6 +546,44 @@ mod tests {
}
}

#[test]
fn check_id_collision_rejects_antigravity_case_insensitively() {
assert!(check_id_collision("antigravity").is_err());
assert!(check_id_collision("AntiGravity").is_err());
assert!(check_id_collision("ANTIGRAVITY").is_err());
assert!(check_id_collision("aNtiGravity").is_err());
assert!(check_id_collision("custom-antigravity").is_ok());
assert!(check_id_collision("antigravity-custom").is_ok());
}

#[test]
fn load_custom_harnesses_drops_file_shadowing_antigravity() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("antigravity.json"),
r#"{"id":"antigravity","label":"Shadow Antigravity","command":"fake-agy"}"#,
)
.unwrap();
assert!(
load_custom_harnesses(dir.path()).is_empty(),
"loader must drop a custom harness file with id 'antigravity'"
);
}

#[test]
fn load_custom_harnesses_drops_file_shadowing_antigravity_mixed_case() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("custom.json"),
r#"{"id":"AntiGravity","label":"Shadow Antigravity","command":"fake-agy"}"#,
)
.unwrap();
assert!(
load_custom_harnesses(dir.path()).is_empty(),
"loader must drop a custom harness file with id 'AntiGravity'"
);
}

#[test]
fn unknown_id_passes_collision_check() {
assert!(check_id_collision("my-custom-agent").is_ok());
Expand Down
45 changes: 42 additions & 3 deletions desktop/src-tauri/src/managed_agents/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ const CLAUDE_CODE_AVATAR_URL: &str = "https://anthropic.gallerycdn.vsassets.io/e
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 =
"https://raw.githubusercontent.com/block/buzz/refs/heads/main/crates/buzz-agent/buzz-agent.png";
const ANTIGRAVITY_AVATAR_URL: &str =
"https://raw.githubusercontent.com/block/buzz/refs/heads/main/desktop/public/harness-logos/antigravity.svg";
fn common_binary_paths() -> &'static [PathBuf] {
static PATHS: OnceLock<Vec<PathBuf>> = OnceLock::new();
PATHS.get_or_init(|| {
Expand Down Expand Up @@ -219,6 +221,39 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[
login_hint: None,
auth_probe_args: None,
},
KnownAcpRuntime {
id: "antigravity",
label: "Antigravity",
commands: &["agy"],
aliases: &["antigravity", "google-antigravity"],
avatar_url: ANTIGRAVITY_AVATAR_URL,
mcp_command: None,
mcp_hooks: false,
underlying_cli: None,
cli_install_commands: &[],
cli_install_commands_windows: &[],
adapter_install_commands: &[],
cli_install_instructions_url: "https://antigravity.google.com",
adapter_install_instructions_url: "",
cli_install_hint: "Buzz expects agy on PATH (install agy via Google Antigravity). No managed download is bundled.",
adapter_install_hint: "",
skill_dir: Some(".antigravity/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,
max_rounds_env_var: None,
required_normalized_fields: &[],
login_hint: Some("Authenticate with Google to use Antigravity."),
auth_probe_args: None,
},
];

/// Skill discovery directories declared by known runtimes.
Expand Down Expand Up @@ -254,7 +289,8 @@ pub(crate) fn normalize_command_identity(command: &str) -> String {
_ => character.to_ascii_lowercase(),
})
.collect::<String>();
let lower = lower.strip_suffix(".exe").unwrap_or(&lower).to_string();
let lower = lower.strip_suffix(".exe").unwrap_or(&lower);
let lower = lower.strip_suffix(".par").unwrap_or(lower).to_string();

if let Some(suffix) = std::env::consts::EXE_SUFFIX.strip_prefix('.') {
return lower
Expand Down Expand Up @@ -448,9 +484,10 @@ pub fn try_record_agent_command(
Ok(default_agent_command())
}

fn default_agent_args(command: &str) -> Option<Vec<String>> {
pub(crate) fn default_agent_args(command: &str) -> Option<Vec<String>> {
match normalize_command_identity(command).as_str() {
"goose" => Some(vec!["acp".to_string()]),
"antigravity" | "google-antigravity" | "agy" | "agy-acp-server" => Some(Vec::new()),
"codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code"
| "claudecode" | "buzz-agent" => Some(Vec::new()),
_ => None,
Expand All @@ -472,7 +509,9 @@ pub fn normalize_agent_args(command: &str, agent_args: Vec<String>) -> Vec<Strin
return default_args;
}

if normalized.len() == 1 && normalized[0].eq_ignore_ascii_case("acp") && default_args.is_empty()
if normalized.len() == 1
&& normalized[0].eq_ignore_ascii_case("acp")
&& normalize_command_identity(command) != "goose"
{
return default_args;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,18 @@ mod tests {
assert!(codex.adapter_install_instructions_url.contains("codex-acp"));
assert!(codex.cli_install_hint.contains("Codex CLI"));
}

#[test]
fn vendor_metadata_antigravity_contract() {
let agy = known_acp_runtime_exact("antigravity").unwrap();
assert_eq!(
agy.cli_install_instructions_url,
"https://antigravity.google.com"
);
assert!(agy.adapter_install_instructions_url.is_empty());
assert!(agy.cli_install_hint.contains("Google Antigravity"));
assert!(agy.cli_install_commands.is_empty());
assert!(agy.adapter_install_commands.is_empty());
assert_eq!(agy.skill_dir, Some(".antigravity/skills"));
}
}
Loading