From eeba93d63dcc77693fcc9640a36683ef7a34c06f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 7 Mar 2026 23:46:56 +0000 Subject: [PATCH] fix(discord): pass gateway auth to exec approvals Pass resolved gateway token/password into the Discord exec approvals GatewayClient startup path so token-auth installs stop failing approvals with gateway token mismatch. Fixes #38179 Adjacent investigation: #35147 by @0riginal-claw Co-authored-by: 0riginal-claw <0rginal_claw@0rginal-claws-Mac-mini.local> --- CHANGELOG.md | 1 + src/discord/monitor/exec-approvals.test.ts | 81 +++++++++++++++++++++- src/discord/monitor/exec-approvals.ts | 6 ++ 3 files changed, 85 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 754961637..df7aa68ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -308,6 +308,7 @@ Docs: https://docs.openclaw.ai - Nodes/system.run dispatch-wrapper boundary: keep shell-wrapper approval classification active at the depth boundary so `env` wrapper stacks cannot reach `/bin/sh -c` execution without the expected approval gate. Thanks @tdjackey for reporting. - Docker/token persistence on reconfigure: reuse the existing `.env` gateway token during `docker-setup.sh` reruns and align compose token env defaults, so Docker installs stop silently rotating tokens and breaking existing dashboard sessions. Landed from contributor PR #33097 by @chengzhichao-xydt. Thanks @chengzhichao-xydt. - Agents/strict OpenAI turn ordering: apply assistant-first transcript bootstrap sanitization to strict OpenAI-compatible providers (for example vLLM/Gemma via `openai-completions`) without adding Google-specific session markers, preventing assistant-first history rejections. (#39252) Thanks @scoootscooob. +- Discord/exec approvals gateway auth: pass resolved shared gateway credentials into the Discord exec-approvals gateway client so token-auth installs stop failing approvals with `gateway token mismatch`. Related to #38179. Thanks @0riginal-claw for the adjacent PR #35147 investigation. ## 2026.3.2 diff --git a/src/discord/monitor/exec-approvals.test.ts b/src/discord/monitor/exec-approvals.test.ts index 1addb7ada..d331087cb 100644 --- a/src/discord/monitor/exec-approvals.test.ts +++ b/src/discord/monitor/exec-approvals.test.ts @@ -33,6 +33,10 @@ beforeEach(() => { const mockRestPost = vi.hoisted(() => vi.fn()); const mockRestPatch = vi.hoisted(() => vi.fn()); const mockRestDelete = vi.hoisted(() => vi.fn()); +const gatewayClientStarts = vi.hoisted(() => vi.fn()); +const gatewayClientStops = vi.hoisted(() => vi.fn()); +const gatewayClientRequests = vi.hoisted(() => vi.fn(async () => ({ ok: true }))); +const gatewayClientParams = vi.hoisted(() => [] as Array>); vi.mock("../send.shared.js", async (importOriginal) => { const actual = await importOriginal(); @@ -54,11 +58,16 @@ vi.mock("../../gateway/client.js", () => ({ private params: Record; constructor(params: Record) { this.params = params; + gatewayClientParams.push(params); + } + start() { + gatewayClientStarts(); + } + stop() { + gatewayClientStops(); } - start() {} - stop() {} async request() { - return { ok: true }; + return gatewayClientRequests(); } }, })); @@ -119,6 +128,17 @@ function createRequest( }; } +beforeEach(() => { + mockRestPost.mockReset(); + mockRestPatch.mockReset(); + mockRestDelete.mockReset(); + gatewayClientStarts.mockReset(); + gatewayClientStops.mockReset(); + gatewayClientRequests.mockReset(); + gatewayClientRequests.mockResolvedValue({ ok: true }); + gatewayClientParams.length = 0; +}); + // ─── buildExecApprovalCustomId ──────────────────────────────────────────────── describe("buildExecApprovalCustomId", () => { @@ -611,6 +631,61 @@ describe("DiscordExecApprovalHandler target config", () => { }); }); +describe("DiscordExecApprovalHandler gateway auth", () => { + it("passes the shared gateway token from config into GatewayClient", async () => { + const handler = new DiscordExecApprovalHandler({ + token: "discord-bot-token", + accountId: "default", + config: { enabled: true, approvers: ["123"] }, + cfg: { + gateway: { + mode: "local", + bind: "loopback", + auth: { mode: "token", token: "shared-gateway-token" }, + }, + }, + }); + + await handler.start(); + + expect(gatewayClientStarts).toHaveBeenCalledTimes(1); + expect(gatewayClientParams[0]).toMatchObject({ + url: "ws://127.0.0.1:18789", + token: "shared-gateway-token", + password: undefined, + scopes: ["operator.approvals"], + }); + }); + + it("prefers OPENCLAW_GATEWAY_TOKEN when config token is missing", async () => { + vi.stubEnv("OPENCLAW_GATEWAY_TOKEN", "env-gateway-token"); + const handler = new DiscordExecApprovalHandler({ + token: "discord-bot-token", + accountId: "default", + config: { enabled: true, approvers: ["123"] }, + cfg: { + gateway: { + mode: "local", + bind: "loopback", + auth: { mode: "token" }, + }, + }, + }); + + try { + await handler.start(); + } finally { + vi.unstubAllEnvs(); + } + + expect(gatewayClientStarts).toHaveBeenCalledTimes(1); + expect(gatewayClientParams[0]).toMatchObject({ + token: "env-gateway-token", + password: undefined, + }); + }); +}); + // ─── Timeout cleanup ───────────────────────────────────────────────────────── describe("DiscordExecApprovalHandler timeout cleanup", () => { diff --git a/src/discord/monitor/exec-approvals.ts b/src/discord/monitor/exec-approvals.ts index 19fef714d..27f5e822c 100644 --- a/src/discord/monitor/exec-approvals.ts +++ b/src/discord/monitor/exec-approvals.ts @@ -15,6 +15,7 @@ import { loadSessionStore, resolveStorePath } from "../../config/sessions.js"; import type { DiscordExecApprovalConfig } from "../../config/types.discord.js"; import { buildGatewayConnectionDetails } from "../../gateway/call.js"; import { GatewayClient } from "../../gateway/client.js"; +import { resolveGatewayCredentialsFromConfig } from "../../gateway/credentials.js"; import type { EventFrame } from "../../gateway/protocol/index.js"; import type { ExecApprovalDecision, @@ -404,9 +405,14 @@ export class DiscordExecApprovalHandler { config: this.opts.cfg, url: this.opts.gatewayUrl, }); + const gatewayCredentials = resolveGatewayCredentialsFromConfig({ + cfg: this.opts.cfg, + }); this.gatewayClient = new GatewayClient({ url: gatewayUrl, + token: gatewayCredentials.token, + password: gatewayCredentials.password, clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT, clientDisplayName: "Discord Exec Approvals", mode: GATEWAY_CLIENT_MODES.BACKEND,