Discord bot /leave kills every node's stream fleet-wide; /join hijacks a node already streaming elsewhere (no guild/org scoping) #203

Closed
opened 2026-10-04 15:34:19 -04:00 by logan · 2 comments
Owner

Found during the Discord system design audit (2026-10-04, design-canvas/discord-system-notes.md).

Bugs, all in drb-server-discord-bot/app/commands/radio.py:

  1. /leave (L95-111) iterates c2.get_nodes() — every node fleet-wide with status online/recording — and sends discord_leave to each. Any user in any guild that has the bot disconnects every DRB voice stream in every guild, and C2 releases every token (routers/nodes.py:122). It never checks which guild the caller is in.
  2. /join (L45-90) picks the first online node for the system (c2_client.find_node_for_system) without checking whether that node is already streaming to another guild. The edge bot has a single VoiceClient (discord_radio.py:48-50 disconnects the existing one), so a /join from guild B silently steals guild A's stream.
  3. No permission check on either command — any member of any guild the bot is in can summon/dismiss. No guild -> org binding exists, so the service-key client sees all orgs' nodes/systems: /status lists every org's nodes to any guild; system autocomplete offers every org's systems; token autocomplete lists the platform token pool by name.
  4. /join replies "Streaming X ... to #channel" as soon as the MQTT publish succeeds. The edge's actual join result (bot not in that guild, Pulse missing, privileged-intent failure) is never reported back, so users get a false success and silence.
  5. /help footer says "Audio is streamed live from SDR edge nodes via Icecast." — false since node-26#2; the path is PulseAudio.

Fix direction (see notes doc, MVP phase): persist a discord_streams doc per (guild, channel) with owner node + token; /leave acts only on streams in interaction.guild_id; /join refuses (or offers) when the node is busy elsewhere; gate both on a Manage Channels / configured DJ role; bind guild -> org and scope every autocomplete and /status by it; report join results back over MQTT (nodes/{id}/status discord block) and edit the ephemeral reply.

Related: node-26 edge session/token issue filed alongside; SAAS_PLAN.md line 100 (SERVICE_KEY is a fleet-wide principal with no org scope).

Found during the Discord system design audit (2026-10-04, `design-canvas/discord-system-notes.md`). **Bugs, all in `drb-server-discord-bot/app/commands/radio.py`:** 1. `/leave` (L95-111) iterates `c2.get_nodes()` — every node fleet-wide with status online/recording — and sends `discord_leave` to each. Any user in any guild that has the bot disconnects **every** DRB voice stream in **every** guild, and C2 releases every token (`routers/nodes.py:122`). It never checks which guild the caller is in. 2. `/join` (L45-90) picks the first online node for the system (`c2_client.find_node_for_system`) without checking whether that node is already streaming to another guild. The edge bot has a single VoiceClient (`discord_radio.py:48-50` disconnects the existing one), so a `/join` from guild B silently steals guild A's stream. 3. No permission check on either command — any member of any guild the bot is in can summon/dismiss. No guild -> org binding exists, so the service-key client sees all orgs' nodes/systems: `/status` lists every org's nodes to any guild; system autocomplete offers every org's systems; token autocomplete lists the platform token pool by name. 4. `/join` replies "Streaming **X** ... to #channel" as soon as the MQTT publish succeeds. The edge's actual join result (bot not in that guild, Pulse missing, privileged-intent failure) is never reported back, so users get a false success and silence. 5. `/help` footer says "Audio is streamed live from SDR edge nodes via Icecast." — false since node-26#2; the path is PulseAudio. **Fix direction** (see notes doc, MVP phase): persist a `discord_streams` doc per (guild, channel) with owner node + token; `/leave` acts only on streams in `interaction.guild_id`; `/join` refuses (or offers) when the node is busy elsewhere; gate both on a Manage Channels / configured DJ role; bind guild -> org and scope every autocomplete and `/status` by it; report join results back over MQTT (`nodes/{id}/status` discord block) and edit the ephemeral reply. Related: node-26 edge session/token issue filed alongside; SAAS_PLAN.md line 100 (`SERVICE_KEY` is a fleet-wide principal with no org scope).
Author
Owner

PR: #216 (server) + logan/node-26#32 (edge). Merge both together. Follow-ups filed: server-26#213 (guild link UI/admin page), server-26#214 (ad-hoc idle dismiss). drb-audio-review still to run on the edge PR before merge.

PR: https://git.vpn.cusano.net/logan/server-26/pulls/216 (server) + https://git.vpn.cusano.net/logan/node-26/pulls/32 (edge). Merge both together. Follow-ups filed: server-26#213 (guild link UI/admin page), server-26#214 (ad-hoc idle dismiss). drb-audio-review still to run on the edge PR before merge.
logan added the area/discord
kind
behavior-bug
priority
p2
type
bug
labels 2026-10-05 02:05:02 -04:00
Author
Owner

Fixed by PR #216 (merge 99407c1). It shipped as part of the desired-state streams + transactional leases change, which is live in prod.

Fixed by PR #216 (merge 99407c1). It shipped as part of the desired-state streams + transactional leases change, which is live in prod.
logan closed this issue 2026-10-05 02:08:55 -04:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: logan/server-26#203