From 64de57bc2295d701eaf3a3995baf02247a898c9c Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 15 Mar 2026 13:52:06 +0000 Subject: [PATCH 1/7] fix(gateway): restore handshake timeout to 10s to fix devices list on slow systems (#47103) Co-authored-by: 0x4C33 --- src/gateway/server-constants.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/gateway/server-constants.ts b/src/gateway/server-constants.ts index 54dc3f794b6..da215d2a525 100644 --- a/src/gateway/server-constants.ts +++ b/src/gateway/server-constants.ts @@ -21,6 +21,8 @@ export const __setMaxChatHistoryMessagesBytesForTest = (value?: number) => { maxChatHistoryMessagesBytes = value; } }; +// Restore 10s timeout: 3s (from #44089) was too aggressive for CLI on slow systems +// (e.g. device identity load + signature, disk I/O). Fixes #47103. export const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000; export const getHandshakeTimeoutMs = () => { // User-facing env var (works in all environments); test-only var gated behind VITEST From c2c7087fc8c2665634698e4f182b6d78db6a1ae6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 15 Mar 2026 14:06:10 +0000 Subject: [PATCH 2/7] fix(gateway): refine handshake timeout comment --- src/gateway/server-constants.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/gateway/server-constants.ts b/src/gateway/server-constants.ts index da215d2a525..937dcc554ea 100644 --- a/src/gateway/server-constants.ts +++ b/src/gateway/server-constants.ts @@ -21,8 +21,9 @@ export const __setMaxChatHistoryMessagesBytesForTest = (value?: number) => { maxChatHistoryMessagesBytes = value; } }; -// Restore 10s timeout: 3s (from #44089) was too aggressive for CLI on slow systems -// (e.g. device identity load + signature, disk I/O). Fixes #47103. +// Allow sufficient time for CLI to load device identity, sign the connect payload, +// and complete the handshake on slow systems (cold start, disk I/O). Too short causes +// premature close with "gateway closed (1000 normal closure)" before connect completes. export const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000; export const getHandshakeTimeoutMs = () => { // User-facing env var (works in all environments); test-only var gated behind VITEST From c99f92fb0547cc559279e15c82450254d33ab219 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 17 Mar 2026 06:05:36 +0000 Subject: [PATCH 3/7] fix(gateway): grant operator.read for token-only auth to fix missing scope (#48167) --- CHANGELOG.md | 2 ++ .../server.auth.default-token.suite.ts | 14 ++++++++-- .../server/ws-connection/message-handler.ts | 28 +++++++++++++------ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 37ff9e33f36..40ba6231f40 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,6 +54,8 @@ Docs: https://docs.openclaw.ai - CLI/Ollama onboarding: keep the interactive model picker for explicit `openclaw onboard --auth-choice ollama` runs so setup still selects a default model without reintroducing pre-picker auto-pulls. (#49249) Thanks @BruceMacD. - Plugins/bundler TDZ: fix `RESERVED_COMMANDS` temporal dead zone error that prevented device-pair, phone-control, and talk-voice plugins from registering when the bundler placed the commands module after call sites in the same output chunk. Thanks @BunsDev. - Plugins/imports: fix stale googlechat runtime-api import paths and signal SDK circular re-exports broken by recent plugin-sdk refactors. Thanks @BunsDev. +- Gateway/auth: grant operator.read for device-less token/password auth so CLI and Dashboard can run read RPCs (devices list, status, probe) instead of getting "missing scope: operator.read". Fixes #48167, #46117, #46716, #17095. +- Gateway/handshake: restore handshake timeout to 10s (was reduced to 3s in #44089) so CLI commands like `openclaw devices list` no longer fail with "gateway closed (1000 normal closure)" on slower systems. (#47103) - Google auth/Node 25: patch `gaxios` to use native fetch without injecting `globalThis.window`, while translating proxy and mTLS transport settings so Google Vertex and Google Chat auth keep working on Node 25. (#47914) Thanks @pdd-cli. - Gateway/startup: load bundled channel plugins from compiled `dist/extensions` entries in built installs, so gateway boot no longer recompiles bundled extension TypeScript on every startup and WhatsApp-class cold starts drop back to seconds instead of tens of seconds or worse. (#47560) Thanks @ngutman. - Agents/openai-responses: strip `prompt_cache_key` and `prompt_cache_retention` for non-OpenAI-compatible Responses endpoints while keeping them on direct OpenAI and Azure OpenAI paths, so third-party OpenAI-compatible providers no longer reject those requests with HTTP 400. (#49877) Thanks @ShaunTsai. diff --git a/src/gateway/server.auth.default-token.suite.ts b/src/gateway/server.auth.default-token.suite.ts index ed15150a029..eaa7f99ba61 100644 --- a/src/gateway/server.auth.default-token.suite.ts +++ b/src/gateway/server.auth.default-token.suite.ts @@ -178,13 +178,14 @@ export function registerDefaultAuthTokenSuite(): void { expectConnectError?: string; expectStatusOk?: boolean; expectStatusError?: string; + expectAdminRestricted?: boolean; }> = [ { - name: "operator + valid shared token => connected with cleared scopes", + name: "operator + valid shared token => connected with operator.read (fixes #48167)", opts: { role: "operator", token, device: null }, expectConnectOk: true, - expectStatusOk: false, - expectStatusError: "missing scope", + expectStatusOk: true, + expectAdminRestricted: true, }, { name: "node + valid shared token => rejected without device", @@ -220,6 +221,13 @@ export function registerDefaultAuthTokenSuite(): void { ); } } + if (scenario.expectAdminRestricted) { + const adminRes = await rpcReq(ws, "set-heartbeats", { enabled: false }); + expect(adminRes.ok, scenario.name).toBe(false); + expect(adminRes.error?.message ?? "", scenario.name).toContain( + "missing scope: operator.admin", + ); + } } finally { ws.close(); } diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index 80aa6437342..544246949b8 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -33,6 +33,7 @@ import { mintCanvasCapabilityToken, } from "../../canvas-capability.js"; import { normalizeDeviceMetadataForAuth } from "../../device-auth.js"; +import { READ_SCOPE } from "../../method-scopes.js"; import { isLocalishHost, isLoopbackAddress, @@ -506,9 +507,12 @@ export function attachGatewayWsMessageHandler(params: { }); close(1008, truncateCloseReason(authMessage)); }; - const clearUnboundScopes = () => { - if (scopes.length > 0) { - scopes = []; + const clearUnboundScopes = (grantDefaultForTokenAuth = false) => { + if (scopes.length > 0 || grantDefaultForTokenAuth) { + // When shared token/password auth succeeds for device-less operator, grant + // operator.read instead of zero scopes so CLI/Dashboard can run read RPCs + // (devices list, status, probe, etc.). Fixes #48167, #46117, #46716, #17095. + scopes = grantDefaultForTokenAuth ? [READ_SCOPE] : []; connectParams.scopes = scopes; } }; @@ -531,11 +535,19 @@ export function attachGatewayWsMessageHandler(params: { hasSharedAuth, isLocalClient, }); - // Shared token/password auth can bypass pairing for trusted operators. - // Device-less clients only keep self-declared scopes on the explicit - // allow path, including trusted token-authenticated backend operators. - if (!device && decision.kind !== "allow") { - clearUnboundScopes(); + // Shared token/password auth can bypass pairing for trusted operators, but + // device-less backend clients must not self-declare scopes. Control UI + // keeps its explicitly allowed device-less scopes on the allow path. + // When allowing device-less token auth, grant operator.read so read RPCs work. + // Restrict read-scope fallback to token/password auth only; trusted-proxy + // sessions must not gain read scope without a bound device identity. + const grantReadForTokenAuth = + decision.kind === "allow" && + sharedAuthOk && + (authMethod === "token" || authMethod === "password"); + if (!device && (!isControlUi || decision.kind !== "allow" || trustedProxyAuthOk)) { + clearUnboundScopes(grantReadForTokenAuth); + } } if (decision.kind === "allow") { return true; From 84ae2b63ebea2520704e88439287f4250f83731b Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 17 Mar 2026 06:10:33 +0000 Subject: [PATCH 4/7] chore(changelog): append gateway fix entries at end of Fixes block --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 40ba6231f40..cd523a99ad1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -190,7 +190,6 @@ Docs: https://docs.openclaw.ai - Exec/env sandbox: block build-tool JVM injection (`MAVEN_OPTS`, `SBT_OPTS`, `GRADLE_OPTS`, `ANT_OPTS`), glibc tunable exploitation (`GLIBC_TUNABLES`), and .NET dependency resolution hijack (`DOTNET_ADDITIONAL_DEPS`) from the host exec environment, and restrict Gradle init script redirect (`GRADLE_USER_HOME`) as an override-only block so user-configured Gradle homes still propagate. (#49702) - Plugins/Matrix: add a new Matrix plugin backed by the official `matrix-js-sdk`. If you are upgrading from the previous public Matrix plugin, follow the migration guide: https://docs.openclaw.ai/install/migrating-matrix Thanks @gumadeiras. - Discord/commands: switch native command deployment to Carbon reconcile by default so Discord restarts stop churning slash commands through OpenClaw’s local deploy path. (#46597) Thanks @huntharo and @thewilloftheshadow. - ## 2026.3.13 ### Changes From 7f0cfb98c7d521525d2b6f10c0ba6f8cf7b354ab Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Tue, 17 Mar 2026 18:40:11 +0000 Subject: [PATCH 5/7] fix(gateway): include device identity in authenticated loopback probes (#48805) --- CHANGELOG.md | 1 + src/gateway/probe.ts | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cd523a99ad1..c64bba039c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -137,6 +137,7 @@ Docs: https://docs.openclaw.ai - Agents/compaction: trigger overflow recovery from the tool-result guard once post-compaction context still exceeds the safe threshold, so long tool loops compact before the next model call hard-fails. (#29371) thanks @keshav55. - macOS/exec approvals: harden exec-host request HMAC verification to use a timing-safe compare and keep malformed or truncated signatures fail-closed in focused IPC auth coverage. - Gateway/exec approvals: surface requested env override keys in gateway-host approval prompts so operators can review surviving env context without inheriting noisy base host env. +- Gateway/probe: include device identity in authenticated loopback probes so `openclaw status` and probe RPCs get full paired scopes instead of being scope-limited. Strip identity only for effectively anonymous probes (opts.auth undefined or empty). (#48805) - Telegram/network: preserve sticky IPv4 fallback state across polling restarts so hosts with unstable IPv6 to `api.telegram.org` stop re-triggering repeated Telegram timeouts after each restart. (#48282) Thanks @yassinebkr. - Plugins/subagents: forward per-run provider and model overrides through gateway plugin subagent dispatch so plugin-launched agent delegations honor explicit model selection again. (#48277) Thanks @jalehman. - Agents/compaction: write minimal boundary summaries for empty preparations while keeping split-turn prefixes on the normal path, so no-summarizable-message sessions stop retriggering the safeguard loop. (#42215) thanks @lml2468. diff --git a/src/gateway/probe.ts b/src/gateway/probe.ts index bbd36639b78..f5c6b94ea87 100644 --- a/src/gateway/probe.ts +++ b/src/gateway/probe.ts @@ -45,9 +45,14 @@ export async function probeGateway(opts: { const disableDeviceIdentity = (() => { try { const hostname = new URL(opts.url).hostname; - // Local authenticated probes should stay device-bound so read/detail RPCs - // are not scope-limited by the shared-auth scope stripping hardening. - return isLoopbackHost(hostname) && !(opts.auth?.token || opts.auth?.password); + // Probes should stay device-bound whenever possible so read/detail RPCs + // are not scope-limited by shared-auth/anonymous scope stripping hardening. + // We used to disable identity for all local probes without token/password, + // but that breaks authenticated status checks when hardening is enabled. + // + // Now we only disable it for literal anonymous loopback probes (opts.auth + // undefined) to maintain legacy "no-setup" local status behavior. + return isLoopbackHost(hostname) && opts.auth === undefined; } catch { return false; } From 4d413b78eb43368eb601ea792625bec5e5d2141f Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Thu, 19 Mar 2026 16:51:24 +0000 Subject: [PATCH 6/7] fix(gateway): address PR review comments --- src/gateway/probe.ts | 12 +++++++++--- src/gateway/server/ws-connection/message-handler.ts | 1 - 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/gateway/probe.ts b/src/gateway/probe.ts index f5c6b94ea87..ffb16d57ae1 100644 --- a/src/gateway/probe.ts +++ b/src/gateway/probe.ts @@ -50,9 +50,15 @@ export async function probeGateway(opts: { // We used to disable identity for all local probes without token/password, // but that breaks authenticated status checks when hardening is enabled. // - // Now we only disable it for literal anonymous loopback probes (opts.auth - // undefined) to maintain legacy "no-setup" local status behavior. - return isLoopbackHost(hostname) && opts.auth === undefined; + // Disable device identity for loopback probes that are effectively + // unauthenticated: opts.auth undefined OR an auth object with no + // credentials. Callers like status/probe pass { token, password } from + // resolveGatewayProbeAuth even when both are missing; treat that as + // anonymous to preserve legacy "no-setup" local status behavior. + const hasCredentials = + (typeof opts.auth?.token === "string" && opts.auth.token.trim().length > 0) || + (typeof opts.auth?.password === "string" && opts.auth.password.trim().length > 0); + return isLoopbackHost(hostname) && !hasCredentials; } catch { return false; } diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index 544246949b8..41d8234f45b 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -548,7 +548,6 @@ export function attachGatewayWsMessageHandler(params: { if (!device && (!isControlUi || decision.kind !== "allow" || trustedProxyAuthOk)) { clearUnboundScopes(grantReadForTokenAuth); } - } if (decision.kind === "allow") { return true; } From 6e4ab036c76f0d94226db56c0edf70017fae2ba6 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Fri, 20 Mar 2026 13:04:55 +0000 Subject: [PATCH 7/7] fix(gateway): preserve trusted-proxy scopes on device-less allow path The previous condition cleared scopes for all device-less non-control-ui connections because !isControlUi made the OR always true. Split logic: clear on non-allow; normalize to operator.read only for allow + token/password. --- .../server/ws-connection/message-handler.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index 41d8234f45b..3abd47dde54 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -538,15 +538,19 @@ export function attachGatewayWsMessageHandler(params: { // Shared token/password auth can bypass pairing for trusted operators, but // device-less backend clients must not self-declare scopes. Control UI // keeps its explicitly allowed device-less scopes on the allow path. - // When allowing device-less token auth, grant operator.read so read RPCs work. - // Restrict read-scope fallback to token/password auth only; trusted-proxy - // sessions must not gain read scope without a bound device identity. + // When allowing device-less token/password auth, normalize to operator.read + // so read RPCs work. Do not auto-grant read for trusted-proxy (proxy proves + // identity but must not upgrade scope); preserve explicit scopes there. const grantReadForTokenAuth = decision.kind === "allow" && sharedAuthOk && (authMethod === "token" || authMethod === "password"); - if (!device && (!isControlUi || decision.kind !== "allow" || trustedProxyAuthOk)) { - clearUnboundScopes(grantReadForTokenAuth); + if (!device) { + if (decision.kind !== "allow") { + clearUnboundScopes(false); + } else if (!isControlUi && grantReadForTokenAuth) { + clearUnboundScopes(true); + } } if (decision.kind === "allow") { return true;