revert(gateway): restore loopback auth setup

This commit is contained in:
Sebastian
2026-02-16 22:28:02 -05:00
parent b7cf28f407
commit 1486eb66fd
3 changed files with 57 additions and 75 deletions

View File

@@ -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("");
});

View File

@@ -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: {

View File

@@ -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 =