mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
## What
Implements a "bring your own harness" (BYOH) generic ACP mechanism —
replacing per-harness backend code with a data-driven 3-tier system:
- **Tier 1 (compiled-in builtins):** goose, claude, codex, buzz-agent —
unchanged behavior
- **Tier 2 (bundled presets):** cursor, omp, grok, opencode, kimi, amp,
hermes, openclaw, and any future additions — defined in
`PRESET_HARNESSES`, no code duplication, icons stay
TerminalSquare/bundled-asset-only
- **Tier 3 (user-defined custom):** JSON definitions saved to
`custom_harnesses/` under app data; managed via Settings → Agents UI
## Changes
### Core data model
- `HarnessDefinition` — id, label, command, args, env, install URL/hint
- `PRESET_HARNESSES` static table — single source of truth for all
presets; `preset_harness_ids()` derives reserved IDs (D-11: no
hand-maintained copy)
- `source: "builtin" | "preset" | "custom"` tagging on every catalog
entry
### Persistence (B-4, B-6)
- `save_custom_harness_to_dir(dir, definition, rename_old_id)` —
backup-swap atomic write (backs up target → .bak, commits temp → target,
restores .bak on failure, removes .bak on success); safe on Windows
where `fs::rename` over an existing file is "access denied"
- `save_and_warm` / `delete_and_warm` — hold `PERSIST_MUTEX` for the
write + registry-warm pair, eliminating the lost-update race (B-6) where
two concurrent saves could interleave their warm calls and leave a stale
registry snapshot
- Validate-before-mutate: both IDs and env validated before any
filesystem mutation
### Env validation boundary (B-3)
- `validate_harness_definition_pub` calls `validate_user_env_keys` on
definition env at save AND load
- Rejects malformed keys (BUZZ_AUTH_TAG=x forgery shape), reserved keys
(BUZZ_MANAGED_AGENT etc.), NUL bytes, oversized values
### TypeScript boundary (B-2 / Thufir CRITICAL)
- `RawAcpRuntimeCatalogEntry` now declares `definition_env?:
Record<string,string>` and `source: "builtin" | "preset" | "custom"`
- `fromRawAcpRuntimeCatalogEntry` maps `definition_env → definitionEnv`
(camelCase); absent field defaults to `{}`
- Edit form reads `entry.definitionEnv` — env no longer erased on
save-then-edit cycle
### Unified descriptor (Phase A / Thufir F4)
- `EffectiveHarnessDescriptor { command, args, env }` in `readiness.rs`
- `resolve_effective_harness_descriptor()` — single resolver used by
spawn, spawn_hash, summary, get_agent_models (both saved and unsaved),
and readiness
- No competing arg-resolution forms
### Other fixes
- B-5: stop freezing `runtime.defaultArgs` into `record.agent_args` on
normal create paths
- B-7: readiness exec-check — `MissingBinary` variant for custom
commands not found on PATH
- B-8: onboarding transition — `setTimeout(0)` removed, parent-owned
route intent via `navigateAfterComplete` prop
- C-9: collector-discriminating sweep tests with injectable filters
- C-10: `HarnessManagementCard` uses `harnessGalleryLogic` helpers
(killed duplicate filter/sort)
- D-11: `BUILTIN_IDS` derived from `PRESET_HARNESSES` (no
hand-maintained copy)
- D-12: `mobile/pubspec.lock` churn reverted
- D-13: false ownership fast-path comment fixed
- D-14: URL scheme validation for `installInstructionsUrl`
- D-15: OpenClaw Gateway env-locus README line
### Tests added
**B-4 persistence (6 tests):**
`save_to_dir_create_writes_file_and_loads_back`,
`save_to_dir_same_id_edit_replaces_content`,
`save_to_dir_backup_is_cleaned_up_after_same_id_edit`,
`save_to_dir_rename_removes_old_file_and_creates_new`,
`save_to_dir_rename_nonexistent_old_id_is_non_fatal`,
`save_to_dir_roundtrip_with_env_preserves_values`
**B-3 env validation (6 tests):**
`validate_rejects_malformed_key_with_equals_sign`,
`validate_rejects_reserved_key_buzz_managed_agent`,
`validate_rejects_reserved_key_case_insensitive`,
`validate_rejects_nul_byte_in_value`,
`validate_rejects_value_over_per_value_size_limit`,
`validate_accepts_well_formed_env`
**B-2 API boundary (4 TS tests in tauri.test.mjs):**
`fromRawAcpRuntimeCatalogEntry maps definition_env to definitionEnv`,
`defaults definitionEnv to {} when absent`, `preserves source preset`,
`env round-trips through edit payload shape`
## Preset catalog
| ID | Label | Command |
|----|-------|---------|
| `cursor` | Cursor | `cursor-agent acp` |
| `omp` | Oh My Pi | `omp acp` |
| `grok` | Grok Build | `grok agent --always-approve stdio` |
| `opencode` | OpenCode | `opencode acp` |
| `kimi` | Kimi Code | `kimi acp` |
| `amp` | Amp | `amp-acp` |
| `hermes` | Hermes Agent | `hermes-acp` |
| `openclaw` | OpenClaw | `openclaw acp` |
## Review-fix pass (2026-07-26, Eva)
Fixes from the three-way review (Wren / Dawn / Eva) in the
buzz-generic-acp-harnesses thread, pushed as new commits (no rewrite):
1. **installHint edit round-trip** — form seeding extracted to
`formValuesFromCatalogEntry` (single source of truth), input rendered,
full-definition lossless round-trip regression.
2. **Dangling-delete coherence** — delete allowed; confirm counts
referencing agents (direct pin + persona-inherited); summary rows render
`harness (deleted): <id>`; spawn errors become actionable sentences
(`user_facing_harness_error`); composed delete→summary→start test.
3. **Comma-in-args** — rejected at `validate_harness_definition` (shared
by save AND disk load), mirrored inline in the form.
4. **Registry publish race** — collision/dup filtering moved into
`load_custom_harnesses` (both loaders inherit shadowing rules);
discovery publishes by re-reading the dir under `persist_mutex` (lock
scoped to publish only); deterministic interleaving regressions for
save-during-discovery and delete-during-discovery.
5. **Mechanical** — discarded `belongs_to_us` sweep arg deleted,
`load_global_agent_config` hoisted out of the per-record summary loop,
duplicated doc paragraph + stray SAFETY comment removed.
6. **PGID test de-flaked** — leader kept alive through the assertion.
Known follow-up (filed in review, not blocking): file-size split-outs
queued in `check-file-sizes.mjs` entries.
## Gate table — head `bf53f1d60`
| Gate | Result |
|------|--------|
| `cargo test --lib` (desktop/src-tauri) | **1701 passed**, 0 failed, 14
ignored |
| desktop JS suite (`pnpm test`) | **3605 passed**, 0 failed |
| `tsc --noEmit` | clean |
| `biome check` + file-size/px/pubkey checks | clean |
| `cargo clippy --lib -- -D warnings` | clean |
| `cargo fmt --check` | clean |
PR head: `bf53f1d60e3cbd07392e1287b83bb37ba90d0d33` — includes merge of
origin/main (`c2a4ee711`, conflicts in agent_models composed with
#2890's live Databricks discovery)
---------
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Signed-off-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@buzz.block.builderlab.xyz>
Co-authored-by: tlongwell-block <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: Tyler Longwell <tlongwell@block.xyz>
Co-authored-by: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
341 lines
18 KiB
Markdown
341 lines
18 KiB
Markdown
# buzz-acp
|
||
|
||
ACP harness that connects AI agents to Buzz. The harness listens for @mentions on the relay, prompts your agent, and the agent replies using the Buzz CLI.
|
||
|
||
```
|
||
Buzz Relay ──WS──→ buzz-acp ──stdio──→ Your Agent
|
||
│
|
||
Buzz CLI
|
||
(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)).
|
||
|
||
## Prerequisites
|
||
|
||
- A running Buzz relay (`just relay` starts Docker services automatically, or use a hosted instance)
|
||
- A Nostr keypair for the agent (see [Generating Keys](#generating-keys))
|
||
|
||
Build:
|
||
|
||
```bash
|
||
cargo build --release -p buzz-acp
|
||
export PATH="$PWD/target/release:$PATH"
|
||
```
|
||
|
||
## Generating Keys
|
||
|
||
Each agent needs a Nostr keypair — this is the agent's identity in Buzz. Use `buzz-admin` to generate one:
|
||
|
||
```bash
|
||
cargo run -p buzz-admin -- generate-key
|
||
```
|
||
|
||
This prints a public and secret key pair as hex. **Save the secret key immediately — it is not stored and cannot be recovered.** Set `BUZZ_PRIVATE_KEY` to the secret key to act as this identity.
|
||
|
||
Then register the agent's public key as a relay member so it can read and publish:
|
||
|
||
```bash
|
||
BUZZ_RELAY_PRIVATE_KEY=<relay signing key> \
|
||
cargo run -p buzz-admin -- add-member --pubkey <agent public key>
|
||
```
|
||
|
||
`add-member` publishes a kind:13534 membership event, so the relay needs a stable signing key: set `BUZZ_RELAY_PRIVATE_KEY` in the relay's environment (uncomment it in `.env`) and restart the relay before running this.
|
||
|
||
> **Running multiple agents?** Mint a separate keypair for each. Every agent needs its own identity.
|
||
|
||
## Channels
|
||
|
||
The harness discovers channels by querying the relay with the agent's authenticated identity.
|
||
|
||
By default, the harness discovers only channels the agent is a **member** of (`GET /api/channels?member=true`). When the agent is added to a new channel, the membership notification subscription auto-subscribes to it.
|
||
|
||
**Private channels** require explicit membership. The relay doesn't yet have a REST/event API for managing channel members — this is a known gap. For now, use `create_channel` via the Buzz CLI to create new channels (the creator is automatically a member).
|
||
|
||
## Quick Start (goose)
|
||
|
||
```bash
|
||
export BUZZ_PRIVATE_KEY="nsec1..." # your agent's key (see "Generating Keys")
|
||
export BUZZ_RELAY_URL="ws://localhost:3000"
|
||
export GOOSE_MODE=auto
|
||
|
||
buzz-acp
|
||
```
|
||
|
||
That's it. The harness spawns `goose acp`, connects to the relay, discovers channels, and starts listening. When someone @mentions the agent, goose receives the message and can reply using the Buzz CLI that the harness configures automatically.
|
||
|
||
## Running with Codex
|
||
|
||
[codex-acp](https://github.com/agentclientprotocol/codex-acp) wraps OpenAI Codex in an ACP interface.
|
||
|
||
```bash
|
||
# Install the adapter (npm package — no Rust build required)
|
||
npm install -g @agentclientprotocol/codex-acp
|
||
|
||
# Run
|
||
export OPENAI_API_KEY="sk-..." # required — use an OpenAI API key, not a ChatGPT subscription
|
||
|
||
buzz-acp
|
||
```
|
||
|
||
> **API key note:** `codex-acp` always attempts a ChatGPT WebSocket login first, which logs a `426 Upgrade Required` error. This is expected and non-fatal — it falls back to `OPENAI_API_KEY` automatically. Set `OPENAI_API_KEY` to ensure it has a working fallback.
|
||
|
||
## Running with Claude Code
|
||
|
||
[claude-agent-acp](https://github.com/agentclientprotocol/claude-agent-acp) wraps the Claude Agent SDK in an ACP interface.
|
||
|
||
```bash
|
||
# Install the current adapter package
|
||
npm install -g @agentclientprotocol/claude-agent-acp
|
||
|
||
# Run
|
||
export ANTHROPIC_API_KEY="sk-ant-..."
|
||
export BUZZ_ACP_AGENT_COMMAND="claude-agent-acp"
|
||
|
||
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.
|
||
|
||
## Configuration
|
||
|
||
All configuration is via environment variables (or CLI flags — every env var has a matching flag).
|
||
|
||
### Core
|
||
|
||
| Variable | Required | Default | Description |
|
||
|----------|----------|---------|-------------|
|
||
| `BUZZ_PRIVATE_KEY` | **yes** | — | Agent's Nostr private key (`nsec1...`). Used for relay auth and agent identity. |
|
||
| `BUZZ_RELAY_URL` | no | `ws://localhost:3000` | Relay WebSocket URL. |
|
||
| `BUZZ_ACP_AGENT_COMMAND` | no | `goose` | Agent binary to spawn. |
|
||
| `BUZZ_ACP_AGENT_ARGS` | no | `acp` | Agent arguments (comma-separated). |
|
||
| `BUZZ_ACP_MCP_COMMAND` | no | `""` (empty) | Path to an optional MCP server binary to provide to the agent subprocess. |
|
||
| `BUZZ_ACP_IDLE_TIMEOUT` | no | `620` | Idle timeout: max seconds of silence before cancelling a turn. Resets on any agent stdout activity. |
|
||
| `BUZZ_ACP_MAX_TURN_DURATION` | no | `7200` | Absolute wall-clock cap per turn (safety valve). |
|
||
| `BUZZ_API_TOKEN` | no | — | API token (required if relay enforces token auth). |
|
||
|
||
**Note:** `BUZZ_ACP_AGENT_ARGS` splits on commas. For args with values, use: `-c,key="value"`.
|
||
|
||
**Legacy env vars:** `BUZZ_ACP_PRIVATE_KEY`, `BUZZ_ACP_API_TOKEN`, and `BUZZ_ACP_TURN_TIMEOUT` (replaced by `BUZZ_ACP_IDLE_TIMEOUT`) are still accepted as fallbacks.
|
||
|
||
### Parallel Agents & Heartbeat
|
||
|
||
| Flag | Env Var | Default | Description |
|
||
|------|---------|---------|-------------|
|
||
| `--agents` | `BUZZ_ACP_AGENTS` | `1` | Number of agent subprocesses (1–32). |
|
||
| `--lazy-pool` | `BUZZ_ACP_LAZY_POOL` | `false` | Connect, subscribe, and queue accepted work before starting ACP/LLM subprocesses. The first accepted event wakes one pool initialization task; failures retry with bounded exponential backoff while work remains. |
|
||
| `--heartbeat-interval` | `BUZZ_ACP_HEARTBEAT_INTERVAL` | `0` | Seconds between heartbeat prompts. `0` = disabled. Must be `0` or ≥10 when enabled. |
|
||
| `--heartbeat-prompt` | `BUZZ_ACP_HEARTBEAT_PROMPT` | (built-in) | Custom heartbeat prompt text. Conflicts with `--heartbeat-prompt-file`. |
|
||
| `--heartbeat-prompt-file` | `BUZZ_ACP_HEARTBEAT_PROMPT_FILE` | — | Read heartbeat prompt from a file. Conflicts with `--heartbeat-prompt`. |
|
||
|
||
### Inbound Author Gate
|
||
|
||
Controls which authors' events the harness forwards to the agent. Events from disallowed authors are silently dropped before reaching subscription rules.
|
||
|
||
| Flag | Env Var | Default | Description |
|
||
|------|---------|---------|-------------|
|
||
| `--respond-to` | `BUZZ_ACP_RESPOND_TO` | `owner-only` | Author gate mode: `owner-only`, `allowlist`, `anyone`, `nobody`. |
|
||
| `--respond-to-allowlist` | `BUZZ_ACP_RESPOND_TO_ALLOWLIST` | — | Comma-separated 64-char hex pubkeys (required when mode is `allowlist`). Owner is always implicitly included. |
|
||
|
||
**Modes:**
|
||
|
||
| Mode | Behavior |
|
||
|------|----------|
|
||
| `owner-only` | Forward only events from the agent's registered owner. If no owner is set, all events are dropped until the owner is resolved. |
|
||
| `allowlist` | Forward events from the listed pubkeys plus the owner. |
|
||
| `anyone` | Forward all events (no author filtering). |
|
||
| `nobody` | Drop all inbound events. Agent only acts on heartbeat prompts. |
|
||
|
||
The gate applies to **all** inbound events — @mentions, DMs, thread replies, and any event delivered by the relay. Owner control commands are checked **before** the gate, so the owner can still manage the harness regardless of mode:
|
||
|
||
| Command | Effect |
|
||
|---------|--------|
|
||
| `!shutdown` | Gracefully exits the harness. |
|
||
| `!cancel` | Cancels the current in-flight turn for that channel, if any. |
|
||
| `!rotate` | Rotates the ACP session for that channel. If a turn is in-flight, it is cancelled and the channel session is invalidated when the task returns; otherwise the cached idle session is invalidated immediately. The next queued/received event starts a fresh session. |
|
||
|
||
Use `!cancel` to stop only the current turn; it is a no-op when the channel is idle. Use `!rotate` when you want the next turn in the channel to start from a fresh ACP session, even if the channel is currently idle.
|
||
|
||
Owner control commands must be kind:9 stream messages from the owner, must mention this agent with a `p` tag, and are consumed by the harness instead of being forwarded to the agent.
|
||
|
||
> **Note:** The default mode is `owner-only`. Agents without a registered `agent_owner_pubkey` will not respond to any events until the owner is resolved. Set `--respond-to anyone` to disable the gate entirely.
|
||
|
||
**Examples:**
|
||
|
||
```bash
|
||
# Default: only respond to owner
|
||
buzz-acp
|
||
|
||
# Respond to a team of three users (owner always included automatically)
|
||
buzz-acp --respond-to allowlist \
|
||
--respond-to-allowlist "abc123...64hex,def456...64hex,789abc...64hex"
|
||
|
||
# Respond to anyone (open agent)
|
||
buzz-acp --respond-to anyone
|
||
|
||
# Broadcast-only: post on heartbeat, ignore all inbound events
|
||
buzz-acp --respond-to nobody --heartbeat-interval 300
|
||
```
|
||
|
||
### Configuration Examples
|
||
|
||
**Single agent, no heartbeat (default):**
|
||
```bash
|
||
buzz-acp
|
||
```
|
||
|
||
**Four agents, no heartbeat (high-throughput event processing):**
|
||
```bash
|
||
buzz-acp --agents 4
|
||
```
|
||
|
||
**Two agents with 5-minute heartbeat:**
|
||
```bash
|
||
buzz-acp --agents 2 --heartbeat-interval 300
|
||
```
|
||
|
||
**Custom heartbeat prompt:**
|
||
```bash
|
||
buzz-acp --agents 2 --heartbeat-interval 300 \
|
||
--heartbeat-prompt "Check get_feed_actions() for pending approvals, then get_feed_mentions() for unanswered mentions. If nothing actionable, end your turn immediately."
|
||
```
|
||
|
||
### Shared Identity
|
||
|
||
All N agents authenticate as the **same Nostr bot identity** — users see one bot regardless of how many agents are running. The same channel is never processed by two agents simultaneously (the queue enforces this). Cross-channel message ordering is not guaranteed when N>1.
|
||
|
||
### Heartbeat Semantics
|
||
|
||
When `--heartbeat-interval` is set, the harness fires a prompt on an idle agent at the configured interval. Heartbeat rules:
|
||
|
||
- **Lower priority than queued events** — if events are pending, they are dispatched first.
|
||
- **Skipped when all agents are busy** — no queuing; the tick is simply dropped.
|
||
- **At most one heartbeat in flight globally** — the next tick is suppressed until the current one completes.
|
||
- **Default prompt** (when `--heartbeat-prompt` is not set) calls `get_feed_actions()` and `get_feed_mentions()` to surface pending work.
|
||
|
||
Heartbeat is designed for idle periods. Under sustained event load it will rarely fire — that's expected.
|
||
|
||
### Choosing N
|
||
|
||
Start with **N=2** for most deployments. Increase if queue depth grows under load. Each agent spawns its own MCP server subprocess, so resource usage scales approximately as N × (agent memory + MCP server memory). Maximum is 32.
|
||
|
||
## Forum Channels
|
||
|
||
By default, the ACP harness subscribes to stream message kinds (9, 46010, 40007). To receive forum events, opt in with `--kinds` and disable the mention filter (forum posts don't @mention agents):
|
||
|
||
**CLI flags:**
|
||
```bash
|
||
buzz-acp --kinds 9,46010,40007,45001,45002,45003 --no-mention-filter
|
||
```
|
||
|
||
**Or with `--subscribe all`:**
|
||
```bash
|
||
buzz-acp --subscribe all --kinds 9,46010,40007,45001,45002,45003
|
||
```
|
||
|
||
**Per-channel config:**
|
||
```toml
|
||
[channel.CHANNEL_UUID]
|
||
kinds = [9, 46010, 40007, 45001, 45002, 45003]
|
||
require_mention = false
|
||
```
|
||
|
||
Forum event kinds:
|
||
- **45001** — Forum post (thread root)
|
||
- **45002** — Vote on a post or comment
|
||
- **45003** — Comment reply on a forum post
|
||
|
||
> **Note:** Without `--no-mention-filter` (or `require_mention = false`), the default `subscribe=mentions` mode filters events that don't @mention the agent — forum posts will be invisible.
|
||
|
||
## How It Works
|
||
|
||
1. **Startup** — Spawns N agent subprocesses (default 1), sends ACP `initialize` to each, connects to the relay with NIP-42 auth.
|
||
2. **Channel discovery** — Queries the relay REST API for accessible channels, subscribes to each.
|
||
3. **Event loop** — Listens for @mention events (kind 9 with the agent's pubkey in a `#p` tag). Events queue per channel.
|
||
4. **Prompting** — When events are pending and no prompt is in flight for that channel, drains all queued events for the oldest channel into a single batched prompt via ACP `session/prompt`.
|
||
5. **Agent response** — The agent processes the prompt and uses the Buzz CLI (`send_message`, `get_messages`, etc.) to interact with Buzz.
|
||
6. **Recovery** — If the agent crashes, the harness respawns it. If the relay disconnects, the harness reconnects with a `since` filter to avoid missing events.
|
||
|
||
Each channel has at most one prompt in flight. Multiple channels can be processed concurrently when agents > 1.
|
||
|
||
> **Note:** On startup, the harness replays all unprocessed @mentions since the last run. Expect a burst of activity if there are stale events in the channel.
|
||
|
||
## Bring Your Own Harness (BYOH)
|
||
|
||
Buzz Desktop supports registering any ACP-speaking agent tool as a selectable runtime without a PR.
|
||
|
||
### 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-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.
|
||
|
||
> **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.
|
||
|
||
**Tier-3 — user custom harnesses**: JSON files in `<app-data>/custom_harnesses/` that the user can create from the Settings UI or drop in directly. Each file describes one harness — no install scripts.
|
||
|
||
### Custom harness JSON schema
|
||
|
||
```json
|
||
{
|
||
"id": "my-agent",
|
||
"label": "My Agent",
|
||
"command": "my-agent-bin",
|
||
"args": ["acp"],
|
||
"env": {
|
||
"MY_AGENT_MODE": "acp"
|
||
},
|
||
"installInstructionsUrl": "https://example.com/docs",
|
||
"installHint": "Download from example.com"
|
||
}
|
||
```
|
||
|
||
Fields:
|
||
- `id` — `[a-z0-9_][a-z0-9_-]*` (used as the runtime picker value and file name)
|
||
- `label` — human-readable name shown in the UI
|
||
- `command` — the executable name or absolute path (must be non-empty)
|
||
- `args` — optional default CLI arguments (array); instance-level args override this when non-empty
|
||
- `env` — optional environment variables injected at spawn time (definition env is a floor; user/persona/global env overrides it; Buzz-reserved keys like `BUZZ_MANAGED_AGENT` are always stripped and cannot be overridden)
|
||
- `installInstructionsUrl` / `installHint` — shown when the binary is not on PATH
|
||
|
||
Invalid files (bad JSON, unknown id, empty command) are skipped with a warning and do not break discovery for other entries.
|
||
|
||
### Security guarantees
|
||
|
||
- No install shell commands in preset or custom definitions — only the user's own PATH is consulted.
|
||
- `can_auto_install` is always `false` for preset and custom entries.
|
||
- No user-supplied icon URLs — icons are bundled assets keyed by id in `RuntimeIcon.tsx`.
|
||
- `BUZZ_MANAGED_AGENT` and other Buzz identity keys cannot be overridden by `env` in a custom definition; they are stripped before merging.
|
||
|
||
### Adding a preset (contributor guide)
|
||
|
||
To add a new runtime to the tier-2 gallery:
|
||
|
||
1. **Verify the ACP entrypoint** from the vendor's own documentation — do not rely on a PR description alone. Test with the actual binary.
|
||
2. **Add a `HarnessDefinition` entry** to the `PRESET_HARNESSES` slice in `desktop/src-tauri/src/managed_agents/discovery.rs`. Fill `id`, `label`, `command`, `args`, `install_instructions_url`, `install_hint`. Leave `env` empty unless the harness requires a specific env var to enable ACP mode.
|
||
3. **Add the preset id to `BUILTIN_IDS`** in `desktop/src-tauri/src/managed_agents/custom_harnesses.rs` so custom JSON files cannot shadow it.
|
||
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.
|
||
|
||
## Using Any ACP Agent
|
||
|
||
The harness works with any agent that implements the [ACP spec](https://agentclientprotocol.com/) over stdio. The requirements are:
|
||
|
||
- Accept `initialize` and return a result
|
||
- Accept `session/new` with `mcpServers` and return a `sessionId`
|
||
- Accept `session/prompt` with a text message and stream `session/update` notifications
|
||
- Return a `stopReason` (`end_turn`, `cancelled`, `max_tokens`, etc.)
|
||
|
||
Set `BUZZ_ACP_AGENT_COMMAND` and `BUZZ_ACP_AGENT_ARGS` to point at your agent binary.
|
||
|
||
## Testing
|
||
|
||
See the [root TESTING.md](../../TESTING.md) for the full integration testing guide — automated test suites, multi-agent E2E testing via the ACP harness, and troubleshooting.
|
||
|
||
## License
|
||
|
||
Apache-2.0
|