diff --git a/extensions/discord/src/monitor/gateway-plugin.ts b/extensions/discord/src/monitor/gateway-plugin.ts index 5acab8d5339..b5fd85bc3e7 100644 --- a/extensions/discord/src/monitor/gateway-plugin.ts +++ b/extensions/discord/src/monitor/gateway-plugin.ts @@ -6,6 +6,7 @@ import { danger } from "openclaw/plugin-sdk/runtime-env"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { ProxyAgent, fetch as undiciFetch } from "undici"; import WebSocket from "ws"; +import { resolveEnvHttpProxyUrl } from "../../../../src/infra/net/proxy-env.js"; const DISCORD_GATEWAY_BOT_URL = "https://discord.com/api/v10/gateway/bot"; const DEFAULT_DISCORD_GATEWAY_URL = "wss://gateway.discord.gg/"; @@ -270,7 +271,7 @@ export function createDiscordGatewayPlugin(params: { runtime: RuntimeEnv; }): GatewayPlugin { const intents = resolveDiscordGatewayIntents(params.discordConfig?.intents); - const proxy = params.discordConfig?.proxy?.trim(); + const proxy = params.discordConfig?.proxy?.trim() || resolveEnvHttpProxyUrl("https"); const options = { reconnect: { maxAttempts: 50 }, intents, @@ -287,7 +288,7 @@ export function createDiscordGatewayPlugin(params: { try { const wsAgent = new HttpsProxyAgent(proxy); - const fetchAgent = new ProxyAgent(proxy); + const fetchAgent = new ProxyAgent({ uri: proxy, connect: { keepAlive: false } }); params.runtime.log?.("discord: gateway proxy enabled"); diff --git a/extensions/discord/src/monitor/provider.proxy.test.ts b/extensions/discord/src/monitor/provider.proxy.test.ts index f8e9f52c198..62d06f8199c 100644 --- a/extensions/discord/src/monitor/provider.proxy.test.ts +++ b/extensions/discord/src/monitor/provider.proxy.test.ts @@ -89,7 +89,8 @@ vi.mock("https-proxy-agent", () => ({ vi.mock("undici", () => ({ ProxyAgent: class { proxyUrl: string; - constructor(proxyUrl: string) { + constructor(opts: string | { uri: string }) { + const proxyUrl = typeof opts === "string" ? opts : opts.uri; this.proxyUrl = proxyUrl; undiciProxyAgentSpy(proxyUrl); restProxyAgentSpy(proxyUrl); @@ -98,6 +99,10 @@ vi.mock("undici", () => ({ fetch: undiciFetchMock, })); +vi.mock("../../../../src/infra/net/proxy-env.js", () => ({ + resolveEnvHttpProxyUrl: () => undefined, +})); + vi.mock("ws", () => ({ default: class MockWebSocket { constructor(url: string, options?: { agent?: unknown }) { diff --git a/extensions/discord/src/monitor/provider.rest-proxy.test.ts b/extensions/discord/src/monitor/provider.rest-proxy.test.ts index 47ed5bb6335..5f8eeb2e90f 100644 --- a/extensions/discord/src/monitor/provider.rest-proxy.test.ts +++ b/extensions/discord/src/monitor/provider.rest-proxy.test.ts @@ -9,7 +9,8 @@ const { undiciFetchMock, proxyAgentSpy } = vi.hoisted(() => ({ vi.mock("undici", () => { class ProxyAgent { proxyUrl: string; - constructor(proxyUrl: string) { + constructor(opts: string | { uri: string }) { + const proxyUrl = typeof opts === "string" ? opts : opts.uri; if (proxyUrl === "bad-proxy") { throw new Error("bad proxy"); } diff --git a/extensions/discord/src/monitor/rest-fetch.ts b/extensions/discord/src/monitor/rest-fetch.ts index 43b4c768381..77b452385f5 100644 --- a/extensions/discord/src/monitor/rest-fetch.ts +++ b/extensions/discord/src/monitor/rest-fetch.ts @@ -12,7 +12,7 @@ export function resolveDiscordRestFetch( return fetch; } try { - const agent = new ProxyAgent(proxy); + const agent = new ProxyAgent({ uri: proxy, connect: { keepAlive: false } }); const fetcher = ((input: RequestInfo | URL, init?: RequestInit) => undiciFetch(input as string | URL, { ...(init as Record), diff --git a/src/cli/gateway-cli/run.ts b/src/cli/gateway-cli/run.ts index 0aa0e8ff36e..32ad39a43d7 100644 --- a/src/cli/gateway-cli/run.ts +++ b/src/cli/gateway-cli/run.ts @@ -17,6 +17,10 @@ import type { GatewayWsLogStyle } from "../../gateway/ws-logging.js"; import { setGatewayWsLogStyle } from "../../gateway/ws-logging.js"; import { setVerbose } from "../../globals.js"; import { GatewayLockError } from "../../infra/gateway-lock.js"; +import { + ensureGlobalUndiciEnvProxyDispatcher, + ensureGlobalUndiciStreamTimeouts, +} from "../../infra/net/undici-global-dispatcher.js"; import { formatPortDiagnostics, inspectPortUsage } from "../../infra/ports.js"; import { cleanStaleGatewayProcessesSync } from "../../infra/restart-stale-pids.js"; import { setConsoleSubsystemFilter, setConsoleTimestampPrefix } from "../../logging/console.js"; @@ -418,6 +422,11 @@ async function runGatewayCommand(opts: GatewayRunOpts) { } : undefined; + // Proxy bootstrap must happen before timeout tuning so the timeouts wrap the + // active EnvHttpProxyAgent instead of being replaced by a bare proxy dispatcher. + ensureGlobalUndiciEnvProxyDispatcher(); + ensureGlobalUndiciStreamTimeouts(); + try { await runGatewayLoop({ runtime: defaultRuntime, diff --git a/src/gateway/node-command-policy.ts b/src/gateway/node-command-policy.ts index d4ff5c0f045..6e21ffffd76 100644 --- a/src/gateway/node-command-policy.ts +++ b/src/gateway/node-command-policy.ts @@ -205,8 +205,9 @@ export function isNodeCommandAllowed(params: { if (!params.allowlist.has(command)) { return { ok: false, reason: "command not allowlisted" }; } - if (Array.isArray(params.declaredCommands) && params.declaredCommands.length > 0) { - if (!params.declaredCommands.includes(command)) { + const { declaredCommands } = params; + if (Array.isArray(declaredCommands) && declaredCommands.length > 0) { + if (!declaredCommands.includes(command)) { return { ok: false, reason: "command not declared by node" }; } } else { diff --git a/src/gateway/server-methods/nodes.ts b/src/gateway/server-methods/nodes.ts index ae6c8090b6c..3fb0ddc7653 100644 --- a/src/gateway/server-methods/nodes.ts +++ b/src/gateway/server-methods/nodes.ts @@ -1003,15 +1003,16 @@ export const nodeHandlers: GatewayRequestHandlers = { `node wake done node=${nodeId} req=${wakeReqId} connected=true totalMs=${totalDurationMs}`, ); } + const session = nodeSession; const cfg = loadConfig(); - const allowlist = resolveNodeCommandAllowlist(cfg, nodeSession); + const allowlist = resolveNodeCommandAllowlist(cfg, session); const allowed = isNodeCommandAllowed({ command, - declaredCommands: nodeSession.commands, + declaredCommands: session.commands, allowlist, }); if (!allowed.ok) { - const hint = buildNodeCommandRejectionHint(allowed.reason, command, nodeSession); + const hint = buildNodeCommandRejectionHint(allowed.reason, command, session); respond( false, undefined, diff --git a/src/infra/net/undici-global-dispatcher.test.ts b/src/infra/net/undici-global-dispatcher.test.ts index 47a97dd6fb6..6e00d587bf0 100644 --- a/src/infra/net/undici-global-dispatcher.test.ts +++ b/src/infra/net/undici-global-dispatcher.test.ts @@ -111,6 +111,7 @@ describe("ensureGlobalUndiciStreamTimeouts", () => { expect(next.options?.bodyTimeout).toBe(DEFAULT_UNDICI_STREAM_TIMEOUT_MS); expect(next.options?.headersTimeout).toBe(DEFAULT_UNDICI_STREAM_TIMEOUT_MS); expect(next.options?.connect).toEqual({ + keepAlive: false, autoSelectFamily: false, autoSelectFamilyAttemptTimeout: 300, }); diff --git a/src/infra/net/undici-global-dispatcher.ts b/src/infra/net/undici-global-dispatcher.ts index 994af564777..753e7e5281e 100644 --- a/src/infra/net/undici-global-dispatcher.ts +++ b/src/infra/net/undici-global-dispatcher.ts @@ -93,7 +93,10 @@ export function ensureGlobalUndiciEnvProxyDispatcher(): void { return; } try { - setGlobalDispatcher(new EnvHttpProxyAgent()); + // Many local proxies (Clash, Surge, etc.) close HTTP CONNECT tunnels after + // the first request, causing ECONNRESET on reuse. Disable keep-alive so + // undici opens a fresh tunnel per request instead of pooling. + setGlobalDispatcher(new EnvHttpProxyAgent({ connect: { keepAlive: false } })); lastAppliedProxyBootstrap = true; } catch { // Best-effort bootstrap only. @@ -120,10 +123,11 @@ export function ensureGlobalUndiciStreamTimeouts(opts?: { timeoutMs?: number }): const connect = resolveConnectOptions(autoSelectFamily); try { if (kind === "env-proxy") { + const proxyConnect = { ...connect, keepAlive: false }; const proxyOptions = { bodyTimeout: timeoutMs, headersTimeout: timeoutMs, - ...(connect ? { connect } : {}), + connect: proxyConnect, } as ConstructorParameters[0]; setGlobalDispatcher(new EnvHttpProxyAgent(proxyOptions)); } else {