Discord: inherit thread allowlists

This commit is contained in:
Shadow
2026-01-17 15:26:59 -06:00
committed by Peter Steinberger
parent 852aa16ca0
commit 277e43e32c
4 changed files with 103 additions and 43 deletions

View File

@@ -20,6 +20,7 @@ Docs: https://docs.clawd.bot
- Memory: split overly long lines to keep embeddings under token limits.
- Memory: skip empty chunks to avoid invalid embedding inputs.
- Sessions: fall back to session labels when listing display names. (#1124) — thanks @abdaraxus.
- Discord: inherit parent channel allowlists for thread slash commands and reactions. (#1123) — thanks @thewilloftheshadow.
## 2026.1.17-1

View File

@@ -137,6 +137,34 @@ export function resolveDiscordGuildEntry(params: {
return null;
}
type DiscordChannelEntry = NonNullable<DiscordGuildEntryResolved["channels"]>[string];
function resolveDiscordChannelEntry(
channels: NonNullable<DiscordGuildEntryResolved["channels"]>,
channelId: string,
channelName?: string,
channelSlug?: string,
): DiscordChannelEntry | null {
if (channelId && channels[channelId]) return channels[channelId];
if (channelSlug && channels[channelSlug]) return channels[channelSlug];
if (channelName && channels[channelName]) return channels[channelName];
return null;
}
function resolveDiscordChannelConfigEntry(
entry: DiscordChannelEntry,
): DiscordChannelConfigResolved {
return {
allowed: entry.allow !== false,
requireMention: entry.requireMention,
skills: entry.skills,
enabled: entry.enabled,
users: entry.users,
systemPrompt: entry.systemPrompt,
autoThread: entry.autoThread,
};
}
export function resolveDiscordChannelConfig(params: {
guildInfo?: DiscordGuildEntryResolved | null;
channelId: string;
@@ -146,40 +174,35 @@ export function resolveDiscordChannelConfig(params: {
const { guildInfo, channelId, channelName, channelSlug } = params;
const channels = guildInfo?.channels;
if (!channels) return null;
const byId = channels[channelId];
if (byId)
return {
allowed: byId.allow !== false,
requireMention: byId.requireMention,
skills: byId.skills,
enabled: byId.enabled,
users: byId.users,
systemPrompt: byId.systemPrompt,
autoThread: byId.autoThread,
};
if (channelSlug && channels[channelSlug]) {
const entry = channels[channelSlug];
return {
allowed: entry.allow !== false,
requireMention: entry.requireMention,
skills: entry.skills,
enabled: entry.enabled,
users: entry.users,
systemPrompt: entry.systemPrompt,
autoThread: entry.autoThread,
};
}
if (channelName && channels[channelName]) {
const entry = channels[channelName];
return {
allowed: entry.allow !== false,
requireMention: entry.requireMention,
skills: entry.skills,
enabled: entry.enabled,
users: entry.users,
systemPrompt: entry.systemPrompt,
autoThread: entry.autoThread,
};
const entry = resolveDiscordChannelEntry(channels, channelId, channelName, channelSlug);
if (!entry) return { allowed: false };
return resolveDiscordChannelConfigEntry(entry);
}
export function resolveDiscordChannelConfigWithFallback(params: {
guildInfo?: DiscordGuildEntryResolved | null;
channelId: string;
channelName?: string;
channelSlug: string;
parentId?: string;
parentName?: string;
parentSlug?: string;
}): DiscordChannelConfigResolved | null {
const { guildInfo, channelId, channelName, channelSlug, parentId, parentName, parentSlug } =
params;
const channels = guildInfo?.channels;
if (!channels) return null;
const entry = resolveDiscordChannelEntry(channels, channelId, channelName, channelSlug);
if (entry) return resolveDiscordChannelConfigEntry(entry);
if (parentId || parentName || parentSlug) {
const resolvedParentSlug = parentSlug ?? (parentName ? normalizeDiscordSlug(parentName) : "");
const parentEntry = resolveDiscordChannelEntry(
channels,
parentId ?? "",
parentName,
resolvedParentSlug,
);
if (parentEntry) return resolveDiscordChannelConfigEntry(parentEntry);
}
return { allowed: false };
}

View File

@@ -22,7 +22,7 @@ import {
isDiscordGroupAllowedByPolicy,
normalizeDiscordAllowList,
normalizeDiscordSlug,
resolveDiscordChannelConfig,
resolveDiscordChannelConfigWithFallback,
resolveDiscordGuildEntry,
resolveDiscordShouldRequireMention,
resolveDiscordUserAllowed,
@@ -236,13 +236,19 @@ export async function preflightDiscordMessage(
guildInfo?.slug ||
(params.data.guild?.name ? normalizeDiscordSlug(params.data.guild.name) : "");
const threadChannelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
const threadParentSlug = threadParentName ? normalizeDiscordSlug(threadParentName) : "";
const baseSessionKey = route.sessionKey;
const channelConfig = isGuildMessage
? resolveDiscordChannelConfig({
? resolveDiscordChannelConfigWithFallback({
guildInfo,
channelId: threadParentId ?? message.channelId,
channelName: configChannelName,
channelSlug: configChannelSlug,
channelId: message.channelId,
channelName,
channelSlug: threadChannelSlug,
parentId: threadParentId ?? undefined,
parentName: threadParentName ?? undefined,
parentSlug: threadParentSlug,
})
: null;
if (isGuildMessage && channelConfig?.enabled === false) {

View File

@@ -47,11 +47,13 @@ import {
isDiscordGroupAllowedByPolicy,
normalizeDiscordAllowList,
normalizeDiscordSlug,
resolveDiscordChannelConfig,
resolveDiscordChannelConfigWithFallback,
resolveDiscordGuildEntry,
resolveDiscordUserAllowed,
} from "./allow-list.js";
import { formatDiscordUserTag } from "./format.js";
import { resolveDiscordChannelInfo } from "./message-utils.js";
import { resolveDiscordThreadParentInfo } from "./threading.js";
type DiscordConfig = NonNullable<ClawdbotConfig["channels"]>["discord"];
@@ -499,8 +501,13 @@ async function dispatchDiscordCommandInteraction(params: {
const channelType = channel?.type;
const isDirectMessage = channelType === ChannelType.DM;
const isGroupDm = channelType === ChannelType.GroupDM;
const isThreadChannel =
channelType === ChannelType.PublicThread ||
channelType === ChannelType.PrivateThread ||
channelType === ChannelType.AnnouncementThread;
const channelName = channel && "name" in channel ? (channel.name as string) : undefined;
const channelSlug = channelName ? normalizeDiscordSlug(channelName) : "";
const rawChannelId = channel?.id ?? "";
const ownerAllowList = normalizeDiscordAllowList(discordConfig?.dm?.allowFrom ?? [], [
"discord:",
"user:",
@@ -517,12 +524,35 @@ async function dispatchDiscordCommandInteraction(params: {
guild: interaction.guild ?? undefined,
guildEntries: discordConfig?.guilds,
});
let threadParentId: string | undefined;
let threadParentName: string | undefined;
let threadParentSlug = "";
if (interaction.guild && channel && isThreadChannel && rawChannelId) {
// Threads inherit parent channel config unless explicitly overridden.
const channelInfo = await resolveDiscordChannelInfo(interaction.client, rawChannelId);
const parentInfo = await resolveDiscordThreadParentInfo({
client: interaction.client,
threadChannel: {
id: rawChannelId,
name: channelName,
parentId: "parentId" in channel ? channel.parentId ?? undefined : undefined,
parent: undefined,
},
channelInfo,
});
threadParentId = parentInfo.id;
threadParentName = parentInfo.name;
threadParentSlug = threadParentName ? normalizeDiscordSlug(threadParentName) : "";
}
const channelConfig = interaction.guild
? resolveDiscordChannelConfig({
? resolveDiscordChannelConfigWithFallback({
guildInfo,
channelId: channel?.id ?? "",
channelId: rawChannelId,
channelName,
channelSlug,
parentId: threadParentId,
parentName: threadParentName,
parentSlug: threadParentSlug,
})
: null;
if (channelConfig?.enabled === false) {
@@ -664,7 +694,7 @@ async function dispatchDiscordCommandInteraction(params: {
}
const isGuild = Boolean(interaction.guild);
const channelId = channel?.id ?? "unknown";
const channelId = rawChannelId || "unknown";
const interactionId = interaction.rawData.id;
const route = resolveAgentRoute({
cfg,