What ainb should port from block/buzz
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-nostrbinaries. - 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.
| Agent | ACP support | Via |
|---|---|---|
| Claude Code | Yes | official claude-agent-acp (agentclientprotocol org) |
| Codex | Yes | official codex-acp |
| Gemini CLI | Yes, native | --acp flag, launch partner |
| Copilot CLI | Yes, native | --acp --port: stdio and TCP (only real remote transport shipping) |
| Goose (Block) · OpenCode · OpenHands · Junie | Yes, native | - |
| Aider · Crush | No | gap / 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
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
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.
| # | Surface | Already exists | Missing | Call |
|---|---|---|---|---|
| 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
6 · Port roadmap, ranked lift candidates
| What | From | Call | Effort | Why |
|---|---|---|---|---|
| ACP provider adapter + AgentPool | buzz-acp patterns + upstream crate | adapt | M | Raises the scrape ceiling; unlocks chat-grade sessions (§3) |
| Daemon chat bus + message model | ainb fleet spine + buzz kind partition | build | M | One bus, three thin UIs (§4) |
| Observer side-channel + transcript reducer | kind 24200 + agentSessionTranscript.ts | adapt | M | Clean timeline; thinking/tools/cost in side pane |
| buzz-dev-mcp tool server | crates/buzz-dev-mcp | port | S-M | Standalone; _Stop/_PostCompact hooks; keyfile hygiene |
.persona.md packs | crates/buzz-persona | port | S | Pure-filesystem agent config; direct fit |
| YAML workflow engine | crates/buzz-workflow | port | M | Schema/executor decoupled; write an ainb action sink |
| CLI error/exit contract | buzz-cli/src/error.rs:87-136 | adapt | S | retryable flag, conflict=5, delivery_unknown; add --follow/--wait buzz lacks |
| Read-state frontier sync | readStateManager.ts / NIP-RS | adapt | S | effective(ctx)=max(own,parent), needed once 3 UIs show one chat |
| NIP-42 challenge + NIP-OA attestation | buzz-auth/nip42.rs + docs/nips/ | hold | S-M | Trigger: second human's machine joins |
| SAS device pairing | buzz-core/src/pairing/ | hold | M | Trigger: multi-machine enrollment UX needed |
| iroh QUIC endpoint + framing | buzz-relay-mesh | hold | M | Trigger: coordinator-less p2p requirement |
| buzz-voice local TTS · sprig multicall | crates/buzz-voice · crates/sprig | optional | S | Nice-to-haves, zero infra |
| Relay substrate (channels/search/audit/git/huddles/media) | relay + Postgres + Redis + S3 | skip | - | Inseparable from buzz's 5-container stack |
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/loadsupport varies by adapter). Needs a per-adapter test matrix before promising resumable chat channels. - Does steering work outside goose?
_session/steeringis an extension. Buzz gates it on aninitializecapability 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
Providerenum (ACP) and addingfleet/message_*are both version events, probably batch them into one fleet protocol bump.