fix(discord): fail-fast when bot identity fetch fails

When fetchUser('@me') fails during Discord provider startup (e.g. due to
transient network issues), the bot previously continued running with
botUserId = undefined. This caused three security issues:

1. Mention gating bypassed: the guard 'if (botId && mentionGate.shouldSkip)'
   short-circuited when botId was undefined, letting all guild messages
   through even with requireMention: true.
2. Self-message filtering disabled: 'author.id === botUserId' never matched,
   risking self-reply loops.
3. Reply detection broken: replies to bot messages weren't recognized as
   implicit mentions.

Fix:
- Throw on fetchUser failure so auto-restart retries (matching the existing
  pattern for fetchDiscordApplicationId).
- Also throw when fetchUser returns no id.
- Defense-in-depth: remove the redundant 'botId &&' guard in the mention
  gate check so mentionGate.shouldSkip is evaluated regardless.

Fixes #42219
This commit is contained in:
Benedikt Schackenberg 2026-03-17 19:44:25 +00:00
parent 4c60956d8e
commit cd0d45b184
3 changed files with 38 additions and 1 deletions

View File

@ -788,7 +788,7 @@ export async function preflightDiscordMessage(
`[discord-preflight] shouldRequireMention=${shouldRequireMention} baseRequireMention=${shouldRequireMentionByConfig} boundThreadSession=${isBoundThreadSession} mentionGate.shouldSkip=${mentionGate.shouldSkip} wasMentioned=${wasMentioned}`,
);
if (isGuildMessage && shouldRequireMention) {
if (botId && mentionGate.shouldSkip) {
if (mentionGate.shouldSkip) {
logDebug(`[discord-preflight] drop: no-mention`);
logVerbose(`discord: drop guild message (mention required, botId=${botId})`);
logger.info(

View File

@ -627,4 +627,32 @@ describe("monitorDiscordProvider", () => {
const messages = vi.mocked(runtime.log).mock.calls.map((call) => String(call[0]));
expect(messages.some((msg) => msg.includes("discord startup ["))).toBe(false);
});
it("throws when fetchUser('@me') fails to prevent running without bot identity (#42219)", async () => {
const { monitorDiscordProvider } = await import("./provider.js");
const runtime = baseRuntime();
clientFetchUserMock.mockRejectedValueOnce(new Error("network timeout"));
await expect(
monitorDiscordProvider({
config: baseConfig(),
runtime,
}),
).rejects.toThrow("discord: cannot start without bot identity");
});
it("throws when fetchUser('@me') returns no user id", async () => {
const { monitorDiscordProvider } = await import("./provider.js");
const runtime = baseRuntime();
clientFetchUserMock.mockResolvedValueOnce({ id: undefined, username: "NoId" });
await expect(
monitorDiscordProvider({
config: baseConfig(),
runtime,
}),
).rejects.toThrow("discord: fetchUser('@me') returned no user id");
});
});

View File

@ -880,6 +880,15 @@ export async function monitorDiscordProvider(opts: MonitorDiscordOpts = {}) {
gateway: lifecycleGateway,
details: String(err),
});
// Fail-fast: without botUserId, mention gating is bypassed (all guild
// messages pass through), self-message filtering is disabled (risk of
// self-reply loops), and reply detection is broken. Let auto-restart
// retry instead of running in a degraded state. See #42219.
throw new Error(`discord: cannot start without bot identity: ${String(err)}`);
}
if (!botUserId) {
// fetchUser succeeded but returned no id — equally unsafe to continue.
throw new Error("discord: fetchUser('@me') returned no user id");
}
if (voiceEnabled) {