From 0f4403346db4a8407af3d3ef12104e997da0a4e6 Mon Sep 17 00:00:00 2001 From: gnanam1990 Date: Fri, 17 Apr 2026 05:41:19 -0700 Subject: [PATCH] fix(discord): DISCORD_ALLOW_BOTS=mentions/all now works without DISCORD_ALLOWED_USERS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #4466. Root cause: two sequential authorization gates both independently rejected bot messages, making DISCORD_ALLOW_BOTS completely ineffective. Gate 1 — `discord.py` `on_message`: _is_allowed_user ran BEFORE the bot filter, so bot senders were dropped before the DISCORD_ALLOW_BOTS policy was ever evaluated. Gate 2 — `gateway/run.py` _is_user_authorized: The gateway-level allowlist check rejected bot IDs with 'Unauthorized user: ' even if they passed Gate 1. Fix: gateway/platforms/discord.py — reorder on_message so DISCORD_ALLOW_BOTS runs BEFORE _is_allowed_user. Bots permitted by the filter skip the user allowlist; non-bots are still checked. gateway/session.py — add is_bot: bool = False to SessionSource so the gateway layer can distinguish bot senders. gateway/platforms/base.py — expose is_bot parameter in build_source. gateway/platforms/discord.py _handle_message — set is_bot=True when building the SessionSource for bot authors. gateway/run.py _is_user_authorized — when source.is_bot is True AND DISCORD_ALLOW_BOTS is 'mentions' or 'all', return True early. Platform filter already validated the message at on_message; don't re-reject. Behavior matrix: | Config | Before | After | | DISCORD_ALLOW_BOTS=none (default) | Blocked | Blocked | | DISCORD_ALLOW_BOTS=all | Blocked | Allowed | | DISCORD_ALLOW_BOTS=mentions + @mention | Blocked | Allowed | | DISCORD_ALLOW_BOTS=mentions, no mention | Blocked | Blocked | | Human in DISCORD_ALLOWED_USERS | Allowed | Allowed | | Human NOT in DISCORD_ALLOWED_USERS | Blocked | Blocked | Co-authored-by: Hermes Maintainer --- gateway/platforms/base.py | 2 ++ gateway/platforms/discord.py | 15 ++++++++++----- gateway/run.py | 10 ++++++++++ gateway/session.py | 1 + 4 files changed, 23 insertions(+), 5 deletions(-) diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 82d09f3a82..7c0376776b 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -1991,6 +1991,7 @@ class BasePlatformAdapter(ABC): chat_topic: Optional[str] = None, user_id_alt: Optional[str] = None, chat_id_alt: Optional[str] = None, + is_bot: bool = False, ) -> SessionSource: """Helper to build a SessionSource for this platform.""" # Normalize empty topic to None @@ -2007,6 +2008,7 @@ class BasePlatformAdapter(ABC): chat_topic=chat_topic.strip() if chat_topic else None, user_id_alt=user_id_alt, chat_id_alt=chat_id_alt, + is_bot=is_bot, ) @abstractmethod diff --git a/gateway/platforms/discord.py b/gateway/platforms/discord.py index f3f13b6f40..bed3fa9c37 100644 --- a/gateway/platforms/discord.py +++ b/gateway/platforms/discord.py @@ -636,14 +636,13 @@ class DiscordAdapter(BasePlatformAdapter): if message.type not in (discord.MessageType.default, discord.MessageType.reply): return - # Check if the message author is in the allowed user list - if not self._is_allowed_user(str(message.author.id)): - return - # Bot message filtering (DISCORD_ALLOW_BOTS): # "none" — ignore all other bots (default) # "mentions" — accept bot messages only when they @mention us # "all" — accept all bot messages + # Must run BEFORE the user allowlist check so that bots + # permitted by DISCORD_ALLOW_BOTS are not rejected for + # not being in DISCORD_ALLOWED_USERS (fixes #4466). if getattr(message.author, "bot", False): allow_bots = os.getenv("DISCORD_ALLOW_BOTS", "none").lower().strip() if allow_bots == "none": @@ -651,7 +650,12 @@ class DiscordAdapter(BasePlatformAdapter): elif allow_bots == "mentions": if not self._client.user or self._client.user not in message.mentions: return - # "all" falls through to handle_message + # "all" falls through; bot is permitted — skip the + # human-user allowlist below (bots aren't in it). + else: + # Non-bot: enforce the configured user allowlist. + if not self._is_allowed_user(str(message.author.id)): + return # Multi-agent filtering: if the message mentions specific bots # but NOT this bot, the sender is talking to another agent — @@ -2787,6 +2791,7 @@ class DiscordAdapter(BasePlatformAdapter): user_name=message.author.display_name, thread_id=thread_id, chat_topic=chat_topic, + is_bot=getattr(message.author, "bot", False), ) # Build media URLs -- download image attachments to local cache so the diff --git a/gateway/run.py b/gateway/run.py index eea7bf27f9..261fecbf85 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -2645,6 +2645,16 @@ class GatewayRunner: if platform_allow_all_var and os.getenv(platform_allow_all_var, "").lower() in ("true", "1", "yes"): return True + # Discord bot senders that passed the DISCORD_ALLOW_BOTS platform + # filter are already authorized at the platform level — skip the + # user allowlist. Without this, bot messages allowed by + # DISCORD_ALLOW_BOTS=mentions/all would be rejected here with + # "Unauthorized user" (fixes #4466). + if source.platform == Platform.DISCORD and getattr(source, "is_bot", False): + allow_bots = os.getenv("DISCORD_ALLOW_BOTS", "none").lower().strip() + if allow_bots in ("mentions", "all"): + return True + # Check pairing store (always checked, regardless of allowlists) platform_name = source.platform.value if source.platform else "" if self.pairing_store.is_approved(platform_name, user_id): diff --git a/gateway/session.py b/gateway/session.py index c14e9bd030..f057d1cfc0 100644 --- a/gateway/session.py +++ b/gateway/session.py @@ -82,6 +82,7 @@ class SessionSource: chat_topic: Optional[str] = None # Channel topic/description (Discord, Slack) user_id_alt: Optional[str] = None # Signal UUID (alternative to phone number) chat_id_alt: Optional[str] = None # Signal group internal ID + is_bot: bool = False # True when the message author is a bot/webhook (Discord) @property def description(self) -> str: