From da53015ef5397fb88635d8321cc53c513101168f Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Thu, 26 Feb 2026 12:09:36 +0000 Subject: [PATCH] fix(onboard): seed Control UI origins for non-loopback binds (land #26157, thanks @stakeswky) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 不做了睡大觉 --- CHANGELOG.md | 1 + src/wizard/onboarding.gateway-config.test.ts | 25 +++++++++++++ src/wizard/onboarding.gateway-config.ts | 37 ++++++++++++++++++++ 3 files changed, 63 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27cb25657..681c6d663 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Docs: https://docs.openclaw.ai - Security/Plugin channel HTTP auth: normalize protected `/api/channels` path checks against canonicalized request paths (case + percent-decoding + slash normalization), and fail closed on malformed `%`-encoded channel prefixes so alternate-path variants cannot bypass gateway auth. - Security/Exec approvals forwarding: prefer turn-source channel/account/thread metadata when resolving approval delivery targets so stale session routes do not misroute approval prompts. +- Onboarding/Gateway: seed default Control UI `allowedOrigins` for non-loopback binds during onboarding (`localhost`/`127.0.0.1` plus custom bind host) so fresh non-loopback setups do not fail startup due to missing origin policy. (#26157) thanks @stakeswky. - Auto-reply/Streaming: suppress only exact `NO_REPLY` final replies while still filtering streaming partial sentinel fragments (`NO_`, `NO_RE`, `HEARTBEAT_...`) so substantive replies ending with `NO_REPLY` are delivered and partial silent tokens do not leak during streaming. (#19576) Thanks @aldoeliacim. - Doctor/State integrity: ignore metadata-only slash routing sessions when checking recent missing transcripts so `openclaw doctor` no longer reports false-positive transcript-missing warnings for `*:slash:*` keys. (#27375) thanks @gumadeiras. - Channels/Multi-account config: when adding a non-default channel account to a single-account top-level channel setup, move existing account-scoped top-level single-account values into `channels..accounts.default` before writing the new account so the original account keeps working without duplicated account values at channel root; `openclaw doctor --fix` now repairs previously mixed channel account shapes the same way. (#27334) thanks @gumadeiras. diff --git a/src/wizard/onboarding.gateway-config.test.ts b/src/wizard/onboarding.gateway-config.test.ts index 1f9cb175d..1bbe3a82f 100644 --- a/src/wizard/onboarding.gateway-config.test.ts +++ b/src/wizard/onboarding.gateway-config.test.ts @@ -111,4 +111,29 @@ describe("configureGatewayForOnboarding", () => { expect(authConfig?.password).toBe(""); expect(authConfig?.password).not.toBe("undefined"); }); + + it("seeds control UI allowed origins for non-loopback binds", async () => { + mocks.randomToken.mockReturnValue("generated-token"); + + const prompter = createPrompter({ + selectQueue: ["lan", "token", "off"], + textQueue: ["18789", undefined], + }); + const runtime = createRuntime(); + + const result = await configureGatewayForOnboarding({ + flow: "advanced", + baseConfig: {}, + nextConfig: {}, + localPort: 18789, + quickstartGateway: createQuickstartGateway("token"), + prompter, + runtime, + }); + + expect(result.nextConfig.gateway?.controlUi?.allowedOrigins).toEqual([ + "http://localhost:18789", + "http://127.0.0.1:18789", + ]); + }); }); diff --git a/src/wizard/onboarding.gateway-config.ts b/src/wizard/onboarding.gateway-config.ts index a57cef19b..6aba767b4 100644 --- a/src/wizard/onboarding.gateway-config.ts +++ b/src/wizard/onboarding.gateway-config.ts @@ -49,6 +49,21 @@ type ConfigureGatewayResult = { settings: GatewayWizardSettings; }; +function buildDefaultControlUiAllowedOrigins(params: { + port: number; + bind: GatewayWizardSettings["bind"]; + customBindHost?: string; +}): string[] { + const origins = new Set([ + `http://localhost:${params.port}`, + `http://127.0.0.1:${params.port}`, + ]); + if (params.bind === "custom" && params.customBindHost) { + origins.add(`http://${params.customBindHost}:${params.port}`); + } + return [...origins]; +} + export async function configureGatewayForOnboarding( opts: ConfigureGatewayOptions, ): Promise { @@ -216,6 +231,28 @@ export async function configureGatewayForOnboarding( }, }; + const controlUiEnabled = nextConfig.gateway?.controlUi?.enabled ?? true; + const hasExplicitControlUiAllowedOrigins = + (nextConfig.gateway?.controlUi?.allowedOrigins ?? []).some( + (origin) => origin.trim().length > 0, + ) || nextConfig.gateway?.controlUi?.dangerouslyAllowHostHeaderOriginFallback === true; + if (controlUiEnabled && bind !== "loopback" && !hasExplicitControlUiAllowedOrigins) { + nextConfig = { + ...nextConfig, + gateway: { + ...nextConfig.gateway, + controlUi: { + ...nextConfig.gateway?.controlUi, + allowedOrigins: buildDefaultControlUiAllowedOrigins({ + port, + bind, + customBindHost, + }), + }, + }, + }; + } + // If this is a new gateway setup (no existing gateway settings), start with a // denylist for high-risk node commands. Users can arm these temporarily via // /phone arm ... (phone-control plugin).