fix(dashboard): guide manual token auth fallback

Summary:
- Add a redaction-safe dashboard fallback hint when tokenized URL delivery fails.
- Document the manual auth path and update the changelog.

Verification:
- PR CI exact head 48ccb97c08 green for relevant CI/security checks.
- pnpm test src/commands/dashboard.links.test.ts src/commands/dashboard.test.ts
- pnpm exec oxfmt --check --threads=1 src/commands/dashboard.ts src/commands/dashboard.links.test.ts
- pnpm format:docs:check
- pnpm docs:check-mdx
- pnpm docs:check-i18n-glossary
- targeted markdownlint for docs/cli/dashboard.md and docs/web/dashboard.md
This commit is contained in:
praveen9354
2026-05-04 16:39:25 -07:00
committed by GitHub
parent 29eb47e736
commit 0677a4f8b3
5 changed files with 54 additions and 2 deletions

View File

@@ -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 `<think>` 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.

View File

@@ -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.

View File

@@ -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.

View File

@@ -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.",
);

View File

@@ -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.",
);
}
}