From c682634188dbdd517af870c1d28315a0928fe4df Mon Sep 17 00:00:00 2001 From: Xinhua Gu Date: Sun, 15 Feb 2026 20:05:46 +0100 Subject: [PATCH] fix(discord): role-based allowlist never matches (Carbon Role objects stringify to mentions) (#16369) * fix(discord): role-based allowlist never matches because Carbon Role objects stringify to mentions Carbon's GuildMember.roles getter returns Role[] objects, not raw ID strings. String(Role) produces '<@&123456>' which never matches the plain role IDs in the guild allowlist config. Use data.rawMember.roles (raw Discord API string array) instead of data.member.roles (Carbon Role[] objects) for role ID extraction. Fixes #16207 * Docs: add discord role allowlist changelog entry --------- Co-authored-by: Shadow --- CHANGELOG.md | 1 + src/discord/monitor/listeners.ts | 4 ++-- src/discord/monitor/message-handler.preflight.ts | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bd1fa37af9..e32292f08e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai - Telegram: finalize streaming preview replies in place instead of sending a second final message, preventing duplicate Telegram assistant outputs at stream completion. (#17218) Thanks @obviyus. - Cron: infer `payload.kind="agentTurn"` for model-only `cron.update` payload patches, so partial agent-turn updates do not fail validation when `kind` is omitted. (#15664) Thanks @rodrigouroz. - Subagents: use child-run-based deterministic announce idempotency keys across direct and queued delivery paths (with legacy queued-item fallback) to prevent duplicate announce retries without collapsing distinct same-millisecond announces. (#17150) Thanks @widingmarcus-cyber. +- Discord: ensure role allowlist matching uses raw role IDs for message routing authorization. Thanks @xinhuagu. ## 2026.2.14 diff --git a/src/discord/monitor/listeners.ts b/src/discord/monitor/listeners.ts index 5b21543fb1..fadca3c82e 100644 --- a/src/discord/monitor/listeners.ts +++ b/src/discord/monitor/listeners.ts @@ -272,8 +272,8 @@ async function handleDiscordReactionEvent(params: { const authorLabel = message?.author ? formatDiscordUserTag(message.author) : undefined; const baseText = `Discord reaction ${action}: ${emojiLabel} by ${actorLabel} on ${guildSlug} ${channelLabel} msg ${data.message_id}`; const text = authorLabel ? `${baseText} from ${authorLabel}` : baseText; - const memberRoleIds = Array.isArray(data.member?.roles) - ? data.member.roles.map((roleId: string) => String(roleId)) + const memberRoleIds = Array.isArray(data.rawMember?.roles) + ? data.rawMember.roles.map((roleId: string) => String(roleId)) : []; const route = resolveAgentRoute({ cfg: params.cfg, diff --git a/src/discord/monitor/message-handler.preflight.ts b/src/discord/monitor/message-handler.preflight.ts index 6dba2ccecf..946eceb6a2 100644 --- a/src/discord/monitor/message-handler.preflight.ts +++ b/src/discord/monitor/message-handler.preflight.ts @@ -224,8 +224,8 @@ export async function preflightDiscordMessage( } // Fresh config for bindings lookup; other routing inputs are payload-derived. - const memberRoleIds = Array.isArray(params.data.member?.roles) - ? params.data.member.roles.map((roleId: string) => String(roleId)) + const memberRoleIds = Array.isArray(params.data.rawMember?.roles) + ? params.data.rawMember.roles.map((roleId: string) => String(roleId)) : []; const route = resolveAgentRoute({ cfg: loadConfig(),