fix: harden discord guild resolution (#6824) (thanks @gavinbmoore)

This commit is contained in:
Shadow
2026-02-13 12:33:48 -06:00
parent 5be61e598f
commit aa1604032b
4 changed files with 76 additions and 3 deletions

View File

@@ -49,6 +49,7 @@ Docs: https://docs.openclaw.ai
- Sessions/Agents: pass `agentId` through status and usage transcript-resolution paths (auto-reply, gateway usage APIs, and session cost/log loaders) so non-default agents can resolve absolute session files without path-validation failures. (#15103) Thanks @jalehman.
- Signal/Install: auto-install `signal-cli` via Homebrew on non-x64 Linux architectures, avoiding x86_64 native binary `Exec format error` failures on arm64/arm hosts. (#15443) Thanks @jogvan-k.
- Discord: avoid misrouting numeric guild allowlist entries to `/channels/<guildId>` by prefixing guild-only inputs with `guild:` during resolution. (#12326) Thanks @headswim.
- Discord: ignore partial guild entries when resolving invite allowlists and directory lookups to avoid crashes on missing `id`/`name` data. (#6824) Thanks @gavinbmoore.
- Config: preserve `${VAR}` env references when writing config files so `openclaw config set/apply/patch` does not persist secrets to disk. Thanks @thewilloftheshadow.
- Process/Exec: avoid shell execution for `.exe` commands on Windows so env overrides work reliably in `runCommandWithTimeout`. Thanks @thewilloftheshadow.
- Web tools/web_fetch: prefer `text/markdown` responses for Cloudflare Markdown for Agents, add `cf-markdown` extraction for markdown bodies, and redact fetched URLs in `x-markdown-tokens` debug logs to avoid leaking raw paths/query params. (#15376) Thanks @Yaxuan42.

View File

@@ -5,7 +5,12 @@ import { fetchDiscord } from "./api.js";
import { normalizeDiscordSlug } from "./monitor/allow-list.js";
import { normalizeDiscordToken } from "./token.js";
type DiscordGuild = { id: string; name: string };
type DiscordGuild = { id?: string; name?: string };
function isDiscordGuildWithId(guild: DiscordGuild): guild is { id: string; name?: string } {
return typeof guild.id === "string";
}
type DiscordUser = { id: string; username: string; global_name?: string; bot?: boolean };
type DiscordMember = { user: DiscordUser; nick?: string | null };
type DiscordChannel = { id: string; name?: string | null };
@@ -28,7 +33,7 @@ export async function listDiscordDirectoryGroupsLive(
}
const query = normalizeQuery(params.query);
const rawGuilds = await fetchDiscord<DiscordGuild[]>("/users/@me/guilds", token);
const guilds = rawGuilds.filter((g) => g.id && g.name);
const guilds = rawGuilds.filter(isDiscordGuildWithId);
const rows: ChannelDirectoryEntry[] = [];
for (const guild of guilds) {
@@ -71,7 +76,7 @@ export async function listDiscordDirectoryPeersLive(
}
const rawGuilds = await fetchDiscord<DiscordGuild[]>("/users/@me/guilds", token);
const guilds = rawGuilds.filter((g) => g.id && g.name);
const guilds = rawGuilds.filter(isDiscordGuildWithId);
const rows: ChannelDirectoryEntry[] = [];
const limit = typeof params.limit === "number" && params.limit > 0 ? params.limit : 25;

View File

@@ -31,6 +31,32 @@ describe("resolveDiscordChannelAllowlist", () => {
expect(res[0]?.channelId).toBe("c1");
});
it("ignores partial guild entries", async () => {
const fetcher = async (url: string) => {
if (url.endsWith("/users/@me/guilds")) {
return jsonResponse([
{ id: "g1", name: "Guild One" },
{ id: "g2" },
{ name: "Missing ID" },
]);
}
if (url.endsWith("/guilds/g1/channels")) {
return jsonResponse([{ id: "c1", name: "general", guild_id: "g1", type: 0 }]);
}
return new Response("not found", { status: 404 });
};
const res = await resolveDiscordChannelAllowlist({
token: "test",
entries: ["Guild One/general"],
fetcher,
});
expect(res[0]?.resolved).toBe(true);
expect(res[0]?.guildId).toBe("g1");
expect(res[0]?.channelId).toBe("c1");
});
it("resolves channel id to guild", async () => {
const fetcher = async (url: string) => {
if (url.endsWith("/users/@me/guilds")) {

View File

@@ -0,0 +1,41 @@
import { describe, expect, it } from "vitest";
import { resolveDiscordUserAllowlist } from "./resolve-users.js";
function jsonResponse(body: unknown) {
return new Response(JSON.stringify(body), { status: 200 });
}
describe("resolveDiscordUserAllowlist", () => {
it("ignores partial guild entries", async () => {
const fetcher = async (url: string) => {
if (url.endsWith("/users/@me/guilds")) {
return jsonResponse([
{ id: "g1", name: "Guild One" },
{ id: "g2" },
{ name: "Missing ID" },
]);
}
if (url.includes("/guilds/g1/members/search")) {
return jsonResponse([
{
user: {
id: "u1",
username: "alex",
},
},
]);
}
return new Response("not found", { status: 404 });
};
const res = await resolveDiscordUserAllowlist({
token: "test",
entries: ["Guild One/alex"],
fetcher,
});
expect(res[0]?.resolved).toBe(true);
expect(res[0]?.id).toBe("u1");
expect(res[0]?.guildId).toBe("g1");
});
});