mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-23 07:51:33 +00:00
311 lines
10 KiB
TypeScript
311 lines
10 KiB
TypeScript
import type { OpenClawConfig } from "../config/types.js";
|
|
import { isValidEnvSecretRefId, type SecretRef } from "../config/types.secrets.js";
|
|
import { encodeJsonPointerToken } from "../secrets/json-pointer.js";
|
|
import { PROVIDER_ENV_VARS } from "../secrets/provider-env-vars.js";
|
|
import {
|
|
formatExecSecretRefIdValidationMessage,
|
|
isValidExecSecretRefId,
|
|
isValidFileSecretRefId,
|
|
resolveDefaultSecretProviderAlias,
|
|
} from "../secrets/ref-contract.js";
|
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
|
|
|
let secretResolvePromise: Promise<typeof import("../secrets/resolve.js")> | undefined;
|
|
|
|
function loadSecretResolve() {
|
|
secretResolvePromise ??= import("../secrets/resolve.js");
|
|
return secretResolvePromise;
|
|
}
|
|
|
|
const ENV_SOURCE_LABEL_RE = /(?:^|:\s)([A-Z][A-Z0-9_]*)$/;
|
|
|
|
type SecretRefChoice = "env" | "provider"; // pragma: allowlist secret
|
|
|
|
export type SecretRefSetupPromptCopy = {
|
|
sourceMessage?: string;
|
|
envVarMessage?: string;
|
|
envVarPlaceholder?: string;
|
|
envVarFormatError?: string;
|
|
envVarMissingError?: (envVar: string) => string;
|
|
noProvidersMessage?: string;
|
|
envValidatedMessage?: (envVar: string) => string;
|
|
providerValidatedMessage?: (provider: string, id: string, source: "file" | "exec") => string;
|
|
};
|
|
|
|
function formatErrorMessage(error: unknown): string {
|
|
if (error instanceof Error && typeof error.message === "string" && error.message.trim()) {
|
|
return error.message;
|
|
}
|
|
return String(error);
|
|
}
|
|
|
|
export function extractEnvVarFromSourceLabel(source: string): string | undefined {
|
|
const match = ENV_SOURCE_LABEL_RE.exec(source.trim());
|
|
return match?.[1];
|
|
}
|
|
|
|
function resolveDefaultProviderEnvVar(provider: string): string | undefined {
|
|
const envVars = PROVIDER_ENV_VARS[provider];
|
|
return envVars?.find((candidate) => candidate.trim().length > 0);
|
|
}
|
|
|
|
function resolveDefaultFilePointerId(provider: string): string {
|
|
return `/providers/${encodeJsonPointerToken(provider)}/apiKey`;
|
|
}
|
|
|
|
export function resolveRefFallbackInput(params: {
|
|
config: OpenClawConfig;
|
|
provider: string;
|
|
preferredEnvVar?: string;
|
|
}): { ref: SecretRef; resolvedValue: string } {
|
|
const fallbackEnvVar = params.preferredEnvVar ?? resolveDefaultProviderEnvVar(params.provider);
|
|
if (!fallbackEnvVar) {
|
|
throw new Error(
|
|
`No default environment variable mapping found for provider "${params.provider}". Set a provider-specific env var, or re-run setup in an interactive terminal to configure a ref.`,
|
|
);
|
|
}
|
|
const value = process.env[fallbackEnvVar]?.trim();
|
|
if (!value) {
|
|
throw new Error(
|
|
`Environment variable "${fallbackEnvVar}" is required for --secret-input-mode ref in non-interactive setup.`,
|
|
);
|
|
}
|
|
return {
|
|
ref: {
|
|
source: "env",
|
|
provider: resolveDefaultSecretProviderAlias(params.config, "env", {
|
|
preferFirstProviderForSource: true,
|
|
}),
|
|
id: fallbackEnvVar,
|
|
},
|
|
resolvedValue: value,
|
|
};
|
|
}
|
|
|
|
async function promptEnvSecretRefForSetup(params: {
|
|
provider: string;
|
|
config: OpenClawConfig;
|
|
prompter: WizardPrompter;
|
|
defaultEnvVar: string;
|
|
copy?: SecretRefSetupPromptCopy;
|
|
}): Promise<{ ref: SecretRef; resolvedValue: string }> {
|
|
const envVarRaw = await params.prompter.text({
|
|
message: params.copy?.envVarMessage ?? "Environment variable name",
|
|
initialValue: params.defaultEnvVar || undefined,
|
|
placeholder: params.copy?.envVarPlaceholder ?? "OPENAI_API_KEY",
|
|
validate: (value) => {
|
|
const candidate = value.trim();
|
|
if (!isValidEnvSecretRefId(candidate)) {
|
|
return (
|
|
params.copy?.envVarFormatError ??
|
|
'Use an env var name like "OPENAI_API_KEY" (uppercase letters, numbers, underscores).'
|
|
);
|
|
}
|
|
if (!process.env[candidate]?.trim()) {
|
|
return (
|
|
params.copy?.envVarMissingError?.(candidate) ??
|
|
`Environment variable "${candidate}" is missing or empty in this session.`
|
|
);
|
|
}
|
|
return undefined;
|
|
},
|
|
});
|
|
const envCandidate = String(envVarRaw ?? "").trim();
|
|
const envVar =
|
|
envCandidate && isValidEnvSecretRefId(envCandidate) ? envCandidate : params.defaultEnvVar;
|
|
if (!envVar) {
|
|
throw new Error(
|
|
`No valid environment variable name provided for provider "${params.provider}".`,
|
|
);
|
|
}
|
|
const resolvedValue = process.env[envVar]?.trim();
|
|
if (!resolvedValue) {
|
|
throw new Error(`Environment variable "${envVar}" is missing or empty in this session.`);
|
|
}
|
|
const ref: SecretRef = {
|
|
source: "env",
|
|
provider: resolveDefaultSecretProviderAlias(params.config, "env", {
|
|
preferFirstProviderForSource: true,
|
|
}),
|
|
id: envVar,
|
|
};
|
|
await params.prompter.note(
|
|
params.copy?.envValidatedMessage?.(envVar) ??
|
|
`Validated environment variable ${envVar}. OpenClaw will store a reference, not the key value.`,
|
|
"Reference validated",
|
|
);
|
|
return { ref, resolvedValue };
|
|
}
|
|
|
|
async function promptProviderSecretRefForSetup(params: {
|
|
provider: string;
|
|
config: OpenClawConfig;
|
|
prompter: WizardPrompter;
|
|
defaultFilePointer: string;
|
|
copy?: SecretRefSetupPromptCopy;
|
|
}): Promise<{ ref: SecretRef; resolvedValue: string }> {
|
|
const externalProviders = Object.entries(params.config.secrets?.providers ?? {}).filter(
|
|
([, provider]) => provider?.source === "file" || provider?.source === "exec",
|
|
);
|
|
if (externalProviders.length === 0) {
|
|
await params.prompter.note(
|
|
params.copy?.noProvidersMessage ??
|
|
"No file/exec secret providers are configured yet. Add one under secrets.providers, or select Environment variable.",
|
|
"No providers configured",
|
|
);
|
|
throw new Error("retry");
|
|
}
|
|
|
|
const defaultProvider = resolveDefaultSecretProviderAlias(params.config, "file", {
|
|
preferFirstProviderForSource: true,
|
|
});
|
|
const selectedProvider = await params.prompter.select<string>({
|
|
message: "Select secret provider",
|
|
initialValue:
|
|
externalProviders.find(([providerName]) => providerName === defaultProvider)?.[0] ??
|
|
externalProviders[0]?.[0],
|
|
options: externalProviders.map(([providerName, provider]) => ({
|
|
value: providerName,
|
|
label: providerName,
|
|
hint: provider?.source === "exec" ? "Exec provider" : "File provider",
|
|
})),
|
|
});
|
|
const providerEntry = params.config.secrets?.providers?.[selectedProvider];
|
|
if (!providerEntry || (providerEntry.source !== "file" && providerEntry.source !== "exec")) {
|
|
await params.prompter.note(
|
|
`Provider "${selectedProvider}" is not a file/exec provider.`,
|
|
"Invalid provider",
|
|
);
|
|
throw new Error("retry");
|
|
}
|
|
|
|
const idPrompt =
|
|
providerEntry.source === "file"
|
|
? "Secret id (JSON pointer for json mode, or 'value' for singleValue mode)"
|
|
: "Secret id for the exec provider";
|
|
const idDefault =
|
|
providerEntry.source === "file"
|
|
? providerEntry.mode === "singleValue"
|
|
? "value"
|
|
: params.defaultFilePointer
|
|
: `${params.provider}/apiKey`;
|
|
const idRaw = await params.prompter.text({
|
|
message: idPrompt,
|
|
initialValue: idDefault,
|
|
placeholder: providerEntry.source === "file" ? "/providers/openai/apiKey" : "openai/api-key",
|
|
validate: (value) => {
|
|
const candidate = value.trim();
|
|
if (!candidate) {
|
|
return "Secret id cannot be empty.";
|
|
}
|
|
if (
|
|
providerEntry.source === "file" &&
|
|
providerEntry.mode !== "singleValue" &&
|
|
!isValidFileSecretRefId(candidate)
|
|
) {
|
|
return 'Use an absolute JSON pointer like "/providers/openai/apiKey".';
|
|
}
|
|
if (
|
|
providerEntry.source === "file" &&
|
|
providerEntry.mode === "singleValue" &&
|
|
candidate !== "value"
|
|
) {
|
|
return 'singleValue mode expects id "value".';
|
|
}
|
|
if (providerEntry.source === "exec" && !isValidExecSecretRefId(candidate)) {
|
|
return formatExecSecretRefIdValidationMessage();
|
|
}
|
|
return undefined;
|
|
},
|
|
});
|
|
const id = String(idRaw ?? "").trim() || idDefault;
|
|
const ref: SecretRef = {
|
|
source: providerEntry.source,
|
|
provider: selectedProvider,
|
|
id,
|
|
};
|
|
|
|
try {
|
|
const { resolveSecretRefString } = await loadSecretResolve();
|
|
const resolvedValue = await resolveSecretRefString(ref, {
|
|
config: params.config,
|
|
env: process.env,
|
|
});
|
|
await params.prompter.note(
|
|
params.copy?.providerValidatedMessage?.(selectedProvider, id, providerEntry.source) ??
|
|
`Validated ${providerEntry.source} reference ${selectedProvider}:${id}. OpenClaw will store a reference, not the key value.`,
|
|
"Reference validated",
|
|
);
|
|
return { ref, resolvedValue };
|
|
} catch (error) {
|
|
await params.prompter.note(
|
|
[
|
|
`Could not validate provider reference ${selectedProvider}:${id}.`,
|
|
formatErrorMessage(error),
|
|
"Check your provider configuration and try again.",
|
|
].join("\n"),
|
|
"Reference check failed",
|
|
);
|
|
throw new Error("retry", { cause: error });
|
|
}
|
|
}
|
|
|
|
export async function promptSecretRefForSetup(params: {
|
|
provider: string;
|
|
config: OpenClawConfig;
|
|
prompter: WizardPrompter;
|
|
preferredEnvVar?: string;
|
|
copy?: SecretRefSetupPromptCopy;
|
|
}): Promise<{ ref: SecretRef; resolvedValue: string }> {
|
|
const defaultEnvVar =
|
|
params.preferredEnvVar ?? resolveDefaultProviderEnvVar(params.provider) ?? "";
|
|
const defaultFilePointer = resolveDefaultFilePointerId(params.provider);
|
|
let sourceChoice: SecretRefChoice = "env"; // pragma: allowlist secret
|
|
|
|
while (true) {
|
|
const sourceRaw: SecretRefChoice = await params.prompter.select<SecretRefChoice>({
|
|
message: params.copy?.sourceMessage ?? "Where is this API key stored?",
|
|
initialValue: sourceChoice,
|
|
options: [
|
|
{
|
|
value: "env",
|
|
label: "Environment variable",
|
|
hint: "Reference a variable from your runtime environment",
|
|
},
|
|
{
|
|
value: "provider",
|
|
label: "Configured secret provider",
|
|
hint: "Use a configured file or exec secret provider",
|
|
},
|
|
],
|
|
});
|
|
const source: SecretRefChoice = sourceRaw === "provider" ? "provider" : "env";
|
|
sourceChoice = source;
|
|
|
|
if (source === "env") {
|
|
return await promptEnvSecretRefForSetup({
|
|
provider: params.provider,
|
|
config: params.config,
|
|
prompter: params.prompter,
|
|
defaultEnvVar,
|
|
copy: params.copy,
|
|
});
|
|
}
|
|
|
|
try {
|
|
return await promptProviderSecretRefForSetup({
|
|
provider: params.provider,
|
|
config: params.config,
|
|
prompter: params.prompter,
|
|
defaultFilePointer,
|
|
copy: params.copy,
|
|
});
|
|
} catch (error) {
|
|
if (error instanceof Error && error.message === "retry") {
|
|
continue;
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
}
|