diff --git a/scripts/generate-openclaw-config.py b/scripts/generate-openclaw-config.py index 0e3e3456d6..98328d3f13 100755 --- a/scripts/generate-openclaw-config.py +++ b/scripts/generate-openclaw-config.py @@ -514,7 +514,7 @@ def _placeholder(channel: str, env_key: str) -> str: } if ch == "slack": account["appToken"] = _placeholder(ch, "SLACK_APP_TOKEN") - if ch == "telegram": + if ch in {"discord", "telegram"}: account["proxy"] = proxy_url if ch == "telegram": account["groupPolicy"] = "open" diff --git a/test/e2e/lib/discord-gateway-proof.sh b/test/e2e/lib/discord-gateway-proof.sh index ffe99ba85a..cc3b39917c 100755 --- a/test/e2e/lib/discord-gateway-proof.sh +++ b/test/e2e/lib/discord-gateway-proof.sh @@ -98,15 +98,20 @@ apply_fake_discord_gateway_policy() { run_fake_discord_gateway_node_client() { local port="$1" local identify_token="$2" + local proxy_url="${3:-}" local host="${FAKE_DISCORD_GATEWAY_HOST:-host.openshell.internal}" - sandbox_exec_stdin "FAKE_DISCORD_GATEWAY_CLIENT_HOST='$host' FAKE_DISCORD_GATEWAY_CLIENT_PORT='$port' FAKE_DISCORD_GATEWAY_IDENTIFY_TOKEN='$identify_token' node - 2>&1" <<'NODE' + local proxy_env="" + if [ -n "$proxy_url" ]; then + printf -v proxy_env ' FAKE_DISCORD_GATEWAY_PROXY_URL=%q' "$proxy_url" + fi + sandbox_exec_stdin "FAKE_DISCORD_GATEWAY_CLIENT_HOST='$host' FAKE_DISCORD_GATEWAY_CLIENT_PORT='$port' FAKE_DISCORD_GATEWAY_IDENTIFY_TOKEN='$identify_token'$proxy_env node - 2>&1" <<'NODE' const crypto = require("crypto"); const net = require("net"); const host = process.env.FAKE_DISCORD_GATEWAY_CLIENT_HOST || "host.openshell.internal"; const port = Number(process.env.FAKE_DISCORD_GATEWAY_CLIENT_PORT); const identifyToken = process.env.FAKE_DISCORD_GATEWAY_IDENTIFY_TOKEN; -const proxyUrl = process.env.HTTP_PROXY || process.env.http_proxy || ""; +const proxyUrl = process.env.FAKE_DISCORD_GATEWAY_PROXY_URL || process.env.HTTP_PROXY || process.env.http_proxy || ""; const results = []; function proxyTarget() { diff --git a/test/e2e/test-messaging-providers.sh b/test/e2e/test-messaging-providers.sh index 66682adb42..c2c871c748 100755 --- a/test/e2e/test-messaging-providers.sh +++ b/test/e2e/test-messaging-providers.sh @@ -916,6 +916,27 @@ print(account.get('token', '')) skip "M9: No Discord token to check" fi + # M9b: Discord Gateway WebSocket routing uses the OpenShell proxy. + # #3894 regressed because OpenClaw's Discord gateway client ignores proxy + # env vars and only uses the per-account proxy setting. The fake Gateway + # proof in M13b-M13g exercises that OpenShell WebSocket relay; this config + # assertion ensures the real OpenClaw Discord account is wired to the relay. + dc_proxy=$(echo "$channel_json" | python3 -c " +import json, sys +d = json.load(sys.stdin) +accounts = d.get('discord', {}).get('accounts', {}) +account = accounts.get('default') or accounts.get('main') or {} +print(account.get('proxy', '')) +" 2>/dev/null || true) + + if [ -n "$dc_token" ] && [ "$dc_proxy" = "http://10.200.0.1:3128" ]; then + pass "M9b: Discord account proxy is baked into openclaw.json for Gateway WebSocket routing" + elif [ -n "$dc_token" ]; then + fail "M9b: Discord account proxy missing or wrong; Gateway WebSocket may bypass OpenShell proxy (proxy='${dc_proxy}')" + else + skip "M9b: No Discord channel config to check" + fi + # M10: Telegram enabled tg_enabled=$(echo "$channel_json" | python3 -c " import json, sys @@ -1416,7 +1437,9 @@ else fail "M13-rest-e: Unexpected fake Discord REST capture counts: ${fake_rest_capture}" fi -# M13b-M13f: Hermetic Discord Gateway over OpenShell's native WebSocket L7 path. +# M13b-M13g: Hermetic Discord Gateway over OpenShell's native WebSocket L7 path. +# M13d-config drives the fake Gateway using the proxy URL from the generated +# OpenClaw Discord account, which is the exact wiring #3894 depends on. fake_gateway_ready=0 if start_fake_discord_gateway "$DISCORD_TOKEN"; then fake_gateway_ready=1 @@ -1432,6 +1455,27 @@ else fail "M13c: Failed to apply fake Discord Gateway policy: $(tail -20 /tmp/nemoclaw-fake-discord-policy.log 2>/dev/null | tr '\n' ' ' | cut -c1-300)" fi +dc_ws_account_proxy="" +dc_proxy_safe="${dc_proxy:-}" +if [ "$fake_gateway_ready" = "1" ] && [ -n "$dc_proxy_safe" ]; then + dc_ws_account_proxy=$(run_fake_discord_gateway_node_client "$FAKE_DISCORD_GATEWAY_PORT" "openshell:resolve:env:DISCORD_BOT_TOKEN" "$dc_proxy_safe" || true) +fi +info "OpenClaw-config fake Discord Gateway probe: ${dc_ws_account_proxy:0:500}" + +if [ "$fake_gateway_ready" != "1" ]; then + skip "M13d-config: Fake Discord Gateway unavailable; skipping OpenClaw account proxy proof" +elif [ -z "$dc_proxy_safe" ]; then + skip "M13d-config: No Discord account proxy in openclaw.json to exercise against fake Gateway" +elif echo "$dc_ws_account_proxy" | grep -q "^UPGRADE$" \ + && echo "$dc_ws_account_proxy" | grep -q "^HELLO$" \ + && echo "$dc_ws_account_proxy" | grep -q "^IDENTIFY_SENT_PLACEHOLDER$" \ + && echo "$dc_ws_account_proxy" | grep -q "^READY$" \ + && echo "$dc_ws_account_proxy" | grep -q "^HEARTBEAT_ACK$"; then + pass "M13d-config: Discord account proxy from openclaw.json reaches fake Gateway through OpenShell" +else + fail "M13d-config: Discord account proxy from openclaw.json failed against fake Gateway: ${dc_ws_account_proxy:0:400}" +fi + dc_ws_native="" if [ "$fake_gateway_ready" = "1" ]; then dc_ws_native=$(run_fake_discord_gateway_node_client "$FAKE_DISCORD_GATEWAY_PORT" "openshell:resolve:env:DISCORD_BOT_TOKEN" || true) diff --git a/test/generate-openclaw-config.test.ts b/test/generate-openclaw-config.test.ts index 5ca812a481..b6f15c20e9 100644 --- a/test/generate-openclaw-config.test.ts +++ b/test/generate-openclaw-config.test.ts @@ -348,7 +348,7 @@ describe("generate-openclaw-config.py: config generation", () => { expect(config.plugins?.entries?.["openclaw-weixin"]?.enabled).toBe(true); }); - it("emits canonical openshell:resolve:env: placeholders for non-Slack channels", () => { + it("emits canonical placeholders and proxy routing for non-Slack channels", () => { const channels = Buffer.from(JSON.stringify(["telegram", "discord"])).toString("base64"); const config = runConfigScript({ NEMOCLAW_MESSAGING_CHANNELS_B64: channels }); expect(config.channels.telegram.accounts.default.botToken).toBe( @@ -358,7 +358,22 @@ describe("generate-openclaw-config.py: 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 the configured OpenShell proxy", () => { + const channels = Buffer.from(JSON.stringify(["discord"])).toString("base64"); + const config = runConfigScript({ + NEMOCLAW_MESSAGING_CHANNELS_B64: channels, + NEMOCLAW_PROXY_HOST: "10.201.0.9", + NEMOCLAW_PROXY_PORT: "43128", + }); + + expect(config.channels.discord.accounts.default).toMatchObject({ + token: "openshell:resolve:env:DISCORD_BOT_TOKEN", + enabled: true, + proxy: "http://10.201.0.9:43128", + }); }); it("emits Bolt-shape placeholders for Slack so the SDK's prefix regex passes", () => {