diff --git a/CHANGELOG.md b/CHANGELOG.md index f06bd2838d9..393fa54d401 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -98,6 +98,7 @@ Docs: https://docs.openclaw.ai - Plugins/web: reuse manifest records already loaded for bundled web provider candidate discovery when falling back to public artifact provider loading. Thanks @shakkernerd. - Mattermost: keep direct-message replies top-level by suppressing reply roots for DM delivery while preserving channel and group thread roots, and derive inbound chat kind from the trusted channel lookup instead of the websocket event channel type. Carries forward #60115, #55186, #72305, and #72659; refs #59758, #59981, #59791, and #57565. Thanks @vincentkoc, @jwchmodx, and @hnykda. - Docker: pre-create `/home/node/.openclaw` with node ownership and private permissions so first-run Docker Compose named volumes no longer fail startup with EACCES. (#48072, #63959; fixes #61279) Thanks @timoxue and @jeanibarz. +- CLI/Gateway: treat local restart probe policy closes for connect, exact `device required`, pairing, and auth failures as Gateway reachability proof without accepting empty, broad standalone token/password/scope/role, or pair-substring 1008 close reasons. Fixes #48771; carries forward #48801; related #63491. Thanks @MarsDoge and @genoooool. - Process/Windows: decode command stdout and stderr from raw bytes with console-codepage awareness, while preserving valid UTF-8 output and multibyte characters split across chunks. Fixes #50519. Thanks @iready, @kevinten10, @zhangyongjie1997, @knightplat-blip, @heiqishi666, and @slepybear. - Bonjour/Windows: hide the bundled mDNS advertiser's Windows ARP shell probe so Gateway startup no longer flashes command-prompt windows. Fixes #70238. Thanks @alexandre-leng, @PratikRai0101, @infinitypacific, and @tomerpeled. - Agents/bootstrap: dedupe hook-injected bootstrap context files by workspace-relative path and store normalized resolved paths so duplicate relative and absolute hook paths no longer depend on the process cwd. (#59344; fixes #59319; related #56721, #56725, and #57587) Thanks @koen666. diff --git a/src/cli/daemon-cli/restart-health.test.ts b/src/cli/daemon-cli/restart-health.test.ts index 1c33c5c0fef..81f4964efa8 100644 --- a/src/cli/daemon-cli/restart-health.test.ts +++ b/src/cli/daemon-cli/restart-health.test.ts @@ -241,14 +241,60 @@ describe("inspectGatewayRestart", () => { expect(snapshot.staleGatewayPids).toEqual([]); }); - it("treats auth-closed probe as healthy gateway reachability", async () => { - const snapshot = await inspectAmbiguousOwnershipWithProbe({ - ok: false, - close: { code: 1008, reason: "auth required" }, - }); + it.each([ + "auth required", + "owner auth required", + "connect failed", + "device required", + "pairing required", + "pairing required: device is asking for more scopes than currently approved", + "unauthorized: gateway token missing (set gateway.remote.token to match gateway.auth.token)", + "unauthorized: gateway password mismatch (set gateway.remote.password to match gateway.auth.password)", + "unauthorized: device token rejected (pair/repair this device, or provide gateway token)", + ])( + "treats local policy-close probe reason %s as healthy gateway reachability", + async (reason) => { + const snapshot = await inspectAmbiguousOwnershipWithProbe({ + ok: false, + close: { code: 1008, reason }, + }); - expect(snapshot.healthy).toBe(true); - }); + expect(snapshot.healthy).toBe(true); + }, + ); + + it.each([ + "", + " ", + "repair required", + "repairing required", + "unpairing required", + "device", + "device required by local spoof", + "device required: identity missing", + "device identity required", + "connect challenge missing nonce", + "connect challenge timeout", + "authoritative policy close", + "device identity mismatch", + "device signature invalid", + "device nonce required", + "token expired", + "password required", + "missing scope: operator.admin", + "role denied", + "unauthorized: session revoked", + ])( + "does not treat ambiguous 1008 close reason %s as healthy gateway reachability", + async (reason) => { + const snapshot = await inspectAmbiguousOwnershipWithProbe({ + ok: false, + close: { code: 1008, reason }, + }); + + expect(snapshot.healthy).toBe(false); + }, + ); it("requires the expected gateway version when provided", async () => { probeGateway.mockResolvedValue({ diff --git a/src/cli/daemon-cli/restart-health.ts b/src/cli/daemon-cli/restart-health.ts index 1eba53b970d..be28920122b 100644 --- a/src/cli/daemon-cli/restart-health.ts +++ b/src/cli/daemon-cli/restart-health.ts @@ -83,12 +83,33 @@ function looksLikeAuthClose(code: number | undefined, reason: string | undefined return false; } const normalized = normalizeLowercaseStringOrEmpty(reason); + if (!normalized) { + return false; + } + // The restart probe runs against loopback only and only decides restart + // liveness, not authorization. Keep this allowlist exact so a local listener + // cannot satisfy the health check with broad device/auth-looking text. return ( - normalized.includes("auth") || - normalized.includes("token") || - normalized.includes("password") || - normalized.includes("scope") || - normalized.includes("role") + normalized === "auth required" || + normalized === "owner auth required" || + normalized === "connect failed" || + normalized === "device required" || + normalized === "pairing required" || + normalized.startsWith("pairing required:") || + normalized.startsWith("unauthorized: gateway token missing") || + normalized.startsWith("unauthorized: gateway token mismatch") || + normalized.startsWith("unauthorized: gateway token not configured") || + normalized.startsWith("unauthorized: gateway password missing") || + normalized.startsWith("unauthorized: gateway password mismatch") || + normalized.startsWith("unauthorized: gateway password not configured") || + normalized.startsWith("unauthorized: bootstrap token invalid or expired") || + normalized.startsWith("unauthorized: tailscale identity missing") || + normalized.startsWith("unauthorized: tailscale proxy headers missing") || + normalized.startsWith("unauthorized: tailscale identity check failed") || + normalized.startsWith("unauthorized: tailscale identity mismatch") || + normalized.startsWith("unauthorized: too many failed authentication attempts") || + normalized.startsWith("unauthorized: device token mismatch") || + normalized.startsWith("unauthorized: device token rejected") ); }