diff --git a/CHANGELOG.md b/CHANGELOG.md index badd1c920d5..a75c3485298 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1735,6 +1735,7 @@ Docs: https://docs.openclaw.ai - Control UI: show loading, reload, and retry states when a lazy dashboard panel cannot load after an upgrade, so the Logs tab no longer appears blank on stale browser bundles. Fixes #72450. Thanks @sobergou. - Gateway/plugins: start the Gateway in degraded mode when a single plugin entry has invalid schema config, and let `openclaw doctor --fix` quarantine that plugin config instead of crash-looping every channel. Fixes #62976 and #70371. Thanks @Doraemon-Claw and @pksidekyk. - Agents/plugins: skip malformed plugin tools with missing schema objects and report plugin diagnostics, so one broken tool no longer crashes Anthropic agent runs. Fixes #69423. Thanks @jmnickels. +- Dashboard: log a CVE-safe self-recovery hint pointing users to `OPENCLAW_GATEWAY_TOKEN`, `gateway.auth.token`, and fragment key `token` when neither clipboard nor browser delivery places the token-bearing URL within reach, so headless and WSL invocations are not stranded on the bare URL. Fixes #72081. Thanks @praveen9354 and @BunsDev. - Agents/reasoning: recover fully wrapped unclosed `` replies that would otherwise sanitize to empty text while keeping strict stripping for closed reasoning blocks and unclosed tails after visible text. Fixes #37696; supersedes #51915. Thanks @druide67 and @okuyam2y. - Control UI/Gateway: bind WebChat handshakes to their active socket and reject post-close server registrations, so aborted connects no longer leave zombie clients or misleading duplicate WebSocket connection logs. Fixes #72753. Thanks @LumenFromTheFuture. - Agents/fallback: split ambiguous provider failures into `empty_response`, `no_error_details`, and `unclassified`, and add flat fallback-step fields to structured fallback logs so primary-model failures stay visible when later fallbacks also fail. Fixes #71922; refs #71744. Thanks @andyk-ms and @nikolaykazakovvs-ux. diff --git a/docs/cli/dashboard.md b/docs/cli/dashboard.md index 1603156eac6..e06caaa1f26 100644 --- a/docs/cli/dashboard.md +++ b/docs/cli/dashboard.md @@ -20,6 +20,10 @@ Notes: - `dashboard` resolves configured `gateway.auth.token` SecretRefs when possible. - `dashboard` follows `gateway.tls.enabled`: TLS-enabled gateways print/open `https://` Control UI URLs and connect over `wss://`. +- If clipboard/browser delivery fails for a token-authenticated dashboard URL, + `dashboard` logs a safe manual-auth hint naming `OPENCLAW_GATEWAY_TOKEN`, + `gateway.auth.token`, and fragment key `token` without printing the token + value. - For SecretRef-managed tokens (resolved or unresolved), `dashboard` prints/copies/opens a non-tokenized URL to avoid exposing external secrets in terminal output, clipboard history, or browser-launch arguments. - If `gateway.auth.token` is SecretRef-managed but unresolved in this command path, the command prints a non-tokenized URL and explicit remediation guidance instead of embedding an invalid token placeholder. diff --git a/docs/web/dashboard.md b/docs/web/dashboard.md index 403f98f921a..e833eee02fc 100644 --- a/docs/web/dashboard.md +++ b/docs/web/dashboard.md @@ -39,6 +39,10 @@ Prefer localhost, Tailscale Serve, or an SSH tunnel. - After onboarding, the CLI auto-opens the dashboard and prints a clean (non-tokenized) link. - Re-open anytime: `openclaw dashboard` (copies link, opens browser if possible, shows SSH hint if headless). +- If clipboard and browser delivery fail, `openclaw dashboard` still prints the + clean URL and tells you to use the token from `OPENCLAW_GATEWAY_TOKEN` or + `gateway.auth.token` as the URL fragment key `token`; it does not print token + values in logs. - If the UI prompts for shared-secret auth, paste the configured token or password into Control UI settings. diff --git a/src/commands/dashboard.links.test.ts b/src/commands/dashboard.links.test.ts index e5a2fb5175c..287430fa6e4 100644 --- a/src/commands/dashboard.links.test.ts +++ b/src/commands/dashboard.links.test.ts @@ -166,6 +166,27 @@ describe("dashboardCommand", () => { } }); + it("guides user to manual auth when delivery channels both fail (CVE-safe)", async () => { + const secretToken = "super-secret-bearer-token"; + mockSnapshot(secretToken); + copyToClipboardMock.mockResolvedValue(false); + detectBrowserOpenSupportMock.mockResolvedValue({ ok: false, reason: "ssh" }); + formatControlUiSshHintMock.mockReturnValue("ssh hint without token"); + + await dashboardCommand(runtime); + + const allLogs = runtime.log.mock.calls.map((call) => String(call[0])).join("\n"); + + // CVE: token value and fragment marker must not appear in logs. + expect(allLogs).not.toContain(secretToken); + expect(allLogs).not.toContain("#token="); + + // UX: user must be pointed to where their token lives so they can self-recover. + expect(allLogs).toMatch(/OPENCLAW_GATEWAY_TOKEN/); + // UX: hint must name the URL fragment key so the user knows the syntax. + expect(allLogs).toContain("key `token`"); + }); + it("respects --no-open and tells user token URL is in clipboard", async () => { mockSnapshot("abc"); copyToClipboardMock.mockResolvedValue(true); @@ -179,12 +200,25 @@ describe("dashboardCommand", () => { ); }); - it("respects --no-open with plain URL hint when clipboard fails", async () => { + it("respects --no-open and falls through to manual-auth hint when clipboard fails (token configured)", async () => { mockSnapshot("abc"); copyToClipboardMock.mockResolvedValue(false); await dashboardCommand(runtime, { noOpen: true }); + // Redundant fallback hint is suppressed when the manual-auth hint speaks. + expect(runtime.log).not.toHaveBeenCalledWith( + "Browser launch disabled (--no-open). Use the URL above.", + ); + expect(runtime.log).toHaveBeenCalledWith(expect.stringContaining("OPENCLAW_GATEWAY_TOKEN")); + }); + + it("respects --no-open with plain URL hint when clipboard fails and no token is configured", async () => { + mockSnapshot(""); + copyToClipboardMock.mockResolvedValue(false); + + await dashboardCommand(runtime, { noOpen: true }); + expect(runtime.log).toHaveBeenCalledWith( "Browser launch disabled (--no-open). Use the URL above.", ); diff --git a/src/commands/dashboard.ts b/src/commands/dashboard.ts index 429031bd8f8..ec4891f8796 100644 --- a/src/commands/dashboard.ts +++ b/src/commands/dashboard.ts @@ -86,9 +86,18 @@ export async function dashboardCommand( : "Browser launch disabled (--no-open). Use the URL above."; } + const fallbackToManualAuth = !copied && !opened && includeTokenInUrl; + const suppressNoOpenHint = options.noOpen === true && fallbackToManualAuth; + if (opened) { runtime.log("Opened in your browser. Keep that tab to control OpenClaw."); - } else if (hint) { + } else if (hint && !suppressNoOpenHint) { runtime.log(hint); } + + if (fallbackToManualAuth) { + runtime.log( + "Token auto-auth not delivered. Append your gateway token (from OPENCLAW_GATEWAY_TOKEN or gateway.auth.token) as a URL fragment with key `token` to authenticate.", + ); + } }