Discord: check bot permissions before handing a token to a node (server-26#225 S3)

start_stream now asks Discord (cached REST snapshots, S2) whether each bot
it could pick may View, Connect and Speak in the voice channel, before the
lease. Covers all four entry points: bot POST /discord/streams, web picker,
nodes discord_join, and the reconciler's re-place.

- pick_token: denied bots skipped; confirmed-ok sorts before unknown;
  unknown is allowed (fail open); all denied -> missing_permissions naming
  each bot, missing permission, trace reason and fix. The reuse branch
  falls through when its token is denied.
- Bounded: discord_perm_check_concurrency at once, within
  discord_perm_check_budget_seconds; unanswered = unknown.
- Non-voice channel -> 400 bad_channel (not missing_permissions).
- systems/{id}.preferred_token_id is now read (plan finding 1), as a hint.
- Pool bots at discord_pool_bot_guild_cap are never offered as invites;
  pool_full when nothing else is left (owner Q3).
- handle_actual drops cached access on missing_permissions, channel_full,
  not_in_guild, bad_channel.
- New GET /discord/nodes/{node_id}/guilds/{guild_id}/voice-check (org
  member, names only) and GET /discord/guilds/{guild_id}/voice-check
  (service key or admin).
- PUT /tokens/{id}/prefer/{system_id}: org owners for their own systems
  and own BYO bots; shared systems and pool bots stay admin-only (owner Q2).
  Keeps both pointers single-valued; audited.
- Kill switch: DISCORD_PERM_CHECK_ENABLED=false.
- tests/conftest.py: autouse fixture so no test ever reaches discord.com.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-05 02:22:45 -04:00
co-authored by Claude Opus 5.5
parent a75149b2eb
commit 2c2cf9d9be
9 changed files with 1232 additions and 47 deletions
+24
View File
@@ -72,3 +72,27 @@ except ModuleNotFoundError:
):
sys.modules.setdefault(_name, ModuleType(_name))
sys.modules["google.cloud.firestore_v1.base_query"].FieldFilter = MagicMock()
import pytest # noqa: E402
@pytest.fixture(autouse=True)
def _no_real_discord(monkeypatch):
"""No test reaches discord.com (server-26#225 S3).
``start_stream`` now asks Discord, over REST, whether a bot may speak in
the channel before it hands the bot to a node. Every Discord call in a
test is answered by a transport that fails to connect, so the check is
"unknown" and fails open — today's token picking — unless the test
installs its own transport or stubs ``discord_rest.guild_snapshot``."""
import httpx
from app.internal import discord_rest
def _offline(request):
raise httpx.ConnectError("tests never reach Discord", request=request)
discord_rest.reset()
monkeypatch.setattr(discord_rest, "_transport", httpx.MockTransport(_offline))
yield
discord_rest.reset()