diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 82d09f3a8..7c0376776 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 f3f13b6f4..bed3fa9c3 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 eea7bf27f..261fecbf8 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 c14e9bd03..f057d1cfc 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: