mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(wizard): honor gateway overrides in manual flow (#122961)
This commit is contained in:
committed by
GitHub
parent
705f043e04
commit
5db09954c7
@@ -41,7 +41,8 @@ describe("configureGatewayForSetup", () => {
|
|||||||
return buildWizardPrompter({
|
return buildWizardPrompter({
|
||||||
select,
|
select,
|
||||||
text: vi.fn(async (paramsLocal) => {
|
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;
|
const error = typeof value === "string" ? paramsLocal.validate?.(value) : undefined;
|
||||||
if (error) {
|
if (error) {
|
||||||
throw new Error(error);
|
throw new Error(error);
|
||||||
@@ -106,6 +107,56 @@ describe("configureGatewayForSetup", () => {
|
|||||||
expect(result.nextConfig.gateway?.nodes?.commands).toBeUndefined();
|
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) => {
|
it.each(["1e3", "0x1000"])("rejects loose gateway port input: %s", async (port) => {
|
||||||
mocks.randomToken.mockReturnValue("generated-token");
|
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 () => {
|
it("persists classic quickstart overrides through gateway safety normalization", async () => {
|
||||||
const password = ["classic", "gateway", "placeholder"].join("-");
|
const password = ["classic", "gateway", "placeholder"].join("-");
|
||||||
mocks.getTailnetHostname.mockResolvedValue("test-tailnet.ts.net");
|
mocks.getTailnetHostname.mockResolvedValue("test-tailnet.ts.net");
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ export async function configureGatewayForSetup(
|
|||||||
hint: t("wizard.gateway.bindCustomHint"),
|
hint: t("wizard.gateway.bindCustomHint"),
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
initialValue: quickstartGateway.bind,
|
||||||
});
|
});
|
||||||
|
|
||||||
let customBindHost = quickstartGateway.customBindHost;
|
let customBindHost = quickstartGateway.customBindHost;
|
||||||
@@ -149,7 +150,7 @@ export async function configureGatewayForSetup(
|
|||||||
},
|
},
|
||||||
{ value: "password", label: t("common.password") },
|
{ value: "password", label: t("common.password") },
|
||||||
],
|
],
|
||||||
initialValue: "token",
|
initialValue: quickstartGateway.authMode,
|
||||||
})) as GatewayAuthChoice);
|
})) as GatewayAuthChoice);
|
||||||
|
|
||||||
const tailscaleMode: GatewayWizardSettings["tailscaleMode"] =
|
const tailscaleMode: GatewayWizardSettings["tailscaleMode"] =
|
||||||
@@ -158,6 +159,7 @@ export async function configureGatewayForSetup(
|
|||||||
: await prompter.select<GatewayWizardSettings["tailscaleMode"]>({
|
: await prompter.select<GatewayWizardSettings["tailscaleMode"]>({
|
||||||
message: t("wizard.gateway.tailscaleExposure"),
|
message: t("wizard.gateway.tailscaleExposure"),
|
||||||
options: getLocalizedTailscaleExposureOptions(),
|
options: getLocalizedTailscaleExposureOptions(),
|
||||||
|
initialValue: quickstartGateway.tailscaleMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Detect Tailscale binary before proceeding with serve/funnel setup.
|
// 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") {
|
if (tailscaleMode !== "off" && flow !== "quickstart") {
|
||||||
await prompter.note(t("wizard.gatewayTailscale.docsNote"), "Tailscale");
|
await prompter.note(t("wizard.gatewayTailscale.docsNote"), "Tailscale");
|
||||||
tailscaleResetOnExit = await prompter.confirm({
|
tailscaleResetOnExit = await prompter.confirm({
|
||||||
message: t("wizard.gateway.tailscaleReset"),
|
message: t("wizard.gateway.tailscaleReset"),
|
||||||
initialValue: false,
|
initialValue: tailscaleResetOnExit,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,11 +209,10 @@ export async function configureGatewayForSetup(
|
|||||||
value: quickstartGateway.token,
|
value: quickstartGateway.token,
|
||||||
defaults: nextConfig.secrets?.defaults,
|
defaults: nextConfig.secrets?.defaults,
|
||||||
}).ref;
|
}).ref;
|
||||||
const tokenMode =
|
const tokenMode = quickstartTokenRef
|
||||||
flow === "quickstart" && opts.secretInputMode !== "ref" // pragma: allowlist secret
|
? "ref"
|
||||||
? quickstartTokenRef
|
: flow === "quickstart" && opts.secretInputMode !== "ref" // pragma: allowlist secret
|
||||||
? "ref"
|
? "plaintext"
|
||||||
: "plaintext"
|
|
||||||
: await resolveSecretInputModeForEnvSelection({
|
: await resolveSecretInputModeForEnvSelection({
|
||||||
prompter,
|
prompter,
|
||||||
explicitMode: opts.secretInputMode,
|
explicitMode: opts.secretInputMode,
|
||||||
@@ -224,7 +225,7 @@ export async function configureGatewayForSetup(
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (tokenMode === "ref") {
|
if (tokenMode === "ref") {
|
||||||
if (flow === "quickstart" && quickstartTokenRef) {
|
if (quickstartTokenRef) {
|
||||||
gatewayTokenInput = quickstartTokenRef;
|
gatewayTokenInput = quickstartTokenRef;
|
||||||
gatewayToken = await resolveSetupSecretInputString({
|
gatewayToken = await resolveSetupSecretInputString({
|
||||||
config: nextConfig,
|
config: nextConfig,
|
||||||
@@ -280,8 +281,13 @@ export async function configureGatewayForSetup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (authMode === "password") {
|
if (authMode === "password") {
|
||||||
|
const existingPassword = normalizeSecretInputString(quickstartGateway.password);
|
||||||
|
const existingPasswordRef = resolveSecretInputRef({
|
||||||
|
value: quickstartGateway.password,
|
||||||
|
defaults: nextConfig.secrets?.defaults,
|
||||||
|
}).ref;
|
||||||
let password: SecretInput | undefined =
|
let password: SecretInput | undefined =
|
||||||
flow === "quickstart" && quickstartGateway.password ? quickstartGateway.password : undefined;
|
flow === "quickstart" ? quickstartGateway.password : (existingPasswordRef ?? undefined);
|
||||||
if (!password) {
|
if (!password) {
|
||||||
const selectedMode = await resolveSecretInputModeForEnvSelection({
|
const selectedMode = await resolveSecretInputModeForEnvSelection({
|
||||||
prompter,
|
prompter,
|
||||||
@@ -305,13 +311,25 @@ export async function configureGatewayForSetup(
|
|||||||
});
|
});
|
||||||
password = resolved.ref;
|
password = resolved.ref;
|
||||||
} else {
|
} else {
|
||||||
password = normalizeWizardTextInput(
|
let passwordInput: string | undefined;
|
||||||
await prompter.text({
|
if (existingPassword) {
|
||||||
message: t("wizard.gateway.passwordPrompt"),
|
const keep = await prompter.confirm({
|
||||||
validate: validateGatewayPasswordInput,
|
message: t("wizard.gateway.existingPasswordConfirm", {
|
||||||
sensitive: true,
|
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 = {
|
nextConfig = {
|
||||||
|
|||||||
@@ -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 () => {
|
it("passes secretInputMode through to local gateway config step", async () => {
|
||||||
configureGatewayForSetup.mockClear();
|
configureGatewayForSetup.mockClear();
|
||||||
const prompter = buildWizardPrompter({});
|
const prompter = buildWizardPrompter({});
|
||||||
|
|||||||
+5
-5
@@ -4,7 +4,7 @@ import { formatCliCommand } from "../cli/command-format.js";
|
|||||||
import { resolveOnboardingAgentTarget } from "../commands/onboard-agent-target.js";
|
import { resolveOnboardingAgentTarget } from "../commands/onboard-agent-target.js";
|
||||||
import type { GatewayAuthChoice, OnboardMode, OnboardOptions } from "../commands/onboard-types.js";
|
import type { GatewayAuthChoice, OnboardMode, OnboardOptions } from "../commands/onboard-types.js";
|
||||||
import { hasResolvedRosterBeforeMigrations } from "../config/agent-roster-provenance.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 { createMergePatch } from "../config/merge-patch.js";
|
||||||
import { applyMergePatch } from "../config/merge-patch.js";
|
import { applyMergePatch } from "../config/merge-patch.js";
|
||||||
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
||||||
@@ -289,7 +289,7 @@ async function runSetupWizardOnce(
|
|||||||
|
|
||||||
const quickstartGateway: QuickstartGatewayDefaults = resolveQuickstartGatewayDefaults(
|
const quickstartGateway: QuickstartGatewayDefaults = resolveQuickstartGatewayDefaults(
|
||||||
baseConfig,
|
baseConfig,
|
||||||
wizardFlow === "quickstart" ? opts : undefined,
|
opts,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (flow === "quickstart") {
|
if (flow === "quickstart") {
|
||||||
@@ -341,13 +341,13 @@ async function runSetupWizardOnce(
|
|||||||
await prompter.note(quickstartLines.join("\n"), "QuickStart");
|
await prompter.note(quickstartLines.join("\n"), "QuickStart");
|
||||||
}
|
}
|
||||||
|
|
||||||
const localPort = resolveGatewayPort(baseConfig);
|
const localPort = quickstartGateway.port;
|
||||||
const localUrl = `ws://127.0.0.1:${localPort}`;
|
const localUrl = `ws://127.0.0.1:${localPort}`;
|
||||||
let localGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN;
|
let localGatewayToken = process.env.OPENCLAW_GATEWAY_TOKEN;
|
||||||
try {
|
try {
|
||||||
const resolvedGatewayToken = await resolveSetupSecretInputString({
|
const resolvedGatewayToken = await resolveSetupSecretInputString({
|
||||||
config: baseConfig,
|
config: baseConfig,
|
||||||
value: baseConfig.gateway?.auth?.token,
|
value: quickstartGateway.token,
|
||||||
path: "gateway.auth.token",
|
path: "gateway.auth.token",
|
||||||
env: process.env,
|
env: process.env,
|
||||||
});
|
});
|
||||||
@@ -367,7 +367,7 @@ async function runSetupWizardOnce(
|
|||||||
try {
|
try {
|
||||||
const resolvedGatewayPassword = await resolveSetupSecretInputString({
|
const resolvedGatewayPassword = await resolveSetupSecretInputString({
|
||||||
config: baseConfig,
|
config: baseConfig,
|
||||||
value: baseConfig.gateway?.auth?.password,
|
value: quickstartGateway.password,
|
||||||
path: "gateway.auth.password",
|
path: "gateway.auth.password",
|
||||||
env: process.env,
|
env: process.env,
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user