From 0e7f3209f2dfdae7b0dba38a1feb7f291d97feac Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 17 Aug 2026 18:22:22 -0700 Subject: [PATCH] fix(gateway): stop pointing local CLI token mismatches at gateway.remote (#125488) A local-mode gateway (gateway.mode=local, no gateway.remote block) sent CLI clients "set gateway.remote.token to match gateway.auth.token" on a wrong-token connect. That hint is a dead end for local connections since they resolve credentials from gateway.auth.token directly. The connect pipeline already records isLocalClient at admission time; thread it into the auth-failure message so local CLI hints point at gateway.auth.token (or pairing) while remote CLI hints keep the gateway.remote.* guidance. --- .../ws-connection/auth-messages.test.ts | 42 +++++++++++++++++++ .../server/ws-connection/auth-messages.ts | 13 ++++-- .../server/ws-connection/connect-auth.ts | 1 + 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/src/gateway/server/ws-connection/auth-messages.test.ts b/src/gateway/server/ws-connection/auth-messages.test.ts index aedf0a59012c..d2594b5ccb87 100644 --- a/src/gateway/server/ws-connection/auth-messages.test.ts +++ b/src/gateway/server/ws-connection/auth-messages.test.ts @@ -37,6 +37,48 @@ describe("formatGatewayAuthFailureMessage", () => { expect(truncateCloseReason(message)).toBe(message); }); + it("points local CLI token mismatches at gateway.auth.token, not gateway.remote", () => { + const message = formatGatewayAuthFailureMessage({ + authMode: "token", + authProvided: "token", + reason: "token_mismatch", + client: { id: GATEWAY_CLIENT_IDS.CLI, mode: GATEWAY_CLIENT_MODES.CLI }, + isLocalClient: true, + }); + + expect(message).toBe( + "unauthorized: gateway token mismatch (use this gateway's gateway.auth.token or pair the device)", + ); + expect(message).not.toContain("gateway.remote"); + expect(truncateCloseReason(message)).toBe(message); + }); + + it("keeps the gateway.remote.token hint for remote CLI token mismatches", () => { + expect( + formatGatewayAuthFailureMessage({ + authMode: "token", + authProvided: "token", + reason: "token_mismatch", + client: { id: GATEWAY_CLIENT_IDS.CLI, mode: GATEWAY_CLIENT_MODES.CLI }, + isLocalClient: false, + }), + ).toBe( + "unauthorized: gateway token mismatch (set gateway.remote.token to match gateway.auth.token)", + ); + }); + + it("points local CLI password mismatches at gateway.auth.password", () => { + expect( + formatGatewayAuthFailureMessage({ + authMode: "password", + authProvided: "password", + reason: "password_mismatch", + client: { id: GATEWAY_CLIENT_IDS.CLI, mode: GATEWAY_CLIENT_MODES.CLI }, + isLocalClient: true, + }), + ).toBe("unauthorized: gateway password mismatch (use this gateway's gateway.auth.password)"); + }); + it("tells rejected node hosts how to diagnose identity-header auth", () => { expect( formatGatewayAuthFailureMessage({ diff --git a/src/gateway/server/ws-connection/auth-messages.ts b/src/gateway/server/ws-connection/auth-messages.ts index 9681e2d9bf73..0eb385c6740d 100644 --- a/src/gateway/server/ws-connection/auth-messages.ts +++ b/src/gateway/server/ws-connection/auth-messages.ts @@ -18,8 +18,9 @@ export function formatGatewayAuthFailureMessage(params: { authProvided: AuthProvidedKind; reason?: string; client?: { id?: string | null; mode?: string | null }; + isLocalClient?: boolean; }): string { - const { authMode, authProvided, reason, client } = params; + const { authMode, authProvided, reason, client, isLocalClient } = params; const isCli = isGatewayCliClient(client); const isControlUi = isOperatorUiClient(client); const isWebchat = isWebchatClient(client); @@ -29,13 +30,19 @@ export function formatGatewayAuthFailureMessage(params: { const uiHint = "open the dashboard URL and paste the token in Control UI settings"; const missingUiTokenHint = "paste in Control UI settings or openclaw doctor --generate-gateway-token; restart"; + // Local CLI clients share this gateway's config and have no gateway.remote + // block; pointing them at gateway.remote.* would be a dead end. const tokenHint = isCli - ? "set gateway.remote.token to match gateway.auth.token" + ? isLocalClient + ? "use this gateway's gateway.auth.token or pair the device" + : "set gateway.remote.token to match gateway.auth.token" : isControlUi || isWebchat ? uiHint : "provide gateway auth token"; const passwordHint = isCli - ? "set gateway.remote.password to match gateway.auth.password" + ? isLocalClient + ? "use this gateway's gateway.auth.password" + : "set gateway.remote.password to match gateway.auth.password" : isControlUi || isWebchat ? "enter the password in Control UI settings" : "provide gateway auth password"; diff --git a/src/gateway/server/ws-connection/connect-auth.ts b/src/gateway/server/ws-connection/connect-auth.ts index 455436d521b9..5951bdce35a9 100644 --- a/src/gateway/server/ws-connection/connect-auth.ts +++ b/src/gateway/server/ws-connection/connect-auth.ts @@ -196,6 +196,7 @@ async function authenticateGatewayConnectCore( authProvided, reason: failedAuth.reason, client: connectParams.client, + isLocalClient, }); const authLogDecision = shouldLimitMissingCredentialAuthLog({ reason: failedAuth.reason,