refactor: unify channel config matching and gating

Co-authored-by: thewilloftheshadow <thewilloftheshadow@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-01-18 01:21:27 +00:00
parent 05f49d2846
commit f73dbdbaea
24 changed files with 430 additions and 120 deletions

View File

@@ -268,6 +268,7 @@ describe("discord mention gating", () => {
scope: "thread",
});
expect(channelConfig?.matchSource).toBe("parent");
expect(channelConfig?.matchKey).toBe("parent-1");
expect(
resolveDiscordShouldRequireMention({
isGuildMessage: true,

View File

@@ -2,7 +2,7 @@ import type { Guild, User } from "@buape/carbon";
import {
buildChannelKeyCandidates,
resolveChannelEntryMatch,
resolveChannelEntryMatchWithFallback,
} from "../../channels/channel-config.js";
import { formatDiscordUserTag } from "./format.js";
@@ -178,40 +178,47 @@ type DiscordChannelLookup = {
slug?: string;
};
type DiscordChannelScope = "channel" | "thread";
type DiscordChannelMatch = {
entry: DiscordChannelEntry;
key: string;
};
function resolveDiscordChannelEntry(
channels: NonNullable<DiscordGuildEntryResolved["channels"]>,
function buildDiscordChannelKeys(
params: DiscordChannelLookup & { allowNameMatch?: boolean },
): DiscordChannelMatch | null {
): string[] {
const allowNameMatch = params.allowNameMatch !== false;
const keys = buildChannelKeyCandidates(
return buildChannelKeyCandidates(
params.id,
allowNameMatch ? params.slug : undefined,
allowNameMatch ? params.name : undefined,
);
const { entry, key } = resolveChannelEntryMatch({ entries: channels, keys });
if (!entry || !key) return null;
return { entry, key };
}
function resolveDiscordChannelEntryMatch(
channels: NonNullable<DiscordGuildEntryResolved["channels"]>,
params: DiscordChannelLookup & { allowNameMatch?: boolean },
parentParams?: DiscordChannelLookup,
) {
const keys = buildDiscordChannelKeys(params);
const parentKeys = parentParams ? buildDiscordChannelKeys(parentParams) : undefined;
return resolveChannelEntryMatchWithFallback({
entries: channels,
keys,
parentKeys,
});
}
function resolveDiscordChannelConfigEntry(
match: DiscordChannelMatch,
entry: DiscordChannelEntry,
matchKey: string | undefined,
matchSource: "direct" | "parent",
): DiscordChannelConfigResolved {
const resolved: DiscordChannelConfigResolved = {
allowed: match.entry.allow !== false,
requireMention: match.entry.requireMention,
skills: match.entry.skills,
enabled: match.entry.enabled,
users: match.entry.users,
systemPrompt: match.entry.systemPrompt,
autoThread: match.entry.autoThread,
allowed: entry.allow !== false,
requireMention: entry.requireMention,
skills: entry.skills,
enabled: entry.enabled,
users: entry.users,
systemPrompt: entry.systemPrompt,
autoThread: entry.autoThread,
};
if (match.key) resolved.matchKey = match.key;
if (matchKey) resolved.matchKey = matchKey;
resolved.matchSource = matchSource;
return resolved;
}
@@ -225,13 +232,13 @@ export function resolveDiscordChannelConfig(params: {
const { guildInfo, channelId, channelName, channelSlug } = params;
const channels = guildInfo?.channels;
if (!channels) return null;
const entry = resolveDiscordChannelEntry(channels, {
const match = resolveDiscordChannelEntryMatch(channels, {
id: channelId,
name: channelName,
slug: channelSlug,
});
if (!entry) return { allowed: false };
return resolveDiscordChannelConfigEntry(entry, "direct");
if (!match.entry || !match.matchKey) return { allowed: false };
return resolveDiscordChannelConfigEntry(match.entry, match.matchKey, "direct");
}
export function resolveDiscordChannelConfigWithFallback(params: {
@@ -256,21 +263,29 @@ export function resolveDiscordChannelConfigWithFallback(params: {
} = params;
const channels = guildInfo?.channels;
if (!channels) return null;
const entry = resolveDiscordChannelEntry(channels, {
id: channelId,
name: channelName,
slug: channelSlug,
allowNameMatch: scope !== "thread",
});
if (entry) return resolveDiscordChannelConfigEntry(entry, "direct");
if (parentId || parentName || parentSlug) {
const resolvedParentSlug = parentSlug ?? (parentName ? normalizeDiscordSlug(parentName) : "");
const parentEntry = resolveDiscordChannelEntry(channels, {
id: parentId ?? "",
name: parentName,
slug: resolvedParentSlug,
});
if (parentEntry) return resolveDiscordChannelConfigEntry(parentEntry, "parent");
const resolvedParentSlug = parentSlug ?? (parentName ? normalizeDiscordSlug(parentName) : "");
const match = resolveDiscordChannelEntryMatch(
channels,
{
id: channelId,
name: channelName,
slug: channelSlug,
allowNameMatch: scope !== "thread",
},
parentId || parentName || parentSlug
? {
id: parentId ?? "",
name: parentName,
slug: resolvedParentSlug,
}
: undefined,
);
if (match.entry && match.matchKey && match.matchSource) {
return resolveDiscordChannelConfigEntry(
match.entry,
match.matchKey,
match.matchSource === "parent" ? "parent" : "direct",
);
}
return { allowed: false };
}

View File

@@ -14,9 +14,9 @@ import {
upsertChannelPairingRequest,
} from "../../pairing/pairing-store.js";
import { resolveAgentRoute } from "../../routing/resolve-route.js";
import { resolveMentionGating } from "../../channels/mention-gating.js";
import { resolveMentionGatingWithBypass } from "../../channels/mention-gating.js";
import { sendMessageDiscord } from "../send.js";
import { resolveCommandAuthorizedFromAuthorizers } from "../../channels/command-gating.js";
import { resolveControlCommandGate } from "../../channels/command-gating.js";
import {
allowListMatches,
isDiscordGroupAllowedByPolicy,
@@ -347,6 +347,7 @@ export async function preflightDiscordMessage(
cfg: params.cfg,
surface: "discord",
});
const hasControlCommandInMessage = hasControlCommand(baseText, params.cfg);
if (!isDirectMessage) {
const ownerAllowList = normalizeDiscordAllowList(params.allowFrom, ["discord:", "user:"]);
@@ -368,36 +369,35 @@ export async function preflightDiscordMessage(
})
: false;
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
commandAuthorized = resolveCommandAuthorizedFromAuthorizers({
const commandGate = resolveControlCommandGate({
useAccessGroups,
authorizers: [
{ configured: ownerAllowList != null, allowed: ownerOk },
{ configured: Array.isArray(channelUsers) && channelUsers.length > 0, allowed: usersOk },
],
modeWhenAccessGroupsOff: "configured",
allowTextCommands,
hasControlCommand: hasControlCommandInMessage,
});
commandAuthorized = commandGate.commandAuthorized;
if (allowTextCommands && hasControlCommand(baseText, params.cfg) && !commandAuthorized) {
if (commandGate.shouldBlock) {
logVerbose(`Blocked discord control command from unauthorized sender ${author.id}`);
return null;
}
}
const shouldBypassMention =
allowTextCommands &&
isGuildMessage &&
shouldRequireMention &&
!wasMentioned &&
!hasAnyMention &&
commandAuthorized &&
hasControlCommand(baseText, params.cfg);
const canDetectMention = Boolean(botId) || mentionRegexes.length > 0;
const mentionGate = resolveMentionGating({
const mentionGate = resolveMentionGatingWithBypass({
isGroup: isGuildMessage,
requireMention: Boolean(shouldRequireMention),
canDetectMention,
wasMentioned,
implicitMention,
shouldBypassMention,
hasAnyMention,
allowTextCommands,
hasControlCommand: hasControlCommandInMessage,
commandAuthorized,
});
const effectiveWasMentioned = mentionGate.effectiveWasMentioned;
if (isGuildMessage && shouldRequireMention) {
@@ -504,7 +504,7 @@ export async function preflightDiscordMessage(
shouldRequireMention,
hasAnyMention,
allowTextCommands,
shouldBypassMention,
shouldBypassMention: mentionGate.shouldBypassMention,
effectiveWasMentioned,
canDetectMention,
historyEntry,