From ade9aaae89fd2c3b60bd96275b31870924732a89 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 11:09:21 +0100 Subject: [PATCH] fix(cli): classify scope-limited status probes as reachable --- CHANGELOG.md | 1 + src/commands/status.scan.shared.test.ts | 36 +++++++++++++++++++++++++ src/commands/status.scan.shared.ts | 3 ++- 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e805e2b52c4..1174a24b93c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai - Tasks/media: infer agent ownership for session-scoped task records so `/tasks` agent-local fallback includes session-backed `video_generate` and other async media jobs even when the current chat session has no linked rows. Thanks @vincentkoc. - Agents/media: keep long-running `video_generate` and `music_generate` tasks fresh while provider jobs are still pending, so task maintenance does not mark active Discord media renders lost before completion. Thanks @vincentkoc. +- CLI/status: treat scope-limited gateway probes as reachable-but-degraded in shared status scans, so `openclaw status --all` no longer reports a live gateway as unreachable after `missing scope: operator.read`. Fixes #49180; supersedes #47981. Thanks @openjay. - Plugins/inspector: keep bundled plugin runtime capture quiet and config-tolerant for Codex, memory-lancedb, Feishu, Mattermost, QQBot, and Tlon so plugin-inspector JSON checks can validate the full bundled set. Thanks @vincentkoc. - Slack/auto-reply: keep fully consumed text reset triggers such as `new session` out of `BodyForAgent` after directive cleanup, so configured Slack reset phrases do not leak into the fresh model turn. Fixes #73137. Thanks @neeravmakwana. - Plugins/runtime deps: prune stale retained bundled runtime deps and keep doctor/secret channel contract scans on lightweight artifacts, so disabled bundled channels stop preserving old dependency trees or importing heavy plugin surfaces. Thanks @SymbolStar and @vincentkoc. diff --git a/src/commands/status.scan.shared.test.ts b/src/commands/status.scan.shared.test.ts index dc882181066..72ebc7c574f 100644 --- a/src/commands/status.scan.shared.test.ts +++ b/src/commands/status.scan.shared.test.ts @@ -142,6 +142,42 @@ describe("resolveGatewayProbeSnapshot", () => { expect(result.gatewayProbe?.error).toBe("timeout; warn"); expect(result.gatewayProbeAuthWarning).toBeUndefined(); }); + + it("treats scope-limited read probes as reachable", async () => { + mocks.resolveGatewayProbeTarget.mockReturnValue({ + mode: "local", + gatewayMode: "local", + remoteUrlMissing: false, + }); + mocks.probeGateway.mockResolvedValue({ + ok: false, + url: "ws://127.0.0.1:18789", + connectLatencyMs: 51, + error: "missing scope: operator.read", + close: null, + auth: { + role: "operator", + scopes: [], + capability: "connected_no_operator_scope", + }, + server: { + version: null, + connId: null, + }, + health: null, + status: null, + presence: null, + configSnapshot: null, + }); + + const result = await resolveGatewayProbeSnapshot({ + cfg: {}, + opts: {}, + }); + + expect(result.gatewayReachable).toBe(true); + expect(result.gatewayProbe?.error).toBe("missing scope: operator.read; warn"); + }); }); describe("resolveSharedMemoryStatusSnapshot", () => { diff --git a/src/commands/status.scan.shared.ts b/src/commands/status.scan.shared.ts index 5d4ff372e2a..03f4e348235 100644 --- a/src/commands/status.scan.shared.ts +++ b/src/commands/status.scan.shared.ts @@ -11,6 +11,7 @@ import { normalizeOptionalString, } from "../shared/string-coerce.js"; import { pickGatewaySelfPresence } from "./gateway-presence.js"; +import { isProbeReachable } from "./gateway-status/helpers.js"; export { pickGatewaySelfPresence } from "./gateway-presence.js"; let gatewayProbeModulePromise: Promise | undefined; @@ -142,7 +143,7 @@ export async function resolveGatewayProbeSnapshot(params: { : gatewayProbeAuthWarning; gatewayProbeAuthWarning = undefined; } - const gatewayReachable = gatewayProbe?.ok === true; + const gatewayReachable = gatewayProbe ? isProbeReachable(gatewayProbe) : false; const gatewaySelf = gatewayProbe?.presence ? pickGatewaySelfPresence(gatewayProbe.presence) : null;