Files
buzz/crates/buzz-acp
Wes 2f3dd850db fix(relay): reject a frame on its own acknowledgement channel (#6961)
Pinky, an AI agent, updated this description on Wes's behalf after
taking over the startup investigation.

**Category:** fix

**User Impact:** An EVENT refused by WebSocket admission or handler
saturation receives a correlated `OK(event_id, false, reason)` instead
of an uncorrelated NOTICE, so the client can settle that refusal without
waiting for its publish timeout. Rate-limited refusals also arm client
backoff. This fixes a protocol failure mechanism; it does not establish
that every startup send will succeed or that the reported Desktop
startup incident is fully resolved.

**Problem:** Startup opens several live subscriptions and publishes at
once, and the relay's WebSocket admission gate is a fixed 5-second
window (`ws_admission_budget` = `human_ws_events_per_sec * 5`). If that
shared per-principal quota is exhausted, `enforce_ws_admission`
previously rejected an EVENT with a bare `["NOTICE", reason]`. Quota
pressure is a possible trigger, not proof of the original incident's
complete cause.

A NOTICE carries no event id. Both clients settle a pending publish
*only* from an `OK` keyed by event id (desktop `pendingEvents`, mobile
`_pendingEvents`), so nothing settled — and `handle_text_message`
returns early, so no `OK` ever followed either. The send **could not
fail**; it could only time out at `PUBLISH_TIMEOUT_MS` = 25s. That
explains how this rejection mechanism can produce a roughly 25-second
timeout; attributing the original report to it still requires the actual
startup/send workflow.

The handler-semaphore saturation path had the identical defect, and that
one needs no quota burst to fire.

**Solution:** NIP-01 gives each request type its own acknowledgement
channel, and a rejection is only actionable on the same one. Reject a
REQ with `CLOSED`, an EVENT with `OK(id, false, reason)`, and fall back
to `NOTICE` only where no per-request correlation exists. COUNT refusals
now also use `CLOSED(query_id, reason)` per NIP-45, covering both quota
admission and handler saturation (added in
`cd12c93804b87a24b61075dfd171dc471a0a527f`).

Reason strings are unchanged, so the `rate-limited:` prefix and `retry
in {N}s` hint that existing client gates parse keep working (desktop
`parseRateLimitHint`, mobile `RelayRateLimitGate`, buzz-acp
`set_rate_limit_gate`). Only the frame *type* changes, so
`docs/multi-tenant-relay.md` L7 stays satisfied.

Two notes on how this landed, both worth a reviewer's attention:

1. **A survived mutation became a design change.**
`send_admission_result` originally took a `RejectionTarget` parameter,
and reverting the *second* call site (the per-minute message quota)
survived the whole suite — with Redis unreachable the first quota check
short-circuits, so that line is unreachable in test. Rather than test
around it, the parameter is gone: the target is derived from the frame,
so no call site can name the wrong channel.

2. **The relay fix would have caused a client regression on its own.**
Gate arming lived only in the NOTICE branch. Once rejections arrive as
`OK:false`, `handleOk` failed the send without ever backing off — the
client would retry straight into the same quota. Desktop and Mobile now
arm on a `rate-limited:` OK rejection. ACP was subsequently fixed in
`3b06dd32493596ec650f20abf8805791c50fdc24`: it arms the gate and
re-parks only the refused observer frame, preserving other in-flight
frames. Desktop gets `activateRateLimitIfSignalled` as the single owner
of that prefix test, called from both `handleOk` and the NOTICE branch.

<details>
<summary>File changes</summary>

**crates/buzz-relay/src/rejection.rs** (new)
Owns the admission-rejection concern: `RejectionTarget`,
`rejection_target_for`, `request_rejection_message`,
`send_admission_result`, and `enforce_ws_admission`, moved out of
`connection.rs`. Six tests, two of which drive the real
`enforce_ws_admission` against a real `AppState`.

**crates/buzz-relay/src/connection.rs**
Fix the EVENT handler-semaphore rejection to correlate to the event id;
delegate admission to the new module. Add two tests that drive the real
`handle_text_message` with every handler permit held. Down from 1319 to
1116 lines.

**crates/buzz-relay/src/state.rs**
Widen the existing `test_state` helper to `pub(crate)` so the rejection
tests reuse it rather than adding a ninth copy of `AppState`
construction.

**desktop/src/shared/api/relayRateLimitGate.ts**
Add `activateRateLimitIfSignalled` — one owner for the `rate-limited:`
prefix test, since three inbound frame types now carry it.

**desktop/src/shared/api/relayClientSession.ts**
Arm the gate on a rate-limited OK rejection; route the NOTICE branch
through the same helper. Net zero lines, which keeps this
already-oversized file within the differential ratchet.

**desktop/src/shared/api/relayClientPublishRejection.test.mjs** (new)
Four tests against the real `RelayClient`: a rate-limited OK settles the
pending publish and arms the gate; an ordinary rejection does not arm
it; an accepted OK still resolves.

**mobile/lib/shared/relay/relay_session.dart**
Arm the gate in `_handleOk` for a rate-limited rejection.

**mobile/test/shared/relay/relay_session_test.dart**
Two tests driving the real `publish` + `debugHandleMessage` path.

</details>

<details>
<summary>Validation</summary>

**Mutation-tested — 5 mutations, all now killed.** Each production call
site was reverted to the defective behaviour to confirm a test fails.
This caught two false-negative tests:

| # | Mutation | Result |
|---|----------|--------|
| 1 | `rejection_target_for`: EVENT → `Connection` | 4 tests fail |
| 2 | EVENT handler-semaphore call site → bare NOTICE | **survived at
first** |
| 3 | per-minute quota call site → `Connection` | **survived**; fixed by
removing the parameter |
| 4 | desktop `handleOk` gate arming removed | 1 test fails |
| 5 | mobile `_handleOk` gate arming removed | 1 test fails |

Mutation 2 is the lesson: my first saturation test called
`request_rejection_message` directly, so reverting the real call site
inside the `match` arm left it green. It now drives
`handle_text_message` itself and dies on that mutation.

- `cargo test -p buzz-relay` — 928 passed, 1 failed:
`api::mesh_demo::tests::demo_join_forwarded_arm_round_trips_echo`,
**pre-existing**, reproduced with all changes stashed at `4dd4d73de`.
- `cd desktop && npm test` — 5721 passed, 0 failed (full suite).
- `cd mobile && flutter test` — 1876 passed, 0 failed (full suite).
- `just fmt-check`, `just clippy`, `just desktop-check`, `just
mobile-check`, `just file-size-check` — clean. Desktop's 5 biome
warnings are pre-existing (reproduced with changes stashed).
- All 9 pre-push lanes green, including `rust-tests` and
`desktop-tauri-checks`.

**Not verified:** not reproduced end-to-end against a live relay under a
forced quota burst. The causal chain is source-proven and
mutation-proven at the frame level; the ~25s attribution follows from
`PUBLISH_TIMEOUT_MS` but is not directly measured. A packaged-build
click-through would close that gap.

</details>

Related work: #6957 bounds Desktop HTTP event submission, but safe
retained-operation recovery after exhausted/ambiguous outcomes remains
unfinished. #6998 is the separately reviewable Desktop
readiness/duplicate-subscription slice. Neither is claimed to complete
native before/after startup-send validation.

Diagnosis note: `RESEARCH/DESKTOP_STARTUP_SEND_STALL_2026_08_27.md`
(Brain's workspace).

## Current review disposition (2026-08-28)

The [review on
`cd12c938`](https://github.com/block/buzz/pull/6961#pullrequestreview-5052902510)
identified ACP's missing rate-limited-OK handling. Commit
`3b06dd32493596ec650f20abf8805791c50fdc24` fixes gate arming, re-parking
the specifically refused observer frame, and the stale NOTICE comment.
Two regressions drive the real frame dispatcher. See [the implementation
and validation
response](https://github.com/block/buzz/pull/6961#issuecomment-5455032054).

The Mobile generation-check inline thread is resolved: its `async
publish` returns a failed Future when superseded; it does not throw
synchronously at invocation. No further production change was indicated
by that comment.

The validation counts above describe the original slice, not a new
rerun. At `3b06dd324`, the current GitHub check rollup has successful
completed test/build checks (non-applicable jobs skipped). The
security-review comment still requires review for the current base/head
range; do not read a green authorization job as a completed security
review. Approval and merge remain human decisions.

---------

Signed-off-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Brain <1a02c72794dcd0f07058a353bc3a81f4028b8c77c92c87fce6d5c8b85970a20b@buzz.block.builderlab.xyz>
Co-authored-by: Carl <9d00794d3df50972eb8b615511783cab12a77a8fd5dd5edd58073ec73b54bd8b@buzz.block.builderlab.xyz>
2026-08-31 14:43:31 -06: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 generate one:

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:

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)

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

Relay-signed workflow messages delegate to their recorded owner only when they explicitly target this agent with authenticated workflow-mention provenance. The owner tag means that owner scheduled the workflow; it does not claim that the owner authored every word after template rendering. ACP verifies the provenance against the relay's NIP-11 self key, then evaluates the owner under the same author policy as ordinary messages. Legacy workflow messages and workflow output without an explicit agent mention remain attributed to the relay signer. nobody remains absolute.

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.

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

{
  "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 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