Files
buzz/crates/buzz-acp
Matt Toohey a4e592ed3a feat(desktop): redirect auth probes to bundled CLIs and retire the cli_missing gate
Since the ACP bundles vendor full native CLIs (Claude Code inside
@anthropic-ai/claude-agent-sdk-*, codex inside @openai/codex-*), agent
sessions never touch a user-installed claude/codex. Yet the auth probes
still resolved those CLIs from the user's PATH, and a missing user CLI
put the runtime behind a CliMissing availability gate — blocking agents
that would have run fine on the bundle. Point the probes at the vendored
CLIs and delete the gate plus everything downstream of it (curl-pipe CLI
install commands, Doctor's user-CLI path row, the cli_missing nudge card).

The vendored CLIs are deliberately NOT staged into resources/acp/bin:
that dir is the highest-priority segment of the agent-spawn PATH, and
CLIs there would shadow the user's (possibly newer) claude/codex inside
every session. Instead a probe-only manifest maps CLI names to paths
inside the staged node trees.

- prepare-acp-tools-resource.sh reads nativePackage/nativeExecutable
  from acp-tools.lock.json, verifies the vendored binary exists in the
  staged tree (chmod +x), and writes harness-clis.json next to
  node-runtime.json: {"clis":[{id,cli,path}]} with resource-root-relative
  paths. The manifest is gitignored like its sibling.
- acp_tools::bundled_harness_cli() resolves a CLI name through the
  manifest — bare names only, relative non-escaping paths only, must be
  an executable file. Absent manifest/entry degrades to None.
- discovery::resolve_probe_binary() tries the bundled CLI first, then
  the user's PATH — auth probes in the discovery sweep and
  readiness::cli_login_requirements both go through it. Auth state now
  reflects the CLI the sessions actually use, while dev builds without
  a staged bundle keep working via the PATH fallback.
- classify_runtime: a resolving adapter is Available unconditionally;
  underlying_cli now only disambiguates AdapterMissing vs NotInstalled.
  CliMissing is removed from the desktop enum (runtime-only, never
  persisted) and from the TS union; buzz-acp's wire mirror keeps the
  variant so payloads from older desktops still parse (AdapterOutdated
  precedent), while the FE nudge validator rejects the retired literal
  so stale JSON can't render a card the UI has no branch for.
- claude/codex catalog entries drop underlying_cli and the curl-pipe
  cli_install_commands; login provisioning copy stays as-is.
- UI: cli_missing branches removed from Doctor (StatusIcon, RuntimeRow,
  user-CLI path row), SetupStep, persona pickers ("(CLI missing)"
  suffix + sort rank), AgentDefinitionDialog warning, and the nudge
  card; retired the cli_missing nudge screenshot spec.

Verification:
- cargo test --lib (desktop/src-tauri): 1436 passed, 0 failed (new
  bundled_harness_cli manifest/escape/absent tests, probe-runs-without-
  underlying-CLI readiness test, adapter-implies-Available discovery test)
- cargo test -p buzz-acp --lib: 497 passed (cli_missing round-trip kept)
- cargo clippy --lib --tests -D warnings (both crates): clean;
  cargo fmt --check (both crates): clean
- tsc --noEmit: clean; biome check + file-size/px-text/pubkey guards: pass
- pnpm test (desktop): 2792 passed, 0 failed (new validator-rejects-
  cli_missing test)
- playwright doctor-states.spec.ts + doctor-cta-screenshots.spec.ts:
  12 passed (Doctor shows no CLI path row; nudge cards unaffected)
- prepare-acp-tools-resource.sh: manifest written; vendored
  claude --version → 2.1.205 (Claude Code); vendored
  codex login status → exit 0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Matt Toohey <contact@matttoohey.com>
2026-07-15 13:48:46 +10:00
..

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 over stdio: goose, codex (via codex-acp), and claude code (via 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)

Build:

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 mint one:

cargo run -p buzz-admin -- mint-token --name "my-agent" --scopes "messages:read,messages:write,channels:read"

This prints an nsec1... private key and an API token. Save both immediately — they're shown only once.

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)

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 wraps OpenAI Codex in an ACP interface.

# 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 wraps the Claude Agent SDK in an ACP interface.

# 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 3600 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).
--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:

# 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):

buzz-acp

Four agents, no heartbeat (high-throughput event processing):

buzz-acp --agents 4

Two agents with 5-minute heartbeat:

buzz-acp --agents 2 --heartbeat-interval 300

Custom heartbeat prompt:

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:

buzz-acp --kinds 9,46010,40007,45001,45002,45003 --no-mention-filter

Or with --subscribe all:

buzz-acp --subscribe all --kinds 9,46010,40007,45001,45002,45003

Per-channel config:

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

Using Any ACP Agent

The harness works with any agent that implements the ACP spec 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 for the full integration testing guide — automated test suites, multi-agent E2E testing via the ACP harness, and troubleshooting.

License

Apache-2.0