refactor(gateway): clarify local mode guardrails

This commit is contained in:
Peter Steinberger
2026-04-03 20:00:20 +09:00
parent 4e22e75697
commit e2e1197fa9
10 changed files with 86 additions and 23 deletions

View File

@@ -227,8 +227,9 @@ describe("gateway run option collisions", () => {
await expect(runGatewayCli(["gateway", "run"])).rejects.toThrow("__exit__:1");
expect(runtimeErrors).toContain(
"Gateway start blocked: set gateway.mode=local (current: unset) or pass --allow-unconfigured.",
"Gateway start blocked: existing config is missing gateway.mode. Treat this as suspicious or clobbered config. Re-run `openclaw onboard --mode local` or `openclaw setup`, set gateway.mode=local manually, or pass --allow-unconfigured.",
);
expect(runtimeErrors).toContain("Config write audit: /tmp/logs/config-audit.jsonl");
expect(startGatewayServer).not.toHaveBeenCalled();
});

View File

@@ -141,6 +141,36 @@ function formatModeErrorList<T extends string>(modes: readonly T[]): string {
return `${quoted.slice(0, -1).join(", ")}, or ${quoted[quoted.length - 1]}`;
}
function getGatewayStartGuardErrors(params: {
allowUnconfigured?: boolean;
configExists: boolean;
configAuditPath: string;
mode: string | undefined;
}): string[] {
if (params.allowUnconfigured || params.mode === "local") {
return [];
}
if (!params.configExists) {
return [
`Missing config. Run \`${formatCliCommand("openclaw setup")}\` or set gateway.mode=local (or pass --allow-unconfigured).`,
];
}
if (params.mode === undefined) {
return [
[
"Gateway start blocked: existing config is missing gateway.mode.",
"Treat this as suspicious or clobbered config.",
`Re-run \`${formatCliCommand("openclaw onboard --mode local")}\` or \`${formatCliCommand("openclaw setup")}\`, set gateway.mode=local manually, or pass --allow-unconfigured.`,
].join(" "),
`Config write audit: ${params.configAuditPath}`,
];
}
return [
`Gateway start blocked: set gateway.mode=local (current: ${params.mode}) or pass --allow-unconfigured.`,
`Config write audit: ${params.configAuditPath}`,
];
}
function resolveGatewayRunOptions(opts: GatewayRunOpts, command?: Command): GatewayRunOpts {
const resolved: GatewayRunOpts = { ...opts };
@@ -349,16 +379,15 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
const configAuditPath = path.join(resolveStateDir(process.env), "logs", "config-audit.jsonl");
const effectiveCfg = snapshot?.valid ? snapshot.config : cfg;
const mode = effectiveCfg.gateway?.mode;
if (!opts.allowUnconfigured && mode !== "local") {
if (!configExists) {
defaultRuntime.error(
`Missing config. Run \`${formatCliCommand("openclaw setup")}\` or set gateway.mode=local (or pass --allow-unconfigured).`,
);
} else {
defaultRuntime.error(
`Gateway start blocked: set gateway.mode=local (current: ${mode ?? "unset"}) or pass --allow-unconfigured.`,
);
defaultRuntime.error(`Config write audit: ${configAuditPath}`);
const guardErrors = getGatewayStartGuardErrors({
allowUnconfigured: opts.allowUnconfigured,
configExists,
configAuditPath,
mode,
});
if (guardErrors.length > 0) {
for (const error of guardErrors) {
defaultRuntime.error(error);
}
defaultRuntime.exit(1);
return;
@@ -536,7 +565,7 @@ export function addGatewayRunCommand(cmd: Command): Command {
)
.option(
"--allow-unconfigured",
"Allow gateway start without gateway.mode=local in config",
"Allow gateway start without enforcing gateway.mode=local in config (does not repair config)",
false,
)
.option("--dev", "Create a dev config + workspace if missing (no BOOTSTRAP.md)", false)

View File

@@ -255,18 +255,48 @@ describe("onboard (non-interactive): gateway and remote auth", () => {
const configPath = resolveStateConfigPath(process.env, stateDir);
const cfg = await readJsonFile<{
gateway?: { auth?: { mode?: string; token?: string } };
gateway?: { mode?: string; auth?: { mode?: string; token?: string } };
agents?: { defaults?: { workspace?: string } };
tools?: { profile?: string };
}>(configPath);
expect(cfg?.agents?.defaults?.workspace).toBe(workspace);
expect(cfg?.gateway?.mode).toBe("local");
expect(cfg?.tools?.profile).toBe("coding");
expect(cfg?.gateway?.auth?.mode).toBe("token");
expect(cfg?.gateway?.auth?.token).toBe(token);
});
}, 60_000);
it("keeps gateway.mode=local on the install-daemon onboarding path", async () => {
await withStateDir("state-install-daemon-local-mode-", async (stateDir) => {
const workspace = path.join(stateDir, "openclaw");
await runNonInteractiveSetup(
{
nonInteractive: true,
mode: "local",
workspace,
authChoice: "skip",
skipSkills: true,
skipHealth: true,
installDaemon: true,
gatewayBind: "loopback",
},
runtime,
);
const configPath = resolveStateConfigPath(process.env, stateDir);
const cfg = await readJsonFile<{
gateway?: { mode?: string; bind?: string };
}>(configPath);
expect(cfg?.gateway?.mode).toBe("local");
expect(cfg?.gateway?.bind).toBe("loopback");
expect(installGatewayDaemonNonInteractiveMock).toHaveBeenCalledTimes(1);
});
}, 60_000);
it("uses OPENCLAW_GATEWAY_TOKEN when --gateway-token is omitted", async () => {
await withStateDir("state-env-token-", async (stateDir) => {
const envToken = "tok_env_fallback_123";