Onboard: require explicit mode for env secret refs

This commit is contained in:
joshavant
2026-02-24 15:04:04 -06:00
committed by Peter Steinberger
parent 103d02f98c
commit 04aa856fc0
15 changed files with 477 additions and 109 deletions

View File

@@ -8,6 +8,7 @@ import { isSecretRef, type SecretInput, type SecretRef } from "../config/types.s
import { KILOCODE_DEFAULT_MODEL_REF } from "../providers/kilocode-shared.js";
import { PROVIDER_ENV_VARS } from "../secrets/provider-env-vars.js";
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
import type { SecretInputMode } from "./onboard-types.js";
export { CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF } from "../agents/cloudflare-ai-gateway.js";
export { MISTRAL_DEFAULT_MODEL_REF, XAI_DEFAULT_MODEL_REF } from "./onboard-auth.models.js";
export { KILOCODE_DEFAULT_MODEL_REF };
@@ -15,6 +16,11 @@ export { KILOCODE_DEFAULT_MODEL_REF };
const resolveAuthAgentDir = (agentDir?: string) => agentDir ?? resolveOpenClawAgentDir();
const ENV_REF_PATTERN = /^\$\{([A-Z][A-Z0-9_]*)\}$/;
export type ApiKeyStorageOptions = {
secretInputMode?: SecretInputMode;
};
function buildEnvSecretRef(id: string): SecretRef {
return { source: "env", id };
}
@@ -27,21 +33,22 @@ function parseEnvSecretRef(value: string): SecretRef | null {
return buildEnvSecretRef(match[1]);
}
function inferProviderEnvSecretRef(provider: string, value: string): SecretRef | null {
function resolveProviderDefaultEnvSecretRef(provider: string): SecretRef {
const envVars = PROVIDER_ENV_VARS[provider];
if (!envVars || value.length === 0) {
return null;
const envVar = envVars?.find((candidate) => candidate.trim().length > 0);
if (!envVar) {
throw new Error(
`Provider "${provider}" does not have a default env var mapping for secret-input-mode=ref.`,
);
}
for (const envVar of envVars) {
const envValue = normalizeSecretInput(process.env[envVar] ?? "");
if (envValue && envValue === value) {
return buildEnvSecretRef(envVar);
}
}
return null;
return buildEnvSecretRef(envVar);
}
function resolveApiKeySecretInput(provider: string, input: SecretInput): SecretInput {
function resolveApiKeySecretInput(
provider: string,
input: SecretInput,
options?: ApiKeyStorageOptions,
): SecretInput {
if (isSecretRef(input)) {
return input;
}
@@ -50,9 +57,8 @@ function resolveApiKeySecretInput(provider: string, input: SecretInput): SecretI
if (inlineEnvRef) {
return inlineEnvRef;
}
const inferredEnvRef = inferProviderEnvSecretRef(provider, normalized);
if (inferredEnvRef) {
return inferredEnvRef;
if (options?.secretInputMode === "ref") {
return resolveProviderDefaultEnvSecretRef(provider);
}
return normalized;
}
@@ -61,6 +67,7 @@ function buildApiKeyCredential(
provider: string,
input: SecretInput,
metadata?: Record<string, string>,
options?: ApiKeyStorageOptions,
): {
type: "api_key";
provider: string;
@@ -68,7 +75,7 @@ function buildApiKeyCredential(
keyRef?: SecretRef;
metadata?: Record<string, string>;
} {
const secretInput = resolveApiKeySecretInput(provider, input);
const secretInput = resolveApiKeySecretInput(provider, input, options);
if (typeof secretInput === "string") {
return {
type: "api_key",
@@ -186,20 +193,40 @@ export async function writeOAuthCredentials(
return profileId;
}
export async function setAnthropicApiKey(key: SecretInput, agentDir?: string) {
export async function setAnthropicApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "anthropic:default",
credential: buildApiKeyCredential("anthropic", key),
credential: buildApiKeyCredential("anthropic", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setGeminiApiKey(key: SecretInput, agentDir?: string) {
export async function setOpenaiApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
upsertAuthProfile({
profileId: "openai:default",
credential: buildApiKeyCredential("openai", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setGeminiApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "google:default",
credential: buildApiKeyCredential("google", key),
credential: buildApiKeyCredential("google", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
@@ -208,48 +235,89 @@ export async function setMinimaxApiKey(
key: SecretInput,
agentDir?: string,
profileId: string = "minimax:default",
options?: ApiKeyStorageOptions,
) {
const provider = profileId.split(":")[0] ?? "minimax";
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId,
credential: buildApiKeyCredential(provider, key),
credential: buildApiKeyCredential(provider, key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setMoonshotApiKey(key: SecretInput, agentDir?: string) {
export async function setMoonshotApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "moonshot:default",
credential: buildApiKeyCredential("moonshot", key),
credential: buildApiKeyCredential("moonshot", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setKimiCodingApiKey(key: SecretInput, agentDir?: string) {
export async function setKimiCodingApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "kimi-coding:default",
credential: buildApiKeyCredential("kimi-coding", key),
credential: buildApiKeyCredential("kimi-coding", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setSyntheticApiKey(key: SecretInput, agentDir?: string) {
export async function setVolcengineApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
upsertAuthProfile({
profileId: "volcengine:default",
credential: buildApiKeyCredential("volcengine", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setByteplusApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
upsertAuthProfile({
profileId: "byteplus:default",
credential: buildApiKeyCredential("byteplus", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setSyntheticApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "synthetic:default",
credential: buildApiKeyCredential("synthetic", key),
credential: buildApiKeyCredential("synthetic", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setVeniceApiKey(key: SecretInput, agentDir?: string) {
export async function setVeniceApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "venice:default",
credential: buildApiKeyCredential("venice", key),
credential: buildApiKeyCredential("venice", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
@@ -262,29 +330,41 @@ export const TOGETHER_DEFAULT_MODEL_REF = "together/moonshotai/Kimi-K2.5";
export const LITELLM_DEFAULT_MODEL_REF = "litellm/claude-opus-4-6";
export const VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF = "vercel-ai-gateway/anthropic/claude-opus-4.6";
export async function setZaiApiKey(key: SecretInput, agentDir?: string) {
export async function setZaiApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "zai:default",
credential: buildApiKeyCredential("zai", key),
credential: buildApiKeyCredential("zai", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setXiaomiApiKey(key: SecretInput, agentDir?: string) {
export async function setXiaomiApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
upsertAuthProfile({
profileId: "xiaomi:default",
credential: buildApiKeyCredential("xiaomi", key),
credential: buildApiKeyCredential("xiaomi", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setOpenrouterApiKey(key: SecretInput, agentDir?: string) {
export async function setOpenrouterApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
// Never persist the literal "undefined" (e.g. when prompt returns undefined and caller used String(key)).
const safeKey = typeof key === "string" && key === "undefined" ? "" : key;
upsertAuthProfile({
profileId: "openrouter:default",
credential: buildApiKeyCredential("openrouter", safeKey),
credential: buildApiKeyCredential("openrouter", safeKey, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
@@ -294,95 +374,125 @@ export async function setCloudflareAiGatewayConfig(
gatewayId: string,
apiKey: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
const normalizedAccountId = accountId.trim();
const normalizedGatewayId = gatewayId.trim();
upsertAuthProfile({
profileId: "cloudflare-ai-gateway:default",
credential: buildApiKeyCredential("cloudflare-ai-gateway", apiKey, {
accountId: normalizedAccountId,
gatewayId: normalizedGatewayId,
}),
credential: buildApiKeyCredential(
"cloudflare-ai-gateway",
apiKey,
{
accountId: normalizedAccountId,
gatewayId: normalizedGatewayId,
},
options,
),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setLitellmApiKey(key: SecretInput, agentDir?: string) {
export async function setLitellmApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
upsertAuthProfile({
profileId: "litellm:default",
credential: buildApiKeyCredential("litellm", key),
credential: buildApiKeyCredential("litellm", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setVercelAiGatewayApiKey(key: SecretInput, agentDir?: string) {
export async function setVercelAiGatewayApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
upsertAuthProfile({
profileId: "vercel-ai-gateway:default",
credential: buildApiKeyCredential("vercel-ai-gateway", key),
credential: buildApiKeyCredential("vercel-ai-gateway", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setOpencodeZenApiKey(key: SecretInput, agentDir?: string) {
export async function setOpencodeZenApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
upsertAuthProfile({
profileId: "opencode:default",
credential: buildApiKeyCredential("opencode", key),
credential: buildApiKeyCredential("opencode", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setTogetherApiKey(key: SecretInput, agentDir?: string) {
export async function setTogetherApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
upsertAuthProfile({
profileId: "together:default",
credential: buildApiKeyCredential("together", key),
credential: buildApiKeyCredential("together", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setHuggingfaceApiKey(key: SecretInput, agentDir?: string) {
export async function setHuggingfaceApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
upsertAuthProfile({
profileId: "huggingface:default",
credential: buildApiKeyCredential("huggingface", key),
credential: buildApiKeyCredential("huggingface", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export function setQianfanApiKey(key: SecretInput, agentDir?: string) {
export function setQianfanApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
upsertAuthProfile({
profileId: "qianfan:default",
credential: buildApiKeyCredential("qianfan", key),
credential: buildApiKeyCredential("qianfan", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export function setXaiApiKey(key: SecretInput, agentDir?: string) {
export function setXaiApiKey(key: SecretInput, agentDir?: string, options?: ApiKeyStorageOptions) {
upsertAuthProfile({
profileId: "xai:default",
credential: buildApiKeyCredential("xai", key),
credential: buildApiKeyCredential("xai", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setMistralApiKey(key: string, agentDir?: string) {
export async function setMistralApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
upsertAuthProfile({
profileId: "mistral:default",
credential: {
type: "api_key",
provider: "mistral",
key,
},
credential: buildApiKeyCredential("mistral", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setKilocodeApiKey(key: string, agentDir?: string) {
export async function setKilocodeApiKey(
key: SecretInput,
agentDir?: string,
options?: ApiKeyStorageOptions,
) {
upsertAuthProfile({
profileId: "kilocode:default",
credential: {
type: "api_key",
provider: "kilocode",
key,
},
credential: buildApiKeyCredential("kilocode", key, undefined, options),
agentDir: resolveAuthAgentDir(agentDir),
});
}