diff --git a/CHANGELOG.md b/CHANGELOG.md index f84b3253b..b9058a58c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai - Auto-reply/TTS: keep tool-result media delivery enabled in group chats and native command sessions (while still suppressing tool summary text) so `NO_REPLY` follow-ups do not drop successful TTS audio. (#17991) Thanks @zerone0x. - Cron: preserve per-job schedule-error isolation in post-run maintenance recompute so malformed sibling jobs no longer abort persistence of successful runs. (#17852) Thanks @pierreeurope. - CLI/Pairing: make `openclaw qr --remote` prefer `gateway.remote.url` over tailscale/public URL resolution and register the `openclaw clawbot qr` legacy alias path. (#18091) +- CLI/QR: restore fail-fast validation for `openclaw qr --remote` when neither `gateway.remote.url` nor tailscale `serve`/`funnel` is configured, preventing unusable remote pairing QR flows. (#18166) Thanks @mbelinky. ## 2026.2.15 diff --git a/src/cli/qr-cli.test.ts b/src/cli/qr-cli.test.ts index 263a640b4..361186e21 100644 --- a/src/cli/qr-cli.test.ts +++ b/src/cli/qr-cli.test.ts @@ -183,6 +183,24 @@ describe("registerQrCli", () => { expect(payload.urlSource).toBe("gateway.remote.url"); }); + it("errors when --remote is set but no remote URL is configured", async () => { + loadConfig.mockReturnValue({ + gateway: { + bind: "custom", + customBindHost: "gateway.local", + auth: { mode: "token", token: "tok" }, + }, + }); + + const program = new Command(); + registerQrCli(program); + + await expect(program.parseAsync(["qr", "--remote"], { from: "user" })).rejects.toThrow("exit"); + + const output = runtime.error.mock.calls.map((call) => String(call[0] ?? "")).join("\n"); + expect(output).toContain("qr --remote requires"); + }); + it("prefers gateway.remote.url over tailscale when --remote is set", async () => { loadConfig.mockReturnValue({ gateway: { diff --git a/src/cli/qr-cli.ts b/src/cli/qr-cli.ts index 06de0291a..947a24b2d 100644 --- a/src/cli/qr-cli.ts +++ b/src/cli/qr-cli.ts @@ -95,6 +95,17 @@ export function registerQrCli(program: Command) { cfg.gateway.auth.token = undefined; } } + if (wantsRemote && !opts.url && !opts.publicUrl) { + const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off"; + const remoteUrl = cfg.gateway?.remote?.url; + const hasRemoteUrl = typeof remoteUrl === "string" && remoteUrl.trim().length > 0; + const hasTailscaleServe = tailscaleMode === "serve" || tailscaleMode === "funnel"; + if (!hasRemoteUrl && !hasTailscaleServe) { + throw new Error( + "qr --remote requires gateway.remote.url (or gateway.tailscale.mode=serve/funnel).", + ); + } + } const explicitUrl = typeof opts.url === "string" && opts.url.trim()