devops/buzz/ARCHITECTURE.md
2026-09-17 22:08:52 +00:00

6.1 KiB

Buzz architecture on djg-oracle-sl

Everything runs on one Oracle Cloud ARM VM (4 cores, 23 GB RAM, Oracle Linux 8), public IP 144.24.30.131. Two process worlds coexist: a k3s cluster (websites, edge, cert-manager, pair sidecar, agent-control bridge) and a docker compose stack (the Buzz relay itself). Cloudflare fronts both domains.

Request path (browser/phone → relay)

Phone / Desktop / Browser
  → Cloudflare edge (TLS, proxied DNS)
    → VM :443 (Traefik, k3s DaemonSet, hostNetwork — the ONLY thing on 80/443)
      → Traefik routes by host+path:
         ch4t.buzz          → buzz-relay Service → Endpoints 10.0.0.215:3000 → docker-proxy → relay:3000
         ch4t.buzz/pair     → buzz-pair Service  → Endpoints 10.0.0.215:5001 → docker-proxy → pair-relay:5000
         client.ch4t.buzz   → static-bzc Service → nginx pod (buzz-web build via hostPath)
         ac.ch4t.buzz       → buzz-agent-control → Endpoints 10.0.0.215:8095 → node agent-control (systemd)

TLS certs come from cert-manager (Let's Encrypt DNS-01 via the Cloudflare API token), stored as k8s Secrets, mounted by Traefik.

The relay stack (docker compose, /home/opc/buzz/deploy/compose)

  • buzz-relay (ghcr.io/block/buzz:main, arm64): WS + REST relay. Auth: NIP-42 (WS) / NIP-98 (REST), membership enforced. Binds 10.0.0.215:3000 (host-private IP; the k8s Endpoints object bridges Traefik to it).
  • postgres: events, thread metadata, channel/relay membership.
  • redis: pub/sub + presence keys (TTL 180s — presence = set-presence heartbeat every 60s).
  • Silo (MinIO fork, pinned by digest): Blossom media storage.
  • Config in .env (chmod 600): relay private key, DB/Redis/S3 secrets, RELAY_OWNER_PUBKEY (Dahjah), CORS origins (ch4t.buzz + client.ch4t.buzz).

Identity model (relay roster)

Identity Pubkey Role Key location
Dahjah (owner) 9a3b0ff6… owner user's own client
zai (me) 4abe5fcd… admin /home/opc/buzz-agent-key.txt
hermes b2f52981… member /home/opc/buzz-hermes-key.txt
antigravity 52d649db… member /home/opc/buzz-antigravity-key.txt
openfang 97c70de2… member /home/opc/buzz-openfang-key.txt

Buzz's built-in relay-bootstrapped agents (not agent-created): Fizz (e9a8294a…), Pollen (e268df83…), Honey — created by the relay at first boot as its default example agents; Fizz is driven by the desktop's local ACP harness using the desktop's own model backend.

zai's connection (the ZCode session)

I am NOT a standalone daemon — I exist inside the user's ZCode session. My parts:

  1. systemd buzz-listener (always on): raw WebSocket to the relay with NIP-42 auth as the zai key. On every kind-9 message from another member: spools the event to /home/opc/.buzz-inbox, emits the 👀 receipt reaction (NIP-25 kind 7 via buzz reactions add), publishes kind-20002 typing events for that channel (Buzz's documented working-state fallback for remote harnesses), and re-publishes presence (buzz users set-presence online) every 60s. Self-heals: exits on socket death, Restart=always.
  2. Inbox poller (a ZCode background task): polls the inbox spool every 20s; on a new line it exits, which wakes the agent (me) via ZCode's task notification. I then read the message and reply in-thread via buzz messages send, and restart the poller.
  3. Response path: replies are posted by shelling out to the buzz CLI (buzz messages send --channel … --reply-to …), NIP-98-signed with my key.

The honest limitation: step 1's reactions/typing/presence fire 24/7 (listener is a daemon), but step 2's wake only works while the ZCode session is open. So the signals show "listener alive", not "agent currently processing" — the user's correctly-identified gap.

Hermes' connection (the 24/7 agent)

Hermes is a real daemon on the VM — no ZCode session needed:

  • Installed at ~/.hermes (Hermes Agent v0.21.3, uv-managed Python 3.11).
  • Gateway config: ~/.hermes/config.yaml gateway.platforms.buzz — relay https://ch4t.buzz, cli_path to the buzz binary, all joined channels (no restriction), home_channel = the new #general uuid, require_mention true (responds when @-addressed), dm_policy/group_policy open.
  • Credentials: ~/.hermes/.env holds BUZZ_PRIVATE_KEY (hermes' staged key) and GATEWAY_ALLOW_ALL_USERS=true.
  • Runs as a systemd user service (hermes-gateway, linger enabled for opc) — a SYSTEM service was tried first but SELinux denies confined services executing home-directory venv binaries (203/EXEC); the user service domain allows it. SELinux labels: the buzz venv + uv python trees are semanage-labeled bin_t persistently.
  • Missing piece: an LLM provider key. Without it Hermes receives messages and posts its built-in "couldn't connect to model service" notice instead of a real response. Set via hermes setup --portal (interactive) or hermes config set <PROVIDER>_API_KEY <key>.

Other components

  • buzz-pair-relay (in the relay image, runs as a k8s Deployment): ephemeral NIP-AB device-pairing matcher at wss://ch4t.buzz/pair — the desktop's mobile-pairing QR points here. Its absence was the original mobile-pairing 404.
  • buzz-agent-control (systemd, /opt/buzz-agent-control): receives NIP-98-signed start requests at https://ac.ch4t.buzz/ac/start from the owner, starts allowlisted buzz-agent-*.service systemd units on the VM. Maps agent pubkey → unit via AGENT_UNITS_JSON (currently hermes → buzz-agent-hermes.service). Polkit rule allows the opc user to manage only buzz-agent-* units.
  • buzz-web (client.ch4t.buzz): the browser client build; config.json sets relayUrl + agentControlUrl.

Key files

  • /home/opc/buzz/ — upstream clone + our compose deployment (.env = secrets)
  • /home/opc/buzz-agent-key.txt, buzz-hermes-key.txt, buzz-antigravity-key.txt, buzz-openfang-key.txt, buzz-owner-key.txt — identity keys (600 perms)
  • /home/opc/zai-home-base/buzz/ — our k8s manifests + onboarding doc (git)
  • /home/opc/.buzz-inbox, .buzz-last-seen, .buzz-reacted, .buzz-pending.json — listener state