refactor(discord): extract inbound context helpers
This commit is contained in:
@@ -63,9 +63,12 @@ import {
|
||||
resolveDiscordGuildEntry,
|
||||
resolveDiscordMemberAccessState,
|
||||
resolveDiscordOwnerAccess,
|
||||
resolveDiscordOwnerAllowFrom,
|
||||
} from "./allow-list.js";
|
||||
import { formatDiscordUserTag } from "./format.js";
|
||||
import {
|
||||
buildDiscordInboundAccessContext,
|
||||
buildDiscordGroupSystemPrompt,
|
||||
} from "./inbound-context.js";
|
||||
import { buildDirectLabel, buildGuildLabel } from "./reply-context.js";
|
||||
import { deliverDiscordReply } from "./reply-delivery.js";
|
||||
import { sendTyping } from "./typing.js";
|
||||
@@ -865,13 +868,14 @@ async function dispatchDiscordComponentEvent(params: {
|
||||
scope: channelCtx.isThread ? "thread" : "channel",
|
||||
});
|
||||
const allowNameMatching = isDangerousNameMatchingEnabled(ctx.discordConfig);
|
||||
const groupSystemPrompt = channelConfig?.systemPrompt?.trim() || undefined;
|
||||
const ownerAllowFrom = resolveDiscordOwnerAllowFrom({
|
||||
const { ownerAllowFrom } = buildDiscordInboundAccessContext({
|
||||
channelConfig,
|
||||
guildInfo,
|
||||
sender: { id: interactionCtx.user.id, name: interactionCtx.user.username, tag: senderTag },
|
||||
allowNameMatching,
|
||||
isGuild: !interactionCtx.isDirectMessage,
|
||||
});
|
||||
const groupSystemPrompt = buildDiscordGroupSystemPrompt(channelConfig);
|
||||
const pinnedMainDmOwner = interactionCtx.isDirectMessage
|
||||
? resolvePinnedMainDmOwnerFromAllowlist({
|
||||
dmScope: ctx.cfg.session?.dmScope,
|
||||
|
||||
55
src/discord/monitor/inbound-context.test.ts
Normal file
55
src/discord/monitor/inbound-context.test.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildDiscordGroupSystemPrompt,
|
||||
buildDiscordInboundAccessContext,
|
||||
buildDiscordUntrustedContext,
|
||||
} from "./inbound-context.js";
|
||||
|
||||
describe("Discord inbound context helpers", () => {
|
||||
it("builds guild access context from channel config and topic", () => {
|
||||
expect(
|
||||
buildDiscordInboundAccessContext({
|
||||
channelConfig: {
|
||||
allowed: true,
|
||||
users: ["discord:user-1"],
|
||||
systemPrompt: "Use the runbook.",
|
||||
},
|
||||
guildInfo: { id: "guild-1" },
|
||||
sender: {
|
||||
id: "user-1",
|
||||
name: "tester",
|
||||
tag: "tester#0001",
|
||||
},
|
||||
isGuild: true,
|
||||
channelTopic: "Production alerts only",
|
||||
}),
|
||||
).toEqual({
|
||||
groupSystemPrompt: "Use the runbook.",
|
||||
untrustedContext: [expect.stringContaining("Production alerts only")],
|
||||
ownerAllowFrom: ["user-1"],
|
||||
});
|
||||
});
|
||||
|
||||
it("omits guild-only metadata for direct messages", () => {
|
||||
expect(
|
||||
buildDiscordInboundAccessContext({
|
||||
sender: {
|
||||
id: "user-1",
|
||||
},
|
||||
isGuild: false,
|
||||
channelTopic: "ignored",
|
||||
}),
|
||||
).toEqual({
|
||||
groupSystemPrompt: undefined,
|
||||
untrustedContext: undefined,
|
||||
ownerAllowFrom: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps direct helper behavior consistent", () => {
|
||||
expect(buildDiscordGroupSystemPrompt({ allowed: true, systemPrompt: " hi " })).toBe("hi");
|
||||
expect(buildDiscordUntrustedContext({ isGuild: true, channelTopic: "topic" })).toEqual([
|
||||
expect.stringContaining("topic"),
|
||||
]);
|
||||
});
|
||||
});
|
||||
59
src/discord/monitor/inbound-context.ts
Normal file
59
src/discord/monitor/inbound-context.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
|
||||
import {
|
||||
resolveDiscordOwnerAllowFrom,
|
||||
type DiscordChannelConfigResolved,
|
||||
type DiscordGuildEntryResolved,
|
||||
} from "./allow-list.js";
|
||||
|
||||
export function buildDiscordGroupSystemPrompt(
|
||||
channelConfig?: DiscordChannelConfigResolved | null,
|
||||
): string | undefined {
|
||||
const systemPromptParts = [channelConfig?.systemPrompt?.trim() || null].filter(
|
||||
(entry): entry is string => Boolean(entry),
|
||||
);
|
||||
return systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
|
||||
}
|
||||
|
||||
export function buildDiscordUntrustedContext(params: {
|
||||
isGuild: boolean;
|
||||
channelTopic?: string;
|
||||
}): string[] | undefined {
|
||||
if (!params.isGuild) {
|
||||
return undefined;
|
||||
}
|
||||
const untrustedChannelMetadata = buildUntrustedChannelMetadata({
|
||||
source: "discord",
|
||||
label: "Discord channel topic",
|
||||
entries: [params.channelTopic],
|
||||
});
|
||||
return untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined;
|
||||
}
|
||||
|
||||
export function buildDiscordInboundAccessContext(params: {
|
||||
channelConfig?: DiscordChannelConfigResolved | null;
|
||||
guildInfo?: DiscordGuildEntryResolved | null;
|
||||
sender: {
|
||||
id: string;
|
||||
name?: string;
|
||||
tag?: string;
|
||||
};
|
||||
allowNameMatching?: boolean;
|
||||
isGuild: boolean;
|
||||
channelTopic?: string;
|
||||
}) {
|
||||
return {
|
||||
groupSystemPrompt: params.isGuild
|
||||
? buildDiscordGroupSystemPrompt(params.channelConfig)
|
||||
: undefined,
|
||||
untrustedContext: buildDiscordUntrustedContext({
|
||||
isGuild: params.isGuild,
|
||||
channelTopic: params.channelTopic,
|
||||
}),
|
||||
ownerAllowFrom: resolveDiscordOwnerAllowFrom({
|
||||
channelConfig: params.channelConfig,
|
||||
guildInfo: params.guildInfo,
|
||||
sender: params.sender,
|
||||
allowNameMatching: params.allowNameMatching,
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -30,7 +30,6 @@ import { convertMarkdownTables } from "../../markdown/tables.js";
|
||||
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
|
||||
import { buildAgentSessionKey } from "../../routing/resolve-route.js";
|
||||
import { resolveThreadSessionKeys } from "../../routing/session-key.js";
|
||||
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
|
||||
import { stripReasoningTagsFromText } from "../../shared/text/reasoning-tags.js";
|
||||
import { truncateUtf16Safe } from "../../utils.js";
|
||||
import { chunkDiscordTextWithMode } from "../chunk.js";
|
||||
@@ -38,8 +37,9 @@ import { resolveDiscordDraftStreamingChunking } from "../draft-chunking.js";
|
||||
import { createDiscordDraftStream } from "../draft-stream.js";
|
||||
import { reactMessageDiscord, removeReactionDiscord } from "../send.js";
|
||||
import { editMessageDiscord } from "../send.messages.js";
|
||||
import { normalizeDiscordSlug, resolveDiscordOwnerAllowFrom } from "./allow-list.js";
|
||||
import { normalizeDiscordSlug } from "./allow-list.js";
|
||||
import { resolveTimestampMs } from "./format.js";
|
||||
import { buildDiscordInboundAccessContext } from "./inbound-context.js";
|
||||
import type { DiscordMessagePreflightContext } from "./message-handler.preflight.js";
|
||||
import {
|
||||
buildDiscordMediaPayload,
|
||||
@@ -212,13 +212,6 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
const forumContextLine = isForumStarter ? `[Forum parent: #${forumParentSlug}]` : null;
|
||||
const groupChannel = isGuildMessage && displayChannelSlug ? `#${displayChannelSlug}` : undefined;
|
||||
const groupSubject = isDirectMessage ? undefined : groupChannel;
|
||||
const untrustedChannelMetadata = isGuildMessage
|
||||
? buildUntrustedChannelMetadata({
|
||||
source: "discord",
|
||||
label: "Discord channel topic",
|
||||
entries: [channelInfo?.topic],
|
||||
})
|
||||
: undefined;
|
||||
const senderName = sender.isPluralKit
|
||||
? (sender.name ?? author.username)
|
||||
: (data.member?.nickname ?? author.globalName ?? author.username);
|
||||
@@ -226,16 +219,13 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
? (sender.tag ?? sender.name ?? author.username)
|
||||
: author.username;
|
||||
const senderTag = sender.tag;
|
||||
const systemPromptParts = [channelConfig?.systemPrompt?.trim() || null].filter(
|
||||
(entry): entry is string => Boolean(entry),
|
||||
);
|
||||
const groupSystemPrompt =
|
||||
systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
|
||||
const ownerAllowFrom = resolveDiscordOwnerAllowFrom({
|
||||
const { groupSystemPrompt, ownerAllowFrom, untrustedContext } = buildDiscordInboundAccessContext({
|
||||
channelConfig,
|
||||
guildInfo,
|
||||
sender: { id: sender.id, name: sender.name, tag: sender.tag },
|
||||
allowNameMatching: isDangerousNameMatchingEnabled(discordConfig),
|
||||
isGuild: isGuildMessage,
|
||||
channelTopic: channelInfo?.topic,
|
||||
});
|
||||
const storePath = resolveStorePath(cfg.session?.store, {
|
||||
agentId: route.agentId,
|
||||
@@ -374,7 +364,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
|
||||
SenderTag: senderTag,
|
||||
GroupSubject: groupSubject,
|
||||
GroupChannel: groupChannel,
|
||||
UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined,
|
||||
UntrustedContext: untrustedContext,
|
||||
GroupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined,
|
||||
GroupSpace: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined,
|
||||
OwnerAllowFrom: ownerAllowFrom,
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import type { CommandArgs } from "../../auto-reply/commands-registry.js";
|
||||
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
|
||||
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
|
||||
import {
|
||||
resolveDiscordOwnerAllowFrom,
|
||||
type DiscordChannelConfigResolved,
|
||||
type DiscordGuildEntryResolved,
|
||||
} from "./allow-list.js";
|
||||
import { type DiscordChannelConfigResolved, type DiscordGuildEntryResolved } from "./allow-list.js";
|
||||
import { buildDiscordInboundAccessContext } from "./inbound-context.js";
|
||||
|
||||
export type BuildDiscordNativeCommandContextParams = {
|
||||
prompt: string;
|
||||
@@ -39,39 +35,17 @@ export type BuildDiscordNativeCommandContextParams = {
|
||||
timestampMs?: number;
|
||||
};
|
||||
|
||||
function buildDiscordNativeCommandSystemPrompt(
|
||||
channelConfig?: DiscordChannelConfigResolved | null,
|
||||
): string | undefined {
|
||||
const systemPromptParts = [channelConfig?.systemPrompt?.trim() || null].filter(
|
||||
(entry): entry is string => Boolean(entry),
|
||||
);
|
||||
return systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
|
||||
}
|
||||
|
||||
function buildDiscordNativeCommandUntrustedContext(params: {
|
||||
isGuild: boolean;
|
||||
channelTopic?: string;
|
||||
}): string[] | undefined {
|
||||
if (!params.isGuild) {
|
||||
return undefined;
|
||||
}
|
||||
const untrustedChannelMetadata = buildUntrustedChannelMetadata({
|
||||
source: "discord",
|
||||
label: "Discord channel topic",
|
||||
entries: [params.channelTopic],
|
||||
});
|
||||
return untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined;
|
||||
}
|
||||
|
||||
export function buildDiscordNativeCommandContext(params: BuildDiscordNativeCommandContextParams) {
|
||||
const conversationLabel = params.isDirectMessage
|
||||
? (params.user.globalName ?? params.user.username)
|
||||
: params.channelId;
|
||||
const ownerAllowFrom = resolveDiscordOwnerAllowFrom({
|
||||
const { groupSystemPrompt, ownerAllowFrom, untrustedContext } = buildDiscordInboundAccessContext({
|
||||
channelConfig: params.channelConfig,
|
||||
guildInfo: params.guildInfo,
|
||||
sender: params.sender,
|
||||
allowNameMatching: params.allowNameMatching,
|
||||
isGuild: params.isGuild,
|
||||
channelTopic: params.channelTopic,
|
||||
});
|
||||
|
||||
return finalizeInboundContext({
|
||||
@@ -92,13 +66,8 @@ export function buildDiscordNativeCommandContext(params: BuildDiscordNativeComma
|
||||
ChatType: params.isDirectMessage ? "direct" : params.isGroupDm ? "group" : "channel",
|
||||
ConversationLabel: conversationLabel,
|
||||
GroupSubject: params.isGuild ? params.guildName : undefined,
|
||||
GroupSystemPrompt: params.isGuild
|
||||
? buildDiscordNativeCommandSystemPrompt(params.channelConfig)
|
||||
: undefined,
|
||||
UntrustedContext: buildDiscordNativeCommandUntrustedContext({
|
||||
isGuild: params.isGuild,
|
||||
channelTopic: params.channelTopic,
|
||||
}),
|
||||
GroupSystemPrompt: groupSystemPrompt,
|
||||
UntrustedContext: untrustedContext,
|
||||
OwnerAllowFrom: ownerAllowFrom,
|
||||
SenderName: params.user.globalName ?? params.user.username,
|
||||
SenderId: params.user.id,
|
||||
|
||||
Reference in New Issue
Block a user