Research · block/buzz @ 39ce3df · ainb branch f/buzz @ 10165efc · 2026-07-31

What ainb should port from block/buzz

TL;DR: Buzz is not a small ACP chat app; it is a self-hosted Nostr-relay workspace where humans and AI agents are equal members. Three verdicts: (1) ACP: hybrid-adopt: add ACP as a new provider transport at ainb's existing ProviderAdapter seam for chat-grade sessions, keep tmux for interactive terminals; (2) Chat: build the daemon-level chat bus first: the fleet daemon already ships composer → send → broadcast → receipts, only a persisted message model is missing; (3) Federation: Tailscale/SSH over the existing daemon now: buzz itself has zero relay-to-relay federation in code; what it federates is identity, and that is the part worth copying later. Full citations: research/2026-07-31_14-56-19_buzz-acp-port.md.

1 · What is Buzz

A self-hostable team workspace built on the Nostr protocol. Every action, message, reaction, workflow step, git push notification, huddle join, is a cryptographically signed event with a kind integer. The relay is the single source of truth; agents hold their own keypairs, channel memberships and audit trails, exactly like human members.

┌──────────────┐  ┌──────────────────┐  ┌────────────┐
│ Desktop      │  │ buzz-acp harness │  │ buzz-cli   │
│ (Tauri+React)│  │ ACP stdio ndjson │  │ agent-first│
│ mobile (Flu.)│  │ goose/codex/     │  │ JSON CLI   │
└──────┬───────┘  │ claude pool 1-32 │  └─────┬──────┘
       │ WS+REST  └────────┬─────────┘        │
       ▼                   ▼ WS+REST          ▼
┌─────────────────────────────────────────────────────┐
│ buzz-relay (Axum): NIP-01 signed events, kinds     │
│ channels·threads·DMs·canvas·media·search·audit·git· │
│ workflows·huddles(voice)·presence                   │
└──────┬──────────────┬──────────────┬────────────────┘
   Postgres         Redis         S3/MinIO
   (events+FTS)     (pubsub)      (Blossom media)
// every event, human or agent:
{
  "id":      "<sha256 of canonical serialization>",
  "pubkey":  "<secp256k1 public key>",
  "kind":    40002,          // the only dispatch switch
  "tags":    [["e","…"],["p","…"]],
  "content": "…",
  "sig":     "<Schnorr signature>"
}

// new feature = new kind number = zero breaking changes.
// 81 kinds defined; custom range 40000-49999.
# minimum self-host = 5 containers (deploy/compose/compose.yml)
relay + postgres:17 + redis:7 + minio + minio-init

no feature flags to drop any of them.
buzz-db: no [features]; hard-errors at boot without Postgres
buzz-pubsub: Redis pool built unconditionally
buzz-media: single concrete S3 Bucket, no trait
# → the chat substrate is NOT liftable; the patterns are.

Feature inventory (port-relevant subset)

Channels · threads · reactions · DMs · search · audit relay + Postgres, pattern only

Channel types Stream/Forum/Dm/Workflow; threads are a relay-computed read model (thread_metadata at ingest, synthetic summary kinds 39005/39006); reactions are NIP-25 overlays; DMs support NIP-17 gift wrap (sender/content/timestamp hidden); search is a Postgres generated tsvector column; audit is a per-community SHA-256 hash chain under a Postgres advisory lock. All hard-wired to the relay substrate, skip the code, keep the shapes: the content/aux/non-conversational kind partition is the design to copy.

buzz-dev-mcp, standalone agent tool server crates/buzz-dev-mcp/src/lib.rs

Pure local MCP server, zero relay/Postgres/Redis coupling: shell, read_file, str_replace, todo, view_image, plus two lifecycle hooks, _Stop (objection gate before the agent ends its turn) and _PostCompact (re-inject state after context compaction). Installs a 0700 tempdir PATH shim exposing rg/tree/buzz; moves the private key from env into a 0600 keyfile so child processes never see it (shim.rs:33-58). Best standalone lift in the repo.

