diff --git a/CHANGELOG.md b/CHANGELOG.md index a1836ec0ac..eeffe7eeac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai - Exec approvals: coerce bare string allowlist entries to objects to prevent allowlist corruption. (#9903, fixes #9790) Thanks @mcaxtr. - Heartbeat: allow explicit accountId routing for multi-account channels. (#8702) Thanks @lsh411. - TUI/Gateway: handle non-streaming finals, refresh history for non-local chat runs, and avoid event gap warnings for targeted tool streams. (#8432) Thanks @gumadeiras. +- Security: stop exposing Gateway auth tokens via URL query parameters in Control UI entrypoints, and reject hook tokens in query parameters. (#9436) Thanks @coygeek. - Shell completion: auto-detect and migrate slow dynamic patterns to cached files for faster terminal startup; add completion health checks to doctor/update/onboard. - Telegram: honor session model overrides in inline model selection. (#8193) Thanks @gildo. - Web UI: fix agent model selection saves for default/non-default agents and wrap long workspace paths. Thanks @Takhoffman. diff --git a/docs/gateway/configuration.md b/docs/gateway/configuration.md index 2c71447b5d..0a5a85f1d7 100644 --- a/docs/gateway/configuration.md +++ b/docs/gateway/configuration.md @@ -3173,8 +3173,7 @@ Defaults: Requests must include the hook token: - `Authorization: Bearer ` **or** -- `x-openclaw-token: ` **or** -- `?token=` +- `x-openclaw-token: ` Endpoints: diff --git a/docs/help/faq.md b/docs/help/faq.md index f43aa1569c..2c9e9f1be7 100644 --- a/docs/help/faq.md +++ b/docs/help/faq.md @@ -334,21 +334,21 @@ If you don't have a global install yet, run it via `pnpm openclaw onboard`. ### How do I open the dashboard after onboarding -The wizard now opens your browser with a tokenized dashboard URL right after onboarding and also prints the full link (with token) in the summary. Keep that tab open; if it didn't launch, copy/paste the printed URL on the same machine. Tokens stay local to your host-nothing is fetched from the browser. +The wizard opens your browser with a clean (non-tokenized) dashboard URL right after onboarding and also prints the link in the summary. Keep that tab open; if it didn't launch, copy/paste the printed URL on the same machine. ### How do I authenticate the dashboard token on localhost vs remote **Localhost (same machine):** - Open `http://127.0.0.1:18789/`. -- If it asks for auth, run `openclaw dashboard` and use the tokenized link (`?token=...`). -- The token is the same value as `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`) and is stored by the UI after first load. +- If it asks for auth, paste the token from `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`) into Control UI settings. +- Retrieve it from the gateway host: `openclaw config get gateway.auth.token` (or generate one: `openclaw doctor --generate-gateway-token`). **Not on localhost:** - **Tailscale Serve** (recommended): keep bind loopback, run `openclaw gateway --tailscale serve`, open `https:///`. If `gateway.auth.allowTailscale` is `true`, identity headers satisfy auth (no token). - **Tailnet bind**: run `openclaw gateway --bind tailnet --token ""`, open `http://:18789/`, paste token in dashboard settings. -- **SSH tunnel**: `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/?token=...` from `openclaw dashboard`. +- **SSH tunnel**: `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/` and paste the token in Control UI settings. See [Dashboard](/web/dashboard) and [Web surfaces](/web) for bind modes and auth details. @@ -2383,15 +2383,14 @@ Your gateway is running with auth enabled (`gateway.auth.*`), but the UI is not Facts (from code): - The Control UI stores the token in browser localStorage key `openclaw.control.settings.v1`. -- The UI can import `?token=...` (and/or `?password=...`) once, then strips it from the URL. Fix: -- Fastest: `openclaw dashboard` (prints + copies tokenized link, tries to open; shows SSH hint if headless). +- Fastest: `openclaw dashboard` (prints + copies the dashboard URL, tries to open; shows SSH hint if headless). - If you don't have a token yet: `openclaw doctor --generate-gateway-token`. -- If remote, tunnel first: `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/?token=...`. +- If remote, tunnel first: `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/`. - Set `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`) on the gateway host. -- In the Control UI settings, paste the same token (or refresh with a one-time `?token=...` link). +- In the Control UI settings, paste the same token. - Still stuck? Run `openclaw status --all` and follow [Troubleshooting](/gateway/troubleshooting). See [Dashboard](/web/dashboard) for auth details. ### I set gatewaybind tailnet but it cant bind nothing listens diff --git a/docs/install/docker.md b/docs/install/docker.md index 788540d9e8..252bdb1ac2 100644 --- a/docs/install/docker.md +++ b/docs/install/docker.md @@ -56,7 +56,7 @@ After it finishes: - Open `http://127.0.0.1:18789/` in your browser. - Paste the token into the Control UI (Settings → token). -- Need the tokenized URL again? Run `docker compose run --rm openclaw-cli dashboard --no-open`. +- Need the URL again? Run `docker compose run --rm openclaw-cli dashboard --no-open`. It writes config/workspace on the host: diff --git a/docs/install/exe-dev.md b/docs/install/exe-dev.md index 36b598de00..687233b114 100644 --- a/docs/install/exe-dev.md +++ b/docs/install/exe-dev.md @@ -103,9 +103,10 @@ server { ## 5) Access OpenClaw and grant privileges -Access `https://.exe.xyz/?token=YOUR-TOKEN-FROM-TERMINAL` (see the Control UI output from onboarding). Approve -devices with `openclaw devices list` and `openclaw devices approve `. When in doubt, -use Shelley from your browser! +Access `https://.exe.xyz/` (see the Control UI output from onboarding). If it prompts for auth, paste the +token from `gateway.auth.token` on the VM (retrieve with `openclaw config get gateway.auth.token`, or generate one +with `openclaw doctor --generate-gateway-token`). Approve devices with `openclaw devices list` and +`openclaw devices approve `. When in doubt, use Shelley from your browser! ## Remote Access diff --git a/docs/start/openclaw.md b/docs/start/openclaw.md index c750fa9c01..c5a4196351 100644 --- a/docs/start/openclaw.md +++ b/docs/start/openclaw.md @@ -74,7 +74,7 @@ openclaw gateway --port 18789 Now message the assistant number from your allowlisted phone. -When onboarding finishes, we auto-open the dashboard with your gateway token and print the tokenized link. To reopen later: `openclaw dashboard`. +When onboarding finishes, we auto-open the dashboard and print a clean (non-tokenized) link. If it prompts for auth, paste the token from `gateway.auth.token` into Control UI settings. To reopen later: `openclaw dashboard`. ## Give the agent a workspace (AGENTS) diff --git a/docs/web/dashboard.md b/docs/web/dashboard.md index 947091774f..d68456821d 100644 --- a/docs/web/dashboard.md +++ b/docs/web/dashboard.md @@ -29,18 +29,18 @@ Prefer localhost, Tailscale Serve, or an SSH tunnel. ## Fast path (recommended) -- After onboarding, the CLI now auto-opens the dashboard with your token and prints the same tokenized link. +- 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). -- The token stays local (query param only); the UI strips it after first load and saves it in localStorage. +- If the UI prompts for auth, paste the token from `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`) into Control UI settings. ## Token basics (local vs remote) -- **Localhost**: open `http://127.0.0.1:18789/`. If you see “unauthorized,” run `openclaw dashboard` and use the tokenized link (`?token=...`). -- **Token source**: `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`); the UI stores it after first load. +- **Localhost**: open `http://127.0.0.1:18789/`. +- **Token source**: `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`); the UI stores a copy in localStorage after you connect. - **Not localhost**: use Tailscale Serve (tokenless if `gateway.auth.allowTailscale: true`), tailnet bind with a token, or an SSH tunnel. See [Web surfaces](/web). ## If you see “unauthorized” / 1008 -- Run `openclaw dashboard` to get a fresh tokenized link. -- Ensure the gateway is reachable (local: `openclaw status`; remote: SSH tunnel `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/?token=...`). -- In the dashboard settings, paste the same token you configured in `gateway.auth.token` (or `OPENCLAW_GATEWAY_TOKEN`). +- Ensure the gateway is reachable (local: `openclaw status`; remote: SSH tunnel `ssh -N -L 18789:127.0.0.1:18789 user@host` then open `http://127.0.0.1:18789/`). +- Retrieve the token from the gateway host: `openclaw config get gateway.auth.token` (or generate one: `openclaw doctor --generate-gateway-token`). +- In the dashboard settings, paste the token into the auth field, then connect. diff --git a/src/agents/session-write-lock.ts b/src/agents/session-write-lock.ts index fce940ae14..7335abaf0b 100644 --- a/src/agents/session-write-lock.ts +++ b/src/agents/session-write-lock.ts @@ -18,34 +18,6 @@ const CLEANUP_SIGNALS = ["SIGINT", "SIGTERM", "SIGQUIT", "SIGABRT"] as const; type CleanupSignal = (typeof CLEANUP_SIGNALS)[number]; const cleanupHandlers = new Map void>(); -/** - * Release all held locks - called on process exit to prevent orphaned locks - */ -async function releaseAllLocks(): Promise { - const locks = Array.from(HELD_LOCKS.values()); - HELD_LOCKS.clear(); - for (const lock of locks) { - try { - await lock.handle.close(); - await fs.rm(lock.lockPath, { force: true }); - } catch { - // Best effort cleanup - } - } -} - -if (process.env.NODE_ENV !== "test" && !process.env.VITEST) { - // Register cleanup handlers to release locks on unexpected termination - process.on("exit", releaseAllLocks); - process.on("SIGTERM", () => { - void releaseAllLocks().then(() => process.exit(0)); - }); - process.on("SIGINT", () => { - void releaseAllLocks().then(() => process.exit(0)); - }); - // Note: unhandledRejection handler will call process.exit() which triggers 'exit' -} - function isAlive(pid: number): boolean { if (!Number.isFinite(pid) || pid <= 0) { return false; diff --git a/src/auto-reply/reply/dispatch-from-config.ts b/src/auto-reply/reply/dispatch-from-config.ts index a903300a20..f04aff0a7b 100644 --- a/src/auto-reply/reply/dispatch-from-config.ts +++ b/src/auto-reply/reply/dispatch-from-config.ts @@ -292,31 +292,32 @@ export async function dispatchReplyFromConfig(params: { let accumulatedBlockText = ""; let blockCount = 0; + const shouldSendToolSummaries = ctx.ChatType !== "group" && ctx.CommandSource !== "native"; + const replyResult = await (params.replyResolver ?? getReplyFromConfig)( ctx, { ...params.replyOptions, - onToolResult: - ctx.ChatType !== "group" && ctx.CommandSource !== "native" - ? (payload: ReplyPayload) => { - const run = async () => { - const ttsPayload = await maybeApplyTtsToPayload({ - payload, - cfg, - channel: ttsChannel, - kind: "tool", - inboundAudio, - ttsAuto: sessionTtsAuto, - }); - if (shouldRouteToOriginating) { - await sendPayloadAsync(ttsPayload, undefined, false); - } else { - dispatcher.sendToolResult(ttsPayload); - } - }; - return run(); - } - : undefined, + onToolResult: shouldSendToolSummaries + ? (payload: ReplyPayload) => { + const run = async () => { + const ttsPayload = await maybeApplyTtsToPayload({ + payload, + cfg, + channel: ttsChannel, + kind: "tool", + inboundAudio, + ttsAuto: sessionTtsAuto, + }); + if (shouldRouteToOriginating) { + await sendPayloadAsync(ttsPayload, undefined, false); + } else { + dispatcher.sendToolResult(ttsPayload); + } + }; + return run(); + } + : undefined, onBlockReply: (payload: ReplyPayload, context) => { const run = async () => { // Accumulate block text for TTS generation after streaming diff --git a/src/commands/dashboard.test.ts b/src/commands/dashboard.test.ts index 7e50a459e7..32112c4d38 100644 --- a/src/commands/dashboard.test.ts +++ b/src/commands/dashboard.test.ts @@ -83,8 +83,8 @@ describe("dashboardCommand", () => { customBindHost: undefined, basePath: undefined, }); - expect(mocks.copyToClipboard).toHaveBeenCalledWith("http://127.0.0.1:18789/?token=abc123"); - expect(mocks.openUrl).toHaveBeenCalledWith("http://127.0.0.1:18789/?token=abc123"); + expect(mocks.copyToClipboard).toHaveBeenCalledWith("http://127.0.0.1:18789/"); + expect(mocks.openUrl).toHaveBeenCalledWith("http://127.0.0.1:18789/"); expect(runtime.log).toHaveBeenCalledWith( "Opened in your browser. Keep that tab to control OpenClaw.", ); diff --git a/src/commands/dashboard.ts b/src/commands/dashboard.ts index bd47237df2..01a08f1a30 100644 --- a/src/commands/dashboard.ts +++ b/src/commands/dashboard.ts @@ -23,7 +23,6 @@ export async function dashboardCommand( const bind = cfg.gateway?.bind ?? "loopback"; const basePath = cfg.gateway?.controlUi?.basePath; const customBindHost = cfg.gateway?.customBindHost; - const token = cfg.gateway?.auth?.token ?? process.env.OPENCLAW_GATEWAY_TOKEN ?? ""; const links = resolveControlUiLinks({ port, @@ -31,11 +30,11 @@ export async function dashboardCommand( customBindHost, basePath, }); - const authedUrl = token ? `${links.httpUrl}?token=${encodeURIComponent(token)}` : links.httpUrl; + const dashboardUrl = links.httpUrl; - runtime.log(`Dashboard URL: ${authedUrl}`); + runtime.log(`Dashboard URL: ${dashboardUrl}`); - const copied = await copyToClipboard(authedUrl).catch(() => false); + const copied = await copyToClipboard(dashboardUrl).catch(() => false); runtime.log(copied ? "Copied to clipboard." : "Copy to clipboard unavailable."); let opened = false; @@ -43,13 +42,12 @@ export async function dashboardCommand( if (!options.noOpen) { const browserSupport = await detectBrowserOpenSupport(); if (browserSupport.ok) { - opened = await openUrl(authedUrl); + opened = await openUrl(dashboardUrl); } if (!opened) { hint = formatControlUiSshHint({ port, basePath, - token: token || undefined, }); } } else { diff --git a/src/commands/onboard-helpers.ts b/src/commands/onboard-helpers.ts index 55dcaa8582..f70c2dfb6d 100644 --- a/src/commands/onboard-helpers.ts +++ b/src/commands/onboard-helpers.ts @@ -179,23 +179,16 @@ export async function detectBrowserOpenSupport(): Promise { return { ok: true, command: resolved.command }; } -export function formatControlUiSshHint(params: { - port: number; - basePath?: string; - token?: string; -}): string { +export function formatControlUiSshHint(params: { port: number; basePath?: string }): string { const basePath = normalizeControlUiBasePath(params.basePath); const uiPath = basePath ? `${basePath}/` : "/"; const localUrl = `http://localhost:${params.port}${uiPath}`; - const tokenParam = params.token ? `?token=${encodeURIComponent(params.token)}` : ""; - const authedUrl = params.token ? `${localUrl}${tokenParam}` : undefined; const sshTarget = resolveSshTargetHint(); return [ "No GUI detected. Open from your computer:", `ssh -N -L ${params.port}:127.0.0.1:${params.port} ${sshTarget}`, "Then open:", localUrl, - authedUrl, "Docs:", "https://docs.openclaw.ai/gateway/remote", "https://docs.openclaw.ai/web/control-ui", diff --git a/src/gateway/hooks.test.ts b/src/gateway/hooks.test.ts index 550ba9caf3..811911221e 100644 --- a/src/gateway/hooks.test.ts +++ b/src/gateway/hooks.test.ts @@ -39,29 +39,25 @@ describe("gateway hooks helpers", () => { expect(() => resolveHooksConfig(cfg)).toThrow("hooks.path may not be '/'"); }); - test("extractHookToken prefers bearer > header > query", () => { + test("extractHookToken prefers bearer > header", () => { const req = { headers: { authorization: "Bearer top", "x-openclaw-token": "header", }, } as unknown as IncomingMessage; - const url = new URL("http://localhost/hooks/wake?token=query"); - const result1 = extractHookToken(req, url); - expect(result1.token).toBe("top"); - expect(result1.fromQuery).toBe(false); + const result1 = extractHookToken(req); + expect(result1).toBe("top"); const req2 = { headers: { "x-openclaw-token": "header" }, } as unknown as IncomingMessage; - const result2 = extractHookToken(req2, url); - expect(result2.token).toBe("header"); - expect(result2.fromQuery).toBe(false); + const result2 = extractHookToken(req2); + expect(result2).toBe("header"); const req3 = { headers: {} } as unknown as IncomingMessage; - const result3 = extractHookToken(req3, url); - expect(result3.token).toBe("query"); - expect(result3.fromQuery).toBe(true); + const result3 = extractHookToken(req3); + expect(result3).toBeUndefined(); }); test("normalizeWakePayload trims + validates", () => { diff --git a/src/gateway/hooks.ts b/src/gateway/hooks.ts index 543faf747a..fe79f0f383 100644 --- a/src/gateway/hooks.ts +++ b/src/gateway/hooks.ts @@ -43,18 +43,13 @@ export function resolveHooksConfig(cfg: OpenClawConfig): HooksConfigResolved | n }; } -export type HookTokenResult = { - token: string | undefined; - fromQuery: boolean; -}; - -export function extractHookToken(req: IncomingMessage, url: URL): HookTokenResult { +export function extractHookToken(req: IncomingMessage): string | undefined { const auth = typeof req.headers.authorization === "string" ? req.headers.authorization.trim() : ""; if (auth.toLowerCase().startsWith("bearer ")) { const token = auth.slice(7).trim(); if (token) { - return { token, fromQuery: false }; + return token; } } const headerToken = @@ -62,13 +57,9 @@ export function extractHookToken(req: IncomingMessage, url: URL): HookTokenResul ? req.headers["x-openclaw-token"].trim() : ""; if (headerToken) { - return { token: headerToken, fromQuery: false }; + return headerToken; } - const queryToken = url.searchParams.get("token"); - if (queryToken) { - return { token: queryToken.trim(), fromQuery: true }; - } - return { token: undefined, fromQuery: false }; + return undefined; } export async function readJsonBody( diff --git a/src/gateway/server-http.ts b/src/gateway/server-http.ts index 8e63deecf8..66a6f725ab 100644 --- a/src/gateway/server-http.ts +++ b/src/gateway/server-http.ts @@ -147,20 +147,22 @@ export function createHooksRequestHandler( return false; } - const { token, fromQuery } = extractHookToken(req, url); + if (url.searchParams.has("token")) { + res.statusCode = 400; + res.setHeader("Content-Type", "text/plain; charset=utf-8"); + res.end( + "Hook token must be provided via Authorization: Bearer or X-OpenClaw-Token header (query parameters are not allowed).", + ); + return true; + } + + const token = extractHookToken(req); if (!token || token !== hooksConfig.token) { res.statusCode = 401; res.setHeader("Content-Type", "text/plain; charset=utf-8"); res.end("Unauthorized"); return true; } - if (fromQuery) { - logHooks.warn( - "Hook token provided via query parameter is deprecated for security reasons. " + - "Tokens in URLs appear in logs, browser history, and referrer headers. " + - "Use Authorization: Bearer or X-OpenClaw-Token header instead.", - ); - } if (req.method !== "POST") { res.statusCode = 405; diff --git a/src/gateway/server.hooks.e2e.test.ts b/src/gateway/server.hooks.e2e.test.ts index 97e4e37ef4..93a311a60f 100644 --- a/src/gateway/server.hooks.e2e.test.ts +++ b/src/gateway/server.hooks.e2e.test.ts @@ -88,10 +88,7 @@ describe("gateway server hooks", () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ text: "Query auth" }), }); - expect(resQuery.status).toBe(200); - const queryEvents = await waitForSystemEvent(); - expect(queryEvents.some((e) => e.includes("Query auth"))).toBe(true); - drainSystemEvents(resolveMainKey()); + expect(resQuery.status).toBe(400); const resBadChannel = await fetch(`http://127.0.0.1:${port}/hooks/agent`, { method: "POST", diff --git a/src/gateway/server/ws-connection/message-handler.ts b/src/gateway/server/ws-connection/message-handler.ts index ad43630280..89bd9531f7 100644 --- a/src/gateway/server/ws-connection/message-handler.ts +++ b/src/gateway/server/ws-connection/message-handler.ts @@ -85,7 +85,7 @@ function formatGatewayAuthFailureMessage(params: { const isCli = isGatewayCliClient(client); const isControlUi = client?.id === GATEWAY_CLIENT_IDS.CONTROL_UI; const isWebchat = isWebchatClient(client); - const uiHint = "open a tokenized dashboard URL or paste token in Control UI settings"; + const uiHint = "open the dashboard URL and paste the token in Control UI settings"; const tokenHint = isCli ? "set gateway.remote.token to match gateway.auth.token" : isControlUi || isWebchat diff --git a/src/wizard/onboarding.finalize.ts b/src/wizard/onboarding.finalize.ts index 3ec0c50aca..568155169f 100644 --- a/src/wizard/onboarding.finalize.ts +++ b/src/wizard/onboarding.finalize.ts @@ -255,11 +255,7 @@ export async function finalizeOnboardingWizard( customBindHost: settings.customBindHost, basePath: controlUiBasePath, }); - const tokenParam = - settings.authMode === "token" && settings.gatewayToken - ? `?token=${encodeURIComponent(settings.gatewayToken)}` - : ""; - const authedUrl = `${links.httpUrl}${tokenParam}`; + const dashboardUrl = links.httpUrl; const gatewayProbe = await probeGatewayReachable({ url: links.wsUrl, token: settings.authMode === "token" ? settings.gatewayToken : undefined, @@ -279,8 +275,7 @@ export async function finalizeOnboardingWizard( await prompter.note( [ - `Web UI: ${links.httpUrl}`, - tokenParam ? `Web UI (with token): ${authedUrl}` : undefined, + `Web UI: ${dashboardUrl}`, `Gateway WS: ${links.wsUrl}`, gatewayStatusLine, "Docs: https://docs.openclaw.ai/web/control-ui", @@ -313,8 +308,11 @@ export async function finalizeOnboardingWizard( [ "Gateway token: shared auth for the Gateway + Control UI.", "Stored in: ~/.openclaw/openclaw.json (gateway.auth.token) or OPENCLAW_GATEWAY_TOKEN.", + `View token: ${formatCliCommand("openclaw config get gateway.auth.token")}`, + `Generate token: ${formatCliCommand("openclaw doctor --generate-gateway-token")}`, "Web UI stores a copy in this browser's localStorage (openclaw.control.settings.v1).", - `Get the tokenized link anytime: ${formatCliCommand("openclaw dashboard --no-open")}`, + `Open the dashboard anytime: ${formatCliCommand("openclaw dashboard --no-open")}`, + "Paste the token into Control UI settings if prompted.", ].join("\n"), "Token", ); @@ -343,24 +341,22 @@ export async function finalizeOnboardingWizard( } else if (hatchChoice === "web") { const browserSupport = await detectBrowserOpenSupport(); if (browserSupport.ok) { - controlUiOpened = await openUrl(authedUrl); + controlUiOpened = await openUrl(dashboardUrl); if (!controlUiOpened) { controlUiOpenHint = formatControlUiSshHint({ port: settings.port, basePath: controlUiBasePath, - token: settings.gatewayToken, }); } } else { controlUiOpenHint = formatControlUiSshHint({ port: settings.port, basePath: controlUiBasePath, - token: settings.gatewayToken, }); } await prompter.note( [ - `Dashboard link (with token): ${authedUrl}`, + `Dashboard link: ${dashboardUrl}`, controlUiOpened ? "Opened in your browser. Keep that tab to control OpenClaw." : "Copy/paste this URL in a browser on this machine to control OpenClaw.", @@ -446,25 +442,23 @@ export async function finalizeOnboardingWizard( if (shouldOpenControlUi) { const browserSupport = await detectBrowserOpenSupport(); if (browserSupport.ok) { - controlUiOpened = await openUrl(authedUrl); + controlUiOpened = await openUrl(dashboardUrl); if (!controlUiOpened) { controlUiOpenHint = formatControlUiSshHint({ port: settings.port, basePath: controlUiBasePath, - token: settings.gatewayToken, }); } } else { controlUiOpenHint = formatControlUiSshHint({ port: settings.port, basePath: controlUiBasePath, - token: settings.gatewayToken, }); } await prompter.note( [ - `Dashboard link (with token): ${authedUrl}`, + `Dashboard link: ${dashboardUrl}`, controlUiOpened ? "Opened in your browser. Keep that tab to control OpenClaw." : "Copy/paste this URL in a browser on this machine to control OpenClaw.", @@ -511,10 +505,10 @@ export async function finalizeOnboardingWizard( await prompter.outro( controlUiOpened - ? "Onboarding complete. Dashboard opened with your token; keep that tab to control OpenClaw." + ? "Onboarding complete. Dashboard opened; keep that tab to control OpenClaw." : seededInBackground - ? "Onboarding complete. Web UI seeded in the background; open it anytime with the tokenized link above." - : "Onboarding complete. Use the tokenized dashboard link above to control OpenClaw.", + ? "Onboarding complete. Web UI seeded in the background; open it anytime with the dashboard link above." + : "Onboarding complete. Use the dashboard link above to control OpenClaw.", ); return { launchedTui }; diff --git a/ui/src/ui/app-settings.ts b/ui/src/ui/app-settings.ts index b54b17ae09..f537ff1eab 100644 --- a/ui/src/ui/app-settings.ts +++ b/ui/src/ui/app-settings.ts @@ -112,19 +112,11 @@ export function applySettingsFromUrl(host: SettingsHost) { let shouldCleanUrl = false; if (tokenRaw != null) { - const token = tokenRaw.trim(); - if (token && token !== host.settings.token) { - applySettings(host, { ...host.settings, token }); - } params.delete("token"); shouldCleanUrl = true; } if (passwordRaw != null) { - const password = passwordRaw.trim(); - if (password) { - (host as unknown as { password: string }).password = password; - } params.delete("password"); shouldCleanUrl = true; } diff --git a/ui/src/ui/navigation.browser.test.ts b/ui/src/ui/navigation.browser.test.ts index c6bafa9c15..02a3e247a0 100644 --- a/ui/src/ui/navigation.browser.test.ts +++ b/ui/src/ui/navigation.browser.test.ts @@ -151,25 +151,25 @@ describe("control UI routing", () => { expect(container.scrollTop).toBe(maxScroll); }); - it("hydrates token from URL params and strips it", async () => { + it("strips token URL params without importing them", async () => { const app = mountApp("/ui/overview?token=abc123"); await app.updateComplete; - expect(app.settings.token).toBe("abc123"); + expect(app.settings.token).toBe(""); expect(window.location.pathname).toBe("/ui/overview"); expect(window.location.search).toBe(""); }); - it("hydrates password from URL params and strips it", async () => { + it("strips password URL params without importing them", async () => { const app = mountApp("/ui/overview?password=sekret"); await app.updateComplete; - expect(app.password).toBe("sekret"); + expect(app.password).toBe(""); expect(window.location.pathname).toBe("/ui/overview"); expect(window.location.search).toBe(""); }); - it("hydrates token from URL params even when settings already set", async () => { + it("does not override stored settings from URL token params", async () => { localStorage.setItem( "openclaw.control.settings.v1", JSON.stringify({ token: "existing-token" }), @@ -177,7 +177,7 @@ describe("control UI routing", () => { const app = mountApp("/ui/overview?token=abc123"); await app.updateComplete; - expect(app.settings.token).toBe("abc123"); + expect(app.settings.token).toBe("existing-token"); expect(window.location.pathname).toBe("/ui/overview"); expect(window.location.search).toBe(""); }); diff --git a/ui/src/ui/navigation.test.ts b/ui/src/ui/navigation.test.ts index c4552f0ca0..4ff0279341 100644 --- a/ui/src/ui/navigation.test.ts +++ b/ui/src/ui/navigation.test.ts @@ -26,23 +26,23 @@ describe("iconForTab", () => { }); it("returns stable icons for known tabs", () => { - expect(iconForTab("chat")).toBe("💬"); - expect(iconForTab("overview")).toBe("📊"); - expect(iconForTab("channels")).toBe("🔗"); - expect(iconForTab("instances")).toBe("📡"); - expect(iconForTab("sessions")).toBe("📄"); - expect(iconForTab("cron")).toBe("⏰"); - expect(iconForTab("skills")).toBe("⚡️"); - expect(iconForTab("nodes")).toBe("🖥️"); - expect(iconForTab("config")).toBe("⚙️"); - expect(iconForTab("debug")).toBe("🐞"); - expect(iconForTab("logs")).toBe("🧾"); + expect(iconForTab("chat")).toBe("messageSquare"); + expect(iconForTab("overview")).toBe("barChart"); + expect(iconForTab("channels")).toBe("link"); + expect(iconForTab("instances")).toBe("radio"); + expect(iconForTab("sessions")).toBe("fileText"); + expect(iconForTab("cron")).toBe("loader"); + expect(iconForTab("skills")).toBe("zap"); + expect(iconForTab("nodes")).toBe("monitor"); + expect(iconForTab("config")).toBe("settings"); + expect(iconForTab("debug")).toBe("bug"); + expect(iconForTab("logs")).toBe("scrollText"); }); it("returns a fallback icon for unknown tab", () => { // TypeScript won't allow this normally, but runtime could receive unexpected values const unknownTab = "unknown" as Tab; - expect(iconForTab(unknownTab)).toBe("📁"); + expect(iconForTab(unknownTab)).toBe("folder"); }); }); diff --git a/ui/src/ui/views/overview.ts b/ui/src/ui/views/overview.ts index 142dbe20e8..fbc417d41e 100644 --- a/ui/src/ui/views/overview.ts +++ b/ui/src/ui/views/overview.ts @@ -44,7 +44,7 @@ export function renderOverview(props: OverviewProps) {
This gateway requires auth. Add a token or password, then click Connect.
- openclaw dashboard --no-open → tokenized URL
+ openclaw dashboard --no-open → open the Control UI
openclaw doctor --generate-gateway-token → set token
@@ -62,8 +62,7 @@ export function renderOverview(props: OverviewProps) { } return html`
- Auth failed. Re-copy a tokenized URL with - openclaw dashboard --no-open, or update the token, then click Connect. + Auth failed. Update the token or password in Control UI settings, then click Connect.