From e8d63737f446c0ea48412755f556d746cde26e2a Mon Sep 17 00:00:00 2001 From: Suhas Kashyap Date: Thu, 23 Jul 2026 20:46:25 +0000 Subject: [PATCH 1/3] docs: document BUZZ_CORS_ORIGINS and Tauri webview origins in .env.example Setting BUZZ_CORS_ORIGINS to only the public origin blocks the desktop app's HTTP API calls (invites, moderation) with a CORS network error, while WebSocket traffic keeps working. The webview origins that must be allowlisted are tauri://localhost (macOS/Linux) and http://tauri.localhost (Windows, per media_proxy.rs and Tauri v2's http default). The relay code documents this in a doc comment (config.rs), but .env.example, the file operators actually copy, had no mention of the variable. Co-Authored-By: Claude Fable 5 --- .env.example | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.env.example b/.env.example index db5a7ea25c7..241ab2bef2d 100644 --- a/.env.example +++ b/.env.example @@ -54,6 +54,13 @@ RELAY_URL=ws://localhost:3000 # the web frontend at / for browser requests. Leave unset for local dev # (use `just web` for Vite HMR instead). # BUZZ_WEB_DIR=./web/dist +# Allowed CORS origins (comma-separated). Unset = permissive (dev mode). +# When set, MUST include the desktop app's webview origins or its HTTP API +# calls (invites, moderation) fail with a network error while WebSocket +# traffic still works: `tauri://localhost` (macOS/Linux) and +# `http://tauri.localhost` (Windows). Invalid values are rejected, not +# treated as permissive. +# BUZZ_CORS_ORIGINS=https://your-relay.example.com,tauri://localhost,http://tauri.localhost # Shared Redis-backed admission limits. Defaults shown below; each value must # be a positive integer. From be8af66384d7002550ddaf3b2ecca6c3617407e0 Mon Sep 17 00:00:00 2001 From: Suhas Kashyap Date: Sat, 25 Jul 2026 19:21:07 +0000 Subject: [PATCH 2/3] deploy: add NIP-AB pairing sidecar and restore loopback port binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device pairing was failing with "WebSocket connection failed: HTTP error: 404 Not Found". The compose bundle never ran the pairing sidecar — upstream ships it only in the Helm chart (deploy/charts/buzz/templates/pairing-relay.yaml). An unpaired device holds only an ephemeral keypair, so it is rejected at NIP-42 AUTH time by enforce_relay_membership (crates/buzz-relay/src/api/mod.rs) on a membership-enforcing relay. The handshake therefore has to run on a separate unauthenticated relay. Clients find it via the NIP-11 `pairing_relay_url` field; with BUZZ_PAIRING_RELAY_URL unset, the desktop client instead infers NIP-43 support and falls back to /pair (desktop/src-tauri/src/commands/pairing.rs), a path nothing served — hence the 404. Adds a `pairing` service on the existing image. Two notes for future edits: - Uses `entrypoint:`, not `command:`. The image ENTRYPOINT is buzz-relay, so `command:` would append args to the wrong binary. The Helm chart's `command:` works only because Kubernetes overrides ENTRYPOINT. - No env_file. The sidecar needs no DB, Redis, S3, or relay private key, so it is not handed the secrets in .env. The reverse proxy must route only /pair to it (exact-match location), since the sidecar is unauthenticated by design. It self-limits: kind 24134 only, no persistence, 128 conns, 4 KiB frames, 120s TTL. Also re-applies the 127.0.0.1 relay port binding, which had drifted back to 0.0.0.0 and exposed port 3300 off-box, bypassing TLS and nginx. Deployed and verified on buzz.axonclaw.cloud: /pair returns 101, / still returns 101, /pairing stays 404. Co-Authored-By: Claude --- deploy/compose/compose.yml | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/deploy/compose/compose.yml b/deploy/compose/compose.yml index bc3c27501e8..0531e6aa494 100644 --- a/deploy/compose/compose.yml +++ b/deploy/compose/compose.yml @@ -18,8 +18,10 @@ services: BUZZ_GIT_REPO_PATH: /data/git BUZZ_AUTO_MIGRATE: ${BUZZ_AUTO_MIGRATE:-false} BUZZ_GIT_CONFORMANCE_PROBE: ${BUZZ_GIT_CONFORMANCE_PROBE:-true} + # Loopback-only: TLS termination and access control live in nginx, so the + # relay port must not be reachable from off-box. ports: - - "${BUZZ_HTTP_PORT:-3000}:3000" + - "127.0.0.1:${BUZZ_HTTP_PORT:-3000}:3000" volumes: - buzz-git-data:/data/git depends_on: @@ -46,6 +48,39 @@ services: networks: - buzz-net + # NIP-AB device-pairing sidecar. An unpaired phone only has an ephemeral key, + # so it cannot pass the main relay's membership gate at NIP-42 AUTH time — + # the pairing handshake has to happen on this separate unauthenticated relay. + # Clients discover it from the NIP-11 `pairing_relay_url` field, which the + # main relay advertises from BUZZ_PAIRING_RELAY_URL in .env. + # + # The binary ships in the same image; ENTRYPOINT is buzz-relay, so override + # `entrypoint` (not `command`, which is what the Helm chart uses — in Compose + # `command` would just be appended as args to buzz-relay). + # + # No env_file here on purpose: this service needs no DB, Redis, S3, or relay + # private key, so it should not be handed the secrets in .env. + pairing: + image: ${BUZZ_IMAGE:-ghcr.io/block/buzz:main} + entrypoint: ["/usr/local/bin/buzz-pair-relay"] + environment: + # Defaults to loopback inside the container, which the port publish below + # could not reach — bind all interfaces in the container namespace and let + # the host-side 127.0.0.1 publish do the actual confinement. + BUZZ_PAIR_RELAY_BIND_ADDR: 0.0.0.0:5000 + ports: + - "127.0.0.1:${BUZZ_PAIR_PORT:-5000}:5000" + # Runtime image has bash but no curl/wget, so probe the TCP port directly. + healthcheck: + test: ["CMD-SHELL", "bash -ec 'exec 3<>/dev/tcp/127.0.0.1/5000'"] + interval: 10s + timeout: 3s + retries: 6 + start_period: 5s + restart: unless-stopped + networks: + - buzz-net + postgres: image: postgres:17-alpine environment: From 33b33d3a9b67e703d0517883e659887c46558e4b Mon Sep 17 00:00:00 2001 From: Suhas Kashyap Date: Fri, 31 Jul 2026 01:57:21 +0530 Subject: [PATCH 3/3] feat(desktop): show external relay agents in the Agents view The Agents view only rendered desktop-managed agents, so agents running behind an external buzz-acp harness (announced via kind:10100) were invisible outside DMs and channel member lists. Add a read-only 'Relay agents' section listing kind:10100 agents not already managed by this desktop. Cards show the relay profile name/avatar and presence status, and open the agent's profile panel; lifecycle stays wherever the harness runs, so there are no start/stop controls. Signed-off-by: Suhas Kashyap --- desktop/src/features/agents/ui/AgentsView.tsx | 13 +++ .../features/agents/ui/RelayAgentsSection.tsx | 109 ++++++++++++++++++ 2 files changed, 122 insertions(+) create mode 100644 desktop/src/features/agents/ui/RelayAgentsSection.tsx diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx index 8e1c47c6157..6af57818559 100644 --- a/desktop/src/features/agents/ui/AgentsView.tsx +++ b/desktop/src/features/agents/ui/AgentsView.tsx @@ -19,6 +19,7 @@ import { TeamShareDialog } from "./TeamShareDialog"; import { SecretRevealDialog } from "./SecretRevealDialog"; import { TeamDeleteDialog } from "./TeamDeleteDialog"; import { TeamDialog } from "./TeamDialog"; +import { RelayAgentsSection } from "./RelayAgentsSection"; import { TeamsSection } from "./TeamsSection"; import { UnifiedAgentsSection } from "./UnifiedAgentsSection"; import { useManagedAgentActions } from "./useManagedAgentActions"; @@ -61,6 +62,11 @@ export function AgentsView() { }, ); + const managedPubkeys = React.useMemo( + () => new Set(agents.managedAgents.map((agent) => agent.pubkey)), + [agents.managedAgents], + ); + const isActionPending = agents.isPending || personas.isPending || @@ -202,6 +208,13 @@ export function AgentsView() { }} /> + { + openProfilePanel?.(pubkey, options); + }} + /> + ; + onOpenAgentProfile: ( + pubkey: string, + options?: ProfilePanelOpenOptions, + ) => void; +}) { + const relayAgentsQuery = useRelayAgentsQuery({ enabled: true }); + const [isCollapsed, setIsCollapsed] = React.useState(false); + + const externalAgents = React.useMemo( + () => + (relayAgentsQuery.data ?? []) + .filter((agent) => !managedPubkeys.has(agent.pubkey)) + .sort((left, right) => left.name.localeCompare(right.name)), + [relayAgentsQuery.data, managedPubkeys], + ); + + if (externalAgents.length === 0) return null; + + return ( +
+ + {!isCollapsed ? ( +
+ {externalAgents.map((agent) => ( + + ))} +
+ ) : null} +
+ ); +} + +function RelayAgentCard({ + agent, + onOpenAgentProfile, +}: { + agent: RelayAgent; + onOpenAgentProfile: ( + pubkey: string, + options?: ProfilePanelOpenOptions, + ) => void; +}) { + const profileQuery = useUserProfileQuery(agent.pubkey); + const title = profileQuery.data?.displayName?.trim() || agent.name; + + return ( + onOpenAgentProfile(agent.pubkey)} + statusBadge={ + agent.status === "online" ? ( + + Online + + ) : ( + + {agent.status === "away" ? "Away" : "Offline"} + + ) + } + /> + ); +}