From 980d1c4ff5a595861c429f5df8d135cba1d7b702 Mon Sep 17 00:00:00 2001 From: Fred Lackey Date: Sat, 25 Jul 2026 13:39:12 -0400 Subject: [PATCH 1/3] feat(agents): add Kiro CLI ACP runtime Signed-off-by: Fred Lackey --- README.md | 4 +- crates/buzz-acp/README.md | 30 ++++- crates/buzz-acp/src/config.rs | 23 +++- crates/buzz-acp/src/lib.rs | 2 +- desktop/src-tauri/src/commands/agent_auth.rs | 117 +++++++++++++++++- .../src-tauri/src/commands/agent_config.rs | 1 + .../config_bridge/reader_tests.rs | 31 +++++ .../src-tauri/src/managed_agents/discovery.rs | 10 +- .../src/managed_agents/discovery/kiro.rs | 41 ++++++ .../managed_agents/discovery/kiro_tests.rs | 14 +++ .../discovery/runtime_metadata.rs | 14 +++ .../src-tauri/src/managed_agents/readiness.rs | 24 ++-- .../src/managed_agents/readiness/cli_login.rs | 2 +- .../src/managed_agents/runtime/tests.rs | 10 ++ desktop/src/features/agents/AGENTS.md | 11 +- .../features/agents/ui/AgentConfigFields.tsx | 2 +- .../agents/ui/agentConfigOptions.test.mjs | 3 +- .../features/agents/ui/agentConfigOptions.tsx | 5 +- .../agents/ui/personaRuntimeModel.test.mjs | 1 + .../features/agents/ui/personaRuntimeModel.ts | 6 +- .../onboarding/ui/agentReadiness.test.mjs | 13 ++ .../features/onboarding/ui/agentReadiness.ts | 7 +- .../ui/onboardingRuntimeSelection.test.mjs | 13 +- .../ui/onboardingRuntimeSelection.ts | 2 +- desktop/src/testing/e2eBridge.ts | 19 +++ .../e2e/onboarding-agent-defaults.spec.ts | 10 +- 26 files changed, 370 insertions(+), 45 deletions(-) create mode 100644 desktop/src-tauri/src/managed_agents/discovery/kiro.rs create mode 100644 desktop/src-tauri/src/managed_agents/discovery/kiro_tests.rs diff --git a/README.md b/README.md index 89a131e73e..ea6a458cfb 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Agents are part of the room, not haunted cron jobs. |---|---|---| | Relay, channels, threads, DMs, canvases, media, search, audit log | Mobile clients (iOS + Android, Flutter) | Web-of-trust reputation across relays | | Desktop app (Tauri + React) | Workflow approval gates (infra exists, glue still drying) | Push notifications | -| `buzz-cli` (agent-first, JSON in / JSON out) + ACP harness (Goose, Codex, Claude Code) | Huddle lifecycle events | Culture features | +| `buzz-cli` (agent-first, JSON in / JSON out) + ACP harness (Goose, Kiro CLI, Codex, Claude Code) | Huddle lifecycle events | Culture features | | YAML workflows: message / reaction / schedule / webhook triggers | | | | Git events (NIP-34: patches, repo announcements, status) | | | | Git hosting backend | | | @@ -206,7 +206,7 @@ A Rust workspace of focused crates. Single source of truth: the relay. See [ARCH **Services** — `buzz-db` (Postgres) · `buzz-auth` (NIP-42/98 Schnorr auth, rate limiting) · `buzz-pubsub` (Redis, presence, typing) · `buzz-search` (Postgres FTS) · `buzz-audit` (hash-chain log). Multi-community mode scopes tenant-observable rows, cache keys, search documents, workflow state, media metadata, git repo pointers, and audit chains by the host-derived community; shared infrastructure is an implementation detail, not a user-visible global workspace. -**Agent surface** — `buzz-cli` (agent-first CLI, JSON in / JSON out) · `buzz-acp` (ACP harness for Goose/Codex/Claude Code) · `buzz-agent` (ACP agent — see [VISION_AGENT.md](VISION_AGENT.md)) · `buzz-dev-mcp` (shell + file-edit tools) · `buzz-workflow` (YAML automation) · `buzz-persona` (agent persona packs) +**Agent surface** — `buzz-cli` (agent-first CLI, JSON in / JSON out) · `buzz-acp` (ACP harness for Goose/Kiro CLI/Codex/Claude Code) · `buzz-agent` (ACP agent — see [VISION_AGENT.md](VISION_AGENT.md)) · `buzz-dev-mcp` (shell + file-edit tools) · `buzz-workflow` (YAML automation) · `buzz-persona` (agent persona packs) **Git & pairing** — `git-sign-nostr` / `git-credential-nostr` (nostr-signed git) · `buzz-pair-relay` / `buzz-pairing-cli` (relay pairing) diff --git a/crates/buzz-acp/README.md b/crates/buzz-acp/README.md index d863729d54..708b45d653 100644 --- a/crates/buzz-acp/README.md +++ b/crates/buzz-acp/README.md @@ -9,7 +9,11 @@ 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**, **Kiro CLI** (native ACP), **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 @@ -98,6 +102,30 @@ 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 Kiro CLI + +Kiro CLI includes a native ACP endpoint, so no separate adapter package is +required. + +```bash +# Install and authenticate Kiro CLI first. +curl -fsSL https://cli.kiro.dev/install | bash +kiro-cli login +kiro-cli whoami + +# Run Kiro as the ACP worker. +export BUZZ_ACP_AGENT_COMMAND="kiro-cli" +export BUZZ_ACP_AGENT_ARGS="acp" + +buzz-acp +``` + +Buzz Desktop discovers `kiro-cli`, verifies the existing login with +`kiro-cli whoami --format json`, and supplies the `acp` subcommand +automatically. Kiro currently negotiates ACP protocol version 1 with Buzz's +version-2 initialization request; Buzz uses its existing legacy prompt framing +for that negotiated version. + ## Configuration All configuration is via environment variables (or CLI flags — every env var has a matching flag). diff --git a/crates/buzz-acp/src/config.rs b/crates/buzz-acp/src/config.rs index a38d6faa14..7a48d1d287 100644 --- a/crates/buzz-acp/src/config.rs +++ b/crates/buzz-acp/src/config.rs @@ -175,7 +175,7 @@ impl std::fmt::Display for PermissionMode { about = "Query available models from the configured agent" )] pub struct ModelsArgs { - /// Agent binary to spawn (e.g. "goose", "claude-agent-acp", "codex-acp"). + /// Agent binary to spawn (e.g. "goose", "kiro-cli", "claude-agent-acp", "codex-acp"). #[command(flatten)] pub agent: AuthAgentArgs, @@ -187,7 +187,7 @@ pub struct ModelsArgs { /// Shared agent-spawn flags for lightweight local ACP helper subcommands. #[derive(Debug, Parser)] pub struct AuthAgentArgs { - /// Agent binary to spawn (e.g. "goose", "claude-agent-acp", "codex-acp"). + /// Agent binary to spawn (e.g. "goose", "kiro-cli", "claude-agent-acp", "codex-acp"). #[arg(long, env = "BUZZ_ACP_AGENT_COMMAND", default_value = "goose")] pub agent_command: String, @@ -616,7 +616,7 @@ pub(crate) fn normalize_agent_command_identity(command: &str) -> String { fn default_agent_args(command: &str) -> Option> { match normalize_agent_command_identity(command).as_str() { - "goose" => Some(vec!["acp".to_string()]), + "goose" | "kiro-cli" => Some(vec!["acp".to_string()]), "codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code" | "claudecode" | "buzz-agent" => Some(Vec::new()), _ => None, @@ -1440,6 +1440,19 @@ mod tests { assert_eq!(normalize_agent_args("goose", vec!["".into()]), vec!["acp"]); } + #[test] + fn normalizes_kiro_cli_args_to_acp() { + assert_eq!(normalize_agent_args("kiro-cli", Vec::new()), vec!["acp"]); + assert_eq!( + normalize_agent_args("/usr/local/bin/kiro-cli", vec!["".into()]), + vec!["acp"] + ); + assert_eq!( + normalize_agent_args("Kiro_CLI.EXE", Vec::new()), + vec!["acp"] + ); + } + #[test] fn normalizes_codex_and_claude_args_to_empty() { assert_eq!( @@ -1512,6 +1525,10 @@ mod tests { normalize_agent_command_identity("Claude Code"), "claude-code" ); + assert_eq!( + normalize_agent_command_identity("/opt/kiro/Kiro_CLI.exe"), + "kiro-cli" + ); assert_eq!(normalize_agent_command_identity("Goose.EXE"), "goose"); // Non-ASCII must not panic. assert_eq!(normalize_agent_command_identity("my-agënt"), "my-agënt"); diff --git a/crates/buzz-acp/src/lib.rs b/crates/buzz-acp/src/lib.rs index 0230ea0875..79a18f720d 100644 --- a/crates/buzz-acp/src/lib.rs +++ b/crates/buzz-acp/src/lib.rs @@ -3138,7 +3138,7 @@ fn handle_prompt_result( "dead-lettering batch immediately — non-retryable auth error" ); let content = "⚠️ I couldn't process the last request: authentication failed. \ - Please re-authenticate the CLI (e.g. run `claude /login` or `codex login`) \ + Please re-authenticate the CLI (e.g. run `claude /login`, `codex login`, or `kiro-cli login`) \ and then re-send." .to_string(); spawn_failure_notice(rest_client, &batch, content); diff --git a/desktop/src-tauri/src/commands/agent_auth.rs b/desktop/src-tauri/src/commands/agent_auth.rs index bba0f33860..9fb8a87111 100644 --- a/desktop/src-tauri/src/commands/agent_auth.rs +++ b/desktop/src-tauri/src/commands/agent_auth.rs @@ -69,12 +69,67 @@ pub async fn connect_acp_runtime( fn discover_acp_auth_methods_blocking(runtime_id: &str) -> Result { let output = run_buzz_acp_auth_command(runtime_id, ["auth-methods", "--json"])?; - if !output.status.success() { - return Err(command_error("buzz-acp auth-methods", &output)); + resolve_acp_auth_methods( + runtime_id, + output.status.success(), + &output.stdout, + command_error("buzz-acp auth-methods", &output), + ) +} + +fn resolve_acp_auth_methods( + runtime_id: &str, + command_succeeded: bool, + stdout: &[u8], + command_failure: String, +) -> Result { + if !command_succeeded { + let mut fallback = AcpAuthMethodsResult { + methods: Vec::new(), + }; + add_catalog_login_fallback(runtime_id, &mut fallback)?; + if !fallback.methods.is_empty() { + return Ok(fallback); + } + return Err(command_failure); } - serde_json::from_slice::(&output.stdout) - .map_err(|error| format!("failed to parse auth methods JSON: {error}")) + let mut result = serde_json::from_slice::(stdout) + .map_err(|error| format!("failed to parse auth methods JSON: {error}"))?; + add_catalog_login_fallback(runtime_id, &mut result)?; + Ok(result) +} + +/// Add a catalog-declared terminal login method when an otherwise compatible +/// ACP runtime does not advertise authentication through ACP itself. +/// +/// Kiro's native ACP endpoint returns an empty `authMethods` list for an +/// existing-auth session and exits during initialization when logged out, while +/// `kiro-cli login` is the documented interactive sign-in path. Keeping that +/// command in the runtime catalog lets onboarding offer a working sign-in +/// action in both states without inventing vendor commands in React. +fn add_catalog_login_fallback( + runtime_id: &str, + result: &mut AcpAuthMethodsResult, +) -> Result<(), String> { + if !result.methods.is_empty() { + return Ok(()); + } + let runtime = known_acp_runtime_exact(runtime_id) + .ok_or_else(|| format!("unknown ACP runtime: {runtime_id}"))?; + let Some(command) = runtime.login_command_args else { + return Ok(()); + }; + result.methods.push(AcpAuthMethod { + id: "cli-login".to_string(), + name: format!("Sign in with {}", runtime.label), + description: runtime.login_hint.map(str::to_string), + method_type: Some("terminal".to_string()), + args: Vec::new(), + command: command.iter().map(|value| value.to_string()).collect(), + meta: None, + }); + Ok(()) } fn connect_acp_runtime_blocking( @@ -461,9 +516,10 @@ fn shell_escape(arg: &str) -> String { #[cfg(test)] mod tests { use super::{ - adapter_terminal_argv, append_inherited_path, is_claude_subscription_login, + adapter_terminal_argv, add_catalog_login_fallback, append_inherited_path, + is_claude_subscription_login, resolve_acp_auth_methods, run_buzz_acp_auth_command_with_paths, shell_escape, shell_join, uses_terminal_auth, - windows_terminal_args, AcpAuthMethod, + windows_terminal_args, AcpAuthMethod, AcpAuthMethodsResult, }; /// Windows regression: the augmented PATH there holds only Buzz-managed @@ -597,6 +653,55 @@ mod tests { assert_eq!(method.command[0], "claude"); } + #[test] + fn kiro_empty_acp_auth_methods_gain_catalog_terminal_login() { + let mut result = AcpAuthMethodsResult { + methods: Vec::new(), + }; + add_catalog_login_fallback("kiro", &mut result).expect("Kiro runtime"); + assert_eq!(result.methods.len(), 1); + let method = &result.methods[0]; + assert_eq!(method.id, "cli-login"); + assert_eq!(method.method_type.as_deref(), Some("terminal")); + assert_eq!(method.command, ["kiro-cli", "login"]); + assert!(method + .description + .as_deref() + .is_some_and(|description| description.contains("kiro-cli login"))); + } + + #[test] + fn kiro_failed_acp_initialization_still_offers_terminal_login() { + let result = resolve_acp_auth_methods( + "kiro", + false, + &[], + "agent process exited unexpectedly".into(), + ) + .expect("catalog fallback"); + + assert_eq!(result.methods.len(), 1); + assert_eq!(result.methods[0].command, ["kiro-cli", "login"]); + } + + #[test] + fn catalog_login_fallback_preserves_adapter_methods() { + let original = AcpAuthMethod { + id: "native".into(), + name: "Native".into(), + description: None, + method_type: None, + args: Vec::new(), + command: Vec::new(), + meta: None, + }; + let mut result = AcpAuthMethodsResult { + methods: vec![original.clone()], + }; + add_catalog_login_fallback("kiro", &mut result).expect("Kiro runtime"); + assert_eq!(result.methods, [original]); + } + #[test] fn claude_subscription_methods_run_without_a_visible_terminal() { for id in ["claude-login", "claude-ai-login"] { diff --git a/desktop/src-tauri/src/commands/agent_config.rs b/desktop/src-tauri/src/commands/agent_config.rs index f6094ed529..2429647dc0 100644 --- a/desktop/src-tauri/src/commands/agent_config.rs +++ b/desktop/src-tauri/src/commands/agent_config.rs @@ -631,6 +631,7 @@ mod tests { required_normalized_fields: &["model", "provider"], login_hint: None, auth_probe_args: None, + login_command_args: None, } } diff --git a/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs b/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs index 4c11cd6c49..478ccf955f 100644 --- a/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs +++ b/desktop/src-tauri/src/managed_agents/config_bridge/reader_tests.rs @@ -6,6 +6,7 @@ use std::{collections::BTreeMap, path::Path, sync::Mutex}; use super::*; use crate::managed_agents::discovery::KnownAcpRuntime; +use crate::managed_agents::known_acp_runtime; use crate::managed_agents::types::ManagedAgentRecord; static GOOSE_PATH_ROOT_LOCK: Mutex<()> = Mutex::new(()); @@ -59,6 +60,7 @@ fn test_runtime() -> &'static KnownAcpRuntime { required_normalized_fields: &["model", "provider"], login_hint: None, auth_probe_args: None, + login_command_args: None, } } @@ -220,6 +222,34 @@ fn provider_locked_shows_locked() { assert_eq!(provider.origin, ConfigOrigin::HarnessConstraint); } +#[test] +fn kiro_live_config_uses_native_acp_model_switching_without_a_provider_field() { + let record = test_record(); + let runtime = known_acp_runtime("kiro-cli").expect("Kiro runtime"); + let cache = SessionConfigCache { + config_options: vec![], + available_modes: vec![], + available_models: vec![AcpModelEntry { + model_id: "kiro-model".to_string(), + name: Some("Kiro model".to_string()), + description: None, + }], + current_model: Some("kiro-model".to_string()), + model_overridden: false, + goose_native_config: None, + captured_at: String::new(), + }; + + let surface = read_config_surface(&record, Some(runtime), Some(&cache), None); + let model = surface.normalized.model.expect("model field"); + assert_eq!(model.value.as_deref(), Some("kiro-model")); + assert!(matches!( + model.write_via, + ConfigWriteMechanism::AcpSetSessionModel + )); + assert!(surface.normalized.provider.is_none()); +} + #[test] fn post_spawn_with_model_config_option_uses_acp() { let record = test_record(); @@ -635,6 +665,7 @@ fn buzz_agent_runtime() -> &'static KnownAcpRuntime { required_normalized_fields: &["model", "provider"], login_hint: None, auth_probe_args: None, + login_command_args: None, } } diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index a2e47d1281..d530eb6a8c 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -9,6 +9,7 @@ use crate::managed_agents::{ AcpAvailabilityStatus, AcpRuntimeCatalogEntry, AuthStatus, CommandAvailabilityInfo, }; +mod kiro; mod runtime_metadata; pub(crate) use runtime_metadata::KnownAcpRuntime; @@ -96,6 +97,7 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[ required_normalized_fields: &["model", "provider"], login_hint: None, auth_probe_args: None, + login_command_args: None, }, KnownAcpRuntime { id: "claude", @@ -128,6 +130,7 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[ required_normalized_fields: &[], login_hint: Some("Run the Claude CLI to complete authentication."), auth_probe_args: Some(&["claude", "auth", "status"]), + login_command_args: None, }, KnownAcpRuntime { id: "codex", @@ -161,7 +164,9 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[ login_hint: Some("Run `codex login` to authenticate."), // Verified: `codex login status` exits 0 when logged in, non-zero otherwise. auth_probe_args: Some(&["codex", "login", "status"]), + login_command_args: None, }, + kiro::RUNTIME, KnownAcpRuntime { id: "buzz-agent", label: "Buzz Agent", @@ -193,6 +198,7 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[ required_normalized_fields: &["model", "provider"], login_hint: None, auth_probe_args: None, + login_command_args: None, }, ]; @@ -348,7 +354,7 @@ 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" => Some(vec!["acp".to_string()]), + "goose" | "kiro-cli" => Some(vec!["acp".to_string()]), "codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code" | "claudecode" | "buzz-agent" => Some(Vec::new()), _ => None, @@ -1364,5 +1370,7 @@ pub fn managed_agent_avatar_url(command: &str) -> Option { Some(runtime.avatar_url.to_string()) } +#[cfg(test)] +mod kiro_tests; #[cfg(test)] mod tests; diff --git a/desktop/src-tauri/src/managed_agents/discovery/kiro.rs b/desktop/src-tauri/src/managed_agents/discovery/kiro.rs new file mode 100644 index 0000000000..4135e295d4 --- /dev/null +++ b/desktop/src-tauri/src/managed_agents/discovery/kiro.rs @@ -0,0 +1,41 @@ +use super::KnownAcpRuntime; + +pub(super) const RUNTIME: KnownAcpRuntime = KnownAcpRuntime { + id: "kiro", + label: "Kiro CLI", + commands: &["kiro-cli"], + aliases: &[], + avatar_url: "https://kiro.dev/images/kiro-wordmark.png", + mcp_command: Some("buzz-dev-mcp"), + mcp_hooks: false, + underlying_cli: Some("kiro-cli"), + cli_install_commands: &["curl -fsSL https://cli.kiro.dev/install | bash"], + cli_install_commands_windows: &[ + "powershell.exe -NoProfile -ExecutionPolicy Bypass -Command \"irm 'https://cli.kiro.dev/install.ps1' | iex\"", + ], + adapter_install_commands: &[], + cli_install_instructions_url: "https://kiro.dev/docs/cli/installation/", + adapter_install_instructions_url: "", + cli_install_hint: "Buzz requires Kiro CLI; the desktop app alone is not enough.", + adapter_install_hint: "", + skill_dir: None, + supports_acp_model_switching: true, + model_env_var: None, + provider_env_var: None, + // Kiro owns provider selection through its model catalog. Keep this false + // because the backend's `provider_locked` display is Claude-specific. + provider_locked: false, + 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 `kiro-cli login` to authenticate."), + // Verified locally: this exits 0 for an authenticated session and is a + // read-only identity probe. + auth_probe_args: Some(&["kiro-cli", "whoami", "--format", "json"]), + login_command_args: Some(&["kiro-cli", "login"]), +}; diff --git a/desktop/src-tauri/src/managed_agents/discovery/kiro_tests.rs b/desktop/src-tauri/src/managed_agents/discovery/kiro_tests.rs new file mode 100644 index 0000000000..bbd395651f --- /dev/null +++ b/desktop/src-tauri/src/managed_agents/discovery/kiro_tests.rs @@ -0,0 +1,14 @@ +use super::normalize_agent_args; + +#[test] +fn normalizes_kiro_cli_args_to_acp() { + assert_eq!(normalize_agent_args("kiro-cli", Vec::new()), vec!["acp"]); + assert_eq!( + normalize_agent_args("/usr/local/bin/kiro-cli", vec!["".into()]), + vec!["acp"] + ); + assert_eq!( + normalize_agent_args("Kiro_CLI.EXE", Vec::new()), + vec!["acp"] + ); +} diff --git a/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs b/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs index 2fb6a471d4..da31609a64 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs @@ -62,6 +62,10 @@ pub(crate) struct KnownAcpRuntime { /// CLI args for probing authentication status. `args[0]` is the binary name; /// the remainder are the subcommand. `None` for runtimes with no login step. pub auth_probe_args: Option<&'static [&'static str]>, + /// Visible terminal command used when the ACP endpoint does not advertise + /// an authentication method. `None` when auth is handled through ACP or is + /// not required. + pub login_command_args: Option<&'static [&'static str]>, } impl KnownAcpRuntime { @@ -120,5 +124,15 @@ mod tests { ); assert!(codex.adapter_install_instructions_url.contains("codex-acp")); assert!(codex.cli_install_hint.contains("desktop app alone")); + + let kiro = known_acp_runtime_exact("kiro").unwrap(); + assert_eq!(kiro.commands, &["kiro-cli"]); + assert_eq!( + kiro.auth_probe_args, + Some(&["kiro-cli", "whoami", "--format", "json"][..]) + ); + assert_eq!(kiro.login_command_args, Some(&["kiro-cli", "login"][..])); + assert!(kiro.adapter_install_commands.is_empty()); + assert!(kiro.cli_install_instructions_url.contains("kiro.dev")); } } diff --git a/desktop/src-tauri/src/managed_agents/readiness.rs b/desktop/src-tauri/src/managed_agents/readiness.rs index c04dfa88a4..4a6ba34074 100644 --- a/desktop/src-tauri/src/managed_agents/readiness.rs +++ b/desktop/src-tauri/src/managed_agents/readiness.rs @@ -244,9 +244,9 @@ impl AgentReadiness { /// - `openai` → `OPENAI_COMPAT_API_KEY` /// - `databricks` / `databricks_v2` → `DATABRICKS_HOST` (token optional — /// OAuth PKCE is the fallback) -/// * **claude**: a successful `claude auth status` probe. -/// * **codex**: a successful `codex login status` probe (checks the codex -/// credential store — NOT `OPENAI_API_KEY`). +/// * **CLI-login runtimes**: a successful catalog-declared auth probe (for +/// example `claude auth status`, `codex login status`, or +/// `kiro-cli whoami --format json`). /// * **unknown / custom command**: always `Ready` (no requirements known). /// /// Databricks note: `DATABRICKS_TOKEN` is `.unwrap_or_default()` in @@ -283,13 +283,15 @@ fn collect_missing_requirements( let file_cfg = read_goose_file_config(); goose_requirements(effective, file_cfg.as_ref()) } - "claude" => cli_login::requirements( - &["claude", "auth", "status"], - "complete Claude Code authentication by running the Claude CLI", - rt, - ), - "codex" => cli_login::requirements(&["codex", "login", "status"], "run `codex login`", rt), - _ => vec![], + _ => match rt.auth_probe_args { + Some(probe_args) => cli_login::requirements( + probe_args, + rt.login_hint + .unwrap_or("complete authentication using the runtime CLI"), + rt, + ), + None => vec![], + }, } } @@ -888,6 +890,7 @@ mod tests { required_normalized_fields: &[], login_hint: None, auth_probe_args: None, + login_command_args: None, } } @@ -1083,6 +1086,7 @@ mod tests { required_normalized_fields: &[], login_hint: None, auth_probe_args: None, + login_command_args: None, } } diff --git a/desktop/src-tauri/src/managed_agents/readiness/cli_login.rs b/desktop/src-tauri/src/managed_agents/readiness/cli_login.rs index 4036d9f239..cdf3ef7fac 100644 --- a/desktop/src-tauri/src/managed_agents/readiness/cli_login.rs +++ b/desktop/src-tauri/src/managed_agents/readiness/cli_login.rs @@ -10,7 +10,7 @@ use crate::managed_agents::{ use super::{cli_probe, Requirement}; -/// Requirements for CLI-login runtimes (claude, codex). +/// Requirements for catalog-declared CLI-login runtimes. pub(super) fn requirements( probe_args: &[&str], setup_copy: &str, diff --git a/desktop/src-tauri/src/managed_agents/runtime/tests.rs b/desktop/src-tauri/src/managed_agents/runtime/tests.rs index fd758047c3..12edf6998c 100644 --- a/desktop/src-tauri/src/managed_agents/runtime/tests.rs +++ b/desktop/src-tauri/src/managed_agents/runtime/tests.rs @@ -98,6 +98,16 @@ fn codex_has_mcp_command() { assert_eq!(p.mcp_command, Some("buzz-dev-mcp")); } +#[test] +fn kiro_uses_native_acp_with_buzz_mcp() { + let runtime = known_acp_runtime("kiro-cli").expect("should resolve"); + assert_eq!(runtime.id, "kiro"); + assert_eq!(runtime.mcp_command, Some("buzz-dev-mcp")); + assert!(!runtime.mcp_hooks); + assert!(runtime.supports_acp_model_switching); + assert!(!runtime.provider_locked); +} + #[test] fn goose_has_no_mcp_hooks() { let p = known_acp_runtime("goose").expect("should resolve"); diff --git a/desktop/src/features/agents/AGENTS.md b/desktop/src/features/agents/AGENTS.md index 2af9ddb98c..9975a2677c 100644 --- a/desktop/src/features/agents/AGENTS.md +++ b/desktop/src/features/agents/AGENTS.md @@ -72,10 +72,11 @@ with a TypeScript lookup table or an id comparison in a component. touching this flow or the shared renderer. 8. **Omit the Model control only after a confirmed successful empty discovery on an optional-model harness.** When the field model marks model - as `acpNative` (Claude Code / Codex), `shouldRenderModelControl` hides the - picker while discovery is in flight and after IPC resolves with no usable - options (`modelDiscoverySuccessfulEmpty` / `isSuccessfulEmptyDiscovery`). - A thrown or unavailable discovery keeps the control so #2246 failure UI can + as `acpNative` (Claude Code / Codex / Kiro CLI), + `shouldRenderModelControl` hides the picker while discovery is in flight and + after IPC resolves with no usable options (`modelDiscoverySuccessfulEmpty` / + `isSuccessfulEmptyDiscovery`). A thrown or unavailable discovery keeps the + control so #2246 failure UI can render, and must not heal/clear persisted model or effort. Full disclosure still shows the control when Custom model is available. Required-model harnesses always keep the field. Gate: `defaults hides model when optional @@ -120,6 +121,8 @@ with a TypeScript lookup table or an id comparison in a component. acceptance coverage for readiness, failure states, defaults, navigation, successful-empty vs failed optional-model discovery, and persistence races. - Rust: `runtime_metadata_env_vars` tests pin spawn-time key application. +- Native ACP runtime catalog tests pin Kiro CLI discovery, `acp` argument + normalization, CLI-login readiness, and model/MCP capability metadata. ## Keep this file true diff --git a/desktop/src/features/agents/ui/AgentConfigFields.tsx b/desktop/src/features/agents/ui/AgentConfigFields.tsx index 1bd8af8976..8cfb6cd095 100644 --- a/desktop/src/features/agents/ui/AgentConfigFields.tsx +++ b/desktop/src/features/agents/ui/AgentConfigFields.tsx @@ -159,7 +159,7 @@ export function shouldShowModelStatusMessage( /** * Whether the Model control should render given discovery state. * - * Optional-model harnesses (Claude Code / Codex, `acpNative`) omit the control + * Optional-model harnesses (Claude Code / Codex / Kiro CLI, `acpNative`) omit the control * while discovery is in flight and after a **confirmed successful empty** * catalog (IPC resolved, no usable options) — there is nothing useful to pick. * Discovery failures / unavailable runtimes keep the control so #2246 failure diff --git a/desktop/src/features/agents/ui/agentConfigOptions.test.mjs b/desktop/src/features/agents/ui/agentConfigOptions.test.mjs index d0690cefc3..bc40eb7edd 100644 --- a/desktop/src/features/agents/ui/agentConfigOptions.test.mjs +++ b/desktop/src/features/agents/ui/agentConfigOptions.test.mjs @@ -141,9 +141,10 @@ test("runtimeSupportsLlmProviderSelection is true for buzz-agent and goose", () assert.equal(runtimeSupportsLlmProviderSelection("goose"), true); }); -test("runtimeSupportsLlmProviderSelection is false for codex and claude", () => { +test("runtimeSupportsLlmProviderSelection is false for CLI-owned providers", () => { assert.equal(runtimeSupportsLlmProviderSelection("codex"), false); assert.equal(runtimeSupportsLlmProviderSelection("claude"), false); + assert.equal(runtimeSupportsLlmProviderSelection("kiro"), false); }); test("resetConfigForHarnessChange clears harness-specific values", () => { diff --git a/desktop/src/features/agents/ui/agentConfigOptions.tsx b/desktop/src/features/agents/ui/agentConfigOptions.tsx index 6ae81ff6cb..347f2099c3 100644 --- a/desktop/src/features/agents/ui/agentConfigOptions.tsx +++ b/desktop/src/features/agents/ui/agentConfigOptions.tsx @@ -133,6 +133,7 @@ const PERSONA_MODEL_OPTIONS_BY_RUNTIME: Record< "buzz-agent": [DEFAULT_MODEL_OPTION], claude: [DEFAULT_MODEL_OPTION], codex: [DEFAULT_MODEL_OPTION], + kiro: [DEFAULT_MODEL_OPTION], }; export function getRuntimePersonaModelOptions( @@ -151,8 +152,8 @@ function isKnownLlmProvider( * Required credential env keys for the given runtime + provider combination. * Derived from PROVIDER_CREDENTIAL_CONFIG — single source of truth. * - * buzz-agent and goose use provider-specific credentials; claude and codex - * handle auth via CLI login (surfaced separately via the CliLogin surface). + * buzz-agent and goose use provider-specific credentials; claude, codex, and + * kiro handle auth via CLI login (surfaced separately via the CliLogin surface). */ export function requiredCredentialEnvKeys( runtimeId: string, diff --git a/desktop/src/features/agents/ui/personaRuntimeModel.test.mjs b/desktop/src/features/agents/ui/personaRuntimeModel.test.mjs index 967bd3e129..d73abe40e4 100644 --- a/desktop/src/features/agents/ui/personaRuntimeModel.test.mjs +++ b/desktop/src/features/agents/ui/personaRuntimeModel.test.mjs @@ -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("kiro", false), "locked"); assert.equal(resolveRuntimeProviderCapability(" claude ", false), "locked"); }); diff --git a/desktop/src/features/agents/ui/personaRuntimeModel.ts b/desktop/src/features/agents/ui/personaRuntimeModel.ts index d138296b5c..565d806d6c 100644 --- a/desktop/src/features/agents/ui/personaRuntimeModel.ts +++ b/desktop/src/features/agents/ui/personaRuntimeModel.ts @@ -18,7 +18,7 @@ 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 / kiro → "locked" (CLI-login runtimes; no LLM provider selection). * - anything else (custom, empty, genuinely unknown) → "unknown". * * `isProviderCapable` is the caller-supplied {@link @@ -33,7 +33,7 @@ export function resolveRuntimeProviderCapability( return "capable"; } const id = runtimeId.trim(); - if (id === "claude" || id === "codex") { + if (id === "claude" || id === "codex" || id === "kiro") { return "locked"; } return "unknown"; @@ -96,7 +96,7 @@ export function resolveAgentCommandUpdate(input: { * A key counts as missing when its env value is absent or an empty string * (matching {@link EnvVarsEditor}'s own `isMissing` rendering). The * `requiredEnvKeys` list is already filtered to keys the dialog can fix — - * CLI-login runtimes (claude/codex) and keys satisfied by the runtime file + * CLI-login runtimes (claude/codex/kiro) and keys satisfied by the runtime file * config contribute no entries, so this never blocks on out-of-band auth. */ export function hasMissingRequiredEnvKey( diff --git a/desktop/src/features/onboarding/ui/agentReadiness.test.mjs b/desktop/src/features/onboarding/ui/agentReadiness.test.mjs index 9d6deafb09..911698cd98 100644 --- a/desktop/src/features/onboarding/ui/agentReadiness.test.mjs +++ b/desktop/src/features/onboarding/ui/agentReadiness.test.mjs @@ -52,6 +52,19 @@ test("resolveAgentReadiness_cli_returns_ready_when_preferred_cli_runtime_is_logg }); }); +test("resolveAgentReadiness_kiro_returns_ready_when_preferred_runtime_is_logged_in", () => { + const runtimes = [makeRuntime({ id: "kiro", label: "Kiro CLI" })]; + const result = resolveAgentReadiness( + runtimes, + makeConfig({ preferred_runtime: "kiro" }), + ); + assert.deepEqual(result, { + ready: true, + reason: "cli", + runtimeLabel: "Kiro CLI", + }); +}); + test("resolveAgentReadiness_uses_only_the_preferred_runtime", () => { const runtimes = [ makeRuntime({ id: "claude", label: "Claude" }), diff --git a/desktop/src/features/onboarding/ui/agentReadiness.ts b/desktop/src/features/onboarding/ui/agentReadiness.ts index 86b9721af3..7de21b9161 100644 --- a/desktop/src/features/onboarding/ui/agentReadiness.ts +++ b/desktop/src/features/onboarding/ui/agentReadiness.ts @@ -12,7 +12,8 @@ export type AgentReadinessResult = /** * Determine whether the user has a working agent path configured. * - * CLI path: the preferred Claude or Codex runtime is available and logged in. + * CLI path: the preferred Claude, Codex, or Kiro runtime is available and + * logged in. * Provider path: the preferred Buzz Agent or Goose runtime has provider and * model set, plus all required credential env vars for that provider. * @@ -47,7 +48,9 @@ export function resolveAgentReadiness( } if ( - (preferredRuntime.id === "claude" || preferredRuntime.id === "codex") && + (preferredRuntime.id === "claude" || + preferredRuntime.id === "codex" || + preferredRuntime.id === "kiro") && (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..011151fc8b 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 Kiro CLI are visible in onboarding", () => { assert.equal(runtimeIsVisibleInOnboarding("claude"), true); assert.equal(runtimeIsVisibleInOnboarding("codex"), true); + assert.equal(runtimeIsVisibleInOnboarding("kiro"), true); assert.equal(runtimeIsVisibleInOnboarding("goose"), false); assert.equal(runtimeIsVisibleInOnboarding("buzz-agent"), false); assert.equal(runtimeIsVisibleInOnboarding("custom"), false); @@ -26,11 +27,12 @@ test("visible onboarding runtimes use the product order", () => { runtime("codex", "available", "logged_in"), runtime("goose", "available", "not_applicable"), runtime("claude", "available", "logged_in"), + runtime("kiro", "available", "logged_in"), ]; assert.deepEqual( getVisibleOnboardingRuntimes(runtimes).map(({ id }) => id), - ["claude", "codex"], + ["claude", "codex", "kiro"], ); }); @@ -49,6 +51,10 @@ test("readiness requires an available and authenticated runtime", () => { runtimeIsReadyForOnboarding(runtime("claude", "available", "logged_out")), false, ); + assert.equal( + runtimeIsReadyForOnboarding(runtime("kiro", "available", "logged_in")), + true, + ); assert.equal( runtimeIsReadyForOnboarding(runtime("codex", "not_installed", "logged_in")), false, @@ -61,10 +67,11 @@ test("ready onboarding runtimes exclude hidden ready harnesses", () => { runtime("codex", "available", "logged_out"), runtime("buzz-agent", "available", "not_applicable"), runtime("claude", "available", "logged_in"), + runtime("kiro", "available", "logged_in"), ]; assert.deepEqual( getReadyOnboardingRuntimes(runtimes).map(({ id }) => id), - ["claude"], + ["claude", "kiro"], ); }); diff --git a/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.ts b/desktop/src/features/onboarding/ui/onboardingRuntimeSelection.ts index 51339e2afe..43c77767fb 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", "kiro"]; const VISIBLE_ONBOARDING_RUNTIME_IDS = new Set( ONBOARDING_RUNTIME_ORDER, diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index da398ca4ba..4c89d16541 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -7016,6 +7016,25 @@ async function handleDiscoverAcpRuntimes( auth_status: { status: "unknown" }, login_hint: undefined, }, + { + id: "kiro", + label: "Kiro CLI", + avatar_url: "https://kiro.dev/images/kiro-wordmark.png", + availability: "not_installed", + command: null, + binary_path: null, + default_args: [], + mcp_command: "buzz-dev-mcp", + install_hint: + "Buzz requires Kiro CLI; the desktop app alone is not enough.", + install_instructions_url: "https://kiro.dev/docs/cli/installation/", + can_auto_install: true, + requires_external_cli: true, + underlying_cli_path: null, + node_required: false, + auth_status: { status: "unknown" }, + login_hint: "Run `kiro-cli login` to authenticate.", + }, { id: "buzz-agent", label: "Buzz Agent", diff --git a/desktop/tests/e2e/onboarding-agent-defaults.spec.ts b/desktop/tests/e2e/onboarding-agent-defaults.spec.ts index d007bd6118..35a78ff6b8 100644 --- a/desktop/tests/e2e/onboarding-agent-defaults.spec.ts +++ b/desktop/tests/e2e/onboarding-agent-defaults.spec.ts @@ -3,7 +3,7 @@ import { installMockBridge } from "../helpers/bridge"; import { passThroughBackupStep } from "../helpers/onboarding"; function runtime( - id: "buzz-agent" | "claude" | "codex" | "goose", + id: "buzz-agent" | "claude" | "codex" | "goose" | "kiro", availability: string, authStatus: Record, overrides: Record = {}, @@ -17,7 +17,9 @@ function runtime( ? "Claude Code" : id === "codex" ? "Codex" - : "Goose", + : id === "kiro" + ? "Kiro CLI" + : "Goose", avatar_url: "", availability, command: availability === "available" ? id : null, @@ -57,7 +59,7 @@ async function readSavedRuntime(page: Parameters[0]) { }); } -test("setup shows only Claude Code and Codex as detected harnesses", async ({ +test("setup shows Claude Code, Codex, and Kiro CLI as detected harnesses", async ({ page, }) => { await installMockBridge( @@ -68,6 +70,7 @@ test("setup shows only Claude Code and Codex as detected harnesses", async ({ runtime("goose", "available", { status: "not_applicable" }), runtime("codex", "available", { status: "logged_in" }), runtime("claude", "available", { status: "logged_in" }), + runtime("kiro", "available", { status: "logged_in" }), ], }, { skipCommunitySeed: true, skipOnboardingSeed: true }, @@ -77,6 +80,7 @@ test("setup shows only Claude Code and Codex as detected harnesses", async ({ await expect(page.getByTestId("onboarding-runtime-claude")).toBeVisible(); await expect(page.getByTestId("onboarding-runtime-codex")).toBeVisible(); + await expect(page.getByTestId("onboarding-runtime-kiro")).toBeVisible(); await expect(page.getByTestId("onboarding-runtime-goose")).toHaveCount(0); await expect(page.getByTestId("onboarding-runtime-buzz-agent")).toHaveCount( 0, From 4ef6c872c9303f002e3ac98e25b17a74e8c9d0ec Mon Sep 17 00:00:00 2001 From: Fred Lackey Date: Sat, 25 Jul 2026 19:44:56 -0400 Subject: [PATCH 2/3] fix(desktop): add Kiro runtime icons Signed-off-by: Fred Lackey --- .../src/managed_agents/discovery/kiro.rs | 3 +- .../managed_agents/discovery/kiro_tests.rs | 11 ++++- desktop/src-tauri/src/managed_agents/mod.rs | 2 + .../src/managed_agents/runtime_avatars.rs | 42 +++++++++++++++++++ .../src-tauri/src/managed_agents/storage.rs | 2 +- .../onboarding/assets/harness-logos/kiro.svg | 11 +++++ .../features/onboarding/ui/RuntimeIcon.tsx | 2 + desktop/src/testing/e2eBridge.ts | 2 +- 8 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 desktop/src-tauri/src/managed_agents/runtime_avatars.rs create mode 100644 desktop/src/features/onboarding/assets/harness-logos/kiro.svg diff --git a/desktop/src-tauri/src/managed_agents/discovery/kiro.rs b/desktop/src-tauri/src/managed_agents/discovery/kiro.rs index 4135e295d4..17170e43c6 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/kiro.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/kiro.rs @@ -1,11 +1,12 @@ use super::KnownAcpRuntime; +use crate::managed_agents::KIRO_AVATAR_URL; pub(super) const RUNTIME: KnownAcpRuntime = KnownAcpRuntime { id: "kiro", label: "Kiro CLI", commands: &["kiro-cli"], aliases: &[], - avatar_url: "https://kiro.dev/images/kiro-wordmark.png", + avatar_url: KIRO_AVATAR_URL, mcp_command: Some("buzz-dev-mcp"), mcp_hooks: false, underlying_cli: Some("kiro-cli"), diff --git a/desktop/src-tauri/src/managed_agents/discovery/kiro_tests.rs b/desktop/src-tauri/src/managed_agents/discovery/kiro_tests.rs index bbd395651f..dc7ee0b007 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/kiro_tests.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/kiro_tests.rs @@ -1,4 +1,5 @@ -use super::normalize_agent_args; +use super::{managed_agent_avatar_url, normalize_agent_args}; +use crate::managed_agents::KIRO_AVATAR_URL; #[test] fn normalizes_kiro_cli_args_to_acp() { @@ -12,3 +13,11 @@ fn normalizes_kiro_cli_args_to_acp() { vec!["acp"] ); } + +#[test] +fn resolves_kiro_avatar() { + assert_eq!( + managed_agent_avatar_url("kiro-cli"), + Some(KIRO_AVATAR_URL.to_string()) + ); +} diff --git a/desktop/src-tauri/src/managed_agents/mod.rs b/desktop/src-tauri/src/managed_agents/mod.rs index 66f3d882b9..ecc2e24f00 100644 --- a/desktop/src-tauri/src/managed_agents/mod.rs +++ b/desktop/src-tauri/src/managed_agents/mod.rs @@ -25,6 +25,7 @@ mod repos; mod restore; pub mod retention; mod runtime; +mod runtime_avatars; mod runtime_commands; mod runtime_types; pub(crate) mod spawn_hash; @@ -68,6 +69,7 @@ pub use repos::{ }; pub use restore::*; pub use runtime::*; +pub(crate) use runtime_avatars::{normalize_avatars, KIRO_AVATAR_URL}; pub use runtime_commands::*; pub use runtime_types::*; pub use storage::*; diff --git a/desktop/src-tauri/src/managed_agents/runtime_avatars.rs b/desktop/src-tauri/src/managed_agents/runtime_avatars.rs new file mode 100644 index 0000000000..3c9041058d --- /dev/null +++ b/desktop/src-tauri/src/managed_agents/runtime_avatars.rs @@ -0,0 +1,42 @@ +use super::ManagedAgentRecord; + +pub(crate) const KIRO_AVATAR_URL: &str = "https://kiro.dev/icon.svg?fe599162bb293ea0"; +const LEGACY_KIRO_AVATAR_URL: &str = "https://kiro.dev/images/kiro-wordmark.png"; + +fn normalize_runtime_avatar(command: &str, avatar_url: &mut Option) { + if super::known_acp_runtime(command).is_some_and(|runtime| runtime.id == "kiro") + && avatar_url.as_deref() == Some(LEGACY_KIRO_AVATAR_URL) + { + *avatar_url = Some(KIRO_AVATAR_URL.to_string()); + } +} + +pub(crate) fn normalize_avatars(records: &mut [ManagedAgentRecord]) -> &mut [ManagedAgentRecord] { + for record in records.iter_mut() { + normalize_runtime_avatar(&record.agent_command, &mut record.avatar_url); + } + records +} + +#[cfg(test)] +mod tests { + use super::{normalize_runtime_avatar, KIRO_AVATAR_URL, LEGACY_KIRO_AVATAR_URL}; + + #[test] + fn updates_only_legacy_kiro_defaults() { + let mut legacy_kiro = Some(LEGACY_KIRO_AVATAR_URL.to_string()); + let mut custom_kiro = Some("https://example.com/custom.png".to_string()); + let mut non_kiro = Some(LEGACY_KIRO_AVATAR_URL.to_string()); + + normalize_runtime_avatar("kiro-cli", &mut legacy_kiro); + normalize_runtime_avatar("kiro-cli", &mut custom_kiro); + normalize_runtime_avatar("codex-acp", &mut non_kiro); + + assert_eq!(legacy_kiro.as_deref(), Some(KIRO_AVATAR_URL)); + assert_eq!( + custom_kiro.as_deref(), + Some("https://example.com/custom.png") + ); + assert_eq!(non_kiro.as_deref(), Some(LEGACY_KIRO_AVATAR_URL)); + } +} diff --git a/desktop/src-tauri/src/managed_agents/storage.rs b/desktop/src-tauri/src/managed_agents/storage.rs index b9c6c7e6cd..2d7fbe4677 100644 --- a/desktop/src-tauri/src/managed_agents/storage.rs +++ b/desktop/src-tauri/src/managed_agents/storage.rs @@ -199,7 +199,7 @@ fn load_agent_store(app: &AppHandle) -> Result, String> pub fn load_managed_agents(app: &AppHandle) -> Result, String> { let mut records = load_agent_store(app)?; records.retain(|record| !record.pubkey.is_empty()); - hydrate_keys(&mut records); + hydrate_keys(crate::managed_agents::normalize_avatars(&mut records)); Ok(records) } diff --git a/desktop/src/features/onboarding/assets/harness-logos/kiro.svg b/desktop/src/features/onboarding/assets/harness-logos/kiro.svg new file mode 100644 index 0000000000..811b8d8335 --- /dev/null +++ b/desktop/src/features/onboarding/assets/harness-logos/kiro.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/desktop/src/features/onboarding/ui/RuntimeIcon.tsx b/desktop/src/features/onboarding/ui/RuntimeIcon.tsx index a410914377..044ee8659f 100644 --- a/desktop/src/features/onboarding/ui/RuntimeIcon.tsx +++ b/desktop/src/features/onboarding/ui/RuntimeIcon.tsx @@ -8,11 +8,13 @@ import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark"; import chatgptLogoUrl from "../assets/harness-logos/chatgpt.png?inline"; import claudeLogoUrl from "../assets/harness-logos/claude.png?inline"; import gooseLogoUrl from "../assets/harness-logos/goose.png?inline"; +import kiroLogoUrl from "../assets/harness-logos/kiro.svg?inline"; const RUNTIME_LOGOS: Record = { claude: claudeLogoUrl, codex: chatgptLogoUrl, goose: gooseLogoUrl, + kiro: kiroLogoUrl, }; function isBuzzRuntime(runtime: AcpRuntimeCatalogEntry): boolean { diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 4c89d16541..7531e8f69c 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -7019,7 +7019,7 @@ async function handleDiscoverAcpRuntimes( { id: "kiro", label: "Kiro CLI", - avatar_url: "https://kiro.dev/images/kiro-wordmark.png", + avatar_url: "https://kiro.dev/icon.svg?fe599162bb293ea0", availability: "not_installed", command: null, binary_path: null, From 553645b04e9570f40caf5bb8f1f35eb184f23afc Mon Sep 17 00:00:00 2001 From: Fred Lackey Date: Sat, 25 Jul 2026 19:45:11 -0400 Subject: [PATCH 3/3] docs(agents): list Kiro in ACP runtime docs Signed-off-by: Fred Lackey --- ARCHITECTURE.md | 2 +- README.md | 4 ++-- TESTING.md | 12 ++++++------ VISION.md | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 90cbbac0cf..f3538bba93 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -648,7 +648,7 @@ Standalone binary that bridges Buzz relay events to AI agents via the [Agent Com **Architecture:** ``` -Buzz Relay ──WS──→ buzz-acp ──stdio (ACP/JSON-RPC)──→ Agent (goose/codex/claude) +Buzz Relay ──WS──→ buzz-acp ──stdio (ACP/JSON-RPC)──→ Agent (goose/kiro/codex/claude) ``` `buzz-acp` spawns AI agent subprocesses (1–32, default 1), connects to the relay via WebSocket with NIP-42 auth, discovers channels via REST API, and queues `@mention` events per channel. At most one prompt is in-flight per channel. Queued events are batched into a single prompt sent via `session/prompt` over ACP. diff --git a/README.md b/README.md index ea6a458cfb..7536932c88 100644 --- a/README.md +++ b/README.md @@ -100,7 +100,7 @@ Agents are part of the room, not haunted cron jobs. |---|---|---| | Relay, channels, threads, DMs, canvases, media, search, audit log | Mobile clients (iOS + Android, Flutter) | Web-of-trust reputation across relays | | Desktop app (Tauri + React) | Workflow approval gates (infra exists, glue still drying) | Push notifications | -| `buzz-cli` (agent-first, JSON in / JSON out) + ACP harness (Goose, Kiro CLI, Codex, Claude Code) | Huddle lifecycle events | Culture features | +| `buzz-cli` (agent-first, JSON in / JSON out) + ACP harness (goose, kiro, codex, claude code) | Huddle lifecycle events | Culture features | | YAML workflows: message / reaction / schedule / webhook triggers | | | | Git events (NIP-34: patches, repo announcements, status) | | | | Git hosting backend | | | @@ -206,7 +206,7 @@ A Rust workspace of focused crates. Single source of truth: the relay. See [ARCH **Services** — `buzz-db` (Postgres) · `buzz-auth` (NIP-42/98 Schnorr auth, rate limiting) · `buzz-pubsub` (Redis, presence, typing) · `buzz-search` (Postgres FTS) · `buzz-audit` (hash-chain log). Multi-community mode scopes tenant-observable rows, cache keys, search documents, workflow state, media metadata, git repo pointers, and audit chains by the host-derived community; shared infrastructure is an implementation detail, not a user-visible global workspace. -**Agent surface** — `buzz-cli` (agent-first CLI, JSON in / JSON out) · `buzz-acp` (ACP harness for Goose/Kiro CLI/Codex/Claude Code) · `buzz-agent` (ACP agent — see [VISION_AGENT.md](VISION_AGENT.md)) · `buzz-dev-mcp` (shell + file-edit tools) · `buzz-workflow` (YAML automation) · `buzz-persona` (agent persona packs) +**Agent surface** — `buzz-cli` (agent-first CLI, JSON in / JSON out) · `buzz-acp` (ACP harness for goose/kiro/codex/claude code) · `buzz-agent` (ACP agent — see [VISION_AGENT.md](VISION_AGENT.md)) · `buzz-dev-mcp` (shell + file-edit tools) · `buzz-workflow` (YAML automation) · `buzz-persona` (agent persona packs) **Git & pairing** — `git-sign-nostr` / `git-credential-nostr` (nostr-signed git) · `buzz-pair-relay` / `buzz-pairing-cli` (relay pairing) diff --git a/TESTING.md b/TESTING.md index 51a5eb44c1..0a4ae96e02 100644 --- a/TESTING.md +++ b/TESTING.md @@ -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, kiro, 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 @@ -221,10 +221,10 @@ 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 kiro / +> 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. 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 diff --git a/VISION.md b/VISION.md index b09f661ee3..7b45401238 100644 --- a/VISION.md +++ b/VISION.md @@ -213,7 +213,7 @@ Greenfield. Agent swarms build in parallel, integrating at the event store bound |-|------| | ✅ | Core relay, auth, pub/sub, search, audit | | ✅ | MCP server — full feature surface | -| ✅ | ACP agent harness — goose, codex, claude code | +| ✅ | ACP agent harness — goose, kiro, codex, claude code | | ✅ | Desktop client (Tauri) — Stream, Home, Forum, DMs, Agents, Workflows, Search, Settings, Profiles, Presence | | ✅ | Channel features — messaging, threads, reactions, canvases, media uploads, editing, deletion, typing indicators, NIP-29, soft-delete | | ✅ | Workflow engine — YAML-as-code, execution traces, message/reaction/schedule/webhook triggers |