From e5a2f82757fa5526613334e232f9ec0017856942 Mon Sep 17 00:00:00 2001 From: Pablo Navarro Date: Wed, 29 Jul 2026 14:40:18 +0000 Subject: [PATCH 1/3] docs(deploy): always-on agents with Desktop as thin client Document the production layout where Buzz Desktop runs on a laptop and buzz-acp agents stay on a server (same idea as a remote Hermes backend). Add systemd template units under deploy/systemd and cross-link from buzz-acp README. Emphasize host-string community selection so self-hosters do not join an empty workspace via localhost vs public hostname mismatch. Signed-off-by: Pablo Navarro --- crates/buzz-acp/README.md | 8 ++ deploy/compose/README.md | 3 + deploy/systemd/buzz-acp.env.example | 20 +++++ deploy/systemd/buzz-acp@.service | 22 +++++ docs/self-host-always-on-agents.md | 128 ++++++++++++++++++++++++++++ 5 files changed, 181 insertions(+) create mode 100644 deploy/systemd/buzz-acp.env.example create mode 100644 deploy/systemd/buzz-acp@.service create mode 100644 docs/self-host-always-on-agents.md diff --git a/crates/buzz-acp/README.md b/crates/buzz-acp/README.md index e6164b02dd..dd3f98f586 100644 --- a/crates/buzz-acp/README.md +++ b/crates/buzz-acp/README.md @@ -338,3 +338,11 @@ See the [root TESTING.md](../../TESTING.md) for the full integration testing gui ## License Apache-2.0 + +## Always-on / remote Desktop + +For the **laptop Desktop + server-side `buzz-acp`** layout (agents stay up when +the laptop sleeps), see +[docs/self-host-always-on-agents.md](../../docs/self-host-always-on-agents.md) +and the systemd unit template under +[deploy/systemd/](../../deploy/systemd/). diff --git a/deploy/compose/README.md b/deploy/compose/README.md index 0de524fb5b..4f580ff82a 100644 --- a/deploy/compose/README.md +++ b/deploy/compose/README.md @@ -54,3 +54,6 @@ $EDITOR .env curl -fsS "http://127.0.0.1:$(grep -E '^BUZZ_HTTP_PORT=' .env | cut -d= -f2-)/_liveness" ./run.sh status ``` + +## Always-on agents (Desktop elsewhere) +See [docs/self-host-always-on-agents.md](../../docs/self-host-always-on-agents.md) and [deploy/systemd/](../systemd/). diff --git a/deploy/systemd/buzz-acp.env.example b/deploy/systemd/buzz-acp.env.example new file mode 100644 index 0000000000..1efc695c2e --- /dev/null +++ b/deploy/systemd/buzz-acp.env.example @@ -0,0 +1,20 @@ +# /etc/buzz-acp/.env +# chmod 600 + +# Agent Nostr identity (hex 64 or nsec1…) +BUZZ_PRIVATE_KEY= + +# MUST match Buzz Desktop relay host string exactly (community selector) +BUZZ_RELAY_URL=ws://buzz.example.com:3000 + +# ACP agent binary + args (comma-separated args for buzz-acp) +BUZZ_ACP_AGENT_COMMAND=goose +# BUZZ_ACP_AGENT_ARGS= + +BUZZ_ACP_SUBSCRIBE=mentions +BUZZ_ACP_RESPOND_TO=anyone +# BUZZ_ACP_DISPLAY_NAME=Coder +# BUZZ_ACP_SYSTEM_PROMPT_FILE=/etc/buzz-acp/coder.prompt.md + +# HOME for the agent process (credentials, cwd defaults) +# HOME=/var/lib/buzz-acp/coder diff --git a/deploy/systemd/buzz-acp@.service b/deploy/systemd/buzz-acp@.service new file mode 100644 index 0000000000..faa9e8f6fe --- /dev/null +++ b/deploy/systemd/buzz-acp@.service @@ -0,0 +1,22 @@ +[Unit] +Description=Buzz ACP agent (%i) +Documentation=https://github.com/block/buzz/blob/main/docs/self-host-always-on-agents.md +After=network-online.target +Wants=network-online.target + +[Service] +Type=simple +# Environment file: /etc/buzz-acp/%i.env (or EnvironmentFile= override) +EnvironmentFile=-/etc/buzz-acp/%i.env +EnvironmentFile=-/etc/buzz-acp/%i +# Binary on PATH after `cargo install` / release build install +ExecStart=/usr/local/bin/buzz-acp +Restart=on-failure +RestartSec=3 +# Harden lightly — agents need homedir tools; tighten further per site +NoNewPrivileges=true +# Limit runaway forks; raise if your agent spawns heavy tool trees +TasksMax=4096 + +[Install] +WantedBy=multi-user.target diff --git a/docs/self-host-always-on-agents.md b/docs/self-host-always-on-agents.md new file mode 100644 index 0000000000..5352d899a8 --- /dev/null +++ b/docs/self-host-always-on-agents.md @@ -0,0 +1,128 @@ +# Self-host: always-on agents (Desktop on laptop, backend on server) + +This is the **Hermes Desktop → remote server** pattern applied to Buzz. + +| Machine | Runs | +|---------|------| +| **Laptop** | Buzz Desktop only (human UI) | +| **Server** | `buzz-relay` + one or more `buzz-acp` processes | + +Agents stay online when the laptop sleeps. Desktop is a thin client over WebSocket. + +``` +Laptop Desktop ──ws(s)://HOST:3000──► buzz-relay + ▲ + buzz-acp @ agent + │ + ACP stdio agent + (goose, claude, codex, + cursor-agent, grok, …) +``` + +## Why the URL string matters + +Buzz selects the **community** from the connection **Host** header +(`req.community = resolve_host(connection.host)`). + +Use the **same host string** everywhere: + +| Good | Bad | +|------|-----| +| `ws://buzz.example.com:3000` on Desktop **and** every `buzz-acp` | Desktop → `localhost`, agents → `127.0.0.1` or a different hostname | +| `ws://myserver.tailnet.ts.net:3000` consistently | Mixing Tailscale DNS and LAN IP for “the same” deploy | + +If Desktop shows empty rooms while the server CLI sees agents, you are almost +always in a **different community** (wrong host) or on a **fresh Nostr identity** +that is not a channel member. + +## 1. Server: relay + +Follow the main deploy guide (`deploy/compose` or your preferred install) so +`GET http://HOST:3000/_liveness` returns ok and WebSocket `ws://HOST:3000` +accepts clients. + +Ensure the relay is reachable from the laptop (bind address, firewall, Tailscale, +or reverse proxy with WebSocket support). + +## 2. Server: always-on agent with systemd + +Install `buzz-acp` and `buzz` (CLI) from this repo (`cargo build --release -p buzz-acp -p buzz-cli`). + +Per agent: + +1. Generate a key: `buzz-admin generate-key` (save the secret). +2. Add the agent as a relay member (closed-relay deploys). +3. Install the unit template from `deploy/systemd/`: + +```bash +# system-wide example +sudo cp deploy/systemd/buzz-acp@.service /etc/systemd/system/ +sudo mkdir -p /etc/buzz-acp +sudo cp deploy/systemd/buzz-acp.env.example /etc/buzz-acp/coder.env +# edit coder.env — BUZZ_PRIVATE_KEY, BUZZ_RELAY_URL, agent command +sudo systemctl daemon-reload +sudo systemctl enable --now buzz-acp@coder +sudo systemctl status buzz-acp@coder +``` + +User-level units work the same under `~/.config/systemd/user/` with paths adjusted. + +### Minimal env + +```bash +BUZZ_PRIVATE_KEY= +BUZZ_RELAY_URL=ws://HOST:3000 # MUST match Desktop +BUZZ_ACP_AGENT_COMMAND=goose # or claude-agent-acp, etc. +# BUZZ_ACP_AGENT_ARGS= # comma-separated if needed +BUZZ_ACP_SUBSCRIBE=mentions +BUZZ_ACP_RESPOND_TO=anyone # or owner-only / allowlist +``` + +See [crates/buzz-acp/README.md](../crates/buzz-acp/README.md) for goose, Codex, +Claude Code, multi-agent pools, and heartbeats. + +## 3. Laptop: Desktop as client + +1. Install [Buzz Desktop](https://github.com/block/buzz/releases). +2. Import or create the **human** Nostr identity you use as channel owner + (a random new key will not see existing private membership). +3. Connect / join community with: + + `ws://HOST:3000` + (exactly the same host agents use) + +4. Confirm the agent is a **member** of the channel (same as adding a person). +5. `@mention` the agent. It should reply from the server-side harness. + +Optional env (builds that honor it): + +```bash +export BUZZ_RELAY_URL=ws://HOST:3000 +export BUZZ_AUTO_CONNECT_DEFAULT_RELAY=1 +buzz-desktop +``` + +## 4. Smoke test (server) + +```bash +export BUZZ_PRIVATE_KEY= +export BUZZ_RELAY_URL=http://HOST:3000 +buzz messages send --channel --content '@AgentName ping — reply PONG' +buzz messages get --channel --limit 10 +``` + +## Troubleshooting + +| Symptom | Check | +|---------|--------| +| Empty channels on laptop | Host string ≠ agent `BUZZ_RELAY_URL`; or new Desktop key | +| Agent never replies | `systemctl status buzz-acp@…`; agent member of channel; subscribe=mentions | +| Works on server CLI only | Desktop connected to different host/community | +| Closed relay rejects Desktop | Add Desktop pubkey as relay member; see issue discussion on closed-relay join UX | +| Voice huddle mic denied | OS microphone permission on the **laptop**; headless/noVNC has no real mic | + +## Related + +- [buzz-acp README](../crates/buzz-acp/README.md) +- [ARCHITECTURE.md](../ARCHITECTURE.md) — host → community binding +- Closed-relay desktop join: track upstream issues on membership / owner bootstrap From e5a882dc6517aa804b76b7986d9fabb7be750774 Mon Sep 17 00:00:00 2001 From: Pablo Navarro Date: Wed, 29 Jul 2026 14:47:34 +0000 Subject: [PATCH 2/3] docs: frame always-on agents for any client, not laptop-only Clarify that Desktop/web/mobile/CLI are interchangeable thin clients and that the backend (relay + buzz-acp) is for every operator/team, not a laptop-specific setup. Signed-off-by: Pablo Navarro --- crates/buzz-acp/README.md | 6 +- deploy/compose/README.md | 2 +- docs/self-host-always-on-agents.md | 119 +++++++++++++++++++---------- 3 files changed, 84 insertions(+), 43 deletions(-) diff --git a/crates/buzz-acp/README.md b/crates/buzz-acp/README.md index dd3f98f586..c0f4fb0913 100644 --- a/crates/buzz-acp/README.md +++ b/crates/buzz-acp/README.md @@ -339,10 +339,10 @@ See the [root TESTING.md](../../TESTING.md) for the full integration testing gui Apache-2.0 -## Always-on / remote Desktop +## Always-on agents (any client) -For the **laptop Desktop + server-side `buzz-acp`** layout (agents stay up when -the laptop sleeps), see +For **server-side `buzz-acp`** with Desktop, web, mobile, or CLI as thin clients +(agents stay up when devices sleep or disconnect), see [docs/self-host-always-on-agents.md](../../docs/self-host-always-on-agents.md) and the systemd unit template under [deploy/systemd/](../../deploy/systemd/). diff --git a/deploy/compose/README.md b/deploy/compose/README.md index 4f580ff82a..516b46ed36 100644 --- a/deploy/compose/README.md +++ b/deploy/compose/README.md @@ -55,5 +55,5 @@ curl -fsS "http://127.0.0.1:$(grep -E '^BUZZ_HTTP_PORT=' .env | cut -d= -f2-)/_l ./run.sh status ``` -## Always-on agents (Desktop elsewhere) +## Always-on agents (any client) See [docs/self-host-always-on-agents.md](../../docs/self-host-always-on-agents.md) and [deploy/systemd/](../systemd/). diff --git a/docs/self-host-always-on-agents.md b/docs/self-host-always-on-agents.md index 5352d899a8..d642e01dc5 100644 --- a/docs/self-host-always-on-agents.md +++ b/docs/self-host-always-on-agents.md @@ -1,52 +1,65 @@ -# Self-host: always-on agents (Desktop on laptop, backend on server) +# Self-host: always-on agents (any client → server backend) -This is the **Hermes Desktop → remote server** pattern applied to Buzz. +Run **agents and the relay on a machine that stays on**. Connect from **any** +Buzz client — Desktop (macOS / Windows / Linux), web, mobile, or `buzz` CLI — +without tying agent lifetime to a single person’s laptop. -| Machine | Runs | -|---------|------| -| **Laptop** | Buzz Desktop only (human UI) | -| **Server** | `buzz-relay` + one or more `buzz-acp` processes | +This is the thin-client / remote-backend layout (same idea as pointing a desktop +app at a remote server process). -Agents stay online when the laptop sleeps. Desktop is a thin client over WebSocket. +| Role | Runs | Examples | +|------|------|----------| +| **Clients** (many) | Human UI only | Desktop on a laptop or workstation, web UI, mobile, `buzz` CLI, CI bots that are *not* coding agents | +| **Backend** (one or more hosts) | `buzz-relay` + `buzz-acp` (+ Postgres/Redis per deploy) | VPS, bare metal, homelab, k8s node | + +Agents stay online when clients disconnect, sleep, or switch devices. ``` -Laptop Desktop ──ws(s)://HOST:3000──► buzz-relay - ▲ - buzz-acp @ agent - │ - ACP stdio agent - (goose, claude, codex, - cursor-agent, grok, …) + Desktop ─┐ + Web ─┼── ws(s)://HOST:3000 ──► buzz-relay + Mobile ─┤ ▲ + CLI ─┘ buzz-acp @ agent + │ + ACP stdio agent + (goose, claude, codex, + cursor-agent, grok, …) ``` +Anyone on the community can @mention server-side agents. You do **not** need a +laptop specifically — only a client that speaks the relay URL. + ## Why the URL string matters Buzz selects the **community** from the connection **Host** header (`req.community = resolve_host(connection.host)`). -Use the **same host string** everywhere: +Use the **same host string** on every client **and** every `buzz-acp`: | Good | Bad | |------|-----| -| `ws://buzz.example.com:3000` on Desktop **and** every `buzz-acp` | Desktop → `localhost`, agents → `127.0.0.1` or a different hostname | +| `ws://buzz.example.com:3000` everywhere | One device uses `localhost`, agents use the public hostname | | `ws://myserver.tailnet.ts.net:3000` consistently | Mixing Tailscale DNS and LAN IP for “the same” deploy | +| `wss://buzz.example.com` behind TLS terminator | Clients on `https://…` / `wss://…` while agents still on plain `ws://127.0.0.1` without a matching host map | -If Desktop shows empty rooms while the server CLI sees agents, you are almost +If a client shows empty rooms while the server CLI sees agents, you are almost always in a **different community** (wrong host) or on a **fresh Nostr identity** that is not a channel member. -## 1. Server: relay +## 1. Backend: relay Follow the main deploy guide (`deploy/compose` or your preferred install) so `GET http://HOST:3000/_liveness` returns ok and WebSocket `ws://HOST:3000` -accepts clients. +(or `wss://…`) accepts clients. -Ensure the relay is reachable from the laptop (bind address, firewall, Tailscale, -or reverse proxy with WebSocket support). +Ensure the relay is reachable from every client network path you care about +(public DNS, Tailscale, LAN, reverse proxy with WebSocket support). Bind and +firewall so remote clients are intentional, not accidental open internet +without TLS/auth policy. -## 2. Server: always-on agent with systemd +## 2. Backend: always-on agent with systemd -Install `buzz-acp` and `buzz` (CLI) from this repo (`cargo build --release -p buzz-acp -p buzz-cli`). +Install `buzz-acp` and `buzz` (CLI) from this repo +(`cargo build --release -p buzz-acp -p buzz-cli`). Per agent: @@ -71,7 +84,7 @@ User-level units work the same under `~/.config/systemd/user/` with paths adjust ```bash BUZZ_PRIVATE_KEY= -BUZZ_RELAY_URL=ws://HOST:3000 # MUST match Desktop +BUZZ_RELAY_URL=ws://HOST:3000 # MUST match every client host string BUZZ_ACP_AGENT_COMMAND=goose # or claude-agent-acp, etc. # BUZZ_ACP_AGENT_ARGS= # comma-separated if needed BUZZ_ACP_SUBSCRIBE=mentions @@ -81,18 +94,20 @@ BUZZ_ACP_RESPOND_TO=anyone # or owner-only / allowlist See [crates/buzz-acp/README.md](../crates/buzz-acp/README.md) for goose, Codex, Claude Code, multi-agent pools, and heartbeats. -## 3. Laptop: Desktop as client +## 3. Clients: anyone joins the same community + +### Buzz Desktop (any OS) -1. Install [Buzz Desktop](https://github.com/block/buzz/releases). -2. Import or create the **human** Nostr identity you use as channel owner +1. Install [Buzz Desktop](https://github.com/block/buzz/releases) on the machine + people actually sit at (laptop, desktop workstation, etc.). +2. Use a Nostr identity that is a **member** of the channels you care about (a random new key will not see existing private membership). -3. Connect / join community with: +3. Connect / join with the **same** relay URL the agents use: - `ws://HOST:3000` - (exactly the same host agents use) + `ws://HOST:3000` or `wss://HOST` -4. Confirm the agent is a **member** of the channel (same as adding a person). -5. `@mention` the agent. It should reply from the server-side harness. +4. Confirm each agent is a **channel member** (same as adding a person). +5. `@mention` the agent. Optional env (builds that honor it): @@ -102,27 +117,53 @@ export BUZZ_AUTO_CONNECT_DEFAULT_RELAY=1 buzz-desktop ``` -## 4. Smoke test (server) +### Web / mobile + +Point the client at the same community URL. Identity and membership rules are +unchanged: same host string, keys that are members of the right channels. + +### CLI / automation + +```bash +export BUZZ_PRIVATE_KEY= +export BUZZ_RELAY_URL=http://HOST:3000 # HTTP origin for REST; host must map to same community +buzz channels list +buzz messages send --channel --content '@AgentName hello' +``` + +## 4. Smoke test (from any machine that can reach the relay) ```bash -export BUZZ_PRIVATE_KEY= +export BUZZ_PRIVATE_KEY= export BUZZ_RELAY_URL=http://HOST:3000 buzz messages send --channel --content '@AgentName ping — reply PONG' buzz messages get --channel --limit 10 ``` +## Who this is for + +| Audience | Use | +|----------|-----| +| Solo operator | Agents on a VPS; Desktop or CLI at home | +| Small team | Shared community URL; each human brings their own client | +| Homelab | Relay + agents on NAS/mini-PC; phones + laptops join via Tailscale | +| CI / ops | CLI and agents on servers; humans on Desktop/web | + +Not laptop-specific. Laptop is only the common *example* of a client that should +not host production agents. + ## Troubleshooting | Symptom | Check | |---------|--------| -| Empty channels on laptop | Host string ≠ agent `BUZZ_RELAY_URL`; or new Desktop key | +| Empty channels on one device | That device’s host string ≠ agent `BUZZ_RELAY_URL`; or new key | | Agent never replies | `systemctl status buzz-acp@…`; agent member of channel; subscribe=mentions | -| Works on server CLI only | Desktop connected to different host/community | -| Closed relay rejects Desktop | Add Desktop pubkey as relay member; see issue discussion on closed-relay join UX | -| Voice huddle mic denied | OS microphone permission on the **laptop**; headless/noVNC has no real mic | +| Works on server CLI only | Other clients connected to different host/community | +| Closed relay rejects a client | Add that client’s pubkey as relay member; improve join/invite UX upstream | +| Voice huddle mic denied | OS microphone permission on the **device running the UI**; remote desktop/noVNC often has no real mic | ## Related - [buzz-acp README](../crates/buzz-acp/README.md) - [ARCHITECTURE.md](../ARCHITECTURE.md) — host → community binding -- Closed-relay desktop join: track upstream issues on membership / owner bootstrap +- Closed-relay client join: track upstream issues on membership / owner bootstrap From 4f2348acf5b38b25bc828a60fe8bd393535a7664 Mon Sep 17 00:00:00 2001 From: Pablo Navarro Date: Wed, 29 Jul 2026 16:37:00 +0000 Subject: [PATCH 3/3] docs: add full self-host backend framework Document the complete operator framework for always-on agents and any client: layered architecture, host=community binding, Nostr + optional operator unlock dual-lock, systemd deploy, join kit, probes, topologies, runbook, and troubleshooting. Cross-link from always-on guide, buzz-acp, compose, ARCHITECTURE, and deploy/systemd README. Signed-off-by: Pablo Navarro --- ARCHITECTURE.md | 5 + crates/buzz-acp/README.md | 9 +- deploy/compose/README.md | 5 +- deploy/systemd/README.md | 33 +++ docs/self-host-always-on-agents.md | 3 + docs/self-host-backend-framework.md | 324 ++++++++++++++++++++++++++++ 6 files changed, 374 insertions(+), 5 deletions(-) create mode 100644 deploy/systemd/README.md create mode 100644 docs/self-host-backend-framework.md diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 90cbbac0cf..7b3ec7bbc8 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -825,3 +825,8 @@ These are verified gaps in the current implementation — not design aspirations | 4 | **Huddle recording/tracks not built** | Voice, room lifecycle, and join/leave/end events are wired (see Huddle Audio above). Recording and per-track publishing have reserved kinds but no producer yet. | | 5 | **Approval gates not wired end-to-end** | The executor returns `StepResult::Suspended` and the relay has grant/deny API endpoints with DB CRUD, but the engine intercepts before creating `WaitingApproval` rows — runs that hit an approval gate are marked as Failed (🚧 WF-08). | | 6 | **Workflow actions partially stubbed** | The `send_dm` and `set_channel_topic` workflow actions are in the schema but return `NotImplemented` — a run that reaches one fails at execution (🚧 WF-07). | + +## Self-host backend framework + +For the operator-facing always-on agents + any-client layout, see +[docs/self-host-backend-framework.md](docs/self-host-backend-framework.md). diff --git a/crates/buzz-acp/README.md b/crates/buzz-acp/README.md index c0f4fb0913..775bb2a410 100644 --- a/crates/buzz-acp/README.md +++ b/crates/buzz-acp/README.md @@ -342,7 +342,8 @@ Apache-2.0 ## Always-on agents (any client) For **server-side `buzz-acp`** with Desktop, web, mobile, or CLI as thin clients -(agents stay up when devices sleep or disconnect), see -[docs/self-host-always-on-agents.md](../../docs/self-host-always-on-agents.md) -and the systemd unit template under -[deploy/systemd/](../../deploy/systemd/). +(agents stay up when devices sleep or disconnect), see: + +- **Full framework:** [docs/self-host-backend-framework.md](../../docs/self-host-backend-framework.md) +- **Quick start:** [docs/self-host-always-on-agents.md](../../docs/self-host-always-on-agents.md) +- **systemd:** [deploy/systemd/](../../deploy/systemd/) diff --git a/deploy/compose/README.md b/deploy/compose/README.md index 516b46ed36..c78dc293d3 100644 --- a/deploy/compose/README.md +++ b/deploy/compose/README.md @@ -56,4 +56,7 @@ curl -fsS "http://127.0.0.1:$(grep -E '^BUZZ_HTTP_PORT=' .env | cut -d= -f2-)/_l ``` ## Always-on agents (any client) -See [docs/self-host-always-on-agents.md](../../docs/self-host-always-on-agents.md) and [deploy/systemd/](../systemd/). + +- Framework: [docs/self-host-backend-framework.md](../../docs/self-host-backend-framework.md) +- Guide: [docs/self-host-always-on-agents.md](../../docs/self-host-always-on-agents.md) +- Units: [deploy/systemd/](../systemd/) diff --git a/deploy/systemd/README.md b/deploy/systemd/README.md new file mode 100644 index 0000000000..77a9192e16 --- /dev/null +++ b/deploy/systemd/README.md @@ -0,0 +1,33 @@ +# systemd units for always-on Buzz agents + +These units run [`buzz-acp`](../../crates/buzz-acp/README.md) as a supervised +service so agents stay online independent of any Desktop or laptop session. + +See the full framework: [docs/self-host-backend-framework.md](../../docs/self-host-backend-framework.md). + +## Files + +| File | Purpose | +|------|---------| +| `buzz-acp@.service` | Template unit (`buzz-acp@coder`) | +| `buzz-acp.env.example` | Environment skeleton (copy per agent) | + +## Install + +```bash +sudo cp buzz-acp@.service /etc/systemd/system/ +sudo mkdir -p /etc/buzz-acp +sudo cp buzz-acp.env.example /etc/buzz-acp/coder.env +sudo edit /etc/buzz-acp/coder.env # keys + BUZZ_RELAY_URL +sudo systemctl daemon-reload +sudo systemctl enable --now buzz-acp@coder +sudo systemctl status buzz-acp@coder +``` + +User units: install under `~/.config/systemd/user/` and use `systemctl --user`. + +## Requirements + +- `buzz-acp` installed at `/usr/local/bin/buzz-acp` (or edit `ExecStart`) +- Relay reachable at the **same host string** clients use +- Agent pubkey is a relay/channel member as required by your join policy diff --git a/docs/self-host-always-on-agents.md b/docs/self-host-always-on-agents.md index d642e01dc5..0c4d26eff0 100644 --- a/docs/self-host-always-on-agents.md +++ b/docs/self-host-always-on-agents.md @@ -4,6 +4,9 @@ Run **agents and the relay on a machine that stays on**. Connect from **any** Buzz client — Desktop (macOS / Windows / Linux), web, mobile, or `buzz` CLI — without tying agent lifetime to a single person’s laptop. +**Full framework (architecture, security, runbook, topologies):** +[self-host-backend-framework.md](./self-host-backend-framework.md). + This is the thin-client / remote-backend layout (same idea as pointing a desktop app at a remote server process). diff --git a/docs/self-host-backend-framework.md b/docs/self-host-backend-framework.md new file mode 100644 index 0000000000..2a9f3e279e --- /dev/null +++ b/docs/self-host-backend-framework.md @@ -0,0 +1,324 @@ +# Self-host backend framework + +This document is the **full framework** for running Buzz as a real backend: +always-on agents, any client, correct community binding, and layered security. +It is the operator-facing companion to the crate READMEs and compose guides. + +| Doc | Scope | +|-----|--------| +| **This file** | End-to-end framework (read first) | +| [self-host-always-on-agents.md](./self-host-always-on-agents.md) | Step-by-step always-on agents | +| [multi-tenant-relay.md](./multi-tenant-relay.md) | Host → community isolation | +| [ARCHITECTURE.md](../ARCHITECTURE.md) | System architecture | +| [crates/buzz-acp/README.md](../crates/buzz-acp/README.md) | ACP harness reference | +| [deploy/systemd/](../deploy/systemd/) | Unit templates | + +--- + +## 1. Goals + +1. **Anyone can connect** — Desktop (any OS), web, mobile, or CLI — not a single laptop. +2. **Agents outlive clients** — coding agents run on a host that stays up (`buzz-acp`), not inside a sleeping laptop process. +3. **One community, one host string** — every client and every agent use the same relay URL host (community selector). +4. **Safer by default** — membership + closed-relay rules; optional **shared unlock secret** for operator join packs (dual credential). +5. **Copy-paste deploy** — systemd units, env examples, smoke checks. + +Non-goals: replacing `buzz-relay`, inventing a parallel protocol, or requiring a vendor-specific desktop fork. + +--- + +## 2. Layered architecture + +``` +┌─────────────────────────────────────────────────────────────┐ +│ CLIENTS (thin) │ +│ Desktop · Web · Mobile · buzz CLI · automation │ +│ — UI + identity only; no requirement to host agents — │ +└────────────────────────────┬────────────────────────────────┘ + │ ws(s)://HOST[:port] + │ Host header ⇒ community +┌────────────────────────────▼────────────────────────────────┐ +│ RELAY (truth) │ +│ buzz-relay · Postgres · Redis · (optional search/object) │ +│ Channels · members · events · invites · NIP-42 auth │ +└────────────────────────────┬────────────────────────────────┘ + │ same HOST string +┌────────────────────────────▼────────────────────────────────┐ +│ AGENT RUNTIME (always-on) │ +│ buzz-acp × N → ACP stdio agents (goose, claude, codex, …) │ +│ systemd · env files · mentions subscribe · channel members │ +└─────────────────────────────────────────────────────────────┘ +``` + +| Layer | Responsibility | Binary / surface | +|-------|----------------|------------------| +| Client | Human UX, local mic/camera, display | Desktop / web / mobile / CLI | +| Relay | Durable community state, fan-out, auth | `buzz-relay` | +| Agent harness | Mention → session → tools → reply | `buzz-acp` | +| Agent program | Model + tools (BYOH) | goose, Claude Code ACP, Codex, … | +| Operator pack (optional) | Doctor, profiles, join kit, unlock key | Site-specific tooling or docs only | + +**Execution boundary:** agent tools and files run on the **machine running `buzz-acp`**, not on the device showing the UI. That is intentional and matches production self-host practice. + +--- + +## 3. Community binding (host string) + +Buzz resolves the community from the connection **Host** (see multi-tenant docs): + +```text +req.community = resolve_host(connection.host) +``` + +| Consistent (good) | Split-brain (bad) | +|-------------------|-------------------| +| All agents + all clients: `ws://buzz.example.com:3000` | Desktop: `localhost` · agents: `buzz.example.com` | +| All on `wss://buzz.example.com` behind TLS | Mix of `127.0.0.1`, LAN IP, and Tailscale DNS for “the same” deploy | + +**Symptom of mismatch:** empty rooms on one device; full rooms on another; agents “online” but never visible. + +**Rule:** pick one canonical `HOST` (DNS or stable Tailscale name). Put it in every `BUZZ_RELAY_URL` and every client join URL. + +--- + +## 4. Identity and security model + +### 4.1 Protocol identity (required) + +- Every human and agent has a **Nostr keypair**. +- Relay membership and channel membership gate who can read/write. +- Closed-relay deploys: unknown pubkeys cannot join without invite/admin add-member flows. + +### 4.2 Always-on agent identity + +- Each `buzz-acp` instance has its **own** key (not the human’s). +- Agent pubkey must be a **channel member** (and relay member when closed). +- Prefer `BUZZ_ACP_SUBSCRIBE=mentions` so agents reply when addressed. + +### 4.3 Operator dual-lock (recommended) + +In addition to Nostr, operators may require a **shared unlock secret** for backend join packs (same idea as a desktop session token on a remote gateway): + +| Credential | Metaphor | Purpose | +|------------|----------|---------| +| Shared unlock secret | House key | Proves the client was issued access to *this* backend pack | +| Nostr key | Name badge | Proves *who* is acting; drives membership | + +**Not automatic trust:** possession of a leaked channel member key alone should not be treated as full operator onboarding. Issue join material that includes: + +1. Canonical `BUZZ_RELAY_URL` (host string) +2. Human or device Nostr secret (or instruct generate-then-add-member) +3. Optional `BUZZ_BACKEND_UNLOCK_KEY` (shared with server operator store) +4. Fingerprint of unlock for out-of-band verify without pasting the secret in chat + +Store unlock material **0600**, never commit, never paste into public issues. + +Relay protocol auth remains Nostr; dual-lock is an **operator onboarding and tooling** control unless you also terminate TLS/auth at a reverse proxy. + +### 4.4 Invites + +Official relay supports invite mint/claim (`/api/invites`, `/invite/...`). Prefer invites for new humans on closed relays instead of copying a single shared nsec across a team. + +--- + +## 5. Always-on agents + +### 5.1 Process model + +- One or more `buzz-acp` processes, supervised by **systemd** (or equivalent). +- Each process: unique key, unique display name, one ACP command line. +- Relay URL **identical** to clients. + +### 5.2 systemd + +Templates live in [`deploy/systemd/`](../deploy/systemd/): + +- `buzz-acp@.service` — instance unit (`buzz-acp@coder`) +- `buzz-acp.env.example` — env skeleton + +```bash +sudo cp deploy/systemd/buzz-acp@.service /etc/systemd/system/ +sudo mkdir -p /etc/buzz-acp +sudo cp deploy/systemd/buzz-acp.env.example /etc/buzz-acp/coder.env +# edit keys + BUZZ_RELAY_URL=ws://HOST:3000 +sudo systemctl daemon-reload +sudo systemctl enable --now buzz-acp@coder +``` + +### 5.3 Minimal env + +```bash +BUZZ_PRIVATE_KEY= +BUZZ_RELAY_URL=ws://HOST:3000 +BUZZ_ACP_AGENT_COMMAND=goose +BUZZ_ACP_SUBSCRIBE=mentions +BUZZ_ACP_RESPOND_TO=anyone # or tighter +``` + +See [buzz-acp README](../crates/buzz-acp/README.md) for pools, heartbeats, allowlists, and editor agents. + +### 5.4 Channel membership + +Agents are participants. After key generation: + +1. Add relay member (closed mode). +2. Create or select channels. +3. `add-member` agent pubkey to each channel they should serve. +4. Smoke with `@AgentName` from any client on the same host string. + +--- + +## 6. Clients (any device) + +### 6.1 Desktop + +1. Install from [releases](https://github.com/block/buzz/releases). +2. Import or create identity that is a **member**. +3. Connect to `ws://HOST:3000` or `wss://HOST` — **same host as agents**. +4. Optional env (when supported by the build): + +```bash +export BUZZ_RELAY_URL=ws://HOST:3000 +export BUZZ_AUTO_CONNECT_DEFAULT_RELAY=1 +``` + +### 6.2 Web / mobile + +Same community URL and membership rules. No special laptop requirement. + +### 6.3 CLI + +```bash +export BUZZ_PRIVATE_KEY= +export BUZZ_RELAY_URL=http://HOST:3000 +buzz channels list +buzz messages send --channel --content '@Agent hello' +``` + +### 6.4 Join kit (operator checklist) + +Hand a private file (or secure channel) containing: + +```text +BUZZ_RELAY_URL=ws://HOST:3000 +BUZZ_COMMUNITY_HOST=HOST:3000 +# identity +NSEC=... # or BUZZ_PRIVATE_KEY= +# optional dual-lock +BUZZ_BACKEND_UNLOCK_KEY=... +BUZZ_UNLOCK_FP= +``` + +Client onboarding: import identity → set relay URL → confirm unlock fingerprint with operator if used → open channel → mention agent. + +--- + +## 7. Health, probe, readiness + +Before calling a deploy “up,” check: + +| Check | Expect | +|-------|--------| +| `GET http://HOST:3000/_liveness` | ok | +| WebSocket upgrade to same host | HTTP 101 | +| `systemctl status buzz-acp@…` | active | +| Mention smoke | Agent reply in channel | +| Host string | Identical on sample client env and agent env | + +**Readiness idea (operators):** after fleet start, record a line such as: + +```text +BUZZ_BACKEND_READY acp=N relay=ws://HOST:3000 +``` + +Do not treat “binary exists” as healthy without liveness + WS + at least one successful agent path. + +--- + +## 8. Multi-channel work layout (optional pattern) + +Communities often split work by channel rather than one mega-room: + +| Channel (example) | Purpose | +|-------------------|---------| +| `orchestrator` | Assignments / priorities | +| `eng-build` | Implementation | +| `eng-review` | Safety / ship gate | +| `eng-ops` | Infra / relay / agents | +| `clients` | Customer-named work | +| … | Team-specific | + +Agents may be members of many channels; humans join the rooms they need. This is organizational, not a protocol requirement. + +--- + +## 9. Deploy topologies + +| Topology | When | +|----------|------| +| Single VPS | Relay + Postgres + Redis + all `buzz-acp` on one host | +| Split | Relay/data on A; agent fleet on B with tools/GPU; same public HOST via proxy | +| Homelab + tailnet | `HOST` = MagicDNS name; clients on phones/laptops via Tailscale | +| TLS edge | `wss://HOST` terminated at Caddy/nginx; agents still use the **public host string** that clients use | + +Bind and firewall deliberately. Prefer Tailscale or private network when not ready for public internet. + +--- + +## 10. Operator runbook (short) + +```text +1. Install relay (compose or binary) → liveness ok +2. Choose canonical HOST string +3. Create human identity; ensure relay/channel membership +4. For each agent: key → member → buzz-acp systemd → channel member +5. Issue join kit to each human device (URL + identity [+ unlock]) +6. From any client: open channel, @mention agent +7. On failure: compare Host strings; check membership; journalctl -u buzz-acp@… +``` + +--- + +## 11. Troubleshooting matrix + +| Symptom | Likely cause | +|---------|----------------| +| Empty channels on one device | Different host string or new Nostr key | +| Agent never replies | Not in channel; acp down; subscribe/filter; model auth | +| Works on server CLI only | Clients not on same community URL | +| Closed relay rejects client | Need invite or admin add-member | +| Voice mic denied | OS permission on the **UI device**; remote desktop often has no mic | +| “I ran grok in tmux” but hive silent | Local CLI ≠ `buzz-acp` seat on the server | + +--- + +## 12. Framework summary + +```text +CLIENTS (any) --same HOST--> RELAY <--same HOST-- ACP AGENTS (always-on) + Nostr membership + (+ optional operator unlock on join packs) +``` + +| Principle | Practice | +|-----------|----------| +| Thin clients | UI devices do not host production agents | +| Always-on agents | systemd + buzz-acp on a stable host | +| One community | One host string everywhere | +| Defense in depth | Membership + invites + optional shared unlock | +| Verify | Liveness, WS, mention smoke | + +--- + +## 13. Related code and deploy paths + +| Path | Role | +|------|------| +| `crates/buzz-relay` | Relay | +| `crates/buzz-acp` | Agent harness | +| `crates/buzz-cli` | Operator/agent CLI | +| `deploy/compose` | Container deploy | +| `deploy/systemd` | Always-on agent units | +| `desktop/` | Official Desktop client | + +Contributions that extend this framework (Desktop connection UX for host mismatch warnings, join-kit import, unlock field, closed-relay owner bootstrap) should link back here so operators keep a single mental model.