refactor(discord): extract native command context builder

This commit is contained in:
Peter Steinberger
2026-03-08 01:15:05 +00:00
parent 189cd99377
commit 9d10697227
3 changed files with 242 additions and 64 deletions

View File

@@ -0,0 +1,95 @@
import { describe, expect, it } from "vitest";
import { buildDiscordNativeCommandContext } from "./native-command-context.js";
describe("buildDiscordNativeCommandContext", () => {
it("builds direct-message slash command context", () => {
const ctx = buildDiscordNativeCommandContext({
prompt: "/status",
commandArgs: {},
sessionKey: "agent:codex:discord:slash:user-1",
commandTargetSessionKey: "agent:codex:discord:direct:user-1",
accountId: "default",
interactionId: "interaction-1",
channelId: "dm-1",
commandAuthorized: true,
isDirectMessage: true,
isGroupDm: false,
isGuild: false,
isThreadChannel: false,
user: {
id: "user-1",
username: "tester",
globalName: "Tester",
},
sender: {
id: "user-1",
tag: "tester#0001",
},
timestampMs: 123,
});
expect(ctx.From).toBe("discord:user-1");
expect(ctx.To).toBe("slash:user-1");
expect(ctx.ChatType).toBe("direct");
expect(ctx.ConversationLabel).toBe("Tester");
expect(ctx.SessionKey).toBe("agent:codex:discord:slash:user-1");
expect(ctx.CommandTargetSessionKey).toBe("agent:codex:discord:direct:user-1");
expect(ctx.OriginatingTo).toBe("user:user-1");
expect(ctx.UntrustedContext).toBeUndefined();
expect(ctx.GroupSystemPrompt).toBeUndefined();
expect(ctx.Timestamp).toBe(123);
});
it("builds guild slash command context with owner allowlist and channel metadata", () => {
const ctx = buildDiscordNativeCommandContext({
prompt: "/status",
commandArgs: { model: "gpt-5.2" },
sessionKey: "agent:codex:discord:slash:user-1",
commandTargetSessionKey: "agent:codex:discord:channel:chan-1",
accountId: "default",
interactionId: "interaction-1",
channelId: "chan-1",
threadParentId: "parent-1",
guildName: "Ops",
channelTopic: "Production alerts only",
channelConfig: {
allowed: true,
users: ["discord:user-1"],
systemPrompt: "Use the runbook.",
},
guildInfo: {
id: "guild-1",
},
allowNameMatching: false,
commandAuthorized: true,
isDirectMessage: false,
isGroupDm: false,
isGuild: true,
isThreadChannel: true,
user: {
id: "user-1",
username: "tester",
},
sender: {
id: "user-1",
name: "tester",
tag: "tester#0001",
},
timestampMs: 456,
});
expect(ctx.From).toBe("discord:channel:chan-1");
expect(ctx.ChatType).toBe("channel");
expect(ctx.ConversationLabel).toBe("chan-1");
expect(ctx.GroupSubject).toBe("Ops");
expect(ctx.GroupSystemPrompt).toBe("Use the runbook.");
expect(ctx.OwnerAllowFrom).toEqual(["user-1"]);
expect(ctx.MessageThreadId).toBe("chan-1");
expect(ctx.ThreadParentId).toBe("parent-1");
expect(ctx.OriginatingTo).toBe("channel:chan-1");
expect(ctx.UntrustedContext).toEqual([
expect.stringContaining("Discord channel topic:\nProduction alerts only"),
]);
expect(ctx.Timestamp).toBe(456);
});
});

View File

@@ -0,0 +1,124 @@
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";
export type BuildDiscordNativeCommandContextParams = {
prompt: string;
commandArgs: CommandArgs;
sessionKey: string;
commandTargetSessionKey: string;
accountId?: string | null;
interactionId: string;
channelId: string;
threadParentId?: string;
guildName?: string;
channelTopic?: string;
channelConfig?: DiscordChannelConfigResolved | null;
guildInfo?: DiscordGuildEntryResolved | null;
allowNameMatching?: boolean;
commandAuthorized: boolean;
isDirectMessage: boolean;
isGroupDm: boolean;
isGuild: boolean;
isThreadChannel: boolean;
user: {
id: string;
username: string;
globalName?: string | null;
};
sender: {
id: string;
name?: string;
tag?: string;
};
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({
channelConfig: params.channelConfig,
guildInfo: params.guildInfo,
sender: params.sender,
allowNameMatching: params.allowNameMatching,
});
return finalizeInboundContext({
Body: params.prompt,
BodyForAgent: params.prompt,
RawBody: params.prompt,
CommandBody: params.prompt,
CommandArgs: params.commandArgs,
From: params.isDirectMessage
? `discord:${params.user.id}`
: params.isGroupDm
? `discord:group:${params.channelId}`
: `discord:channel:${params.channelId}`,
To: `slash:${params.user.id}`,
SessionKey: params.sessionKey,
CommandTargetSessionKey: params.commandTargetSessionKey,
AccountId: params.accountId ?? undefined,
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,
}),
OwnerAllowFrom: ownerAllowFrom,
SenderName: params.user.globalName ?? params.user.username,
SenderId: params.user.id,
SenderUsername: params.user.username,
SenderTag: params.sender.tag,
Provider: "discord" as const,
Surface: "discord" as const,
WasMentioned: true,
MessageSid: params.interactionId,
MessageThreadId: params.isThreadChannel ? params.channelId : undefined,
Timestamp: params.timestampMs ?? Date.now(),
CommandAuthorized: params.commandAuthorized,
CommandSource: "native" as const,
// Native slash contexts use To=slash:<user> for interaction routing.
// For follow-up delivery (for example subagent completion announces),
// preserve the real Discord target separately.
OriginatingChannel: "discord" as const,
OriginatingTo: params.isDirectMessage
? `user:${params.user.id}`
: `channel:${params.channelId}`,
ThreadParentId: params.isThreadChannel ? params.threadParentId : undefined,
});
}

