mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-24 11:25:50 -06:00
fix(onboard): reject --gateway-bind custom without a valid customBindHost (#127779)
Non-interactive onboarding accepted `--gateway-bind custom` with no `gateway.customBindHost`, wrote `bind: "custom"` to the config, and reported success. The Gateway refuses that config outright: Gateway failed to start: gateway.bind=custom requires gateway.customBindHost. `doctor` and `config validate` both call the written config healthy, so the first sign of trouble is a Gateway that will not start. Every other onboarding flag already rejects a mode whose companion value is missing (--gateway-auth password, --auth-choice openai-api-key, --auth-choice custom-api-key). This restores the same contract for bind, reusing the same validateDottedDecimalIPv4Input the interactive wizard and `openclaw configure` apply to this field. Pre-seeding gateway.customBindHost keeps working.
This commit is contained in:
committed by
GitHub
parent
de4fbf964f
commit
ec39376967
@@ -366,4 +366,60 @@ describe("applyNonInteractiveGatewayConfig auth resolution", () => {
|
||||
|
||||
expect(result?.nextConfig.gateway?.auth).toEqual({ mode: "password" });
|
||||
});
|
||||
|
||||
it("rejects --gateway-bind custom when no gateway.customBindHost is configured", () => {
|
||||
const runtime = createRuntime();
|
||||
|
||||
const result = applyGatewayConfig({
|
||||
opts: { gatewayBind: "custom" } as OnboardOptions,
|
||||
runtime,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(runtime.error).toHaveBeenCalledWith(
|
||||
expect.stringContaining("--gateway-bind custom requires gateway.customBindHost"),
|
||||
);
|
||||
expect(runtime.exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("rejects --gateway-bind custom when gateway.customBindHost is not a dotted-decimal IPv4", () => {
|
||||
const runtime = createRuntime();
|
||||
|
||||
const result = applyGatewayConfig({
|
||||
nextConfig: { gateway: { customBindHost: "not-an-ip" } } as OpenClawConfig,
|
||||
opts: { gatewayBind: "custom" } as OnboardOptions,
|
||||
runtime,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("Invalid IPv4 address"));
|
||||
expect(runtime.exit).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("accepts --gateway-bind custom when gateway.customBindHost is already configured", () => {
|
||||
const runtime = createRuntime();
|
||||
|
||||
const result = applyGatewayConfig({
|
||||
nextConfig: { gateway: { customBindHost: "192.168.1.100" } } as OpenClawConfig,
|
||||
opts: { gatewayBind: "custom" } as OnboardOptions,
|
||||
runtime,
|
||||
});
|
||||
|
||||
expect(result?.nextConfig.gateway?.bind).toBe("custom");
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
expect(runtime.exit).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps loopback normalization ahead of the custom bind guard when Tailscale is enabled", () => {
|
||||
const runtime = createRuntime();
|
||||
|
||||
const result = applyGatewayConfig({
|
||||
opts: { gatewayBind: "custom", tailscale: "serve" } as OnboardOptions,
|
||||
runtime,
|
||||
});
|
||||
|
||||
expect(result?.nextConfig.gateway?.bind).toBe("loopback");
|
||||
expect(runtime.error).not.toHaveBeenCalled();
|
||||
expect(runtime.exit).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* This module owns port/bind/auth validation and existing-setting preservation
|
||||
* before the final config write happens.
|
||||
*/
|
||||
import { validateDottedDecimalIPv4Input } from "@openclaw/net-policy/ipv4";
|
||||
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
|
||||
import { formatCliCommand } from "../../../cli/command-format.js";
|
||||
import { formatInvalidPortOption } from "../../../cli/error-format.js";
|
||||
@@ -97,6 +98,24 @@ export function applyNonInteractiveGatewayConfig(params: {
|
||||
if (changesBindOrTailscale && tailscaleMode !== "off" && bind !== "loopback") {
|
||||
bind = "loopback";
|
||||
}
|
||||
|
||||
// bind=custom is only startable alongside a valid gateway.customBindHost, and the non-interactive
|
||||
// path has no prompt to collect one. Checked after the Tailscale normalization above so a bind
|
||||
// forced back to loopback never trips it. Without this, setup writes a config the Gateway refuses.
|
||||
if (bind === "custom") {
|
||||
const customBindHostIssue = validateDottedDecimalIPv4Input(
|
||||
normalizeOptionalString(existingGateway?.customBindHost ?? ""),
|
||||
);
|
||||
if (customBindHostIssue) {
|
||||
const setCommand = formatCliCommand("openclaw config set gateway.customBindHost <ipv4>");
|
||||
const interactiveCommand = formatCliCommand("openclaw onboard");
|
||||
runtime.error(
|
||||
`--gateway-bind custom requires gateway.customBindHost: ${customBindHostIssue}. Set it with ${setCommand} and rerun, or run ${interactiveCommand} interactively to be prompted for it.`,
|
||||
);
|
||||
runtime.exit(1);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const changesAuthOrTailscale =
|
||||
explicitAuthMode !== undefined || hasExplicitTokenAuthInput || opts.tailscale !== undefined;
|
||||
if (changesAuthOrTailscale && tailscaleMode === "serve" && authMode === "none") {
|
||||
|
||||
Reference in New Issue
Block a user