diff --git a/crates/buzz-acp/README.md b/crates/buzz-acp/README.md index e6164b02dd3..5f7eab28428 100644 --- a/crates/buzz-acp/README.md +++ b/crates/buzz-acp/README.md @@ -267,9 +267,11 @@ 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, OpenCode, Buzz Agent): have auto-installers, auth probes, and first-class onboarding. Their IDs (`goose`, `claude`, `codex`, `opencode`, `buzz-agent`) are reserved and cannot be overridden. -**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. +**Tier-2 — preset catalog** (Cursor, Oh My Pi, Grok Build, 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. + +> **Note — default engine:** new agents default to the bundled `buzz-agent`. Set `BUZZ_DEFAULT_RUNTIME=` (e.g. `opencode`, `goose`, or any tier-2/tier-3 id) in the Desktop process environment to make another harness the site default for newly-created agents; unresolvable ids fall back to the bundled default with a warning. > **Note — OpenClaw:** `openclaw acp` is a Gateway-backed bridge; PATH availability shows "Available" even when the OpenClaw Gateway daemon is not running. This is expected tier-2 semantics (same class as a preset with unconfigured auth). The Gateway URL is configured via `OPENCLAW_GATEWAY_URL` (or the equivalent env var from OpenClaw's docs) — set it in the agent's **env vars** in Edit Agent, not in the definition env (the preset definition carries no env entries). Note that `openclaw acp` executes tools inside the Gateway daemon, not the Desktop process, so Desktop-injected `BUZZ_*` env vars do NOT reach the execution locus unless you also set them on the Gateway's own environment. diff --git a/crates/buzz-acp/src/config.rs b/crates/buzz-acp/src/config.rs index 5244ef5537a..fe1d1201f66 100644 --- a/crates/buzz-acp/src/config.rs +++ b/crates/buzz-acp/src/config.rs @@ -730,7 +730,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" | "opencode" => Some(vec!["acp".to_string()]), "codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code" | "claudecode" | "buzz-agent" => Some(Vec::new()), _ => None, @@ -1582,6 +1582,13 @@ mod tests { fn normalizes_goose_args_to_acp() { assert_eq!(normalize_agent_args("goose", Vec::new()), vec!["acp"]); assert_eq!(normalize_agent_args("goose", vec!["".into()]), vec!["acp"]); + // opencode is native ACP like goose: empty args must mean `acp`, or a + // bare `opencode` spawn would start the TUI instead of the harness. + assert_eq!(normalize_agent_args("opencode", Vec::new()), vec!["acp"]); + assert_eq!( + normalize_agent_args("opencode", vec!["acp".into()]), + vec!["acp"] + ); } #[test] diff --git a/desktop/src-tauri/src/managed_agents/discovery.rs b/desktop/src-tauri/src/managed_agents/discovery.rs index 78592357c9b..1ed0ecb9b65 100644 --- a/desktop/src-tauri/src/managed_agents/discovery.rs +++ b/desktop/src-tauri/src/managed_agents/discovery.rs @@ -26,7 +26,7 @@ pub(crate) use presets::{ preset_harness_ids, }; use presets::{preset_catalog_entry, PRESET_HARNESSES}; -pub(crate) use runtime_metadata::KnownAcpRuntime; +pub(crate) use runtime_metadata::{KnownAcpRuntime, OPENCODE_RUNTIME}; 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"; @@ -55,6 +55,8 @@ fn common_binary_paths() -> &'static [PathBuf] { home.join(".volta/bin"), home.join(".asdf/shims"), home.join(".bun/bin"), + // opencode's native installer target; it only edits rc files. + home.join(".opencode/bin"), ]); } // Windows well-known dirs for npm global shims and standalone installer targets. @@ -186,6 +188,7 @@ const KNOWN_ACP_RUNTIMES: &[KnownAcpRuntime] = &[ // Verified: `codex login status` exits 0 when logged in, non-zero otherwise. auth_probe_args: Some(&["codex", "login", "status"]), }, + OPENCODE_RUNTIME, KnownAcpRuntime { id: "buzz-agent", label: "Buzz Agent", @@ -291,15 +294,12 @@ pub(crate) fn known_acp_runtime_exact(id: &str) -> Option<&'static KnownAcpRunti } /// The agent command a freshly-created agent defaults to when the create -/// request supplies none. Resolves the bundled `buzz-agent` from the catalog so -/// the default cannot drift from the provider definition. Falls back to the id -/// if the catalog entry is missing. (Previous default was bare `goose`, which -/// is not on PATH on a stock Windows install; buzz-agent ships with the app.) +/// request supplies none: the bundled `buzz-agent`, unless the operator set +/// `BUZZ_DEFAULT_RUNTIME` to a resolvable harness id (three-tier lookup — +/// builtins, presets, custom registry; unresolvable ids warn and fall back). pub fn default_agent_command() -> String { - known_acp_runtime_exact("buzz-agent") - .and_then(|p| p.commands.first().copied()) - .unwrap_or("buzz-agent") - .to_string() + runtime_metadata::default_runtime_override() + .unwrap_or_else(runtime_metadata::bundled_default_agent_command) } /// Record-first harness resolution (unified agent model, Phase 1A). @@ -450,7 +450,7 @@ pub fn try_record_agent_command( fn default_agent_args(command: &str) -> Option> { match normalize_command_identity(command).as_str() { - "goose" => Some(vec!["acp".to_string()]), + "goose" | "opencode" => Some(vec!["acp".to_string()]), "codex" | "codex-acp" | "claude-agent-acp" | "claude-code-acp" | "claude-code" | "claudecode" | "buzz-agent" => Some(Vec::new()), _ => None, diff --git a/desktop/src-tauri/src/managed_agents/discovery/presets.rs b/desktop/src-tauri/src/managed_agents/discovery/presets.rs index fd853094515..bcb4e5846a2 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/presets.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/presets.rs @@ -126,15 +126,6 @@ pub(super) const PRESET_HARNESSES: &[PresetHarness] = &[ install_hint: "Buzz talks to Grok Build through its CLI's agent stdio mode.", underlying_cli: None, }, - PresetHarness { - id: "opencode", - label: "OpenCode", - command: "opencode", - args: &["acp"], - install_instructions_url: "https://opencode.ai/docs", - install_hint: "Buzz talks to OpenCode through its CLI's ACP mode (opencode acp).", - underlying_cli: None, - }, PresetHarness { id: "kimi", label: "Kimi Code", 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 34edecdcd9c..bb102a2840f 100644 --- a/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs +++ b/desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs @@ -83,6 +83,95 @@ impl KnownAcpRuntime { } } +const OPENCODE_AVATAR_URL: &str = + "https://raw.githubusercontent.com/block/buzz/refs/heads/main/desktop/public/harness-logos/opencode.svg"; + +/// Compiled-in opencode runtime (tier-1). Native ACP: the CLI is the agent, so +/// `underlying_cli` doubles as the Phase-1 auto-install marker. Two deliberate +/// `None`s, verified against opencode source: `auth_probe_args` (`opencode +/// auth list` exits 0 even with zero credentials — no faithful probe) and +/// `model_env_var` (opencode core reads no model env var; pinning is +/// config-side via `~/.config/opencode/opencode.json`). +pub(crate) const OPENCODE_RUNTIME: KnownAcpRuntime = KnownAcpRuntime { + id: "opencode", + label: "OpenCode", + commands: &["opencode"], + aliases: &[], + avatar_url: OPENCODE_AVATAR_URL, + mcp_command: None, + mcp_hooks: false, + underlying_cli: Some("opencode"), + // The vendor installer is one cross-platform bash script (Git Bash on + // Windows); opencode publishes no PowerShell installer to route through + // the Defender-safe two-step form, so both OSes use the pipe. + cli_install_commands: &["curl -fsSL https://opencode.ai/install | bash"], + cli_install_commands_windows: &[], + adapter_install_commands: &[], + cli_install_instructions_url: "https://opencode.ai/docs", + adapter_install_instructions_url: "", + cli_install_hint: "Buzz talks to OpenCode through the OpenCode CLI's ACP mode (opencode acp).", + adapter_install_hint: "", + skill_dir: Some(".opencode/skills"), + supports_acp_model_switching: false, + model_env_var: None, + provider_env_var: None, + provider_locked: false, + default_env: &[], + config_file_path: Some("~/.config/opencode/opencode.json"), + config_file_format: Some("json"), + supports_acp_native_config: false, + thinking_env_var: None, + max_tokens_env_var: None, + context_limit_env_var: None, + max_rounds_env_var: None, + required_normalized_fields: &[], + login_hint: None, + auth_probe_args: None, +}; + +/// Resolve a `BUZZ_DEFAULT_RUNTIME` value to a harness command via the +/// authoritative three-tier lookup (builtins → presets → loaded registry). +/// Empty or unresolvable values yield `None` so callers keep the bundled +/// default; unknown ids log a warning rather than pinning agents to a +/// dangling command. +pub(super) fn resolve_default_runtime(id: &str) -> Option { + let trimmed = id.trim(); + if trimmed.is_empty() { + return None; + } + match super::presets::command_for_runtime_id(trimmed) { + Some(cmd) => Some(cmd), + None => { + tracing::warn!( + runtime = trimmed, + "BUZZ_DEFAULT_RUNTIME does not resolve to a known harness — using bundled buzz-agent" + ); + None + } + } +} + +/// `BUZZ_DEFAULT_RUNTIME` engine override for newly-created agents, read live +/// from the process environment. Documented in crates/buzz-acp/README.md. +pub(super) fn default_runtime_override() -> Option { + resolve_default_runtime(&std::env::var("BUZZ_DEFAULT_RUNTIME").ok()?) +} + +/// The bundled default engine: buzz-agent ships with the app, so it is safe +/// on a stock install where no third-party CLI is on PATH. +pub(super) fn bundled_default_agent_command() -> String { + super::known_acp_runtime_exact("buzz-agent") + .and_then(|p| p.commands.first().copied()) + .unwrap_or("buzz-agent") + .to_string() +} + +/// opencode runtime tests live in a sibling file (file-size ratchet keeps +/// discovery.rs/tests.rs at their merge-base sizes; this module has headroom). +#[cfg(test)] +#[path = "tests/opencode.rs"] +mod opencode_tests; + #[cfg(test)] mod tests { use super::super::known_acp_runtime_exact; @@ -123,4 +212,33 @@ mod tests { assert!(codex.adapter_install_instructions_url.contains("codex-acp")); assert!(codex.cli_install_hint.contains("Codex CLI")); } + + #[test] + fn opencode_metadata_pins_native_acp_shape() { + let opencode = known_acp_runtime_exact("opencode").unwrap(); + + // Native ACP: the CLI is the agent — no adapter tier, and the CLI + // itself doubles as the underlying-CLI marker so Phase-1 install runs. + assert_eq!(opencode.commands, &["opencode"]); + assert_eq!(opencode.underlying_cli, Some("opencode")); + assert!(opencode.adapter_install_commands.is_empty()); + assert!(opencode + .cli_install_commands + .iter() + .any(|cmd| cmd.contains("https://opencode.ai/install"))); + + // Skills and config follow opencode's documented project layout. + assert_eq!(opencode.skill_dir, Some(".opencode/skills")); + assert_eq!( + opencode.config_file_path, + Some("~/.config/opencode/opencode.json") + ); + + // No faithful exit-code auth probe exists (`opencode auth list` exits + // 0 with zero credentials) and no native model env var is read by + // opencode core — both stay None until those become probeable. + assert_eq!(opencode.auth_probe_args, None); + assert_eq!(opencode.model_env_var, None); + assert_eq!(opencode.provider_env_var, None); + } } diff --git a/desktop/src-tauri/src/managed_agents/discovery/tests/opencode.rs b/desktop/src-tauri/src/managed_agents/discovery/tests/opencode.rs new file mode 100644 index 00000000000..1c3a4913bdd --- /dev/null +++ b/desktop/src-tauri/src/managed_agents/discovery/tests/opencode.rs @@ -0,0 +1,32 @@ +//! opencode runtime behavior: ACP-mode default args and the +//! `BUZZ_DEFAULT_RUNTIME` engine override resolution. + +use super::resolve_default_runtime; +use crate::managed_agents::discovery::normalize_agent_args; + +#[test] +fn opencode_defaults_to_acp_mode_args() { + // Bare `opencode` starts the TUI; ACP needs the `acp` subcommand, so the + // empty-args default must be `["acp"]` (goose pattern). + assert_eq!( + normalize_agent_args("opencode", Vec::new()), + vec!["acp".to_string()] + ); +} + +#[test] +fn resolve_default_runtime_accepts_known_harness_ids() { + // Tier-1 builtin id and a raw command form both resolve; whitespace is + // tolerated so operators can quote the value loosely. + assert_eq!(resolve_default_runtime("opencode"), Some("opencode".into())); + assert_eq!(resolve_default_runtime(" goose "), Some("goose".into())); +} + +#[test] +fn resolve_default_runtime_rejects_empty_and_unknown_ids() { + // Empty/blank keeps the bundled default silently; unknown ids do too but + // are worth a warning — pinned agents must never dangle. + assert_eq!(resolve_default_runtime(""), None); + assert_eq!(resolve_default_runtime(" "), None); + assert_eq!(resolve_default_runtime("definitely-not-a-harness"), None); +} diff --git a/desktop/src/features/onboarding/assets/harness-logos/opencode.svg b/desktop/src/features/onboarding/assets/harness-logos/opencode.svg new file mode 100644 index 00000000000..157edc4d752 --- /dev/null +++ b/desktop/src/features/onboarding/assets/harness-logos/opencode.svg @@ -0,0 +1,7 @@ + + + + + \ No newline at end of file diff --git a/desktop/src/features/onboarding/ui/RuntimeIcon.tsx b/desktop/src/features/onboarding/ui/RuntimeIcon.tsx index 5b247c31f73..53f5528a3e5 100644 --- a/desktop/src/features/onboarding/ui/RuntimeIcon.tsx +++ b/desktop/src/features/onboarding/ui/RuntimeIcon.tsx @@ -5,6 +5,7 @@ import type { AcpRuntimeCatalogEntry } from "@/shared/api/types"; import { cn } from "@/shared/lib/cn"; import { BuzzMark } from "@/shared/ui/buzz-logo/BuzzMark"; import claudeLogoUrl from "../assets/harness-logos/claude.png?inline"; +import opencodeLogoUrl from "../assets/harness-logos/opencode.svg?inline"; import { RUNTIME_MARKS } from "./HarnessMarks"; // Bundled logos for compiled-in runtimes (inline base64, no network fetch). @@ -12,15 +13,16 @@ import { RUNTIME_MARKS } from "./HarnessMarks"; // `currentColor`, so they adapt to dark/light without bitmap filters. const RUNTIME_LOGOS: Record = { claude: claudeLogoUrl, + opencode: opencodeLogoUrl, }; -// Public-path logos for bundled presets. Served from /harness-logos/ at runtime. -// Keys match the preset `id` values emitted by the backend PRESET_HARNESSES. +// Public-path logos for bundled presets. Served from /harness-logos/ at +// runtime. Keys match the preset `id` values emitted by the backend +// PRESET_HARNESSES; builtin runtimes belong in RUNTIME_LOGOS above. export const PRESET_LOGOS: Record = { devin: "/harness-logos/devin.svg", omp: "/harness-logos/omp.svg", grok: "/harness-logos/grok.svg", - opencode: "/harness-logos/opencode.svg", kimi: "/harness-logos/kimi.png", amp: "/harness-logos/amp.png", hermes: "/harness-logos/hermes.png", diff --git a/desktop/src/features/settings/ui/harnessCatalogCopy.ts b/desktop/src/features/settings/ui/harnessCatalogCopy.ts index 9a71ff70f92..d5169d24014 100644 --- a/desktop/src/features/settings/ui/harnessCatalogCopy.ts +++ b/desktop/src/features/settings/ui/harnessCatalogCopy.ts @@ -21,6 +21,8 @@ const HARNESS_DESCRIPTIONS: Record = { // Source: https://block.github.io/goose/ — "an open source, extensible AI // agent". goose: "Block's open-source, extensible AI agent.", + // Source: https://github.com/anomalyco/opencode + opencode: "An open-source coding agent.", // Bundled presets — sources per RESEARCH/BYOH_CATALOG_IA.md. // Source: https://cursor.com/docs/cli/acp @@ -30,8 +32,6 @@ const HARNESS_DESCRIPTIONS: Record = { // Source: https://build.x.ai (docs unavailable during research; kept // deliberately conservative). grok: "xAI's coding agent, connected to Buzz through its ACP entrypoint.", - // Source: https://github.com/anomalyco/opencode - opencode: "An open-source coding agent.", // Sources: https://github.com/MoonshotAI/kimi-cli, // https://moonshotai.github.io/kimi-cli/en/ kimi: "A terminal coding agent for software development and command-line tasks.", diff --git a/desktop/tests/e2e/harness-catalog-screenshots.spec.ts b/desktop/tests/e2e/harness-catalog-screenshots.spec.ts index c5613f41f2a..48ea56f5f26 100644 --- a/desktop/tests/e2e/harness-catalog-screenshots.spec.ts +++ b/desktop/tests/e2e/harness-catalog-screenshots.spec.ts @@ -61,6 +61,23 @@ const CATALOG = [ node_required: false, auth_status: { status: "unknown" }, }, + { + id: "opencode", + label: "OpenCode", + avatar_url: "", + availability: "not_installed", + command: null, + binary_path: null, + default_args: ["acp"], + mcp_command: null, + install_hint: + "Buzz talks to OpenCode through the OpenCode CLI's ACP mode (opencode acp).", + install_instructions_url: "https://opencode.ai/docs", + can_auto_install: true, + underlying_cli_path: null, + node_required: false, + auth_status: { status: "not_applicable" }, + }, { id: "cursor", label: "Cursor", @@ -97,24 +114,6 @@ const CATALOG = [ auth_status: { status: "not_applicable" }, source: "preset", }, - { - id: "opencode", - label: "OpenCode", - avatar_url: "", - availability: "not_installed", - command: null, - binary_path: null, - default_args: ["acp"], - mcp_command: null, - install_hint: - "Buzz talks to OpenCode through its CLI's ACP mode (opencode acp).", - install_instructions_url: "https://opencode.ai/docs", - can_auto_install: false, - underlying_cli_path: null, - node_required: false, - auth_status: { status: "not_applicable" }, - source: "preset", - }, { id: "my-custom", label: "My Custom Harness", diff --git a/docs/remote-agents.md b/docs/remote-agents.md index 45289ef910a..9611d428936 100644 --- a/docs/remote-agents.md +++ b/docs/remote-agents.md @@ -106,7 +106,7 @@ The defining constraint, stated as a design axiom: An agent's identity is a Nostr keypair. The **agent record** on `D` carries: `name`, `relay_url`, the nsec (keyring-hydrated), the NIP-OA `auth` tag attesting owner authorization, `agent_command`/`agent_args` (the ACP agent the -harness spawns — `goose`, `claude-agent-acp`, `codex-acp`, `buzz-agent`, or +harness spawns — `goose`, `claude-agent-acp`, `codex-acp`, `opencode`, `buzz-agent`, or any user-supplied command: this is the **configurable harness** requirement), effective `system_prompt`/`model`/`provider`, timeout and parallelism knobs, the `respond_to` gate, merged `env_vars`, and a `backend` discriminator: @@ -1028,8 +1028,9 @@ the local spawn's `credential./git.helper` scoping — never a global `credential.helper`: a global nostr helper would answer for every remote, including github.com. ~15–25MB; not FROM-scratch (bash and git preclude it). Sprig-only: alternate-harness -dependencies (node for Claude Code / Codex) come via the `image` override -field, not a fatter default. Tagging follows the relay image's matrix — +dependencies (node for Claude Code / Codex, the OpenCode binary) come via the +`image` override field, not a fatter default — see +`examples/sprig-opencode/` for the reference derived image. Tagging follows the relay image's matrix — `sha-` on main, semver on `sprig-v*` tags (the sprig tarball's `+git.` version string is not a legal Docker tag). **The default image reference MUST be pinned by digest, not tag**: the provider bakes, at diff --git a/examples/README.md b/examples/README.md index 8649ba360dd..c2d50f52076 100644 --- a/examples/README.md +++ b/examples/README.md @@ -2,6 +2,12 @@ This directory contains reference material for building on Buzz beyond the desktop app and AI agents. +## `sprig-opencode/` + +A derived remote-agent image adding the OpenCode runtime (`opencode acp`) on +top of the published sprig image — the spec-blessed `image` override pattern +for alternate harnesses. See [`sprig-opencode/README.md`](sprig-opencode/README.md). + ## `countdown-bot/` A small non-AI bot that connects directly to the Buzz relay over WebSocket, authenticates with NIP-42, subscribes to one channel, and replies to deterministic commands like `!countdown 5` and `!fib 8`. diff --git a/examples/sprig-opencode/Dockerfile b/examples/sprig-opencode/Dockerfile new file mode 100644 index 00000000000..69a780f413f --- /dev/null +++ b/examples/sprig-opencode/Dockerfile @@ -0,0 +1,29 @@ +# syntax=docker/dockerfile:1.7 +# Derived sprig image with the OpenCode runtime for remote agents. +# +# Spec context (docs/remote-agents.md, Image): the default +# `ghcr.io/block/buzz-sprig` stays lean (~15-25MB) and alternate-harness +# dependencies come via the `image` override field, not a fatter default. +# This file is the "buzz-sprig plus your tools" pattern: build it, push it +# to a registry your cluster can pull, and set it as the agent's `image` +# override when `agent_command` is `opencode`. +# +# Multi-arch: build on native amd64 and arm64 runners, like Dockerfile.sprig. +# The tag/digest below is an example — pin to the digest you actually built +# against (the spec requires digest pinning for default references and +# records overrides in the pod annotation). +FROM ghcr.io/block/buzz-sprig:latest + +# OpenCode runtime (tier-1 harness, native ACP via `opencode acp`). +# Pinned release — bump deliberately. Musl asset naming: +# opencode-linux--musl.tar.gz with arch arm64|x64, single root-level +# binary. Same URL shape as https://opencode.ai/install. +ARG OPENCODE_VERSION=1.18.21 +RUN set -eux; \ + arch="$(uname -m)"; \ + case "$arch" in aarch64) arch="arm64" ;; x86_64) arch="x64" ;; *) echo "unsupported arch $arch" >&2; exit 1 ;; esac; \ + curl -fsSL "https://github.com/anomalyco/opencode/releases/download/v${OPENCODE_VERSION}/opencode-linux-${arch}-musl.tar.gz" \ + | tar -xz -C /usr/local/bin + +# The sprig entrypoint, PATH (includes /usr/local/bin), user, and baked +# gitconfig are inherited unchanged — this image only adds the runtime. diff --git a/examples/sprig-opencode/README.md b/examples/sprig-opencode/README.md new file mode 100644 index 00000000000..866916c05c6 --- /dev/null +++ b/examples/sprig-opencode/README.md @@ -0,0 +1,52 @@ +# sprig-opencode + +A derived sprig image that adds the [OpenCode](https://opencode.ai) runtime, +for remote agents whose `agent_command` is `opencode` (native ACP mode, +`opencode acp`). + +## Why a derived image + +The default `ghcr.io/block/buzz-sprig` intentionally ships only Buzz's own +multicall binary (~15-25MB). Per the remote-agent spec +(`docs/remote-agents.md`, *Image*), alternate-harness dependencies come via +the agent's `image` override — "buzz-sprig plus your tools" — so OpenCode +(and its credentials path) is added here instead of fattening the default. + +## Build and push + +Build on the architectures your cluster runs (the base image and the musl +assets are multi-arch): + +```sh +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --tag /buzz-sprig-opencode: \ + --push \ + . +``` + +Bump the `OPENCODE_VERSION` build arg deliberately — it is pinned so image +builds are reproducible. + +## Use it + +Set the agent's image override to the pushed reference (digest form is the +most traceable) and the harness command to OpenCode: + +- `agent_command`: `opencode` (resolved in-image; sprig's `opencode acp` + default args apply) +- `image`: `/buzz-sprig-opencode@sha256:` + +## Credentials + +OpenCode resolves providers from its own config/auth store +(`~/.config/opencode/opencode.json`, `~/.local/share/opencode/auth.json`) +plus environment variables. For remote pods, provider credentials are +typically injected through the agent record's `env_vars` rather than baked +into the image — never bake secrets into a pushed layer. + +## Conformance + +An image override MUST contain the runtime ABI — the `buzz-acp` entrypoint +and everything the launch ABI requires. Building `FROM` the published sprig +image inherits all of it; only add tools on top.