From 5db09954c76b06553aa67d1b295674df8a9f9c23 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 12 Aug 2026 20:34:00 -0700 Subject: [PATCH] fix(wizard): honor gateway overrides in manual flow (#122961) --- src/wizard/setup.gateway-config.test.ts | 89 ++++++++++++++++++++++++- src/wizard/setup.gateway-config.ts | 52 ++++++++++----- src/wizard/setup.test.ts | 72 ++++++++++++++++++++ src/wizard/setup.ts | 10 +-- 4 files changed, 200 insertions(+), 23 deletions(-) diff --git a/src/wizard/setup.gateway-config.test.ts b/src/wizard/setup.gateway-config.test.ts index e5d998515d15..08c177f0e1a6 100644 --- a/src/wizard/setup.gateway-config.test.ts +++ b/src/wizard/setup.gateway-config.test.ts @@ -41,7 +41,8 @@ describe("configureGatewayForSetup", () => { return buildWizardPrompter({ select, text: vi.fn(async (paramsLocal) => { - const value = textQueue.shift() as string; + const hasQueuedValue = textQueue.length > 0; + const value = hasQueuedValue ? textQueue.shift() : paramsLocal.initialValue; const error = typeof value === "string" ? paramsLocal.validate?.(value) : undefined; if (error) { throw new Error(error); @@ -106,6 +107,56 @@ describe("configureGatewayForSetup", () => { expect(result.nextConfig.gateway?.nodes?.commands).toBeUndefined(); }); + it("seeds advanced gateway prompts from explicit classic options", async () => { + const gatewayDefaults = resolveQuickstartGatewayDefaults( + {}, + { + gatewayPort: 19511, + gatewayBind: "lan", + gatewayAuth: "password", + gatewayPassword: "manual-gateway-password-placeholder", + tailscale: "off", + }, + ); + const select = vi.fn(async (params: WizardSelectParams) => { + return params.initialValue ?? params.options[0]?.value; + }) as unknown as WizardPrompter["select"]; + const text = vi.fn(async (params: { initialValue?: string }) => params.initialValue ?? ""); + const confirm = vi.fn( + async (params: { initialValue?: boolean }) => params.initialValue ?? false, + ); + const prompter = buildWizardPrompter({ select, text, confirm }); + + const result = await configureGatewayForSetup({ + flow: "advanced", + baseConfig: {}, + nextConfig: {}, + localPort: gatewayDefaults.port, + quickstartGateway: gatewayDefaults, + prompter, + runtime: createRuntime(), + }); + + expect(text).toHaveBeenCalledWith( + expect.objectContaining({ message: "Gateway port", initialValue: "19511" }), + ); + expect(select).toHaveBeenCalledWith( + expect.objectContaining({ message: "Gateway bind address", initialValue: "lan" }), + ); + expect(select).toHaveBeenCalledWith( + expect.objectContaining({ message: "Gateway access protection", initialValue: "password" }), + ); + expect(select).toHaveBeenCalledWith( + expect.objectContaining({ message: "Tailscale exposure", initialValue: "off" }), + ); + expect(result.nextConfig.gateway).toMatchObject({ + port: 19511, + bind: "lan", + auth: { mode: "password", password: "manual-gateway-password-placeholder" }, + tailscale: { mode: "off" }, + }); + }); + it.each(["1e3", "0x1000"])("rejects loose gateway port input: %s", async (port) => { mocks.randomToken.mockReturnValue("generated-token"); @@ -374,6 +425,42 @@ describe("configureGatewayForSetup", () => { } }); + it("seeds an explicit env token ref into advanced gateway setup", async () => { + const previous = process.env.OPENCLAW_GATEWAY_TOKEN; + process.env.OPENCLAW_GATEWAY_TOKEN = "token-from-env-ref"; + try { + const gatewayDefaults = resolveQuickstartGatewayDefaults( + {}, + { gatewayPort: 19511, gatewayTokenRefEnv: "OPENCLAW_GATEWAY_TOKEN" }, + ); + const result = await configureGatewayForSetup({ + flow: "advanced", + baseConfig: {}, + nextConfig: {}, + localPort: gatewayDefaults.port, + quickstartGateway: gatewayDefaults, + prompter: createPrompter({ selectQueue: [], textQueue: [] }), + runtime: createRuntime(), + }); + + expect(result.nextConfig.gateway?.auth).toEqual({ + mode: "token", + token: { + source: "env", + provider: "default", + id: "OPENCLAW_GATEWAY_TOKEN", + }, + }); + expect(result.settings.gatewayToken).toBe("token-from-env-ref"); + } finally { + if (previous === undefined) { + delete process.env.OPENCLAW_GATEWAY_TOKEN; + } else { + process.env.OPENCLAW_GATEWAY_TOKEN = previous; + } + } + }); + it("persists classic quickstart overrides through gateway safety normalization", async () => { const password = ["classic", "gateway", "placeholder"].join("-"); mocks.getTailnetHostname.mockResolvedValue("test-tailnet.ts.net"); diff --git a/src/wizard/setup.gateway-config.ts b/src/wizard/setup.gateway-config.ts index 234e413d0db8..ae42316755d5 100644 --- a/src/wizard/setup.gateway-config.ts +++ b/src/wizard/setup.gateway-config.ts @@ -120,6 +120,7 @@ export async function configureGatewayForSetup( hint: t("wizard.gateway.bindCustomHint"), }, ], + initialValue: quickstartGateway.bind, }); let customBindHost = quickstartGateway.customBindHost; @@ -149,7 +150,7 @@ export async function configureGatewayForSetup( }, { value: "password", label: t("common.password") }, ], - initialValue: "token", + initialValue: quickstartGateway.authMode, })) as GatewayAuthChoice); const tailscaleMode: GatewayWizardSettings["tailscaleMode"] = @@ -158,6 +159,7 @@ export async function configureGatewayForSetup( : await prompter.select({ message: t("wizard.gateway.tailscaleExposure"), options: getLocalizedTailscaleExposureOptions(), + initialValue: quickstartGateway.tailscaleMode, }); // Detect Tailscale binary before proceeding with serve/funnel setup. @@ -173,12 +175,12 @@ export async function configureGatewayForSetup( } } - let tailscaleResetOnExit = flow === "quickstart" ? quickstartGateway.tailscaleResetOnExit : false; + let tailscaleResetOnExit = quickstartGateway.tailscaleResetOnExit; if (tailscaleMode !== "off" && flow !== "quickstart") { await prompter.note(t("wizard.gatewayTailscale.docsNote"), "Tailscale"); tailscaleResetOnExit = await prompter.confirm({ message: t("wizard.gateway.tailscaleReset"), - initialValue: false, + initialValue: tailscaleResetOnExit, }); } @@ -207,11 +209,10 @@ export async function configureGatewayForSetup( value: quickstartGateway.token, defaults: nextConfig.secrets?.defaults, }).ref; - const tokenMode = - flow === "quickstart" && opts.secretInputMode !== "ref" // pragma: allowlist secret - ? quickstartTokenRef - ? "ref" - : "plaintext" + const tokenMode = quickstartTokenRef + ? "ref" + : flow === "quickstart" && opts.secretInputMode !== "ref" // pragma: allowlist secret + ? "plaintext" : await resolveSecretInputModeForEnvSelection({ prompter, explicitMode: opts.secretInputMode, @@ -224,7 +225,7 @@ export async function configureGatewayForSetup( }, }); if (tokenMode === "ref") { - if (flow === "quickstart" && quickstartTokenRef) { + if (quickstartTokenRef) { gatewayTokenInput = quickstartTokenRef; gatewayToken = await resolveSetupSecretInputString({ config: nextConfig, @@ -280,8 +281,13 @@ export async function configureGatewayForSetup( } if (authMode === "password") { + const existingPassword = normalizeSecretInputString(quickstartGateway.password); + const existingPasswordRef = resolveSecretInputRef({ + value: quickstartGateway.password, + defaults: nextConfig.secrets?.defaults, + }).ref; let password: SecretInput | undefined = - flow === "quickstart" && quickstartGateway.password ? quickstartGateway.password : undefined; + flow === "quickstart" ? quickstartGateway.password : (existingPasswordRef ?? undefined); if (!password) { const selectedMode = await resolveSecretInputModeForEnvSelection({ prompter, @@ -305,13 +311,25 @@ export async function configureGatewayForSetup( }); password = resolved.ref; } else { - password = normalizeWizardTextInput( - await prompter.text({ - message: t("wizard.gateway.passwordPrompt"), - validate: validateGatewayPasswordInput, - sensitive: true, - }), - ); + let passwordInput: string | undefined; + if (existingPassword) { + const keep = await prompter.confirm({ + message: t("wizard.gateway.existingPasswordConfirm", { + password: maskApiKey(existingPassword), + }), + initialValue: true, + }); + passwordInput = keep ? existingPassword : undefined; + } + password = + passwordInput ?? + normalizeWizardTextInput( + await prompter.text({ + message: t("wizard.gateway.passwordPrompt"), + validate: validateGatewayPasswordInput, + sensitive: true, + }), + ); } } nextConfig = { diff --git a/src/wizard/setup.test.ts b/src/wizard/setup.test.ts index 5eeac8ac9128..e24758654f70 100644 --- a/src/wizard/setup.test.ts +++ b/src/wizard/setup.test.ts @@ -2544,6 +2544,78 @@ describe("runSetupWizard", () => { ); }); + it.each([ + { + label: "explicit CLI gateway values", + gatewayOptions: { + gatewayPort: 19511, + gatewayBind: "lan" as const, + gatewayAuth: "password" as const, + gatewayToken: "manual-gateway-token-placeholder", + gatewayPassword: "manual-gateway-password-placeholder", + tailscale: "off" as const, + tailscaleResetOnExit: false, + }, + expectedPort: 19511, + expectedProbeAuth: { + token: "manual-gateway-token-placeholder", + password: "manual-gateway-password-placeholder", + }, + }, + { + label: "derived port when gateway values are omitted", + gatewayOptions: {}, + expectedPort: 18789, + expectedProbeAuth: {}, + }, + ])( + "uses the $label for the manual probe and port prompt", + async ({ gatewayOptions, expectedPort, expectedProbeAuth }) => { + const prompter = buildWizardPrompter({}); + const runtime = createRuntime(); + + await runSetupWizard( + { + acceptRisk: true, + flow: "advanced", + mode: "local", + authChoice: "skip", + ...gatewayOptions, + installDaemon: false, + skipChannels: true, + skipSkills: true, + skipSearch: true, + skipHealth: true, + skipUi: true, + }, + runtime, + prompter, + ); + + expectRecordFields( + getMockCallArg(probeGatewayReachable, 0, 0, "gateway probe"), + { url: `ws://127.0.0.1:${expectedPort}`, ...expectedProbeAuth }, + "gateway probe params", + ); + const gatewaySetup = expectRecordFields( + getMockCallArg(configureGatewayForSetup, 0, 0, "gateway setup"), + { localPort: expectedPort }, + "gateway setup params", + ); + if (gatewayOptions.gatewayPort !== undefined) { + expect(gatewaySetup.quickstartGateway).toMatchObject({ + port: 19511, + bind: "lan", + authMode: "password", + token: "manual-gateway-token-placeholder", + password: "manual-gateway-password-placeholder", + tailscaleMode: "off", + tailscaleResetOnExit: false, + }); + } + }, + ); + it("passes secretInputMode through to local gateway config step", async () => { configureGatewayForSetup.mockClear(); const prompter = buildWizardPrompter({}); diff --git a/src/wizard/setup.ts b/src/wizard/setup.ts index 215d040f011b..5abcec8a56aa 100644 --- a/src/wizard/setup.ts +++ b/src/wizard/setup.ts @@ -4,7 +4,7 @@ import { formatCliCommand } from "../cli/command-format.js"; import { resolveOnboardingAgentTarget } from "../commands/onboard-agent-target.js"; import type { GatewayAuthChoice, OnboardMode, OnboardOptions } from "../commands/onboard-types.js"; import { hasResolvedRosterBeforeMigrations } from "../config/agent-roster-provenance.js"; -import { ConfigMutationConflictError, resolveGatewayPort } from "../config/config.js"; +import { ConfigMutationConflictError } from "../config/config.js"; import { createMergePatch } from "../config/merge-patch.js"; import { applyMergePatch } from "../config/merge-patch.js"; import { resolveAgentModelPrimaryValue } from "../config/model-input.js"; @@ -289,7 +289,7 @@ async function runSetupWizardOnce( const quickstartGateway: QuickstartGatewayDefaults = resolveQuickstartGatewayDefaults( baseConfig, - wizardFlow === "quickstart" ? opts : undefined, + opts, ); if (flow === "quickstart") { @@ -341,13 +341,13 @@ async function runSetupWizardOnce( await prompter.note(quickstartLines.join("\n"), "QuickStart"); } - const localPort = resolveGatewayPort(baseConfig); + const localPort = quickstartGateway.port; const localUrl = `ws://127.0.0.1:${localPort}`; let localGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN; try { const resolvedGatewayToken = await resolveSetupSecretInputString({ config: baseConfig, - value: baseConfig.gateway?.auth?.token, + value: quickstartGateway.token, path: "gateway.auth.token", env: process.env, }); @@ -367,7 +367,7 @@ async function runSetupWizardOnce( try { const resolvedGatewayPassword = await resolveSetupSecretInputString({ config: baseConfig, - value: baseConfig.gateway?.auth?.password, + value: quickstartGateway.password, path: "gateway.auth.password", env: process.env, });