Skip to content

Commit 547436b

Browse files
committed
refactor(discord): extract inbound context helpers
1 parent 08597e8 commit 547436b

File tree

5 files changed

+134
-57
lines changed

5 files changed

+134
-57
lines changed

src/discord/monitor/agent-components.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,12 @@ import {
6363
resolveDiscordGuildEntry,
6464
resolveDiscordMemberAccessState,
6565
resolveDiscordOwnerAccess,
66-
resolveDiscordOwnerAllowFrom,
6766
} from "./allow-list.js";
6867
import { formatDiscordUserTag } from "./format.js";
68+
import {
69+
buildDiscordInboundAccessContext,
70+
buildDiscordGroupSystemPrompt,
71+
} from "./inbound-context.js";
6972
import { buildDirectLabel, buildGuildLabel } from "./reply-context.js";
7073
import { deliverDiscordReply } from "./reply-delivery.js";
7174
import { sendTyping } from "./typing.js";
@@ -865,13 +868,14 @@ async function dispatchDiscordComponentEvent(params: {
865868
scope: channelCtx.isThread ? "thread" : "channel",
866869
});
867870
const allowNameMatching = isDangerousNameMatchingEnabled(ctx.discordConfig);
868-
const groupSystemPrompt = channelConfig?.systemPrompt?.trim() || undefined;
869-
const ownerAllowFrom = resolveDiscordOwnerAllowFrom({
871+
const { ownerAllowFrom } = buildDiscordInboundAccessContext({
870872
channelConfig,
871873
guildInfo,
872874
sender: { id: interactionCtx.user.id, name: interactionCtx.user.username, tag: senderTag },
873875
allowNameMatching,
876+
isGuild: !interactionCtx.isDirectMessage,
874877
});
878+
const groupSystemPrompt = buildDiscordGroupSystemPrompt(channelConfig);
875879
const pinnedMainDmOwner = interactionCtx.isDirectMessage
876880
? resolvePinnedMainDmOwnerFromAllowlist({
877881
dmScope: ctx.cfg.session?.dmScope,
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
buildDiscordGroupSystemPrompt,
4+
buildDiscordInboundAccessContext,
5+
buildDiscordUntrustedContext,
6+
} from "./inbound-context.js";
7+
8+
describe("Discord inbound context helpers", () => {
9+
it("builds guild access context from channel config and topic", () => {
10+
expect(
11+
buildDiscordInboundAccessContext({
12+
channelConfig: {
13+
allowed: true,
14+
users: ["discord:user-1"],
15+
systemPrompt: "Use the runbook.",
16+
},
17+
guildInfo: { id: "guild-1" },
18+
sender: {
19+
id: "user-1",
20+
name: "tester",
21+
tag: "tester#0001",
22+
},
23+
isGuild: true,
24+
channelTopic: "Production alerts only",
25+
}),
26+
).toEqual({
27+
groupSystemPrompt: "Use the runbook.",
28+
untrustedContext: [expect.stringContaining("Production alerts only")],
29+
ownerAllowFrom: ["user-1"],
30+
});
31+
});
32+
33+
it("omits guild-only metadata for direct messages", () => {
34+
expect(
35+
buildDiscordInboundAccessContext({
36+
sender: {
37+
id: "user-1",
38+
},
39+
isGuild: false,
40+
channelTopic: "ignored",
41+
}),
42+
).toEqual({
43+
groupSystemPrompt: undefined,
44+
untrustedContext: undefined,
45+
ownerAllowFrom: undefined,
46+
});
47+
});
48+
49+
it("keeps direct helper behavior consistent", () => {
50+
expect(buildDiscordGroupSystemPrompt({ allowed: true, systemPrompt: " hi " })).toBe("hi");
51+
expect(buildDiscordUntrustedContext({ isGuild: true, channelTopic: "topic" })).toEqual([
52+
expect.stringContaining("topic"),
53+
]);
54+
});
55+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
2+
import {
3+
resolveDiscordOwnerAllowFrom,
4+
type DiscordChannelConfigResolved,
5+
type DiscordGuildEntryResolved,
6+
} from "./allow-list.js";
7+
8+
export function buildDiscordGroupSystemPrompt(
9+
channelConfig?: DiscordChannelConfigResolved | null,
10+
): string | undefined {
11+
const systemPromptParts = [channelConfig?.systemPrompt?.trim() || null].filter(
12+
(entry): entry is string => Boolean(entry),
13+
);
14+
return systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
15+
}
16+
17+
export function buildDiscordUntrustedContext(params: {
18+
isGuild: boolean;
19+
channelTopic?: string;
20+
}): string[] | undefined {
21+
if (!params.isGuild) {
22+
return undefined;
23+
}
24+
const untrustedChannelMetadata = buildUntrustedChannelMetadata({
25+
source: "discord",
26+
label: "Discord channel topic",
27+
entries: [params.channelTopic],
28+
});
29+
return untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined;
30+
}
31+
32+
export function buildDiscordInboundAccessContext(params: {
33+
channelConfig?: DiscordChannelConfigResolved | null;
34+
guildInfo?: DiscordGuildEntryResolved | null;
35+
sender: {
36+
id: string;
37+
name?: string;
38+
tag?: string;
39+
};
40+
allowNameMatching?: boolean;
41+
isGuild: boolean;
42+
channelTopic?: string;
43+
}) {
44+
return {
45+
groupSystemPrompt: params.isGuild
46+
? buildDiscordGroupSystemPrompt(params.channelConfig)
47+
: undefined,
48+
untrustedContext: buildDiscordUntrustedContext({
49+
isGuild: params.isGuild,
50+
channelTopic: params.channelTopic,
51+
}),
52+
ownerAllowFrom: resolveDiscordOwnerAllowFrom({
53+
channelConfig: params.channelConfig,
54+
guildInfo: params.guildInfo,
55+
sender: params.sender,
56+
allowNameMatching: params.allowNameMatching,
57+
}),
58+
};
59+
}

src/discord/monitor/message-handler.process.ts

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -30,16 +30,16 @@ import { convertMarkdownTables } from "../../markdown/tables.js";
3030
import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
3131
import { buildAgentSessionKey } from "../../routing/resolve-route.js";
3232
import { resolveThreadSessionKeys } from "../../routing/session-key.js";
33-
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
3433
import { stripReasoningTagsFromText } from "../../shared/text/reasoning-tags.js";
3534
import { truncateUtf16Safe } from "../../utils.js";
3635
import { chunkDiscordTextWithMode } from "../chunk.js";
3736
import { resolveDiscordDraftStreamingChunking } from "../draft-chunking.js";
3837
import { createDiscordDraftStream } from "../draft-stream.js";
3938
import { reactMessageDiscord, removeReactionDiscord } from "../send.js";
4039
import { editMessageDiscord } from "../send.messages.js";
41-
import { normalizeDiscordSlug, resolveDiscordOwnerAllowFrom } from "./allow-list.js";
40+
import { normalizeDiscordSlug } from "./allow-list.js";
4241
import { resolveTimestampMs } from "./format.js";
42+
import { buildDiscordInboundAccessContext } from "./inbound-context.js";
4343
import type { DiscordMessagePreflightContext } from "./message-handler.preflight.js";
4444
import {
4545
buildDiscordMediaPayload,
@@ -212,30 +212,20 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
212212
const forumContextLine = isForumStarter ? `[Forum parent: #${forumParentSlug}]` : null;
213213
const groupChannel = isGuildMessage && displayChannelSlug ? `#${displayChannelSlug}` : undefined;
214214
const groupSubject = isDirectMessage ? undefined : groupChannel;
215-
const untrustedChannelMetadata = isGuildMessage
216-
? buildUntrustedChannelMetadata({
217-
source: "discord",
218-
label: "Discord channel topic",
219-
entries: [channelInfo?.topic],
220-
})
221-
: undefined;
222215
const senderName = sender.isPluralKit
223216
? (sender.name ?? author.username)
224217
: (data.member?.nickname ?? author.globalName ?? author.username);
225218
const senderUsername = sender.isPluralKit
226219
? (sender.tag ?? sender.name ?? author.username)
227220
: author.username;
228221
const senderTag = sender.tag;
229-
const systemPromptParts = [channelConfig?.systemPrompt?.trim() || null].filter(
230-
(entry): entry is string => Boolean(entry),
231-
);
232-
const groupSystemPrompt =
233-
systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
234-
const ownerAllowFrom = resolveDiscordOwnerAllowFrom({
222+
const { groupSystemPrompt, ownerAllowFrom, untrustedContext } = buildDiscordInboundAccessContext({
235223
channelConfig,
236224
guildInfo,
237225
sender: { id: sender.id, name: sender.name, tag: sender.tag },
238226
allowNameMatching: isDangerousNameMatchingEnabled(discordConfig),
227+
isGuild: isGuildMessage,
228+
channelTopic: channelInfo?.topic,
239229
});
240230
const storePath = resolveStorePath(cfg.session?.store, {
241231
agentId: route.agentId,
@@ -374,7 +364,7 @@ export async function processDiscordMessage(ctx: DiscordMessagePreflightContext)
374364
SenderTag: senderTag,
375365
GroupSubject: groupSubject,
376366
GroupChannel: groupChannel,
377-
UntrustedContext: untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined,
367+
UntrustedContext: untrustedContext,
378368
GroupSystemPrompt: isGuildMessage ? groupSystemPrompt : undefined,
379369
GroupSpace: isGuildMessage ? (guildInfo?.id ?? guildSlug) || undefined : undefined,
380370
OwnerAllowFrom: ownerAllowFrom,

src/discord/monitor/native-command-context.ts

Lines changed: 7 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,7 @@
11
import type { CommandArgs } from "../../auto-reply/commands-registry.js";
22
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
3-
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
4-
import {
5-
resolveDiscordOwnerAllowFrom,
6-
type DiscordChannelConfigResolved,
7-
type DiscordGuildEntryResolved,
8-
} from "./allow-list.js";
3+
import { type DiscordChannelConfigResolved, type DiscordGuildEntryResolved } from "./allow-list.js";
4+
import { buildDiscordInboundAccessContext } from "./inbound-context.js";
95

106
export type BuildDiscordNativeCommandContextParams = {
117
prompt: string;
@@ -39,39 +35,17 @@ export type BuildDiscordNativeCommandContextParams = {
3935
timestampMs?: number;
4036
};
4137

42-
function buildDiscordNativeCommandSystemPrompt(
43-
channelConfig?: DiscordChannelConfigResolved | null,
44-
): string | undefined {
45-
const systemPromptParts = [channelConfig?.systemPrompt?.trim() || null].filter(
46-
(entry): entry is string => Boolean(entry),
47-
);
48-
return systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
49-
}
50-
51-
function buildDiscordNativeCommandUntrustedContext(params: {
52-
isGuild: boolean;
53-
channelTopic?: string;
54-
}): string[] | undefined {
55-
if (!params.isGuild) {
56-
return undefined;
57-
}
58-
const untrustedChannelMetadata = buildUntrustedChannelMetadata({
59-
source: "discord",
60-
label: "Discord channel topic",
61-
entries: [params.channelTopic],
62-
});
63-
return untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined;
64-
}
65-
6638
export function buildDiscordNativeCommandContext(params: BuildDiscordNativeCommandContextParams) {
6739
const conversationLabel = params.isDirectMessage
6840
? (params.user.globalName ?? params.user.username)
6941
: params.channelId;
70-
const ownerAllowFrom = resolveDiscordOwnerAllowFrom({
42+
const { groupSystemPrompt, ownerAllowFrom, untrustedContext } = buildDiscordInboundAccessContext({
7143
channelConfig: params.channelConfig,
7244
guildInfo: params.guildInfo,
7345
sender: params.sender,
7446
allowNameMatching: params.allowNameMatching,
47+
isGuild: params.isGuild,
48+
channelTopic: params.channelTopic,
7549
});
7650

7751
return finalizeInboundContext({
@@ -92,13 +66,8 @@ export function buildDiscordNativeCommandContext(params: BuildDiscordNativeComma
9266
ChatType: params.isDirectMessage ? "direct" : params.isGroupDm ? "group" : "channel",
9367
ConversationLabel: conversationLabel,
9468
GroupSubject: params.isGuild ? params.guildName : undefined,
95-
GroupSystemPrompt: params.isGuild
96-
? buildDiscordNativeCommandSystemPrompt(params.channelConfig)
97-
: undefined,
98-
UntrustedContext: buildDiscordNativeCommandUntrustedContext({
99-
isGuild: params.isGuild,
100-
channelTopic: params.channelTopic,
101-
}),
69+
GroupSystemPrompt: groupSystemPrompt,
70+
UntrustedContext: untrustedContext,
10271
OwnerAllowFrom: ownerAllowFrom,
10372
SenderName: params.user.globalName ?? params.user.username,
10473
SenderId: params.user.id,

0 commit comments

Comments
 (0)