From 2c7eea8f101f2117f064892242d9aa205cc227a5 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 3 Apr 2026 19:48:46 +0900 Subject: [PATCH] fix(gateway): fail closed on missing mode --- docs/cli/gateway.md | 1 + docs/cli/onboard.md | 1 + docs/gateway/troubleshooting.md | 2 +- src/cli/gateway-cli/run.option-collisions.test.ts | 10 ++++++---- src/cli/gateway-cli/run.ts | 7 +------ 5 files changed, 10 insertions(+), 11 deletions(-) diff --git a/docs/cli/gateway.md b/docs/cli/gateway.md index 609aa0b3e0d..f3f2c2f6ced 100644 --- a/docs/cli/gateway.md +++ b/docs/cli/gateway.md @@ -36,6 +36,7 @@ openclaw gateway run Notes: - By default, the Gateway refuses to start unless `gateway.mode=local` is set in `~/.openclaw/openclaw.json`. Use `--allow-unconfigured` for ad-hoc/dev runs. +- `openclaw onboard --mode local` and `openclaw setup` are expected to write `gateway.mode=local`. If the file exists but `gateway.mode` is missing, treat that as a broken or clobbered config and repair it instead of assuming local mode implicitly. - Binding beyond loopback without auth is blocked (safety guardrail). - `SIGUSR1` triggers an in-process restart when authorized (`commands.restart` is enabled by default; set `commands.restart: false` to block manual restart, while gateway tool/config apply/update remain allowed). - `SIGINT`/`SIGTERM` handlers stop the gateway process, but they don’t restore any custom terminal state. If you wrap the CLI with a TUI or raw-mode input, restore the terminal before exit. diff --git a/docs/cli/onboard.md b/docs/cli/onboard.md index df80c78ab3a..fe70c1f081c 100644 --- a/docs/cli/onboard.md +++ b/docs/cli/onboard.md @@ -82,6 +82,7 @@ Gateway token options in non-interactive mode: - With `--install-daemon`, when token auth requires a token, SecretRef-managed gateway tokens are validated but not persisted as resolved plaintext in supervisor service environment metadata. - With `--install-daemon`, if token mode requires a token and the configured token SecretRef is unresolved, onboarding fails closed with remediation guidance. - With `--install-daemon`, if both `gateway.auth.token` and `gateway.auth.password` are configured and `gateway.auth.mode` is unset, onboarding blocks install until mode is set explicitly. +- Local onboarding writes `gateway.mode="local"` into the config. If a later config file is missing `gateway.mode`, treat that as config damage or an incomplete manual edit, not as a valid local-mode shortcut. Example: diff --git a/docs/gateway/troubleshooting.md b/docs/gateway/troubleshooting.md index 98aa4768876..7275f3c5cea 100644 --- a/docs/gateway/troubleshooting.md +++ b/docs/gateway/troubleshooting.md @@ -170,7 +170,7 @@ Look for: Common signatures: -- `Gateway start blocked: set gateway.mode=local` → local gateway mode is not enabled. Fix: set `gateway.mode="local"` in your config (or run `openclaw configure`). If you are running OpenClaw via Podman, the default config path is `~/.openclaw/openclaw.json`. +- `Gateway start blocked: set gateway.mode=local` → local gateway mode is not enabled, or the config file was clobbered and lost `gateway.mode`. Fix: set `gateway.mode="local"` in your config, or re-run `openclaw onboard --mode local` / `openclaw setup` to restamp the expected local-mode config. If you are running OpenClaw via Podman, the default config path is `~/.openclaw/openclaw.json`. - `refusing to bind gateway ... without auth` → non-loopback bind without token/password. - `another gateway instance is already listening` / `EADDRINUSE` → port conflict. diff --git a/src/cli/gateway-cli/run.option-collisions.test.ts b/src/cli/gateway-cli/run.option-collisions.test.ts index 7302cc230e6..874f7a5cd5e 100644 --- a/src/cli/gateway-cli/run.option-collisions.test.ts +++ b/src/cli/gateway-cli/run.option-collisions.test.ts @@ -207,7 +207,7 @@ describe("gateway run option collisions", () => { ); }); - it("defaults to local when snapshot is valid but has no gateway.mode", async () => { + it("blocks startup when the observed snapshot loses gateway.mode even if loadConfig still says local", async () => { configState.cfg = { gateway: { mode: "local", @@ -224,10 +224,12 @@ describe("gateway run option collisions", () => { }, }; - // Should NOT block — gateway.mode defaults to "local" when unset (#54801) - await runGatewayCli(["gateway", "run"]); + await expect(runGatewayCli(["gateway", "run"])).rejects.toThrow("__exit__:1"); - expect(startGatewayServer).toHaveBeenCalled(); + expect(runtimeErrors).toContain( + "Gateway start blocked: set gateway.mode=local (current: unset) or pass --allow-unconfigured.", + ); + expect(startGatewayServer).not.toHaveBeenCalled(); }); it.each(["none", "trusted-proxy"] as const)("accepts --auth %s override", async (mode) => { diff --git a/src/cli/gateway-cli/run.ts b/src/cli/gateway-cli/run.ts index 1b05242cd85..9764f14c3e5 100644 --- a/src/cli/gateway-cli/run.ts +++ b/src/cli/gateway-cli/run.ts @@ -348,12 +348,7 @@ async function runGatewayCommand(opts: GatewayRunOpts) { const configExists = snapshot?.exists ?? fs.existsSync(CONFIG_PATH); const configAuditPath = path.join(resolveStateDir(process.env), "logs", "config-audit.jsonl"); const effectiveCfg = snapshot?.valid ? snapshot.config : cfg; - // Default to "local" when gateway.mode is unset. Prior to v2026.3.24 the - // gateway started without an explicit mode; the guard added in 3.24 - // regressed startup on Windows (and other platforms) when the config file - // exists but doesn't contain gateway.mode — e.g. after `openclaw onboard` - // writes a minimal config. (#54801) - const mode = effectiveCfg.gateway?.mode ?? "local"; + const mode = effectiveCfg.gateway?.mode; if (!opts.allowUnconfigured && mode !== "local") { if (!configExists) { defaultRuntime.error(