From 1486eb66fd17c08e284cf819585511bafaf9d33e Mon Sep 17 00:00:00 2001 From: Sebastian <19554889+sebslight@users.noreply.github.com> Date: Mon, 16 Feb 2026 22:28:02 -0500 Subject: [PATCH] revert(gateway): restore loopback auth setup --- src/commands/configure.gateway.e2e.test.ts | 17 ++-- src/commands/configure.gateway.ts | 99 ++++++++++------------ src/commands/doctor.ts | 16 ++-- 3 files changed, 57 insertions(+), 75 deletions(-) diff --git a/src/commands/configure.gateway.e2e.test.ts b/src/commands/configure.gateway.e2e.test.ts index a36f7acbf3d..a4d78463256 100644 --- a/src/commands/configure.gateway.e2e.test.ts +++ b/src/commands/configure.gateway.e2e.test.ts @@ -82,33 +82,30 @@ async function runTrustedProxyPrompt(params: { tailscaleMode?: "off" | "serve"; }) { return runGatewayPrompt({ - selectQueue: ["lan", params.tailscaleMode ?? "off", "trusted-proxy"], + selectQueue: ["loopback", "trusted-proxy", params.tailscaleMode ?? "off"], textQueue: params.textQueue, authConfigFactory: ({ mode, trustedProxy }) => ({ mode, trustedProxy }), }); } describe("promptGatewayConfig", () => { - it("skips gateway auth setup for loopback-only gateways", async () => { + it("generates a token when the prompt returns undefined", async () => { const { result } = await runGatewayPrompt({ - selectQueue: ["loopback", "off"], - textQueue: ["18789"], + selectQueue: ["loopback", "token", "off"], + textQueue: ["18789", undefined], randomToken: "generated-token", authConfigFactory: ({ mode, token, password }) => ({ mode, token, password }), }); - expect(result.token).toBeUndefined(); - expect(result.config.gateway?.auth).toBeUndefined(); - expect(mocks.buildGatewayAuthConfig).not.toHaveBeenCalled(); + expect(result.token).toBe("generated-token"); }); - it("configures password auth when gateway is exposed", async () => { + it("does not set password to literal 'undefined' when prompt returns undefined", async () => { const { call } = await runGatewayPrompt({ - selectQueue: ["lan", "off", "password"], + selectQueue: ["loopback", "password", "off"], textQueue: ["18789", undefined], randomToken: "unused", authConfigFactory: ({ mode, token, password }) => ({ mode, token, password }), }); - expect(call?.mode).toBe("password"); expect(call?.password).not.toBe("undefined"); expect(call?.password).toBe(""); }); diff --git a/src/commands/configure.gateway.ts b/src/commands/configure.gateway.ts index a05cd31bcf1..b0676f311a3 100644 --- a/src/commands/configure.gateway.ts +++ b/src/commands/configure.gateway.ts @@ -1,4 +1,5 @@ import type { OpenClawConfig } from "../config/config.js"; +import type { RuntimeEnv } from "../runtime.js"; import { resolveGatewayPort } from "../config/config.js"; import { TAILSCALE_DOCS_LINES, @@ -6,7 +7,6 @@ import { TAILSCALE_MISSING_BIN_NOTE_LINES, } from "../gateway/gateway-config-prompts.shared.js"; import { findTailscaleBinary } from "../infra/tailscale.js"; -import type { RuntimeEnv } from "../runtime.js"; import { validateIPv4AddressInput } from "../shared/net/ipv4.js"; import { note } from "../terminal/note.js"; import { buildGatewayAuthConfig } from "./configure.gateway-auth.js"; @@ -85,7 +85,22 @@ export async function promptGatewayConfig( customBindHost = typeof input === "string" ? input : undefined; } - let authMode: GatewayAuthChoice = "token"; + let authMode = guardCancel( + await select({ + message: "Gateway auth", + options: [ + { value: "token", label: "Token", hint: "Recommended default" }, + { value: "password", label: "Password" }, + { + value: "trusted-proxy", + label: "Trusted Proxy", + hint: "Behind reverse proxy (Pomerium, Caddy, Traefik, etc.)", + }, + ], + initialValue: "token", + }), + runtime, + ) as GatewayAuthChoice; let tailscaleMode = guardCancel( await select({ @@ -122,44 +137,22 @@ export async function promptGatewayConfig( bind = "loopback"; } - const loopbackOnlyGateway = bind === "loopback" && tailscaleMode === "off"; - if (loopbackOnlyGateway) { - note("Loopback-only gateway does not require gateway.auth. Keeping auth disabled.", "Note"); - } else { - authMode = guardCancel( - await select({ - message: "Gateway auth", - options: [ - { value: "token", label: "Token", hint: "Recommended default" }, - { value: "password", label: "Password" }, - { - value: "trusted-proxy", - label: "Trusted Proxy", - hint: "Behind reverse proxy (Pomerium, Caddy, Traefik, etc.)", - }, - ], - initialValue: tailscaleMode === "funnel" ? "password" : "token", - }), - runtime, - ) as GatewayAuthChoice; + if (tailscaleMode === "funnel" && authMode !== "password") { + note("Tailscale funnel requires password auth.", "Note"); + authMode = "password"; + } - if (tailscaleMode === "funnel" && authMode !== "password") { - note("Tailscale funnel requires password auth.", "Note"); - authMode = "password"; - } - - if (authMode === "trusted-proxy" && bind === "loopback") { - note("Trusted proxy auth requires network bind. Adjusting bind to lan.", "Note"); - bind = "lan"; - } - if (authMode === "trusted-proxy" && tailscaleMode !== "off") { - note( - "Trusted proxy auth is incompatible with Tailscale serve/funnel. Disabling Tailscale.", - "Note", - ); - tailscaleMode = "off"; - tailscaleResetOnExit = false; - } + if (authMode === "trusted-proxy" && bind === "loopback") { + note("Trusted proxy auth requires network bind. Adjusting bind to lan.", "Note"); + bind = "lan"; + } + if (authMode === "trusted-proxy" && tailscaleMode !== "off") { + note( + "Trusted proxy auth is incompatible with Tailscale serve/funnel. Disabling Tailscale.", + "Note", + ); + tailscaleMode = "off"; + tailscaleResetOnExit = false; } let gatewayToken: string | undefined; @@ -170,7 +163,7 @@ export async function promptGatewayConfig( let trustedProxies: string[] | undefined; let next = cfg; - if (!loopbackOnlyGateway && authMode === "token") { + if (authMode === "token") { const tokenInput = guardCancel( await text({ message: "Gateway token (blank to generate)", @@ -181,7 +174,7 @@ export async function promptGatewayConfig( gatewayToken = normalizeGatewayTokenInput(tokenInput) || randomToken(); } - if (!loopbackOnlyGateway && authMode === "password") { + if (authMode === "password") { const password = guardCancel( await text({ message: "Gateway password", @@ -192,7 +185,7 @@ export async function promptGatewayConfig( gatewayPassword = String(password ?? "").trim(); } - if (!loopbackOnlyGateway && authMode === "trusted-proxy") { + if (authMode === "trusted-proxy") { note( [ "Trusted proxy mode: OpenClaw trusts user identity from a reverse proxy.", @@ -268,26 +261,22 @@ export async function promptGatewayConfig( }; } - const authConfig = loopbackOnlyGateway - ? undefined - : buildGatewayAuthConfig({ - existing: next.gateway?.auth, - mode: authMode, - token: gatewayToken, - password: gatewayPassword, - trustedProxy: trustedProxyConfig, - }); + const authConfig = buildGatewayAuthConfig({ + existing: next.gateway?.auth, + mode: authMode, + token: gatewayToken, + password: gatewayPassword, + trustedProxy: trustedProxyConfig, + }); - const gatewayWithoutAuth = { ...next.gateway }; - delete gatewayWithoutAuth.auth; next = { ...next, gateway: { - ...gatewayWithoutAuth, + ...next.gateway, mode: "local", port, bind, - ...(authConfig ? { auth: authConfig } : {}), + auth: authConfig, ...(customBindHost && { customBindHost }), ...(trustedProxies && { trustedProxies }), tailscale: { diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index 0208a712c9b..9fb8183c269 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -1,5 +1,7 @@ -import fs from "node:fs"; import { intro as clackIntro, outro as clackOutro } from "@clack/prompts"; +import fs from "node:fs"; +import type { OpenClawConfig } from "../config/config.js"; +import type { RuntimeEnv } from "../runtime.js"; import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js"; import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../agents/defaults.js"; import { loadModelCatalog } from "../agents/model-catalog.js"; @@ -9,14 +11,12 @@ import { resolveHooksGmailModel, } from "../agents/model-selection.js"; import { formatCliCommand } from "../cli/command-format.js"; -import type { OpenClawConfig } from "../config/config.js"; import { CONFIG_PATH, readConfigFileSnapshot, writeConfigFile } from "../config/config.js"; import { logConfigUpdated } from "../config/logging.js"; import { resolveGatewayService } from "../daemon/service.js"; import { resolveGatewayAuth } from "../gateway/auth.js"; import { buildGatewayConnectionDetails } from "../gateway/call.js"; import { resolveOpenClawPackageRoot } from "../infra/openclaw-root.js"; -import type { RuntimeEnv } from "../runtime.js"; import { defaultRuntime } from "../runtime.js"; import { note } from "../terminal/note.js"; import { stylePromptTitle } from "../terminal/prompt-style.js"; @@ -124,18 +124,14 @@ export async function doctorCommand( note(gatewayDetails.remoteFallbackNote, "Gateway"); } if (resolveMode(cfg) === "local") { - const gatewayBind = cfg.gateway?.bind ?? "loopback"; - const tailscaleMode = cfg.gateway?.tailscale?.mode ?? "off"; - const requireGatewayAuth = gatewayBind !== "loopback" || tailscaleMode !== "off"; const auth = resolveGatewayAuth({ authConfig: cfg.gateway?.auth, - tailscaleMode, + tailscaleMode: cfg.gateway?.tailscale?.mode ?? "off", }); - const needsToken = - requireGatewayAuth && auth.mode !== "password" && (auth.mode !== "token" || !auth.token); + const needsToken = auth.mode !== "password" && (auth.mode !== "token" || !auth.token); if (needsToken) { note( - "Gateway auth is off or missing a token. Token auth is recommended when the gateway is exposed beyond local loopback.", + "Gateway auth is off or missing a token. Token auth is now the recommended default (including loopback).", "Gateway auth", ); const shouldSetToken =