refactor(gateway): use auth mode provenance for bootstrap persistence

This commit is contained in:
Gustavo Madeira Santana 2026-02-19 02:15:13 -05:00
parent 5006b67829
commit 0275281e8d
6 changed files with 82 additions and 40 deletions

View File

@ -239,14 +239,17 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
}
const miskeys = extractGatewayMiskeys(snapshot?.parsed);
const authConfig = {
...cfg.gateway?.auth,
...(authMode ? { mode: authMode } : {}),
...(passwordRaw ? { password: passwordRaw } : {}),
...(tokenRaw ? { token: tokenRaw } : {}),
};
const authOverride =
authMode || passwordRaw || tokenRaw || authModeRaw
? {
...(authMode ? { mode: authMode } : {}),
...(tokenRaw ? { token: tokenRaw } : {}),
...(passwordRaw ? { password: passwordRaw } : {}),
}
: undefined;
const resolvedAuth = resolveGatewayAuth({
authConfig,
authConfig: cfg.gateway?.auth,
authOverride,
env: process.env,
tailscaleMode: tailscaleMode ?? cfg.gateway?.tailscale?.mode ?? "off",
});
@ -303,14 +306,6 @@ async function runGatewayCommand(opts: GatewayRunOpts) {
defaultRuntime.exit(1);
return;
}
const authOverride =
authMode || passwordRaw || tokenRaw || authModeRaw
? {
...(authMode ? { mode: authMode } : {}),
...(tokenRaw ? { token: tokenRaw } : {}),
...(passwordRaw ? { password: passwordRaw } : {}),
}
: undefined;
const tailscaleOverride =
tailscaleMode || opts.tailscaleResetOnExit
? {

View File

@ -34,6 +34,7 @@ describe("gateway auth", () => {
}),
).toMatchObject({
mode: "password",
modeSource: "password",
token: "env-token",
password: "env-password",
});
@ -50,6 +51,7 @@ describe("gateway auth", () => {
}),
).toMatchObject({
mode: "token",
modeSource: "default",
token: undefined,
password: undefined,
});
@ -63,11 +65,27 @@ describe("gateway auth", () => {
}),
).toMatchObject({
mode: "none",
modeSource: "config",
token: undefined,
password: undefined,
});
});
it("marks mode source as override when runtime mode override is provided", () => {
expect(
resolveGatewayAuth({
authConfig: { mode: "password", password: "config-password" },
authOverride: { mode: "token" },
env: {} as NodeJS.ProcessEnv,
}),
).toMatchObject({
mode: "token",
modeSource: "override",
token: undefined,
password: "config-password",
});
});
it("does not throw when req is missing socket", async () => {
const res = await authorizeGatewayConnect({
auth: { mode: "token", token: "secret", allowTailscale: false },

View File

@ -20,9 +20,16 @@ import {
} from "./net.js";
export type ResolvedGatewayAuthMode = "none" | "token" | "password" | "trusted-proxy";
export type ResolvedGatewayAuthModeSource =
| "override"
| "config"
| "password"
| "token"
| "default";
export type ResolvedGatewayAuth = {
mode: ResolvedGatewayAuthMode;
modeSource?: ResolvedGatewayAuthModeSource;
token?: string;
password?: string;
allowTailscale: boolean;
@ -178,24 +185,55 @@ async function resolveVerifiedTailscaleUser(params: {
export function resolveGatewayAuth(params: {
authConfig?: GatewayAuthConfig | null;
authOverride?: GatewayAuthConfig | null;
env?: NodeJS.ProcessEnv;
tailscaleMode?: GatewayTailscaleMode;
}): ResolvedGatewayAuth {
const authConfig = params.authConfig ?? {};
const baseAuthConfig = params.authConfig ?? {};
const authOverride = params.authOverride ?? undefined;
const authConfig: GatewayAuthConfig = { ...baseAuthConfig };
if (authOverride) {
if (authOverride.mode !== undefined) {
authConfig.mode = authOverride.mode;
}
if (authOverride.token !== undefined) {
authConfig.token = authOverride.token;
}
if (authOverride.password !== undefined) {
authConfig.password = authOverride.password;
}
if (authOverride.allowTailscale !== undefined) {
authConfig.allowTailscale = authOverride.allowTailscale;
}
if (authOverride.rateLimit !== undefined) {
authConfig.rateLimit = authOverride.rateLimit;
}
if (authOverride.trustedProxy !== undefined) {
authConfig.trustedProxy = authOverride.trustedProxy;
}
}
const env = params.env ?? process.env;
const token = authConfig.token ?? env.OPENCLAW_GATEWAY_TOKEN ?? undefined;
const password = authConfig.password ?? env.OPENCLAW_GATEWAY_PASSWORD ?? undefined;
const trustedProxy = authConfig.trustedProxy;
let mode: ResolvedGatewayAuth["mode"];
if (authConfig.mode) {
let modeSource: ResolvedGatewayAuth["modeSource"];
if (authOverride?.mode !== undefined) {
mode = authOverride.mode;
modeSource = "override";
} else if (authConfig.mode) {
mode = authConfig.mode;
modeSource = "config";
} else if (password) {
mode = "password";
modeSource = "password";
} else if (token) {
mode = "token";
modeSource = "token";
} else {
mode = "token";
modeSource = "default";
}
const allowTailscale =
@ -204,6 +242,7 @@ export function resolveGatewayAuth(params: {
return {
mode,
modeSource,
token,
password,
allowTailscale,

View File

@ -12,7 +12,7 @@ import {
import { normalizeControlUiBasePath } from "./control-ui-shared.js";
import { resolveHooksConfig } from "./hooks.js";
import { isLoopbackHost, resolveGatewayBindHost } from "./net.js";
import { mergeGatewayAuthConfig, mergeGatewayTailscaleConfig } from "./startup-auth.js";
import { mergeGatewayTailscaleConfig } from "./startup-auth.js";
export type GatewayRuntimeConfig = {
bindHost: string;
@ -58,15 +58,13 @@ export async function resolveGatewayRuntimeConfig(params: {
typeof controlUiRootRaw === "string" && controlUiRootRaw.trim().length > 0
? controlUiRootRaw.trim()
: undefined;
const authBase = params.cfg.gateway?.auth ?? {};
const authOverrides = params.auth ?? {};
const authConfig = mergeGatewayAuthConfig(authBase, authOverrides);
const tailscaleBase = params.cfg.gateway?.tailscale ?? {};
const tailscaleOverrides = params.tailscale ?? {};
const tailscaleConfig = mergeGatewayTailscaleConfig(tailscaleBase, tailscaleOverrides);
const tailscaleMode = tailscaleConfig.mode ?? "off";
const resolvedAuth = resolveGatewayAuth({
authConfig,
authConfig: params.cfg.gateway?.auth,
authOverride: params.auth,
env: process.env,
tailscaleMode,
});

View File

@ -243,8 +243,8 @@ export async function startGatewayServer(
"Gateway auth token was missing. Generated a new token and saved it to config (gateway.auth.token).",
);
} else {
log.info(
"Gateway auth token was missing. Generated a runtime token for this startup without changing config.",
log.warn(
"Gateway auth token was missing. Generated a runtime token for this startup without changing config; restart will generate a different token. Persist one with `openclaw config set gateway.auth.mode token` and `openclaw config set gateway.auth.token <token>`.",
);
}
}

View File

@ -1,12 +1,11 @@
import crypto from "node:crypto";
import type {
GatewayAuthConfig,
GatewayAuthMode,
GatewayTailscaleConfig,
OpenClawConfig,
} from "../config/config.js";
import { writeConfigFile } from "../config/config.js";
import { resolveGatewayAuth } from "./auth.js";
import { resolveGatewayAuth, type ResolvedGatewayAuth } from "./auth.js";
export function mergeGatewayAuthConfig(
base?: GatewayAuthConfig,
@ -60,13 +59,13 @@ function resolveGatewayAuthFromConfig(params: {
authOverride?: GatewayAuthConfig;
tailscaleOverride?: GatewayTailscaleConfig;
}) {
const authConfig = mergeGatewayAuthConfig(params.cfg.gateway?.auth, params.authOverride);
const tailscaleConfig = mergeGatewayTailscaleConfig(
params.cfg.gateway?.tailscale,
params.tailscaleOverride,
);
return resolveGatewayAuth({
authConfig,
authConfig: params.cfg.gateway?.auth,
authOverride: params.authOverride,
env: params.env,
tailscaleMode: tailscaleConfig.mode ?? "off",
});
@ -74,16 +73,15 @@ function resolveGatewayAuthFromConfig(params: {
function shouldPersistGeneratedToken(params: {
persistRequested: boolean;
baselineMode: GatewayAuthMode;
overrideMode?: GatewayAuthMode;
resolvedAuth: ResolvedGatewayAuth;
}): boolean {
if (!params.persistRequested) {
return false;
}
// Keep CLI/runtime auth mode overrides ephemeral when they switch from an
// explicit non-token config mode to token mode.
if (params.overrideMode === "token" && params.baselineMode !== "token") {
// Keep CLI/runtime mode overrides ephemeral: startup should not silently
// mutate durable auth policy when mode was chosen by an override flag.
if (params.resolvedAuth.modeSource === "override") {
return false;
}
@ -104,11 +102,6 @@ export async function ensureGatewayStartupAuth(params: {
}> {
const env = params.env ?? process.env;
const persistRequested = params.persist === true;
const baselineResolved = resolveGatewayAuthFromConfig({
cfg: params.cfg,
env,
tailscaleOverride: params.tailscaleOverride,
});
const resolved = resolveGatewayAuthFromConfig({
cfg: params.cfg,
env,
@ -133,8 +126,7 @@ export async function ensureGatewayStartupAuth(params: {
};
const persist = shouldPersistGeneratedToken({
persistRequested,
baselineMode: baselineResolved.mode,
overrideMode: params.authOverride?.mode,
resolvedAuth: resolved,
});
if (persist) {
await writeConfigFile(nextCfg);