View File

@@ -37,7 +37,6 @@ import {
resolveCommandArgMenu,
serializeCommandArgs,
} from "../../auto-reply/commands-registry.js";
import { finalizeInboundContext } from "../../auto-reply/reply/inbound-context.js";
import { resolveStoredModelOverride } from "../../auto-reply/reply/model-selection.js";
import { dispatchReplyWithDispatcher } from "../../auto-reply/reply/provider-dispatcher.js";
import type { ReplyPayload } from "../../auto-reply/types.js";
@@ -53,7 +52,6 @@ import { getAgentScopedMediaLocalRoots } from "../../media/local-roots.js";
import { buildPairingReply } from "../../pairing/pairing-messages.js";
import { executePluginCommand, matchPluginCommand } from "../../plugins/commands.js";
import type { ResolvedAgentRoute } from "../../routing/resolve-route.js";
import { buildUntrustedChannelMetadata } from "../../security/channel-metadata.js";
import { chunkItems } from "../../utils/chunk-items.js";
import { withTimeout } from "../../utils/with-timeout.js";
import { loadWebMedia } from "../../web/media.js";
@@ -65,7 +63,6 @@ import {
resolveDiscordGuildEntry,
resolveDiscordMemberAccessState,
resolveDiscordOwnerAccess,
resolveDiscordOwnerAllowFrom,
} from "./allow-list.js";
import { resolveDiscordDmCommandAccess } from "./dm-command-auth.js";
import { handleDiscordDmCommandDecision } from "./dm-command-decision.js";
@@ -85,6 +82,7 @@ import {
toDiscordModelPickerMessagePayload,
type DiscordModelPickerCommandContext,
} from "./model-picker.js";
import { buildDiscordNativeCommandContext } from "./native-command-context.js";
import {
buildDiscordRoutePeer,
resolveDiscordConversationRoute,
@@ -1653,70 +1651,31 @@ async function dispatchDiscordCommandInteraction(params: {
configuredRoute,
matchedBy: configuredBinding ? "binding.channel" : undefined,
});
const conversationLabel = isDirectMessage ? (user.globalName ?? user.username) : channelId;
const ownerAllowFrom = resolveDiscordOwnerAllowFrom({
const ctxPayload = buildDiscordNativeCommandContext({
prompt,
commandArgs,
sessionKey: boundSessionKey ?? `agent:${effectiveRoute.agentId}:${sessionPrefix}:${user.id}`,
commandTargetSessionKey: boundSessionKey ?? effectiveRoute.sessionKey,
accountId: effectiveRoute.accountId,
interactionId,
channelId,
threadParentId,
guildName: interaction.guild?.name,
channelTopic: channel && "topic" in channel ? (channel.topic ?? undefined) : undefined,
channelConfig,
guildInfo,
sender: { id: sender.id, name: sender.name, tag: sender.tag },
allowNameMatching,
});
const ctxPayload = finalizeInboundContext({
Body: prompt,
BodyForAgent: prompt,
RawBody: prompt,
CommandBody: prompt,
CommandArgs: commandArgs,
From: isDirectMessage
? `discord:${user.id}`
: isGroupDm
? `discord:group:${channelId}`
: `discord:channel:${channelId}`,
To: `slash:${user.id}`,
SessionKey: boundSessionKey ?? `agent:${effectiveRoute.agentId}:${sessionPrefix}:${user.id}`,
CommandTargetSessionKey: boundSessionKey ?? effectiveRoute.sessionKey,
AccountId: effectiveRoute.accountId,
ChatType: isDirectMessage ? "direct" : isGroupDm ? "group" : "channel",
ConversationLabel: conversationLabel,
GroupSubject: isGuild ? interaction.guild?.name : undefined,
GroupSystemPrompt: isGuild
? (() => {
const systemPromptParts = [channelConfig?.systemPrompt?.trim() || null].filter(
(entry): entry is string => Boolean(entry),
);
return systemPromptParts.length > 0 ? systemPromptParts.join("\n\n") : undefined;
})()
: undefined,
UntrustedContext: isGuild
? (() => {
const channelTopic =
channel && "topic" in channel ? (channel.topic ?? undefined) : undefined;
const untrustedChannelMetadata = buildUntrustedChannelMetadata({
source: "discord",
label: "Discord channel topic",
entries: [channelTopic],
});
return untrustedChannelMetadata ? [untrustedChannelMetadata] : undefined;
})()
: undefined,
OwnerAllowFrom: ownerAllowFrom,
SenderName: user.globalName ?? user.username,
SenderId: user.id,
SenderUsername: user.username,
SenderTag: sender.tag,
Provider: "discord" as const,
Surface: "discord" as const,
WasMentioned: true,
MessageSid: interactionId,
MessageThreadId: isThreadChannel ? channelId : undefined,
Timestamp: Date.now(),
CommandAuthorized: commandAuthorized,
CommandSource: "native" as const,
// Native slash contexts use To=slash:<user> for interaction routing.
// For follow-up delivery (for example subagent completion announces),
// preserve the real Discord target separately.
OriginatingChannel: "discord" as const,
OriginatingTo: isDirectMessage ? `user:${user.id}` : `channel:${channelId}`,
ThreadParentId: isThreadChannel ? threadParentId : undefined,
commandAuthorized,
isDirectMessage,
isGroupDm,
isGuild,
isThreadChannel,
user: {
id: user.id,
username: user.username,
globalName: user.globalName,
},
sender: { id: sender.id, name: sender.name, tag: sender.tag },
});
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({