Gateway: add SecretRef support for gateway.auth.token with auth-mode guardrails (#35094)

This commit is contained in:
Josh Avant
2026-03-05 12:53:56 -06:00
committed by GitHub
parent bc66a8fa81
commit 72cf9253fc
112 changed files with 5750 additions and 465 deletions

View File

@@ -16,6 +16,38 @@ export type GatewayCredentialPrecedence = "env-first" | "config-first";
export type GatewayRemoteCredentialPrecedence = "remote-first" | "env-first";
export type GatewayRemoteCredentialFallback = "remote-env-local" | "remote-only";
const GATEWAY_SECRET_REF_UNAVAILABLE_ERROR_CODE = "GATEWAY_SECRET_REF_UNAVAILABLE";
export class GatewaySecretRefUnavailableError extends Error {
readonly code = GATEWAY_SECRET_REF_UNAVAILABLE_ERROR_CODE;
readonly path: string;
constructor(path: string) {
super(
[
`${path} is configured as a secret reference but is unavailable in this command path.`,
"Fix: set OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD, pass explicit --token/--password,",
"or run a gateway command path that resolves secret references before credential selection.",
].join("\n"),
);
this.name = "GatewaySecretRefUnavailableError";
this.path = path;
}
}
export function isGatewaySecretRefUnavailableError(
error: unknown,
expectedPath?: string,
): error is GatewaySecretRefUnavailableError {
if (!(error instanceof GatewaySecretRefUnavailableError)) {
return false;
}
if (!expectedPath) {
return true;
}
return error.path === expectedPath;
}
export function trimToUndefined(value: unknown): string | undefined {
if (typeof value !== "string") {
return undefined;
@@ -34,13 +66,7 @@ function firstDefined(values: Array<string | undefined>): string | undefined {
}
function throwUnresolvedGatewaySecretInput(path: string): never {
throw new Error(
[
`${path} is configured as a secret reference but is unavailable in this command path.`,
"Fix: set OPENCLAW_GATEWAY_TOKEN/OPENCLAW_GATEWAY_PASSWORD, pass explicit --token/--password,",
"or run a gateway command path that resolves secret references before credential selection.",
].join("\n"),
);
throw new GatewaySecretRefUnavailableError(path);
}
function readGatewayTokenEnv(
@@ -144,10 +170,28 @@ export function resolveGatewayCredentialsFromConfig(params: {
const envToken = readGatewayTokenEnv(env, includeLegacyEnv);
const envPassword = readGatewayPasswordEnv(env, includeLegacyEnv);
const remoteToken = trimToUndefined(remote?.token);
const remotePassword = trimToUndefined(remote?.password);
const localToken = trimToUndefined(params.cfg.gateway?.auth?.token);
const localPassword = trimToUndefined(params.cfg.gateway?.auth?.password);
const localTokenRef = resolveSecretInputRef({
value: params.cfg.gateway?.auth?.token,
defaults,
}).ref;
const localPasswordRef = resolveSecretInputRef({
value: params.cfg.gateway?.auth?.password,
defaults,
}).ref;
const remoteTokenRef = resolveSecretInputRef({
value: remote?.token,
defaults,
}).ref;
const remotePasswordRef = resolveSecretInputRef({
value: remote?.password,
defaults,
}).ref;
const remoteToken = remoteTokenRef ? undefined : trimToUndefined(remote?.token);
const remotePassword = remotePasswordRef ? undefined : trimToUndefined(remote?.password);
const localToken = localTokenRef ? undefined : trimToUndefined(params.cfg.gateway?.auth?.token);
const localPassword = localPasswordRef
? undefined
: trimToUndefined(params.cfg.gateway?.auth?.password);
const localTokenPrecedence = params.localTokenPrecedence ?? "env-first";
const localPasswordPrecedence = params.localPasswordPrecedence ?? "env-first";
@@ -172,10 +216,15 @@ export function resolveGatewayCredentialsFromConfig(params: {
authMode !== "none" &&
authMode !== "trusted-proxy" &&
!localResolved.token);
const localPasswordRef = resolveSecretInputRef({
value: params.cfg.gateway?.auth?.password,
defaults,
}).ref;
const localTokenCanWin =
authMode === "token" ||
(authMode !== "password" &&
authMode !== "none" &&
authMode !== "trusted-proxy" &&
!localResolved.password);
if (localTokenRef && !localResolved.token && !envToken && localTokenCanWin) {
throwUnresolvedGatewaySecretInput("gateway.auth.token");
}
if (localPasswordRef && !localResolved.password && !envPassword && localPasswordCanWin) {
throwUnresolvedGatewaySecretInput("gateway.auth.password");
}
@@ -200,14 +249,10 @@ export function resolveGatewayCredentialsFromConfig(params: {
? firstDefined([envPassword, remotePassword, localPassword])
: firstDefined([remotePassword, envPassword, localPassword]);
const remoteTokenRef = resolveSecretInputRef({
value: remote?.token,
defaults,
}).ref;
const remotePasswordRef = resolveSecretInputRef({
value: remote?.password,
defaults,
}).ref;
const localTokenCanWin =
authMode === "token" ||
(authMode !== "password" && authMode !== "none" && authMode !== "trusted-proxy");
const localTokenFallbackEnabled = remoteTokenFallback !== "remote-only";
const localTokenFallback = remoteTokenFallback === "remote-only" ? undefined : localToken;
const localPasswordFallback =
remotePasswordFallback === "remote-only" ? undefined : localPassword;
@@ -217,6 +262,17 @@ export function resolveGatewayCredentialsFromConfig(params: {
if (remotePasswordRef && !password && !envPassword && !localPasswordFallback && !token) {
throwUnresolvedGatewaySecretInput("gateway.remote.password");
}
if (
localTokenRef &&
localTokenFallbackEnabled &&
!token &&
!password &&
!envToken &&
!remoteToken &&
localTokenCanWin
) {
throwUnresolvedGatewaySecretInput("gateway.auth.token");
}
return { token, password };
}