mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-11 04:24:31 +00:00
fix(secrets): align ref contracts and non-interactive ref persistence
This commit is contained in:
committed by
Peter Steinberger
parent
86622ebea9
commit
8944b75e16
@@ -11,6 +11,14 @@ import type { SecretInputMode } from "../onboard-types.js";
|
||||
|
||||
export type NonInteractiveApiKeySource = "flag" | "env" | "profile";
|
||||
|
||||
function parseEnvVarNameFromSourceLabel(source: string | undefined): string | undefined {
|
||||
if (!source) {
|
||||
return undefined;
|
||||
}
|
||||
const match = /^(?:shell env: |env: )([A-Z][A-Z0-9_]*)$/.exec(source.trim());
|
||||
return match?.[1];
|
||||
}
|
||||
|
||||
async function resolveApiKeyFromProfiles(params: {
|
||||
provider: string;
|
||||
cfg: OpenClawConfig;
|
||||
@@ -52,7 +60,7 @@ export async function resolveNonInteractiveApiKey(params: {
|
||||
allowProfile?: boolean;
|
||||
required?: boolean;
|
||||
secretInputMode?: SecretInputMode;
|
||||
}): Promise<{ key: string; source: NonInteractiveApiKeySource } | null> {
|
||||
}): Promise<{ key: string; source: NonInteractiveApiKeySource; envVarName?: string } | null> {
|
||||
const flagKey = normalizeOptionalSecretInput(params.flagValue);
|
||||
const envResolved = resolveEnvApiKey(params.provider);
|
||||
const explicitEnvVar = params.envVarName?.trim();
|
||||
@@ -60,6 +68,7 @@ export async function resolveNonInteractiveApiKey(params: {
|
||||
? normalizeOptionalSecretInput(process.env[explicitEnvVar])
|
||||
: undefined;
|
||||
const resolvedEnvKey = envResolved?.apiKey ?? explicitEnvKey;
|
||||
const resolvedEnvVarName = parseEnvVarNameFromSourceLabel(envResolved?.source) ?? explicitEnvVar;
|
||||
|
||||
if (params.secretInputMode === "ref") {
|
||||
if (!resolvedEnvKey && flagKey) {
|
||||
@@ -73,7 +82,17 @@ export async function resolveNonInteractiveApiKey(params: {
|
||||
return null;
|
||||
}
|
||||
if (resolvedEnvKey) {
|
||||
return { key: resolvedEnvKey, source: "env" };
|
||||
if (!resolvedEnvVarName) {
|
||||
params.runtime.error(
|
||||
[
|
||||
`--secret-input-mode ref requires an explicit environment variable for provider "${params.provider}".`,
|
||||
`Set ${params.envVar} in env and retry, or use --secret-input-mode plaintext.`,
|
||||
].join("\n"),
|
||||
);
|
||||
params.runtime.exit(1);
|
||||
return null;
|
||||
}
|
||||
return { key: resolvedEnvKey, source: "env", envVarName: resolvedEnvVarName };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,7 +101,7 @@ export async function resolveNonInteractiveApiKey(params: {
|
||||
}
|
||||
|
||||
if (resolvedEnvKey) {
|
||||
return { key: resolvedEnvKey, source: "env" };
|
||||
return { key: resolvedEnvKey, source: "env", envVarName: resolvedEnvVarName };
|
||||
}
|
||||
|
||||
if (params.allowProfile ?? true) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { parseDurationMs } from "../../../cli/parse-duration.js";
|
||||
import type { OpenClawConfig } from "../../../config/config.js";
|
||||
import type { SecretInput } from "../../../config/types.secrets.js";
|
||||
import type { RuntimeEnv } from "../../../runtime.js";
|
||||
import { resolveDefaultSecretProviderAlias } from "../../../secrets/ref-contract.js";
|
||||
import { normalizeSecretInput } from "../../../utils/normalize-secret-input.js";
|
||||
import { normalizeSecretInputModeInput } from "../../auth-choice.apply-helpers.js";
|
||||
import { buildTokenProfileId, validateAnthropicSetupToken } from "../../auth-token.js";
|
||||
@@ -67,6 +68,10 @@ import { applyOpenAIConfig } from "../../openai-model-default.js";
|
||||
import { detectZaiEndpoint } from "../../zai-endpoint-detect.js";
|
||||
import { resolveNonInteractiveApiKey } from "../api-keys.js";
|
||||
|
||||
type ResolvedNonInteractiveApiKey = NonNullable<
|
||||
Awaited<ReturnType<typeof resolveNonInteractiveApiKey>>
|
||||
>;
|
||||
|
||||
export async function applyNonInteractiveAuthChoice(params: {
|
||||
nextConfig: OpenClawConfig;
|
||||
authChoice: AuthChoice;
|
||||
@@ -85,13 +90,50 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
const apiKeyStorageOptions = requestedSecretInputMode
|
||||
? { secretInputMode: requestedSecretInputMode }
|
||||
: undefined;
|
||||
const resolveApiKey = (
|
||||
input: Parameters<typeof resolveNonInteractiveApiKey>[0],
|
||||
): ReturnType<typeof resolveNonInteractiveApiKey> =>
|
||||
const toStoredSecretInput = (resolved: ResolvedNonInteractiveApiKey): SecretInput | null => {
|
||||
if (requestedSecretInputMode !== "ref") {
|
||||
return resolved.key;
|
||||
}
|
||||
if (resolved.source !== "env") {
|
||||
return resolved.key;
|
||||
}
|
||||
if (!resolved.envVarName) {
|
||||
runtime.error(
|
||||
[
|
||||
`Unable to determine which environment variable to store as a ref for provider "${authChoice}".`,
|
||||
"Set an explicit provider env var and retry, or use --secret-input-mode plaintext.",
|
||||
].join("\n"),
|
||||
);
|
||||
runtime.exit(1);
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
source: "env",
|
||||
provider: resolveDefaultSecretProviderAlias(baseConfig, "env", {
|
||||
preferFirstProviderForSource: true,
|
||||
}),
|
||||
id: resolved.envVarName,
|
||||
};
|
||||
};
|
||||
const resolveApiKey = (input: Parameters<typeof resolveNonInteractiveApiKey>[0]) =>
|
||||
resolveNonInteractiveApiKey({
|
||||
...input,
|
||||
secretInputMode: requestedSecretInputMode,
|
||||
});
|
||||
const maybeSetResolvedApiKey = async (
|
||||
resolved: ResolvedNonInteractiveApiKey,
|
||||
setter: (value: SecretInput) => Promise<void> | void,
|
||||
): Promise<boolean> => {
|
||||
if (resolved.source === "profile") {
|
||||
return true;
|
||||
}
|
||||
const stored = toStoredSecretInput(resolved);
|
||||
if (!stored) {
|
||||
return false;
|
||||
}
|
||||
await setter(stored);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (authChoice === "claude-cli" || authChoice === "codex-cli") {
|
||||
runtime.error(
|
||||
@@ -138,8 +180,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setAnthropicApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setAnthropicApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
return applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "anthropic:default",
|
||||
@@ -215,8 +261,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setGeminiApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setGeminiApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "google:default",
|
||||
@@ -244,8 +294,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setZaiApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setZaiApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "zai:default",
|
||||
@@ -293,8 +347,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setXiaomiApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setXiaomiApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "xiaomi:default",
|
||||
@@ -316,8 +374,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
setXaiApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setXaiApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "xai:default",
|
||||
@@ -339,8 +401,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setMistralApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setMistralApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "mistral:default",
|
||||
@@ -362,8 +428,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setVolcengineApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setVolcengineApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "volcengine:default",
|
||||
@@ -385,8 +455,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setByteplusApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setByteplusApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "byteplus:default",
|
||||
@@ -408,8 +482,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
setQianfanApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setQianfanApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "qianfan:default",
|
||||
@@ -431,8 +509,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setOpenaiApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setOpenaiApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "openai:default",
|
||||
@@ -454,8 +536,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setOpenrouterApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setOpenrouterApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "openrouter:default",
|
||||
@@ -477,8 +563,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setKilocodeApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setKilocodeApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "kilocode:default",
|
||||
@@ -500,8 +590,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setLitellmApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setLitellmApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "litellm:default",
|
||||
@@ -523,8 +617,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setVercelAiGatewayApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setVercelAiGatewayApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "vercel-ai-gateway:default",
|
||||
@@ -559,10 +657,14 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
const stored = toStoredSecretInput(resolved);
|
||||
if (!stored) {
|
||||
return null;
|
||||
}
|
||||
await setCloudflareAiGatewayConfig(
|
||||
accountId,
|
||||
gatewayId,
|
||||
resolved.key,
|
||||
stored,
|
||||
undefined,
|
||||
apiKeyStorageOptions,
|
||||
);
|
||||
@@ -592,8 +694,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setMoonshotApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setMoonshotApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "moonshot:default",
|
||||
@@ -623,8 +729,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setKimiCodingApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setKimiCodingApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "kimi-coding:default",
|
||||
@@ -646,8 +756,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setSyntheticApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setSyntheticApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "synthetic:default",
|
||||
@@ -669,8 +783,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setVeniceApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setVeniceApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "venice:default",
|
||||
@@ -700,8 +818,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setMinimaxApiKey(resolved.key, undefined, profileId, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setMinimaxApiKey(value, undefined, profileId, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId,
|
||||
@@ -731,8 +853,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setOpencodeZenApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setOpencodeZenApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "opencode:default",
|
||||
@@ -754,8 +880,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setTogetherApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setTogetherApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "together:default",
|
||||
@@ -777,8 +907,12 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
if (!resolved) {
|
||||
return null;
|
||||
}
|
||||
if (resolved.source !== "profile") {
|
||||
await setHuggingfaceApiKey(resolved.key, undefined, apiKeyStorageOptions);
|
||||
if (
|
||||
!(await maybeSetResolvedApiKey(resolved, (value) =>
|
||||
setHuggingfaceApiKey(value, undefined, apiKeyStorageOptions),
|
||||
))
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
nextConfig = applyAuthProfileConfig(nextConfig, {
|
||||
profileId: "huggingface:default",
|
||||
@@ -812,10 +946,18 @@ export async function applyNonInteractiveAuthChoice(params: {
|
||||
runtime,
|
||||
required: false,
|
||||
});
|
||||
const customApiKeyInput: SecretInput | undefined =
|
||||
requestedSecretInputMode === "ref" && resolvedCustomApiKey?.source === "env"
|
||||
? { source: "env", provider: "default", id: "CUSTOM_API_KEY" }
|
||||
: resolvedCustomApiKey?.key;
|
||||
let customApiKeyInput: SecretInput | undefined;
|
||||
if (resolvedCustomApiKey) {
|
||||
if (requestedSecretInputMode === "ref") {
|
||||
const stored = toStoredSecretInput(resolvedCustomApiKey);
|
||||
if (!stored) {
|
||||
return null;
|
||||
}
|
||||
customApiKeyInput = stored;
|
||||
} else {
|
||||
customApiKeyInput = resolvedCustomApiKey.key;
|
||||
}
|
||||
}
|
||||
const result = applyCustomApiConfig({
|
||||
config: nextConfig,
|
||||
baseUrl: customAuth.baseUrl,
|
||||
|
||||
Reference in New Issue
Block a user