Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/lib/messaging/channels/discord/template-resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ import {
nonEmptyArray,
nonEmptyCsv,
nonEmptyObject,
nonEmptyString,
parseBoolean,
parseList,
resolvedRenderTemplateReference,
stateValue,
} from "../template-resolver-utils";

const DEFAULT_PROXY_HOST = "10.200.0.1";
const DEFAULT_PROXY_PORT = "3128";

type DiscordGuildConfig = {
readonly requireMention?: boolean;
readonly users?: readonly string[];
Expand All @@ -23,7 +27,8 @@ export const resolveDiscordTemplateReference: BuiltInRenderTemplateResolver = (
reference,
context,
) => {
if (reference === "discordProxyUrl") return resolvedRenderTemplateReference(undefined);
if (reference === "discordProxyUrl")
return resolvedRenderTemplateReference(proxyUrl(context.env));

switch (reference) {
case "discord.guilds":
Expand Down Expand Up @@ -83,3 +88,9 @@ function discordRequireMention(context: RenderTemplateContext): boolean {
}
return true;
}

function proxyUrl(env: RenderTemplateContext["env"]): string {
const host = nonEmptyString(env?.NEMOCLAW_PROXY_HOST) ?? DEFAULT_PROXY_HOST;
const port = nonEmptyString(env?.NEMOCLAW_PROXY_PORT) ?? DEFAULT_PROXY_PORT;
return `http://${host}:${port}`;
}
30 changes: 30 additions & 0 deletions test/discord-template-resolver-proxy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { describe, expect, it } from "vitest";

import { resolveDiscordTemplateReference } from "../dist/lib/messaging/channels/discord/template-resolver.js";

// The Discord gateway client honors only the per-account proxy (it ignores the
// managed env proxy), so channels.discord.accounts.default.proxy must resolve to
// the sandbox proxy or the gateway WebSocket cannot egress the deny-by-default
// network namespace. Telegram already resolves its proxy this way; #5075.
const ctx = (env: Record<string, string | undefined>) => ({ inputs: [], env });

describe("discord template-resolver: discordProxyUrl", () => {
it("resolves discordProxyUrl to the default sandbox proxy (was previously undefined, #5075)", () => {
expect(resolveDiscordTemplateReference("discordProxyUrl", ctx({}))).toEqual({
matched: true,
value: "http://10.200.0.1:3128",
});
});

it("honors NEMOCLAW_PROXY_HOST / NEMOCLAW_PROXY_PORT overrides", () => {
expect(
resolveDiscordTemplateReference(
"discordProxyUrl",
ctx({ NEMOCLAW_PROXY_HOST: "10.201.0.9", NEMOCLAW_PROXY_PORT: "43128" }),
),
).toEqual({ matched: true, value: "http://10.201.0.9:43128" });
});
});
22 changes: 11 additions & 11 deletions test/generate-openclaw-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@
// Runs the actual TypeScript script with controlled env vars and asserts on
// the generated openclaw.json output.

import { describe, it, expect, beforeEach, afterEach } from "vitest";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { afterEach, beforeEach, describe, expect, it } from "vitest";

import { buildConfig, main } from "../scripts/generate-openclaw-config.mts";
import {
Expand Down Expand Up @@ -613,10 +613,10 @@ describe("generate-openclaw-config.mts: config generation", () => {
"openshell:resolve:env:DISCORD_BOT_TOKEN",
);
expect(config.channels.telegram.accounts.default.proxy).toBe("http://10.200.0.1:3128");
expect(config.channels.discord.accounts.default.proxy).toBeUndefined();
expect(config.channels.discord.accounts.default.proxy).toBe("http://10.200.0.1:3128");
});

it("#3894: routes Discord gateway traffic through OpenClaw's managed proxy", () => {
it("#3894: routes Discord gateway traffic through the per-account proxy", () => {
const channels = Buffer.from(JSON.stringify(["discord"])).toString("base64");
const config = runConfigScript({
NEMOCLAW_MESSAGING_CHANNELS_B64: channels,
Expand All @@ -633,18 +633,18 @@ describe("generate-openclaw-config.mts: config generation", () => {
token: "openshell:resolve:env:DISCORD_BOT_TOKEN",
enabled: true,
});
expect(config.channels.discord.accounts.default.proxy).toBeUndefined();
expect(config.channels.discord.accounts.default.proxy).toBe("http://10.201.0.9:43128");
});

it("does not write a Discord account proxy when the managed proxy is configured", () => {
it("writes the Discord account proxy alongside the managed proxy", () => {
const channels = Buffer.from(JSON.stringify(["discord"])).toString("base64");
const config = runConfigScript({
NEMOCLAW_MESSAGING_CHANNELS_B64: channels,
NEMOCLAW_PROXY_PORT: "43128",
});

expect(config.proxy.proxyUrl).toBe("http://10.200.0.1:43128");
expect(config.channels.discord.accounts.default.proxy).toBeUndefined();
expect(config.channels.discord.accounts.default.proxy).toBe("http://10.200.0.1:43128");
});

it("can defer OpenClaw managed proxy config for build-time doctor", () => {
Expand All @@ -655,7 +655,7 @@ describe("generate-openclaw-config.mts: config generation", () => {
});

expect(config.proxy).toBeUndefined();
expect(config.channels.discord.accounts.default.proxy).toBeUndefined();
expect(config.channels.discord.accounts.default.proxy).toBe("http://10.200.0.1:3128");
});

it("ignores the OpenShell loopback proxy env var when using OpenClaw managed proxy", () => {
Expand All @@ -667,10 +667,10 @@ describe("generate-openclaw-config.mts: config generation", () => {
});

expect(config.proxy.proxyUrl).toBe("http://10.200.0.1:3128");
expect(config.channels.discord.accounts.default.proxy).toBeUndefined();
expect(config.channels.discord.accounts.default.proxy).toBe("http://10.200.0.1:3128");
});

it("keeps Telegram on the OpenShell proxy while Discord relies on the managed proxy", () => {
it("routes both Telegram and Discord through the per-account proxy", () => {
const channels = Buffer.from(JSON.stringify(["telegram", "discord"])).toString("base64");
const config = runConfigScript({
NEMOCLAW_MESSAGING_CHANNELS_B64: channels,
Expand All @@ -680,7 +680,7 @@ describe("generate-openclaw-config.mts: config generation", () => {

expect(config.proxy.proxyUrl).toBe("http://10.201.0.9:43128");
expect(config.channels.telegram.accounts.default.proxy).toBe("http://10.201.0.9:43128");
expect(config.channels.discord.accounts.default.proxy).toBeUndefined();
expect(config.channels.discord.accounts.default.proxy).toBe("http://10.201.0.9:43128");
});

it("emits Bolt-shape placeholders for Slack so the SDK's prefix regex passes", () => {
Expand Down
Loading