fix(wizard): honor gateway overrides in manual flow (#122961)

This commit is contained in:
Peter Steinberger
2026-08-12 20:34:00 -07:00
committed by GitHub
parent 705f043e04
commit 5db09954c7
4 changed files with 200 additions and 23 deletions
+88 -1
View File
@@ -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<unknown>) => {
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");
+35 -17
View File
@@ -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<GatewayWizardSettings["tailscaleMode"]>({
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 = {
+72
View File
@@ -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({});
+5 -5
View File
@@ -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,
});