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>
This commit is contained in:
Peter Steinberger
2026-03-07 23:46:56 +00:00
parent f304ca09b1
commit eeba93d63d
3 changed files with 85 additions and 3 deletions

View File

@@ -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

View File

@@ -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<Record<string, unknown>>);
vi.mock("../send.shared.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("../send.shared.js")>();
@@ -54,11 +58,16 @@ vi.mock("../../gateway/client.js", () => ({
private params: Record<string, unknown>;
constructor(params: Record<string, unknown>) {
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", () => {

View File

@@ -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,