fix: harden discord guild allowlist resolution

This commit is contained in:
Peter Steinberger 2026-03-14 02:08:57 +00:00
parent 5c73ed62d5
commit 10afde99c1
10 changed files with 127 additions and 9 deletions

View File

@ -58,6 +58,7 @@ Docs: https://docs.openclaw.ai
- Dashboard/chat UI: render oversized plain-text replies as normal paragraphs instead of capped gray code blocks, so long desktop chat responses stay readable without tab-switching refreshes.
- Gateway/Control UI: restore the operator-only device-auth bypass and classify browser connect failures so origin and device-identity problems no longer show up as auth errors in the Control UI and web chat. (#45512) thanks @sallyom.
- macOS/voice wake: stop crashing wake-word command extraction when speech segment ranges come from a different transcript instance.
- Discord/allowlists: honor raw `guild_id` when hydrated guild objects are missing so allowlisted channels and threads like `#maintainers` no longer get false-dropped before channel allowlist checks.
## 2026.3.12

View File

@ -247,6 +247,18 @@ describe("discord guild/channel resolution", () => {
expect(resolved?.slug).toBe("friends-of-openclaw");
});
it("resolves guild entry by raw guild id when guild object is missing", () => {
const guildEntries = makeEntries({
"123": { slug: "friends-of-openclaw" },
});
const resolved = resolveDiscordGuildEntry({
guildId: "123",
guildEntries,
});
expect(resolved?.id).toBe("123");
expect(resolved?.slug).toBe("friends-of-openclaw");
});
it("resolves guild entry by slug key", () => {
const guildEntries = makeEntries({
"friends-of-openclaw": { slug: "friends-of-openclaw" },

View File

@ -360,6 +360,7 @@ async function ensureAgentComponentInteractionAllowed(params: {
}): Promise<{ parentId: string | undefined } | null> {
const guildInfo = resolveDiscordGuildEntry({
guild: params.interaction.guild ?? undefined,
guildId: params.rawGuildId,
guildEntries: params.ctx.guildEntries,
});
const channelCtx = resolveDiscordChannelContext(params.interaction);
@ -1094,6 +1095,7 @@ async function handleDiscordComponentEvent(params: {
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
const guildInfo = resolveDiscordGuildEntry({
guild: params.interaction.guild ?? undefined,
guildId: rawGuildId,
guildEntries: params.ctx.guildEntries,
});
const channelCtx = resolveDiscordChannelContext(params.interaction);
@ -1246,6 +1248,7 @@ async function handleDiscordModalTrigger(params: {
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
const guildInfo = resolveDiscordGuildEntry({
guild: params.interaction.guild ?? undefined,
guildId: rawGuildId,
guildEntries: params.ctx.guildEntries,
});
const channelCtx = resolveDiscordChannelContext(params.interaction);
@ -1696,6 +1699,7 @@ class DiscordComponentModal extends Modal {
const { channelId, user, replyOpts, rawGuildId, memberRoleIds } = interactionCtx;
const guildInfo = resolveDiscordGuildEntry({
guild: interaction.guild ?? undefined,
guildId: rawGuildId,
guildEntries: this.ctx.guildEntries,
});
const channelCtx = resolveDiscordChannelContext(interaction);

View File

@ -321,25 +321,30 @@ export function resolveDiscordCommandAuthorized(params: {
export function resolveDiscordGuildEntry(params: {
guild?: Guild<true> | Guild | null;
guildId?: string | null;
guildEntries?: Record<string, DiscordGuildEntryResolved>;
}): DiscordGuildEntryResolved | null {
const guild = params.guild;
const entries = params.guildEntries;
if (!guild || !entries) {
const guildId = params.guildId?.trim() || guild?.id;
if (!entries) {
return null;
}
const byId = entries[guild.id];
const byId = guildId ? entries[guildId] : undefined;
if (byId) {
return { ...byId, id: guild.id };
return { ...byId, id: guildId };
}
if (!guild) {
return null;
}
const slug = normalizeDiscordSlug(guild.name ?? "");
const bySlug = entries[slug];
if (bySlug) {
return { ...bySlug, id: guild.id, slug: slug || bySlug.slug };
return { ...bySlug, id: guildId ?? guild.id, slug: slug || bySlug.slug };
}
const wildcard = entries["*"];
if (wildcard) {
return { ...wildcard, id: guild.id, slug: slug || wildcard.slug };
return { ...wildcard, id: guildId ?? guild.id, slug: slug || wildcard.slug };
}
return null;
}

View File

@ -430,6 +430,7 @@ async function handleDiscordReactionEvent(
const guildInfo = isGuildMessage
? resolveDiscordGuildEntry({
guild: data.guild ?? undefined,
guildId: data.guild_id ?? undefined,
guildEntries,
})
: null;

View File

@ -34,14 +34,19 @@ export function createGuildEvent(params: {
guildId: string;
author: import("@buape/carbon").Message["author"];
message: import("@buape/carbon").Message;
includeGuildObject?: boolean;
}): DiscordMessageEvent {
return {
channel_id: params.channelId,
guild_id: params.guildId,
guild: {
id: params.guildId,
name: "Guild One",
},
...(params.includeGuildObject === false
? {}
: {
guild: {
id: params.guildId,
name: "Guild One",
},
}),
author: params.author,
message: params.message,
} as unknown as DiscordMessageEvent;

View File

@ -138,6 +138,7 @@ async function runGuildPreflight(params: {
discordConfig: DiscordConfig;
cfg?: import("../../config/config.js").OpenClawConfig;
guildEntries?: Parameters<typeof preflightDiscordMessage>[0]["guildEntries"];
includeGuildObject?: boolean;
}) {
return preflightDiscordMessage({
...createPreflightArgs({
@ -148,6 +149,7 @@ async function runGuildPreflight(params: {
guildId: params.guildId,
author: params.message.author,
message: params.message,
includeGuildObject: params.includeGuildObject,
}),
client: createGuildTextClient(params.channelId),
}),
@ -374,6 +376,91 @@ describe("preflightDiscordMessage", () => {
expect(result).not.toBeNull();
});
it("accepts allowlisted guild messages when guild object is missing", async () => {
const message = createDiscordMessage({
id: "m-guild-id-only",
channelId: "ch-1",
content: "hello from maintainers",
author: {
id: "user-1",
bot: false,
username: "Peter",
},
});
const result = await runGuildPreflight({
channelId: "ch-1",
guildId: "guild-1",
message,
discordConfig: {} as DiscordConfig,
guildEntries: {
"guild-1": {
channels: {
"ch-1": {
allow: true,
requireMention: false,
},
},
},
},
includeGuildObject: false,
});
expect(result).not.toBeNull();
expect(result?.guildInfo?.id).toBe("guild-1");
expect(result?.channelConfig?.allowed).toBe(true);
expect(result?.shouldRequireMention).toBe(false);
});
it("inherits parent thread allowlist when guild object is missing", async () => {
const threadId = "thread-1";
const parentId = "parent-1";
const message = createDiscordMessage({
id: "m-thread-id-only",
channelId: threadId,
content: "thread hello",
author: {
id: "user-1",
bot: false,
username: "Peter",
},
});
const result = await preflightDiscordMessage({
...createPreflightArgs({
cfg: DEFAULT_PREFLIGHT_CFG,
discordConfig: {} as DiscordConfig,
data: createGuildEvent({
channelId: threadId,
guildId: "guild-1",
author: message.author,
message,
includeGuildObject: false,
}),
client: createThreadClient({
threadId,
parentId,
}),
}),
guildEntries: {
"guild-1": {
channels: {
[parentId]: {
allow: true,
requireMention: false,
},
},
},
},
});
expect(result).not.toBeNull();
expect(result?.guildInfo?.id).toBe("guild-1");
expect(result?.threadParentId).toBe(parentId);
expect(result?.channelConfig?.allowed).toBe(true);
expect(result?.shouldRequireMention).toBe(false);
});
it("drops guild messages that mention another user when ignoreOtherMentions=true", async () => {
const channelId = "channel-other-mention-1";
const guildId = "guild-other-mention-1";

View File

@ -430,6 +430,7 @@ export async function preflightDiscordMessage(
const guildInfo = isGuildMessage
? resolveDiscordGuildEntry({
guild: params.data.guild ?? undefined,
guildId: params.data.guild_id ?? undefined,
guildEntries: params.guildEntries,
})
: null;

View File

@ -1355,6 +1355,7 @@ async function dispatchDiscordCommandInteraction(params: {
});
const guildInfo = resolveDiscordGuildEntry({
guild: interaction.guild ?? undefined,
guildId: interaction.guild?.id ?? undefined,
guildEntries: discordConfig?.guilds,
});
let threadParentId: string | undefined;

View File

@ -107,6 +107,7 @@ async function authorizeVoiceCommand(
const guildInfo = resolveDiscordGuildEntry({
guild: interaction.guild ?? undefined,
guildId: interaction.guild?.id ?? interaction.rawData.guild_id ?? undefined,
guildEntries: params.discordConfig.guilds,
});