buzz-workflow, YAML automation DSL crates/buzz-workflow/src/schema.rs

Triggers: message_posted, reaction_added, diff_posted, schedule (cron), webhook. Actions: send_message, send_dm, add_reaction, call_webhook (SSRF-guarded), request_approval, delay. Conditions via evalexpr with a 100 ms timeout. The schema/executor have zero Nostr/Postgres coupling, only the concrete action_sink is relay-bound. Swap in an ainb sink ("send prompt to session", "broadcast", "notify") and this becomes ainb's automation engine.

buzz-persona, .persona.md agent packs crates/buzz-persona/src/persona.rs:101-169

YAML frontmatter + markdown-body-as-system-prompt: name, avatar, skills, MCP servers, triggers (mentions/keywords/all), model, runtime, temperature, hooks. Parser/merge/validate are pure filesystem logic. Direct fit for per-agent chat configuration in ainb.

Identity primitives, NIP-42 challenge, NIP-OA/AA attestation buzz-auth/src/nip42.rs:37-86 · docs/nips/

NIP-42: relay sends 32 random bytes; client signs {challenge, relay-URL}; relay verifies Schnorr + URL binding + ±60 s window. ~50 transport-agnostic lines. NIP-OA: an owner key authorizes an agent key with bounded conditions (kind=…&created_at<…) without transferring identity, the event stays authored by the agent. NIP-AA: an agent whose owner is a member inherits access; revoking the human revokes their agents. This trio is the identity model for "my agents on my machines".

Also inventoried full table in the research doc
  • Git hosting on object storage: Smart HTTP over ephemeral worktrees hydrated from S3, TLA+-checked CAS-manifest publish; plus standalone git-credential-nostr / git-sign-nostr binaries.
  • Huddles: dumb Opus-frame fan-out over WS inside the relay, no SFU. buzz-voice - separate, fully offline local TTS crate (zero infra; optional lift).
  • Device pairing (NIP-AB): QR + ECDH + human-compared 6-digit SAS + stateless throwaway relay; complete device-enrollment protocol in ~small code.
  • Agent job kinds 43001-43006: wire convention only; no dedicated relay dispatcher found.
  • sprig: multicall binary (argv[0] dispatch) packaging pattern.

2 · ACP deep dive

The protocol and its ecosystem (fetched 2026-07-31)

ACP (Agent Client Protocol, agentclientprotocol.com) is JSON-RPC 2.0 over stdio: the client spawns the agent as a subprocess. initialize negotiates an integer protocolVersion (stable = 1; v2 schema in alpha). Sessions: session/new (carries MCP server configs + cwd), session/prompt, streamed session/update notifications, session/request_permission, session/cancel. Co-governed by Zed + JetBrains, Apache 2.0; first-party Rust crate agent-client-protocol v1.6.0. Remote transport is explicitly work-in-progress in the spec, stdio is the baseline.

AgentACP supportVia
Claude CodeYesofficial claude-agent-acp (agentclientprotocol org)
CodexYesofficial codex-acp
Gemini CLIYes, native--acp flag, launch partner
Copilot CLIYes, native--acp --port: stdio and TCP (only real remote transport shipping)
Goose (Block) · OpenCode · OpenHands · JunieYes, native-
Aider · CrushNogap / open feature request

