diff --git a/gateway/authz_mixin.py b/gateway/authz_mixin.py index b98118eb5d6..ad3aa854982 100644 --- a/gateway/authz_mixin.py +++ b/gateway/authz_mixin.py @@ -207,6 +207,11 @@ class GatewayAuthorizationMixin: if platform_allow_all_var and os.getenv(platform_allow_all_var, "").lower() in {"true", "1", "yes"}: return True + # Adapter-verified role auth: the Discord adapter already confirmed the + # user holds a role in DISCORD_ALLOWED_ROLES before dispatching the message. + if getattr(source, "role_authorized", False): + return True + if getattr(source, "is_bot", False): allow_bots_var = platform_allow_bots_map.get(source.platform) if allow_bots_var and os.getenv(allow_bots_var, "none").lower().strip() in {"mentions", "all"}: diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 50ca91df263..5e00c9f1ddd 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -4651,6 +4651,7 @@ class BasePlatformAdapter(ABC): guild_id: Optional[str] = None, parent_chat_id: Optional[str] = None, message_id: Optional[str] = None, + role_authorized: bool = False, ) -> SessionSource: """Helper to build a SessionSource for this platform.""" # Normalize empty topic to None @@ -4671,6 +4672,7 @@ class BasePlatformAdapter(ABC): guild_id=str(guild_id) if guild_id else None, parent_chat_id=str(parent_chat_id) if parent_chat_id else None, message_id=str(message_id) if message_id else None, + role_authorized=role_authorized, ) @abstractmethod diff --git a/gateway/session.py b/gateway/session.py index 4d1d26b6467..19aa0cdb776 100644 --- a/gateway/session.py +++ b/gateway/session.py @@ -91,6 +91,7 @@ class SessionSource: guild_id: Optional[str] = None # Discord guild / Slack workspace / Matrix server scope parent_chat_id: Optional[str] = None # Parent channel when chat_id refers to a thread message_id: Optional[str] = None # ID of the triggering message (for pin/reply/react) + role_authorized: bool = False # True when adapter granted access via role (not user ID) @property def description(self) -> str: diff --git a/plugins/platforms/discord/adapter.py b/plugins/platforms/discord/adapter.py index c0dd7221718..d9db208fc4f 100644 --- a/plugins/platforms/discord/adapter.py +++ b/plugins/platforms/discord/adapter.py @@ -794,6 +794,7 @@ class DiscordAdapter(BasePlatformAdapter): # 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). + _role_authorized = False if getattr(message.author, "bot", False): allow_bots = os.getenv("DISCORD_ALLOW_BOTS", "none").lower().strip() if allow_bots == "none": @@ -817,6 +818,7 @@ class DiscordAdapter(BasePlatformAdapter): is_dm=_is_dm, ): return + _role_authorized = bool(getattr(self, "_allowed_role_ids", set())) # Multi-agent filtering: if the message mentions specific bots # but NOT this bot, the sender is talking to another agent — @@ -858,7 +860,7 @@ class DiscordAdapter(BasePlatformAdapter): if "*" not in _free_channels and not (_channel_ids & _free_channels): return - await self._handle_message(message) + await self._handle_message(message, role_authorized=_role_authorized) @self._client.event async def on_voice_state_update(member, before, after): @@ -4726,7 +4728,7 @@ class DiscordAdapter(BasePlatformAdapter): raise Exception(f"HTTP {resp.status}") return await resp.read() - async def _handle_message(self, message: DiscordMessage) -> None: + async def _handle_message(self, message: DiscordMessage, role_authorized: bool = False) -> None: """Handle incoming Discord messages.""" # In server channels (not DMs), require the bot to be @mentioned # UNLESS the channel is in the free-response list or the message is @@ -4910,6 +4912,7 @@ class DiscordAdapter(BasePlatformAdapter): guild_id=str(guild.id) if guild else None, parent_chat_id=parent_channel_id, message_id=str(message.id), + role_authorized=role_authorized, ) # Build media URLs -- download image attachments to local cache so the