Clients: Zed (reference), JetBrains IDEs, Neovim (CodeCompanion), Emacs, Toad, marimo, community VS Code extensions. Known weaknesses: stdio-only breaks SSH/remote setups (open Zed issues #47910, #52254); session persistence optional per spec; auth late-bound.

How buzz actually uses ACP

The harness loop buzz-acp/src/{acp,pool,queue}.rs

Hand-rolled client (no upstream crate existed then): NDJSON JSON-RPC over child stdio, 10 MB line cap. Pool of 1–32 agent subprocesses with claim/return and channel affinity; one ACP session per (process × channel), reused across prompts so the adapter's own history carries context. Per-channel FIFO queue: at most one prompt in flight per channel, up to 50 events batched into one prompt, cross-channel fairness by oldest head. Two clocks per turn: idle 620 s (reset on any output and on tool_call) and hard 7200 s. Per-slot crash circuit breaker with jittered exponential backoff.

The surprising part: replies don't return via ACP buzz-acp/src/base_prompt.md:5-24

session/update chunks (message, thought, tool_call, plan) are only logged and mirrored to an encrypted owner-only observer feed (kind 24200). The agent posts to chat by running the buzz CLI from an MCP shell tool, it is a first-class chat client, not a pipe. The chat timeline therefore only ever receives the agent's final message; thinking, tool calls, plans, permission requests and token cost render in a side transcript reduced from the observer frames. The chat renderer never handles partial markdown.

Steering, permissions, MCP wiring buzz-acp/src/acp.rs:1865-1940, lib.rs:4179-4232

Mid-turn arrivals: Queue / Steer (default, non-cancelling injection via _session/steering, capability-gated because codex-acp answers unknown methods with {} success and would silently eat messages) / Interrupt / OwnerInterrupt. Tool permissions are auto-approved (allow_once selected by kind); a session-level permission mode is pushed via session/set_config_option. MCP servers are passed to the agent through ACP's session/new params, buzz passes exactly one (buzz-dev-mcp).

3 · ACP verdict for ainb

verdict · hybrid-adopt Adopt ACP as a new agent transport for chat/broadcast sessions at the existing ProviderAdapter seam, inside the hangar daemon. Keep tmux for interactive terminal sessions. Do not touch the hangar plugin protocol or the fleet daemon protocol, ACP is not for those layers.

ainb's transport today is terminal scraping: tmux send-keys with a 1000 ms paste settle and 3× Enter retry (fleet/send/tmux.rs:84-104, born from the "8 unsubmitted pastes" incident), observation via capture-pane box-drawing heuristics and tailing Claude Code's own JSONL transcripts. ACP replaces every one of those with a structured call:

┌─────────┐ send-keys -l --   ┌───────────────┐
│ fleet-  │──── + settle ────▶│ tmux pane     │
│ core    │      + retry ×3   │ claude / codex│
│         │◀──────────────────│               │
└─────────┘  capture-pane +   └───────────────┘
             jsonl tail
# inject: paste heuristics, no ack
# observe: scrollback scraping, re-implemented
#          Claude path-slugging rules
# permissions: detected via '?' + │┌└ chars
┌─────────┐ fleet/* ┌──────────────────────────┐
│ TUI /   │────────▶│ hangar daemon            │
│ Fleet / │         │  chat store (hangar-store)│
│ web     │◀────────│  AgentPool (ACP)          │
└─────────┘ events  └───┬──────────────────┬───┘
                 ACP stdio│                │tmux (kept)
        ┌────────────────▼───┐   ┌────────▼─────────┐
        │ claude-agent-acp / │   │ interactive tmux │
        │ codex-acp / gemini │   │ sessions (watch- │
        │ (headless, pooled) │   │ able, attachable)│
        └────────────────────┘   └──────────────────┘
# chat sessions: structured prompt/stream/permission/cancel
# terminal sessions: unchanged, ainb's core value stays
1. ainb-acp crate on agent-client-protocol v1.x:
   spawn adapter → initialize → session/new
   (pass ainb MCP servers) → session/prompt →
   reduce session/update into fleet events
2. daemon-owned AgentPool (adapt buzz pool/queue/
   circuit-breaker; session per chat channel)
3. AcpProviderAdapter impl of existing trait
   (fleet-core/src/fleet/provider.rs:149-166);
   capability bitset advertises send/observe/
   cancel/steer
4. chat store + fleet/message_* methods (§4)
5. later: structured session/request_permission
   replaces scrape heuristics for ACP sessions
// Provider enum is on the wire → batch the
// growth with one fleet protocol version bump
What ACP does not replace: the visible tmux session. An ACP agent is a headless subprocess of the harness. ainb's watchable/attachable terminals stay on tmux; ACP powers the chat-grade sessions where headless is fine. And unlike buzz, ainb should use the upstream Rust crate, buzz hand-rolled the protocol only because the crate didn't exist yet.

4 · Chat surfaces, ranked build order

Headline from the ainb audit: a chat transport already exists. The macOS Fleet app ships a per-session prompt composer and a multi-recipient broadcast form with delivery receipts, over hangar.sock with token auth, capability gating, and revision-contiguous replay proven against two independent clients. What's missing is a persisted message model, not a wire protocol.

#SurfaceAlready existsMissingCall
1 Daemon-level chat bus fleet negotiate/subscribe/replay/resync; broadcast + receipts (Pending|Delivered|Failed|Unknown|Rejected); capability gating; hangar/comment_*, inbox, mentions; SQLite store fleet/message_send/_list/thread_*; message table; ACP AgentPool for agent participants build first one bus, every frontend attaches
2 Fleet chat + broadcast composer (FleetSessionDetailView.swift:87-117); broadcast form with recipient checklist + confirm (FleetWindowView.swift:430-481); receipts UI transcript timeline over the bus; thread panel; agent-transcript inspector (observer pattern) second broadcast = message to N sessions, replies threaded per session
3 TUI chat tab hangar-tui plugin-as-daemon-client template; captures_text for text entry; dormant ClaudeChatComponent UI shell new plugin + core screen registration (tab set is compile-time); ratatui chat list + composer third highest UI effort; benefits from bus + Fleet patterns first

UX contract carried over from buzz: clean timeline (final message only) + side transcript from ACP updates; three-way kind partition (content / aux-overlay / non-conversational); optimistic send with a carried local key; message grouping into continuations; one-level threads for v1; agents as members with profiles; sticky "addressed agents" audience per channel. Two deliberate improvements over buzz: slash commands (buzz has none, a TUI has no button bar to fall back on) and actionable permission rows (buzz renders session/request_permission read-only; render [1] allow once · [2] always · [3] reject and answer by JSON-RPC id).

5 · Federation, three sketches, one pick

Evidence first: buzz has no relay-to-relay federation in code. buzz-relay-mesh is pod-to-pod QUIC inside one deployment, peer discovery is a shared Redis registry, and membership fails closed against any relay identity that isn't its own (membership.rs:90-102). What buzz federates is identity: the portable keypair, NIP-42 challenge auth, NIP-OA owner→agent attestation. Community state deliberately does not travel.

 machine 1              hub (1 binary)          machine 2
┌──────────┐   wss    ┌──────────────┐   wss   ┌──────────┐
│ ainb     │─────────▶│ event store  │◀────────│ ainb     │
│ + key    │◀─────────│ + sub filter │────────▶│ + key    │
└──────────┘  signed  │  (sqlite)    │         └──────────┘
              events  └──────────────┘
  auth: NIP-42 challenge (50 lines, buzz-auth/nip42.rs)
  identity: NIP-OA owner→agent tags
# the buzz-shaped answer. cost: you now own a hub,
# a key format, and a migration story.
┌──────────┐                            ┌──────────┐
│ ainb A   │═════ iroh QUIC direct ═════│ ainb B   │
│ ed25519  │      (ALPN "ainb/1")       │ ed25519  │
└────┬─────┘                            └────┬─────┘
     │      ┌────────────────────┐           │
     └─────▶│ rendezvous blob:   │◀──────────┘
            │ signed peer records│
            └────────────────────┘
  admission: peer ∈ allowlist, else drop (fail closed)
# copy endpoint.rs + wire.rs framing (~140 lines) from
# buzz-relay-mesh. no server, NAT handled by iroh.
# cost: rendezvous is still a distributed-systems pick.
┌──────────┐  tailnet (WireGuard, MagicDNS)  ┌──────────┐
│ ainb TUI │────────────────────────────────▶│ hangar   │
│ laptop   │  existing fleet protocol,       │ daemon   │
└──────────┘  unchanged, over 100.x.y.z      │ devbox   │
                                             └──────────┘
  authn: tailnet ACL + device auth (already solved)
  already in-tree: ainb-web binds non-loopback behind a
  bearer token (config.rs:97-136); docs describe tailnet
  usage; FleetTransport seam fits a remote-peer impl
# zero new protocol, zero new crypto, zero new binary
pick · C now Tailscale/SSH over the existing daemon. Threat-model check: "my laptop ↔ my devbox" has no untrusted party, so buzz's signature layers buy nothing yet. Sequencing: adopt the NIP-42 challenge (~50 transport-agnostic lines) the moment a second person's machine joins; NIP-OA-style agent attestation when agents get their own keys; iroh p2p (B) only if coordinator-less becomes a real requirement. Buzz's own history endorses this order, thorough identity layer, still no federation, and both times they went p2p they kept a central bootstrap anyway.

6 · Port roadmap, ranked lift candidates

WhatFromCallEffortWhy
ACP provider adapter + AgentPoolbuzz-acp patterns + upstream crateadaptMRaises the scrape ceiling; unlocks chat-grade sessions (§3)
Daemon chat bus + message modelainb fleet spine + buzz kind partitionbuildMOne bus, three thin UIs (§4)
Observer side-channel + transcript reducerkind 24200 + agentSessionTranscript.tsadaptMClean timeline; thinking/tools/cost in side pane
buzz-dev-mcp tool servercrates/buzz-dev-mcpportS-MStandalone; _Stop/_PostCompact hooks; keyfile hygiene
.persona.md packscrates/buzz-personaportSPure-filesystem agent config; direct fit
YAML workflow enginecrates/buzz-workflowportMSchema/executor decoupled; write an ainb action sink
CLI error/exit contractbuzz-cli/src/error.rs:87-136adaptSretryable flag, conflict=5, delivery_unknown; add --follow/--wait buzz lacks
Read-state frontier syncreadStateManager.ts / NIP-RSadaptSeffective(ctx)=max(own,parent), needed once 3 UIs show one chat
NIP-42 challenge + NIP-OA attestationbuzz-auth/nip42.rs + docs/nips/holdS-MTrigger: second human's machine joins
SAS device pairingbuzz-core/src/pairing/holdMTrigger: multi-machine enrollment UX needed
iroh QUIC endpoint + framingbuzz-relay-meshholdMTrigger: coordinator-less p2p requirement
buzz-voice local TTS · sprig multicallcrates/buzz-voice · crates/sprigoptionalSNice-to-haves, zero infra
Relay substrate (channels/search/audit/git/huddles/media)relay + Postgres + Redis + S3skip-Inseparable from buzz's 5-container stack
!
Independent fix worth doing regardless: ainb has a second, unhardened send path (ainb-core/src/cli/run.rs:417-430, bare send-keys + C-m, no -l, no --, no verify) that violates fleet-core's declared "ONE verified send path" invariant.

Open questions

Can ACP chat sessions survive a daemon restart?
Session persistence is optional in the spec (session/load support varies by adapter). Needs a per-adapter test matrix before promising resumable chat channels.
Does steering work outside goose?
_session/steering is an extension. Buzz gates it on an initialize capability flag because codex-acp replies {} to unknown methods (a false success that would silently drop the user's message). Verify claude-agent-acp/codex-acp behavior before promising broadcast-steer semantics.
How rich should the v1 message model be?
Options: adopt buzz's content/aux/non-conversational partition directly, or simplify to messages + receipts and add overlays later. The partition costs little up front and prevents the phantom-unread class of bugs buzz documented.
One protocol bump or two?
Growing the wire Provider enum (ACP) and adding fleet/message_* are both version events, probably batch them into one fleet protocol bump.