refactor: isolate provider sdk auth and model helpers
This commit is contained in:
parent
ad7924b0ac
commit
dde89d2a83
@ -41,10 +41,6 @@ import { collectWhatsAppStatusIssues } from "./status-issues.js";
|
|||||||
|
|
||||||
const meta = getChatChannelMeta("whatsapp");
|
const meta = getChatChannelMeta("whatsapp");
|
||||||
|
|
||||||
async function loadWhatsAppChannelRuntime() {
|
|
||||||
return await import("./channel.runtime.js");
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeWhatsAppPayloadText(text: string | undefined): string {
|
function normalizeWhatsAppPayloadText(text: string | undefined): string {
|
||||||
return (text ?? "").replace(/^(?:[ \t]*\r?\n)+/, "");
|
return (text ?? "").replace(/^(?:[ \t]*\r?\n)+/, "");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import {
|
|
||||||
promptSecretRefForSetup,
|
|
||||||
resolveSecretInputModeForEnvSelection,
|
|
||||||
} from "../../commands/auth-choice.apply-helpers.js";
|
|
||||||
import type { OpenClawConfig } from "../../config/config.js";
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
import type { DmPolicy, GroupPolicy } from "../../config/types.js";
|
import type { DmPolicy, GroupPolicy } from "../../config/types.js";
|
||||||
import type { SecretInput } from "../../config/types.secrets.js";
|
import type { SecretInput } from "../../config/types.secrets.js";
|
||||||
|
import {
|
||||||
|
promptSecretRefForSetup,
|
||||||
|
resolveSecretInputModeForEnvSelection,
|
||||||
|
} from "../../plugins/provider-auth-input.js";
|
||||||
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../../routing/session-key.js";
|
||||||
import type { WizardPrompter } from "../../wizard/prompts.js";
|
import type { WizardPrompter } from "../../wizard/prompts.js";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -1,48 +1,5 @@
|
|||||||
const DEFAULT_KEY_PREVIEW = { head: 4, tail: 4 };
|
export {
|
||||||
|
formatApiKeyPreview,
|
||||||
export function normalizeApiKeyInput(raw: string): string {
|
normalizeApiKeyInput,
|
||||||
const trimmed = String(raw ?? "").trim();
|
validateApiKeyInput,
|
||||||
if (!trimmed) {
|
} from "../plugins/provider-auth-input.js";
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle shell-style assignments: export KEY="value" or KEY=value
|
|
||||||
const assignmentMatch = trimmed.match(/^(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=\s*(.+)$/);
|
|
||||||
const valuePart = assignmentMatch ? assignmentMatch[1].trim() : trimmed;
|
|
||||||
|
|
||||||
const unquoted =
|
|
||||||
valuePart.length >= 2 &&
|
|
||||||
((valuePart.startsWith('"') && valuePart.endsWith('"')) ||
|
|
||||||
(valuePart.startsWith("'") && valuePart.endsWith("'")) ||
|
|
||||||
(valuePart.startsWith("`") && valuePart.endsWith("`")))
|
|
||||||
? valuePart.slice(1, -1)
|
|
||||||
: valuePart;
|
|
||||||
|
|
||||||
const withoutSemicolon = unquoted.endsWith(";") ? unquoted.slice(0, -1) : unquoted;
|
|
||||||
|
|
||||||
return withoutSemicolon.trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
export const validateApiKeyInput = (value: string) =>
|
|
||||||
normalizeApiKeyInput(value).length > 0 ? undefined : "Required";
|
|
||||||
|
|
||||||
export function formatApiKeyPreview(
|
|
||||||
raw: string,
|
|
||||||
opts: { head?: number; tail?: number } = {},
|
|
||||||
): string {
|
|
||||||
const trimmed = raw.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return "…";
|
|
||||||
}
|
|
||||||
const head = opts.head ?? DEFAULT_KEY_PREVIEW.head;
|
|
||||||
const tail = opts.tail ?? DEFAULT_KEY_PREVIEW.tail;
|
|
||||||
if (trimmed.length <= head + tail) {
|
|
||||||
const shortHead = Math.min(2, trimmed.length);
|
|
||||||
const shortTail = Math.min(2, trimmed.length - shortHead);
|
|
||||||
if (shortTail <= 0) {
|
|
||||||
return `${trimmed.slice(0, shortHead)}…`;
|
|
||||||
}
|
|
||||||
return `${trimmed.slice(0, shortHead)}…${trimmed.slice(-shortTail)}`;
|
|
||||||
}
|
|
||||||
return `${trimmed.slice(0, head)}…${trimmed.slice(-tail)}`;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,280 +1,19 @@
|
|||||||
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
|
||||||
import type { OpenClawConfig } from "../config/types.js";
|
|
||||||
import {
|
|
||||||
isValidEnvSecretRefId,
|
|
||||||
type SecretInput,
|
|
||||||
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 { resolveSecretRefString } from "../secrets/resolve.js";
|
|
||||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
|
||||||
import { formatApiKeyPreview } from "./auth-choice.api-key.js";
|
|
||||||
import type { ApplyAuthChoiceParams } from "./auth-choice.apply.js";
|
import type { ApplyAuthChoiceParams } from "./auth-choice.apply.js";
|
||||||
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
|
import { applyDefaultModelChoice } from "./auth-choice.default-model.js";
|
||||||
import type { SecretInputMode } from "./onboard-types.js";
|
|
||||||
|
|
||||||
const ENV_SOURCE_LABEL_RE = /(?:^|:\s)([A-Z][A-Z0-9_]*)$/;
|
export type {
|
||||||
|
SecretInputModePromptCopy,
|
||||||
type SecretRefChoice = "env" | "provider"; // pragma: allowlist secret
|
SecretRefSetupPromptCopy,
|
||||||
|
} from "../plugins/provider-auth-input.js";
|
||||||
export type SecretInputModePromptCopy = {
|
export {
|
||||||
modeMessage?: string;
|
ensureApiKeyFromEnvOrPrompt,
|
||||||
plaintextLabel?: string;
|
ensureApiKeyFromOptionEnvOrPrompt,
|
||||||
plaintextHint?: string;
|
maybeApplyApiKeyFromOption,
|
||||||
refLabel?: string;
|
normalizeSecretInputModeInput,
|
||||||
refHint?: string;
|
normalizeTokenProviderInput,
|
||||||
};
|
promptSecretRefForSetup,
|
||||||
|
resolveSecretInputModeForEnvSelection,
|
||||||
export type SecretRefSetupPromptCopy = {
|
} from "../plugins/provider-auth-input.js";
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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`;
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
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") {
|
|
||||||
const envVarRaw = await params.prompter.text({
|
|
||||||
message: params.copy?.envVarMessage ?? "Environment variable name",
|
|
||||||
initialValue: 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 : defaultEnvVar;
|
|
||||||
if (!envVar) {
|
|
||||||
throw new Error(
|
|
||||||
`No valid environment variable name provided for provider "${params.provider}".`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const ref: SecretRef = {
|
|
||||||
source: "env",
|
|
||||||
provider: resolveDefaultSecretProviderAlias(params.config, "env", {
|
|
||||||
preferFirstProviderForSource: true,
|
|
||||||
}),
|
|
||||||
id: envVar,
|
|
||||||
};
|
|
||||||
const resolvedValue = await resolveSecretRefString(ref, {
|
|
||||||
config: params.config,
|
|
||||||
env: process.env,
|
|
||||||
});
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
|
|
||||||
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",
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
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",
|
|
||||||
);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
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"
|
|
||||||
: 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 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",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createAuthChoiceAgentModelNoter(
|
export function createAuthChoiceAgentModelNoter(
|
||||||
params: ApplyAuthChoiceParams,
|
params: ApplyAuthChoiceParams,
|
||||||
@ -358,180 +97,3 @@ export function createAuthChoiceDefaultModelApplierForMutableState(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeTokenProviderInput(
|
|
||||||
tokenProvider: string | null | undefined,
|
|
||||||
): string | undefined {
|
|
||||||
const normalized = String(tokenProvider ?? "")
|
|
||||||
.trim()
|
|
||||||
.toLowerCase();
|
|
||||||
return normalized || undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function normalizeSecretInputModeInput(
|
|
||||||
secretInputMode: string | null | undefined,
|
|
||||||
): SecretInputMode | undefined {
|
|
||||||
const normalized = String(secretInputMode ?? "")
|
|
||||||
.trim()
|
|
||||||
.toLowerCase();
|
|
||||||
if (normalized === "plaintext" || normalized === "ref") {
|
|
||||||
return normalized;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function resolveSecretInputModeForEnvSelection(params: {
|
|
||||||
prompter: WizardPrompter;
|
|
||||||
explicitMode?: SecretInputMode;
|
|
||||||
copy?: SecretInputModePromptCopy;
|
|
||||||
}): Promise<SecretInputMode> {
|
|
||||||
if (params.explicitMode) {
|
|
||||||
return params.explicitMode;
|
|
||||||
}
|
|
||||||
// Some tests pass partial prompt harnesses without a select implementation.
|
|
||||||
// Preserve backward-compatible behavior by defaulting to plaintext in that case.
|
|
||||||
if (typeof params.prompter.select !== "function") {
|
|
||||||
return "plaintext";
|
|
||||||
}
|
|
||||||
const selected = await params.prompter.select<SecretInputMode>({
|
|
||||||
message: params.copy?.modeMessage ?? "How do you want to provide this API key?",
|
|
||||||
initialValue: "plaintext",
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
value: "plaintext",
|
|
||||||
label: params.copy?.plaintextLabel ?? "Paste API key now",
|
|
||||||
hint: params.copy?.plaintextHint ?? "Stores the key directly in OpenClaw config",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "ref",
|
|
||||||
label: params.copy?.refLabel ?? "Use external secret provider",
|
|
||||||
hint:
|
|
||||||
params.copy?.refHint ??
|
|
||||||
"Stores a reference to env or configured external secret providers",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
return selected === "ref" ? "ref" : "plaintext";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function maybeApplyApiKeyFromOption(params: {
|
|
||||||
token: string | undefined;
|
|
||||||
tokenProvider: string | undefined;
|
|
||||||
secretInputMode?: SecretInputMode;
|
|
||||||
expectedProviders: string[];
|
|
||||||
normalize: (value: string) => string;
|
|
||||||
setCredential: (apiKey: SecretInput, mode?: SecretInputMode) => Promise<void>;
|
|
||||||
}): Promise<string | undefined> {
|
|
||||||
const tokenProvider = normalizeTokenProviderInput(params.tokenProvider);
|
|
||||||
const expectedProviders = params.expectedProviders
|
|
||||||
.map((provider) => normalizeTokenProviderInput(provider))
|
|
||||||
.filter((provider): provider is string => Boolean(provider));
|
|
||||||
if (!params.token || !tokenProvider || !expectedProviders.includes(tokenProvider)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
const apiKey = params.normalize(params.token);
|
|
||||||
await params.setCredential(apiKey, params.secretInputMode);
|
|
||||||
return apiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function ensureApiKeyFromOptionEnvOrPrompt(params: {
|
|
||||||
token: string | undefined;
|
|
||||||
tokenProvider: string | undefined;
|
|
||||||
secretInputMode?: SecretInputMode;
|
|
||||||
config: OpenClawConfig;
|
|
||||||
expectedProviders: string[];
|
|
||||||
provider: string;
|
|
||||||
envLabel: string;
|
|
||||||
promptMessage: string;
|
|
||||||
normalize: (value: string) => string;
|
|
||||||
validate: (value: string) => string | undefined;
|
|
||||||
prompter: WizardPrompter;
|
|
||||||
setCredential: (apiKey: SecretInput, mode?: SecretInputMode) => Promise<void>;
|
|
||||||
noteMessage?: string;
|
|
||||||
noteTitle?: string;
|
|
||||||
}): Promise<string> {
|
|
||||||
const optionApiKey = await maybeApplyApiKeyFromOption({
|
|
||||||
token: params.token,
|
|
||||||
tokenProvider: params.tokenProvider,
|
|
||||||
secretInputMode: params.secretInputMode,
|
|
||||||
expectedProviders: params.expectedProviders,
|
|
||||||
normalize: params.normalize,
|
|
||||||
setCredential: params.setCredential,
|
|
||||||
});
|
|
||||||
if (optionApiKey) {
|
|
||||||
return optionApiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (params.noteMessage) {
|
|
||||||
await params.prompter.note(params.noteMessage, params.noteTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
return await ensureApiKeyFromEnvOrPrompt({
|
|
||||||
config: params.config,
|
|
||||||
provider: params.provider,
|
|
||||||
envLabel: params.envLabel,
|
|
||||||
promptMessage: params.promptMessage,
|
|
||||||
normalize: params.normalize,
|
|
||||||
validate: params.validate,
|
|
||||||
prompter: params.prompter,
|
|
||||||
secretInputMode: params.secretInputMode,
|
|
||||||
setCredential: params.setCredential,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function ensureApiKeyFromEnvOrPrompt(params: {
|
|
||||||
config: OpenClawConfig;
|
|
||||||
provider: string;
|
|
||||||
envLabel: string;
|
|
||||||
promptMessage: string;
|
|
||||||
normalize: (value: string) => string;
|
|
||||||
validate: (value: string) => string | undefined;
|
|
||||||
prompter: WizardPrompter;
|
|
||||||
secretInputMode?: SecretInputMode;
|
|
||||||
setCredential: (apiKey: SecretInput, mode?: SecretInputMode) => Promise<void>;
|
|
||||||
}): Promise<string> {
|
|
||||||
const selectedMode = await resolveSecretInputModeForEnvSelection({
|
|
||||||
prompter: params.prompter,
|
|
||||||
explicitMode: params.secretInputMode,
|
|
||||||
});
|
|
||||||
const envKey = resolveEnvApiKey(params.provider);
|
|
||||||
|
|
||||||
if (selectedMode === "ref") {
|
|
||||||
if (typeof params.prompter.select !== "function") {
|
|
||||||
const fallback = resolveRefFallbackInput({
|
|
||||||
config: params.config,
|
|
||||||
provider: params.provider,
|
|
||||||
preferredEnvVar: envKey?.source ? extractEnvVarFromSourceLabel(envKey.source) : undefined,
|
|
||||||
});
|
|
||||||
await params.setCredential(fallback.ref, selectedMode);
|
|
||||||
return fallback.resolvedValue;
|
|
||||||
}
|
|
||||||
const resolved = await promptSecretRefForSetup({
|
|
||||||
provider: params.provider,
|
|
||||||
config: params.config,
|
|
||||||
prompter: params.prompter,
|
|
||||||
preferredEnvVar: envKey?.source ? extractEnvVarFromSourceLabel(envKey.source) : undefined,
|
|
||||||
});
|
|
||||||
await params.setCredential(resolved.ref, selectedMode);
|
|
||||||
return resolved.resolvedValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (envKey && selectedMode === "plaintext") {
|
|
||||||
const useExisting = await params.prompter.confirm({
|
|
||||||
message: `Use existing ${params.envLabel} (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
|
||||||
initialValue: true,
|
|
||||||
});
|
|
||||||
if (useExisting) {
|
|
||||||
await params.setCredential(envKey.apiKey, selectedMode);
|
|
||||||
return envKey.apiKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = await params.prompter.text({
|
|
||||||
message: params.promptMessage,
|
|
||||||
validate: params.validate,
|
|
||||||
});
|
|
||||||
const apiKey = params.normalize(String(key ?? ""));
|
|
||||||
await params.setCredential(apiKey, selectedMode);
|
|
||||||
return apiKey;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -8,7 +8,7 @@ import { upsertAuthProfile } from "../agents/auth-profiles.js";
|
|||||||
import { resolveDefaultAgentWorkspaceDir } from "../agents/workspace.js";
|
import { resolveDefaultAgentWorkspaceDir } from "../agents/workspace.js";
|
||||||
import { enablePluginInConfig } from "../plugins/enable.js";
|
import { enablePluginInConfig } from "../plugins/enable.js";
|
||||||
import { applyAuthProfileConfig } from "../plugins/provider-auth-helpers.js";
|
import { applyAuthProfileConfig } from "../plugins/provider-auth-helpers.js";
|
||||||
import type { ProviderAuthMethod } from "../plugins/types.js";
|
import type { ProviderAuthMethod, ProviderAuthOptionBag } from "../plugins/types.js";
|
||||||
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
import type { ApplyAuthChoiceParams, ApplyAuthChoiceResult } from "./auth-choice.apply.js";
|
||||||
import { isRemoteEnvironment } from "./oauth-env.js";
|
import { isRemoteEnvironment } from "./oauth-env.js";
|
||||||
import { createVpsAwareOAuthHandlers } from "./oauth-flow.js";
|
import { createVpsAwareOAuthHandlers } from "./oauth-flow.js";
|
||||||
@ -97,7 +97,7 @@ export async function runProviderPluginAuthMethod(params: {
|
|||||||
workspaceDir,
|
workspaceDir,
|
||||||
prompter: params.prompter,
|
prompter: params.prompter,
|
||||||
runtime: params.runtime,
|
runtime: params.runtime,
|
||||||
opts: params.opts,
|
opts: params.opts as ProviderAuthOptionBag | undefined,
|
||||||
secretInputMode: params.secretInputMode,
|
secretInputMode: params.secretInputMode,
|
||||||
allowSecretRefPrompt: params.allowSecretRefPrompt,
|
allowSecretRefPrompt: params.allowSecretRefPrompt,
|
||||||
isRemote,
|
isRemote,
|
||||||
@ -173,7 +173,7 @@ export async function applyAuthChoiceLoadedPluginProvider(
|
|||||||
workspaceDir,
|
workspaceDir,
|
||||||
secretInputMode: params.opts?.secretInputMode,
|
secretInputMode: params.opts?.secretInputMode,
|
||||||
allowSecretRefPrompt: false,
|
allowSecretRefPrompt: false,
|
||||||
opts: params.opts,
|
opts: params.opts as ProviderAuthOptionBag | undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
let nextConfig = applied.config;
|
let nextConfig = applied.config;
|
||||||
@ -260,7 +260,7 @@ export async function applyAuthChoicePluginProvider(
|
|||||||
workspaceDir,
|
workspaceDir,
|
||||||
secretInputMode: params.opts?.secretInputMode,
|
secretInputMode: params.opts?.secretInputMode,
|
||||||
allowSecretRefPrompt: false,
|
allowSecretRefPrompt: false,
|
||||||
opts: params.opts,
|
opts: params.opts as ProviderAuthOptionBag | undefined,
|
||||||
});
|
});
|
||||||
nextConfig = applied.config;
|
nextConfig = applied.config;
|
||||||
|
|
||||||
|
|||||||
@ -1,38 +1,8 @@
|
|||||||
import { normalizeProviderId } from "../agents/model-selection.js";
|
export {
|
||||||
|
ANTHROPIC_SETUP_TOKEN_MIN_LENGTH,
|
||||||
export const ANTHROPIC_SETUP_TOKEN_PREFIX = "sk-ant-oat01-";
|
ANTHROPIC_SETUP_TOKEN_PREFIX,
|
||||||
export const ANTHROPIC_SETUP_TOKEN_MIN_LENGTH = 80;
|
buildTokenProfileId,
|
||||||
export const DEFAULT_TOKEN_PROFILE_NAME = "default";
|
DEFAULT_TOKEN_PROFILE_NAME,
|
||||||
|
normalizeTokenProfileName,
|
||||||
export function normalizeTokenProfileName(raw: string): string {
|
validateAnthropicSetupToken,
|
||||||
const trimmed = raw.trim();
|
} from "../plugins/provider-auth-token.js";
|
||||||
if (!trimmed) {
|
|
||||||
return DEFAULT_TOKEN_PROFILE_NAME;
|
|
||||||
}
|
|
||||||
const slug = trimmed
|
|
||||||
.toLowerCase()
|
|
||||||
.replace(/[^a-z0-9._-]+/g, "-")
|
|
||||||
.replace(/-+/g, "-")
|
|
||||||
.replace(/^-+|-+$/g, "");
|
|
||||||
return slug || DEFAULT_TOKEN_PROFILE_NAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildTokenProfileId(params: { provider: string; name: string }): string {
|
|
||||||
const provider = normalizeProviderId(params.provider);
|
|
||||||
const name = normalizeTokenProfileName(params.name);
|
|
||||||
return `${provider}:${name}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function validateAnthropicSetupToken(raw: string): string | undefined {
|
|
||||||
const trimmed = raw.trim();
|
|
||||||
if (!trimmed) {
|
|
||||||
return "Required";
|
|
||||||
}
|
|
||||||
if (!trimmed.startsWith(ANTHROPIC_SETUP_TOKEN_PREFIX)) {
|
|
||||||
return `Expected token starting with ${ANTHROPIC_SETUP_TOKEN_PREFIX}`;
|
|
||||||
}
|
|
||||||
if (trimmed.length < ANTHROPIC_SETUP_TOKEN_MIN_LENGTH) {
|
|
||||||
return "Token looks too short; paste the full setup-token";
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,11 +1,4 @@
|
|||||||
import type { OpenClawConfig } from "../config/config.js";
|
export {
|
||||||
import { applyAgentDefaultPrimaryModel } from "./model-default.js";
|
applyGoogleGeminiModelDefault,
|
||||||
|
GOOGLE_GEMINI_DEFAULT_MODEL,
|
||||||
export const GOOGLE_GEMINI_DEFAULT_MODEL = "google/gemini-3.1-pro-preview";
|
} from "../plugins/provider-model-defaults.js";
|
||||||
|
|
||||||
export function applyGoogleGeminiModelDefault(cfg: OpenClawConfig): {
|
|
||||||
next: OpenClawConfig;
|
|
||||||
changed: boolean;
|
|
||||||
} {
|
|
||||||
return applyAgentDefaultPrimaryModel({ cfg, model: GOOGLE_GEMINI_DEFAULT_MODEL });
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,41 +1 @@
|
|||||||
import { DEFAULT_PROVIDER } from "../agents/defaults.js";
|
export { ensureModelAllowlistEntry } from "../plugins/provider-model-allowlist.js";
|
||||||
import { resolveAllowlistModelKey } from "../agents/model-selection.js";
|
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
|
||||||
|
|
||||||
export function ensureModelAllowlistEntry(params: {
|
|
||||||
cfg: OpenClawConfig;
|
|
||||||
modelRef: string;
|
|
||||||
defaultProvider?: string;
|
|
||||||
}): OpenClawConfig {
|
|
||||||
const rawModelRef = params.modelRef.trim();
|
|
||||||
if (!rawModelRef) {
|
|
||||||
return params.cfg;
|
|
||||||
}
|
|
||||||
|
|
||||||
const models = { ...params.cfg.agents?.defaults?.models };
|
|
||||||
const keySet = new Set<string>([rawModelRef]);
|
|
||||||
const canonicalKey = resolveAllowlistModelKey(
|
|
||||||
rawModelRef,
|
|
||||||
params.defaultProvider ?? DEFAULT_PROVIDER,
|
|
||||||
);
|
|
||||||
if (canonicalKey) {
|
|
||||||
keySet.add(canonicalKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const key of keySet) {
|
|
||||||
models[key] = {
|
|
||||||
...models[key],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...params.cfg,
|
|
||||||
agents: {
|
|
||||||
...params.cfg.agents,
|
|
||||||
defaults: {
|
|
||||||
...params.cfg.agents?.defaults,
|
|
||||||
models,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,45 +1,4 @@
|
|||||||
import type { OpenClawConfig } from "../config/config.js";
|
export {
|
||||||
import type { AgentModelListConfig } from "../config/types.js";
|
applyAgentDefaultPrimaryModel,
|
||||||
|
resolvePrimaryModel,
|
||||||
export function resolvePrimaryModel(model?: AgentModelListConfig | string): string | undefined {
|
} from "../plugins/provider-model-primary.js";
|
||||||
if (typeof model === "string") {
|
|
||||||
return model;
|
|
||||||
}
|
|
||||||
if (model && typeof model === "object" && typeof model.primary === "string") {
|
|
||||||
return model.primary;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyAgentDefaultPrimaryModel(params: {
|
|
||||||
cfg: OpenClawConfig;
|
|
||||||
model: string;
|
|
||||||
legacyModels?: Set<string>;
|
|
||||||
}): { next: OpenClawConfig; changed: boolean } {
|
|
||||||
const current = resolvePrimaryModel(params.cfg.agents?.defaults?.model)?.trim();
|
|
||||||
const normalizedCurrent = current && params.legacyModels?.has(current) ? params.model : current;
|
|
||||||
if (normalizedCurrent === params.model) {
|
|
||||||
return { next: params.cfg, changed: false };
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
next: {
|
|
||||||
...params.cfg,
|
|
||||||
agents: {
|
|
||||||
...params.cfg.agents,
|
|
||||||
defaults: {
|
|
||||||
...params.cfg.agents?.defaults,
|
|
||||||
model:
|
|
||||||
params.cfg.agents?.defaults?.model &&
|
|
||||||
typeof params.cfg.agents.defaults.model === "object"
|
|
||||||
? {
|
|
||||||
...params.cfg.agents.defaults.model,
|
|
||||||
primary: params.model,
|
|
||||||
}
|
|
||||||
: { primary: params.model },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
changed: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -11,10 +11,13 @@ import {
|
|||||||
} from "../agents/model-selection.js";
|
} from "../agents/model-selection.js";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
import { resolveAgentModelPrimaryValue } from "../config/model-input.js";
|
||||||
|
import { applyPrimaryModel } from "../plugins/provider-model-primary.js";
|
||||||
import type { ProviderPlugin } from "../plugins/types.js";
|
import type { ProviderPlugin } from "../plugins/types.js";
|
||||||
import type { WizardPrompter, WizardSelectOption } from "../wizard/prompts.js";
|
import type { WizardPrompter, WizardSelectOption } from "../wizard/prompts.js";
|
||||||
import { formatTokenK } from "./models/shared.js";
|
import { formatTokenK } from "./models/shared.js";
|
||||||
|
|
||||||
|
export { applyPrimaryModel } from "../plugins/provider-model-primary.js";
|
||||||
|
|
||||||
const KEEP_VALUE = "__keep__";
|
const KEEP_VALUE = "__keep__";
|
||||||
const MANUAL_VALUE = "__manual__";
|
const MANUAL_VALUE = "__manual__";
|
||||||
const PROVIDER_FILTER_THRESHOLD = 30;
|
const PROVIDER_FILTER_THRESHOLD = 30;
|
||||||
@ -516,33 +519,6 @@ export async function promptModelAllowlist(params: {
|
|||||||
return { models: [] };
|
return { models: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyPrimaryModel(cfg: OpenClawConfig, model: string): OpenClawConfig {
|
|
||||||
const defaults = cfg.agents?.defaults;
|
|
||||||
const existingModel = defaults?.model;
|
|
||||||
const existingModels = defaults?.models;
|
|
||||||
const fallbacks =
|
|
||||||
typeof existingModel === "object" && existingModel !== null && "fallbacks" in existingModel
|
|
||||||
? (existingModel as { fallbacks?: string[] }).fallbacks
|
|
||||||
: undefined;
|
|
||||||
return {
|
|
||||||
...cfg,
|
|
||||||
agents: {
|
|
||||||
...cfg.agents,
|
|
||||||
defaults: {
|
|
||||||
...defaults,
|
|
||||||
model: {
|
|
||||||
...(fallbacks ? { fallbacks } : undefined),
|
|
||||||
primary: model,
|
|
||||||
},
|
|
||||||
models: {
|
|
||||||
...existingModels,
|
|
||||||
[model]: existingModels?.[model] ?? {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyModelAllowlist(cfg: OpenClawConfig, models: string[]): OpenClawConfig {
|
export function applyModelAllowlist(cfg: OpenClawConfig, models: string[]): OpenClawConfig {
|
||||||
const defaults = cfg.agents?.defaults;
|
const defaults = cfg.agents?.defaults;
|
||||||
const normalized = normalizeModelKeys(models);
|
const normalized = normalizeModelKeys(models);
|
||||||
|
|||||||
@ -1,53 +1 @@
|
|||||||
import type { RuntimeEnv } from "../runtime.js";
|
export * from "../plugins/provider-oauth-flow.js";
|
||||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
|
||||||
|
|
||||||
type OAuthPrompt = { message: string; placeholder?: string };
|
|
||||||
|
|
||||||
const validateRequiredInput = (value: string) => (value.trim().length > 0 ? undefined : "Required");
|
|
||||||
|
|
||||||
export function createVpsAwareOAuthHandlers(params: {
|
|
||||||
isRemote: boolean;
|
|
||||||
prompter: WizardPrompter;
|
|
||||||
runtime: RuntimeEnv;
|
|
||||||
spin: ReturnType<WizardPrompter["progress"]>;
|
|
||||||
openUrl: (url: string) => Promise<unknown>;
|
|
||||||
localBrowserMessage: string;
|
|
||||||
manualPromptMessage?: string;
|
|
||||||
}): {
|
|
||||||
onAuth: (event: { url: string }) => Promise<void>;
|
|
||||||
onPrompt: (prompt: OAuthPrompt) => Promise<string>;
|
|
||||||
} {
|
|
||||||
const manualPromptMessage = params.manualPromptMessage ?? "Paste the redirect URL";
|
|
||||||
let manualCodePromise: Promise<string> | undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
onAuth: async ({ url }) => {
|
|
||||||
if (params.isRemote) {
|
|
||||||
params.spin.stop("OAuth URL ready");
|
|
||||||
params.runtime.log(`\nOpen this URL in your LOCAL browser:\n\n${url}\n`);
|
|
||||||
manualCodePromise = params.prompter
|
|
||||||
.text({
|
|
||||||
message: manualPromptMessage,
|
|
||||||
validate: validateRequiredInput,
|
|
||||||
})
|
|
||||||
.then((value) => String(value));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
params.spin.update(params.localBrowserMessage);
|
|
||||||
await params.openUrl(url);
|
|
||||||
params.runtime.log(`Open: ${url}`);
|
|
||||||
},
|
|
||||||
onPrompt: async (prompt) => {
|
|
||||||
if (manualCodePromise) {
|
|
||||||
return manualCodePromise;
|
|
||||||
}
|
|
||||||
const code = await params.prompter.text({
|
|
||||||
message: prompt.message,
|
|
||||||
placeholder: prompt.placeholder,
|
|
||||||
validate: validateRequiredInput,
|
|
||||||
});
|
|
||||||
return String(code);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,164 +1 @@
|
|||||||
import path from "node:path";
|
export * from "../plugins/provider-openai-codex-oauth-tls.js";
|
||||||
import { formatCliCommand } from "../cli/command-format.js";
|
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
|
||||||
import { note } from "../terminal/note.js";
|
|
||||||
|
|
||||||
const TLS_CERT_ERROR_CODES = new Set([
|
|
||||||
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
|
||||||
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
|
||||||
"CERT_HAS_EXPIRED",
|
|
||||||
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
||||||
"SELF_SIGNED_CERT_IN_CHAIN",
|
|
||||||
"ERR_TLS_CERT_ALTNAME_INVALID",
|
|
||||||
]);
|
|
||||||
|
|
||||||
const TLS_CERT_ERROR_PATTERNS = [
|
|
||||||
/unable to get local issuer certificate/i,
|
|
||||||
/unable to verify the first certificate/i,
|
|
||||||
/self[- ]signed certificate/i,
|
|
||||||
/certificate has expired/i,
|
|
||||||
];
|
|
||||||
|
|
||||||
const OPENAI_AUTH_PROBE_URL =
|
|
||||||
"https://auth.openai.com/oauth/authorize?response_type=code&client_id=openclaw-preflight&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback&scope=openid+profile+email";
|
|
||||||
|
|
||||||
type PreflightFailureKind = "tls-cert" | "network";
|
|
||||||
|
|
||||||
export type OpenAIOAuthTlsPreflightResult =
|
|
||||||
| { ok: true }
|
|
||||||
| {
|
|
||||||
ok: false;
|
|
||||||
kind: PreflightFailureKind;
|
|
||||||
code?: string;
|
|
||||||
message: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
function asRecord(value: unknown): Record<string, unknown> | null {
|
|
||||||
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractFailure(error: unknown): {
|
|
||||||
code?: string;
|
|
||||||
message: string;
|
|
||||||
kind: PreflightFailureKind;
|
|
||||||
} {
|
|
||||||
const root = asRecord(error);
|
|
||||||
const rootCause = asRecord(root?.cause);
|
|
||||||
const code = typeof rootCause?.code === "string" ? rootCause.code : undefined;
|
|
||||||
const message =
|
|
||||||
typeof rootCause?.message === "string"
|
|
||||||
? rootCause.message
|
|
||||||
: typeof root?.message === "string"
|
|
||||||
? root.message
|
|
||||||
: String(error);
|
|
||||||
const isTlsCertError =
|
|
||||||
(code ? TLS_CERT_ERROR_CODES.has(code) : false) ||
|
|
||||||
TLS_CERT_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
|
||||||
return {
|
|
||||||
code,
|
|
||||||
message,
|
|
||||||
kind: isTlsCertError ? "tls-cert" : "network",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveHomebrewPrefixFromExecPath(execPath: string): string | null {
|
|
||||||
const marker = `${path.sep}Cellar${path.sep}`;
|
|
||||||
const idx = execPath.indexOf(marker);
|
|
||||||
if (idx > 0) {
|
|
||||||
return execPath.slice(0, idx);
|
|
||||||
}
|
|
||||||
const envPrefix = process.env.HOMEBREW_PREFIX?.trim();
|
|
||||||
return envPrefix ? envPrefix : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveCertBundlePath(): string | null {
|
|
||||||
const prefix = resolveHomebrewPrefixFromExecPath(process.execPath);
|
|
||||||
if (!prefix) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return path.join(prefix, "etc", "openssl@3", "cert.pem");
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasOpenAICodexOAuthProfile(cfg: OpenClawConfig): boolean {
|
|
||||||
const profiles = cfg.auth?.profiles;
|
|
||||||
if (!profiles) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return Object.values(profiles).some(
|
|
||||||
(profile) => profile.provider === "openai-codex" && profile.mode === "oauth",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldRunOpenAIOAuthTlsPrerequisites(params: {
|
|
||||||
cfg: OpenClawConfig;
|
|
||||||
deep?: boolean;
|
|
||||||
}): boolean {
|
|
||||||
if (params.deep === true) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return hasOpenAICodexOAuthProfile(params.cfg);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runOpenAIOAuthTlsPreflight(options?: {
|
|
||||||
timeoutMs?: number;
|
|
||||||
fetchImpl?: typeof fetch;
|
|
||||||
}): Promise<OpenAIOAuthTlsPreflightResult> {
|
|
||||||
const timeoutMs = options?.timeoutMs ?? 5000;
|
|
||||||
const fetchImpl = options?.fetchImpl ?? fetch;
|
|
||||||
try {
|
|
||||||
await fetchImpl(OPENAI_AUTH_PROBE_URL, {
|
|
||||||
method: "GET",
|
|
||||||
redirect: "manual",
|
|
||||||
signal: AbortSignal.timeout(timeoutMs),
|
|
||||||
});
|
|
||||||
return { ok: true };
|
|
||||||
} catch (error) {
|
|
||||||
const failure = extractFailure(error);
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
kind: failure.kind,
|
|
||||||
code: failure.code,
|
|
||||||
message: failure.message,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function formatOpenAIOAuthTlsPreflightFix(
|
|
||||||
result: Exclude<OpenAIOAuthTlsPreflightResult, { ok: true }>,
|
|
||||||
): string {
|
|
||||||
if (result.kind !== "tls-cert") {
|
|
||||||
return [
|
|
||||||
"OpenAI OAuth prerequisites check failed due to a network error before the browser flow.",
|
|
||||||
`Cause: ${result.message}`,
|
|
||||||
"Verify DNS/firewall/proxy access to auth.openai.com and retry.",
|
|
||||||
].join("\n");
|
|
||||||
}
|
|
||||||
const certBundlePath = resolveCertBundlePath();
|
|
||||||
const lines = [
|
|
||||||
"OpenAI OAuth prerequisites check failed: Node/OpenSSL cannot validate TLS certificates.",
|
|
||||||
`Cause: ${result.code ? `${result.code} (${result.message})` : result.message}`,
|
|
||||||
"",
|
|
||||||
"Fix (Homebrew Node/OpenSSL):",
|
|
||||||
`- ${formatCliCommand("brew postinstall ca-certificates")}`,
|
|
||||||
`- ${formatCliCommand("brew postinstall openssl@3")}`,
|
|
||||||
];
|
|
||||||
if (certBundlePath) {
|
|
||||||
lines.push(`- Verify cert bundle exists: ${certBundlePath}`);
|
|
||||||
}
|
|
||||||
lines.push("- Retry the OAuth login flow.");
|
|
||||||
return lines.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function noteOpenAIOAuthTlsPrerequisites(params: {
|
|
||||||
cfg: OpenClawConfig;
|
|
||||||
deep?: boolean;
|
|
||||||
}): Promise<void> {
|
|
||||||
if (!shouldRunOpenAIOAuthTlsPrerequisites(params)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const result = await runOpenAIOAuthTlsPreflight({ timeoutMs: 4000 });
|
|
||||||
if (result.ok || result.kind !== "tls-cert") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
note(formatOpenAIOAuthTlsPreflightFix(result), "OAuth TLS prerequisites");
|
|
||||||
}
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { resolveDefaultAgentWorkspaceDir } from "../../../agents/workspace.js";
|
|||||||
import type { OpenClawConfig } from "../../../config/config.js";
|
import type { OpenClawConfig } from "../../../config/config.js";
|
||||||
import { enablePluginInConfig } from "../../../plugins/enable.js";
|
import { enablePluginInConfig } from "../../../plugins/enable.js";
|
||||||
import type {
|
import type {
|
||||||
|
ProviderAuthOptionBag,
|
||||||
ProviderNonInteractiveApiKeyCredentialParams,
|
ProviderNonInteractiveApiKeyCredentialParams,
|
||||||
ProviderResolveNonInteractiveApiKeyParams,
|
ProviderResolveNonInteractiveApiKeyParams,
|
||||||
} from "../../../plugins/types.js";
|
} from "../../../plugins/types.js";
|
||||||
@ -130,7 +131,7 @@ export async function applyNonInteractivePluginProviderChoice(params: {
|
|||||||
authChoice: params.authChoice,
|
authChoice: params.authChoice,
|
||||||
config: enableResult.config,
|
config: enableResult.config,
|
||||||
baseConfig: params.baseConfig,
|
baseConfig: params.baseConfig,
|
||||||
opts: params.opts,
|
opts: params.opts as ProviderAuthOptionBag,
|
||||||
runtime: params.runtime,
|
runtime: params.runtime,
|
||||||
agentDir,
|
agentDir,
|
||||||
workspaceDir,
|
workspaceDir,
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import type { ChannelId } from "../channels/plugins/types.js";
|
import type { ChannelId } from "../channels/plugins/types.js";
|
||||||
|
import type { SecretInputMode } from "../plugins/provider-auth-types.js";
|
||||||
import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
|
import type { GatewayDaemonRuntime } from "./daemon-runtime.js";
|
||||||
|
|
||||||
export type OnboardMode = "local" | "remote";
|
export type OnboardMode = "local" | "remote";
|
||||||
@ -90,7 +91,7 @@ export type NodeManagerChoice = "npm" | "pnpm" | "bun";
|
|||||||
export type ChannelChoice = ChannelId;
|
export type ChannelChoice = ChannelId;
|
||||||
// Legacy alias (pre-rename).
|
// Legacy alias (pre-rename).
|
||||||
export type ProviderChoice = ChannelChoice;
|
export type ProviderChoice = ChannelChoice;
|
||||||
export type SecretInputMode = "plaintext" | "ref"; // pragma: allowlist secret
|
export type { SecretInputMode } from "../plugins/provider-auth-types.js";
|
||||||
|
|
||||||
export type OnboardOptions = {
|
export type OnboardOptions = {
|
||||||
mode?: OnboardMode;
|
mode?: OnboardMode;
|
||||||
|
|||||||
@ -1,65 +1 @@
|
|||||||
import { loginOpenAICodex, type OAuthCredentials } from "@mariozechner/pi-ai/oauth";
|
export { loginOpenAICodexOAuth } from "../plugins/provider-openai-codex-oauth.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
|
||||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
|
||||||
import { createVpsAwareOAuthHandlers } from "./oauth-flow.js";
|
|
||||||
import {
|
|
||||||
formatOpenAIOAuthTlsPreflightFix,
|
|
||||||
runOpenAIOAuthTlsPreflight,
|
|
||||||
} from "./oauth-tls-preflight.js";
|
|
||||||
|
|
||||||
export async function loginOpenAICodexOAuth(params: {
|
|
||||||
prompter: WizardPrompter;
|
|
||||||
runtime: RuntimeEnv;
|
|
||||||
isRemote: boolean;
|
|
||||||
openUrl: (url: string) => Promise<void>;
|
|
||||||
localBrowserMessage?: string;
|
|
||||||
}): Promise<OAuthCredentials | null> {
|
|
||||||
const { prompter, runtime, isRemote, openUrl, localBrowserMessage } = params;
|
|
||||||
const preflight = await runOpenAIOAuthTlsPreflight();
|
|
||||||
if (!preflight.ok && preflight.kind === "tls-cert") {
|
|
||||||
const hint = formatOpenAIOAuthTlsPreflightFix(preflight);
|
|
||||||
runtime.error(hint);
|
|
||||||
await prompter.note(hint, "OAuth prerequisites");
|
|
||||||
throw new Error(preflight.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
await prompter.note(
|
|
||||||
isRemote
|
|
||||||
? [
|
|
||||||
"You are running in a remote/VPS environment.",
|
|
||||||
"A URL will be shown for you to open in your LOCAL browser.",
|
|
||||||
"After signing in, paste the redirect URL back here.",
|
|
||||||
].join("\n")
|
|
||||||
: [
|
|
||||||
"Browser will open for OpenAI authentication.",
|
|
||||||
"If the callback doesn't auto-complete, paste the redirect URL.",
|
|
||||||
"OpenAI OAuth uses localhost:1455 for the callback.",
|
|
||||||
].join("\n"),
|
|
||||||
"OpenAI Codex OAuth",
|
|
||||||
);
|
|
||||||
|
|
||||||
const spin = prompter.progress("Starting OAuth flow…");
|
|
||||||
try {
|
|
||||||
const { onAuth: baseOnAuth, onPrompt } = createVpsAwareOAuthHandlers({
|
|
||||||
isRemote,
|
|
||||||
prompter,
|
|
||||||
runtime,
|
|
||||||
spin,
|
|
||||||
openUrl,
|
|
||||||
localBrowserMessage: localBrowserMessage ?? "Complete sign-in in browser…",
|
|
||||||
});
|
|
||||||
|
|
||||||
const creds = await loginOpenAICodex({
|
|
||||||
onAuth: baseOnAuth,
|
|
||||||
onPrompt,
|
|
||||||
onProgress: (msg: string) => spin.update(msg),
|
|
||||||
});
|
|
||||||
spin.stop("OpenAI OAuth complete");
|
|
||||||
return creds ?? null;
|
|
||||||
} catch (err) {
|
|
||||||
spin.stop("OpenAI OAuth failed");
|
|
||||||
runtime.error(String(err));
|
|
||||||
await prompter.note("Trouble with OAuth? See https://docs.openclaw.ai/start/faq", "OAuth help");
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,47 +1,5 @@
|
|||||||
import type { OpenClawConfig } from "../config/config.js";
|
export {
|
||||||
import { ensureModelAllowlistEntry } from "./model-allowlist.js";
|
applyOpenAIConfig,
|
||||||
|
applyOpenAIProviderConfig,
|
||||||
export const OPENAI_DEFAULT_MODEL = "openai/gpt-5.1-codex";
|
OPENAI_DEFAULT_MODEL,
|
||||||
|
} from "../plugins/provider-model-defaults.js";
|
||||||
export function applyOpenAIProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
|
||||||
const next = ensureModelAllowlistEntry({
|
|
||||||
cfg,
|
|
||||||
modelRef: OPENAI_DEFAULT_MODEL,
|
|
||||||
});
|
|
||||||
const models = { ...next.agents?.defaults?.models };
|
|
||||||
models[OPENAI_DEFAULT_MODEL] = {
|
|
||||||
...models[OPENAI_DEFAULT_MODEL],
|
|
||||||
alias: models[OPENAI_DEFAULT_MODEL]?.alias ?? "GPT",
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
...next,
|
|
||||||
agents: {
|
|
||||||
...next.agents,
|
|
||||||
defaults: {
|
|
||||||
...next.agents?.defaults,
|
|
||||||
models,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function applyOpenAIConfig(cfg: OpenClawConfig): OpenClawConfig {
|
|
||||||
const next = applyOpenAIProviderConfig(cfg);
|
|
||||||
return {
|
|
||||||
...next,
|
|
||||||
agents: {
|
|
||||||
...next.agents,
|
|
||||||
defaults: {
|
|
||||||
...next.agents?.defaults,
|
|
||||||
model:
|
|
||||||
next.agents?.defaults?.model && typeof next.agents.defaults.model === "object"
|
|
||||||
? {
|
|
||||||
...next.agents.defaults.model,
|
|
||||||
primary: OPENAI_DEFAULT_MODEL,
|
|
||||||
}
|
|
||||||
: { primary: OPENAI_DEFAULT_MODEL },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,11 +1,4 @@
|
|||||||
import type { OpenClawConfig } from "../config/config.js";
|
export {
|
||||||
import { applyAgentDefaultPrimaryModel } from "./model-default.js";
|
applyOpencodeGoModelDefault,
|
||||||
|
OPENCODE_GO_DEFAULT_MODEL_REF,
|
||||||
export const OPENCODE_GO_DEFAULT_MODEL_REF = "opencode-go/kimi-k2.5";
|
} from "../plugins/provider-model-defaults.js";
|
||||||
|
|
||||||
export function applyOpencodeGoModelDefault(cfg: OpenClawConfig): {
|
|
||||||
next: OpenClawConfig;
|
|
||||||
changed: boolean;
|
|
||||||
} {
|
|
||||||
return applyAgentDefaultPrimaryModel({ cfg, model: OPENCODE_GO_DEFAULT_MODEL_REF });
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,19 +1,4 @@
|
|||||||
import type { OpenClawConfig } from "../config/config.js";
|
export {
|
||||||
import { applyAgentDefaultPrimaryModel } from "./model-default.js";
|
applyOpencodeZenModelDefault,
|
||||||
|
OPENCODE_ZEN_DEFAULT_MODEL,
|
||||||
export const OPENCODE_ZEN_DEFAULT_MODEL = "opencode/claude-opus-4-6";
|
} from "../plugins/provider-model-defaults.js";
|
||||||
const LEGACY_OPENCODE_ZEN_DEFAULT_MODELS = new Set([
|
|
||||||
"opencode/claude-opus-4-5",
|
|
||||||
"opencode-zen/claude-opus-4-5",
|
|
||||||
]);
|
|
||||||
|
|
||||||
export function applyOpencodeZenModelDefault(cfg: OpenClawConfig): {
|
|
||||||
next: OpenClawConfig;
|
|
||||||
changed: boolean;
|
|
||||||
} {
|
|
||||||
return applyAgentDefaultPrimaryModel({
|
|
||||||
cfg,
|
|
||||||
model: OPENCODE_ZEN_DEFAULT_MODEL,
|
|
||||||
legacyModels: LEGACY_OPENCODE_ZEN_DEFAULT_MODELS,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import type {
|
|||||||
ProviderAuthMethodNonInteractiveContext,
|
ProviderAuthMethodNonInteractiveContext,
|
||||||
ProviderNonInteractiveApiKeyResult,
|
ProviderNonInteractiveApiKeyResult,
|
||||||
} from "../plugins/types.js";
|
} from "../plugins/types.js";
|
||||||
|
import { normalizeOptionalSecretInput } from "../utils/normalize-secret-input.js";
|
||||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
@ -240,11 +241,10 @@ export async function configureOpenAICompatibleSelfHostedProviderNonInteractive(
|
|||||||
contextWindow?: number;
|
contextWindow?: number;
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
}): Promise<OpenClawConfig | null> {
|
}): Promise<OpenClawConfig | null> {
|
||||||
const baseUrl = (params.ctx.opts.customBaseUrl?.trim() || params.defaultBaseUrl).replace(
|
const baseUrl = (
|
||||||
/\/+$/,
|
normalizeOptionalSecretInput(params.ctx.opts.customBaseUrl) ?? params.defaultBaseUrl
|
||||||
"",
|
).replace(/\/+$/, "");
|
||||||
);
|
const modelId = normalizeOptionalSecretInput(params.ctx.opts.customModelId);
|
||||||
const modelId = params.ctx.opts.customModelId?.trim();
|
|
||||||
if (!modelId) {
|
if (!modelId) {
|
||||||
params.ctx.runtime.error(
|
params.ctx.runtime.error(
|
||||||
buildMissingNonInteractiveModelIdMessage({
|
buildMissingNonInteractiveModelIdMessage({
|
||||||
@ -259,7 +259,7 @@ export async function configureOpenAICompatibleSelfHostedProviderNonInteractive(
|
|||||||
|
|
||||||
const resolved = await params.ctx.resolveApiKey({
|
const resolved = await params.ctx.resolveApiKey({
|
||||||
provider: params.providerId,
|
provider: params.providerId,
|
||||||
flagValue: params.ctx.opts.customApiKey,
|
flagValue: normalizeOptionalSecretInput(params.ctx.opts.customApiKey),
|
||||||
flagName: "--custom-api-key",
|
flagName: "--custom-api-key",
|
||||||
envVar: params.defaultApiKeyEnvVar,
|
envVar: params.defaultApiKeyEnvVar,
|
||||||
envVarName: params.defaultApiKeyEnvVar,
|
envVarName: params.defaultApiKeyEnvVar,
|
||||||
|
|||||||
@ -21,17 +21,20 @@ export {
|
|||||||
formatApiKeyPreview,
|
formatApiKeyPreview,
|
||||||
normalizeApiKeyInput,
|
normalizeApiKeyInput,
|
||||||
validateApiKeyInput,
|
validateApiKeyInput,
|
||||||
} from "../commands/auth-choice.api-key.js";
|
} from "../plugins/provider-auth-input.js";
|
||||||
export {
|
export {
|
||||||
ensureApiKeyFromOptionEnvOrPrompt,
|
ensureApiKeyFromOptionEnvOrPrompt,
|
||||||
normalizeSecretInputModeInput,
|
normalizeSecretInputModeInput,
|
||||||
promptSecretRefForSetup,
|
promptSecretRefForSetup,
|
||||||
resolveSecretInputModeForEnvSelection,
|
resolveSecretInputModeForEnvSelection,
|
||||||
} from "../commands/auth-choice.apply-helpers.js";
|
} from "../plugins/provider-auth-input.js";
|
||||||
export { buildTokenProfileId, validateAnthropicSetupToken } from "../commands/auth-token.js";
|
export {
|
||||||
|
buildTokenProfileId,
|
||||||
|
validateAnthropicSetupToken,
|
||||||
|
} from "../plugins/provider-auth-token.js";
|
||||||
export { applyAuthProfileConfig, buildApiKeyCredential } from "../plugins/provider-auth-helpers.js";
|
export { applyAuthProfileConfig, buildApiKeyCredential } from "../plugins/provider-auth-helpers.js";
|
||||||
export { githubCopilotLoginCommand } from "../providers/github-copilot-auth.js";
|
export { githubCopilotLoginCommand } from "../providers/github-copilot-auth.js";
|
||||||
export { loginOpenAICodexOAuth } from "../commands/openai-codex-oauth.js";
|
export { loginOpenAICodexOAuth } from "../plugins/provider-openai-codex-oauth.js";
|
||||||
export { createProviderApiKeyAuthMethod } from "../plugins/provider-api-key-auth.js";
|
export { createProviderApiKeyAuthMethod } from "../plugins/provider-api-key-auth.js";
|
||||||
export { coerceSecretRef } from "../config/types.secrets.js";
|
export { coerceSecretRef } from "../config/types.secrets.js";
|
||||||
export { resolveDefaultSecretProviderAlias } from "../secrets/ref-contract.js";
|
export { resolveDefaultSecretProviderAlias } from "../secrets/ref-contract.js";
|
||||||
|
|||||||
@ -14,10 +14,10 @@ export { normalizeProviderId } from "../agents/provider-id.js";
|
|||||||
export {
|
export {
|
||||||
applyGoogleGeminiModelDefault,
|
applyGoogleGeminiModelDefault,
|
||||||
GOOGLE_GEMINI_DEFAULT_MODEL,
|
GOOGLE_GEMINI_DEFAULT_MODEL,
|
||||||
} from "../commands/google-gemini-model-default.js";
|
} from "../plugins/provider-model-defaults.js";
|
||||||
export { applyOpenAIConfig, OPENAI_DEFAULT_MODEL } from "../commands/openai-model-default.js";
|
export { applyOpenAIConfig, OPENAI_DEFAULT_MODEL } from "../plugins/provider-model-defaults.js";
|
||||||
export { OPENCODE_GO_DEFAULT_MODEL_REF } from "../commands/opencode-go-model-default.js";
|
export { OPENCODE_GO_DEFAULT_MODEL_REF } from "../plugins/provider-model-defaults.js";
|
||||||
export { OPENCODE_ZEN_DEFAULT_MODEL } from "../commands/opencode-zen-model-default.js";
|
export { OPENCODE_ZEN_DEFAULT_MODEL } from "../plugins/provider-model-defaults.js";
|
||||||
export { OPENCODE_ZEN_DEFAULT_MODEL_REF } from "../agents/opencode-zen-models.js";
|
export { OPENCODE_ZEN_DEFAULT_MODEL_REF } from "../agents/opencode-zen-models.js";
|
||||||
|
|
||||||
export * from "../plugins/provider-model-definitions.js";
|
export * from "../plugins/provider-model-definitions.js";
|
||||||
|
|||||||
@ -13,4 +13,4 @@ export {
|
|||||||
applyProviderConfigWithDefaultModels,
|
applyProviderConfigWithDefaultModels,
|
||||||
applyProviderConfigWithModelCatalog,
|
applyProviderConfigWithModelCatalog,
|
||||||
} from "../plugins/provider-onboarding-config.js";
|
} from "../plugins/provider-onboarding-config.js";
|
||||||
export { ensureModelAllowlistEntry } from "../commands/model-allowlist.js";
|
export { ensureModelAllowlistEntry } from "../plugins/provider-model-allowlist.js";
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import { normalizeApiKeyInput, validateApiKeyInput } from "../commands/auth-choice.api-key.js";
|
|
||||||
import { ensureApiKeyFromOptionEnvOrPrompt } from "../commands/auth-choice.apply-helpers.js";
|
|
||||||
import { applyPrimaryModel } from "../commands/model-picker.js";
|
|
||||||
import { applyAuthProfileConfig, buildApiKeyCredential } from "./provider-auth-helpers.js";
|
import { applyAuthProfileConfig, buildApiKeyCredential } from "./provider-auth-helpers.js";
|
||||||
|
import {
|
||||||
|
ensureApiKeyFromOptionEnvOrPrompt,
|
||||||
|
normalizeApiKeyInput,
|
||||||
|
validateApiKeyInput,
|
||||||
|
} from "./provider-auth-input.js";
|
||||||
|
import { applyPrimaryModel } from "./provider-model-primary.js";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
applyAuthProfileConfig,
|
applyAuthProfileConfig,
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import type { OAuthCredentials } from "@mariozechner/pi-ai";
|
|||||||
import { resolveOpenClawAgentDir } from "../agents/agent-paths.js";
|
import { resolveOpenClawAgentDir } from "../agents/agent-paths.js";
|
||||||
import { upsertAuthProfile } from "../agents/auth-profiles.js";
|
import { upsertAuthProfile } from "../agents/auth-profiles.js";
|
||||||
import { normalizeProviderIdForAuth } from "../agents/provider-id.js";
|
import { normalizeProviderIdForAuth } from "../agents/provider-id.js";
|
||||||
import type { SecretInputMode } from "../commands/onboard-types.js";
|
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import { resolveStateDir } from "../config/paths.js";
|
import { resolveStateDir } from "../config/paths.js";
|
||||||
import {
|
import {
|
||||||
@ -15,6 +14,7 @@ import {
|
|||||||
} from "../config/types.secrets.js";
|
} from "../config/types.secrets.js";
|
||||||
import { PROVIDER_ENV_VARS } from "../secrets/provider-env-vars.js";
|
import { PROVIDER_ENV_VARS } from "../secrets/provider-env-vars.js";
|
||||||
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
import { normalizeSecretInput } from "../utils/normalize-secret-input.js";
|
||||||
|
import type { SecretInputMode } from "./provider-auth-types.js";
|
||||||
|
|
||||||
const ENV_REF_PATTERN = /^\$\{([A-Z][A-Z0-9_]*)\}$/;
|
const ENV_REF_PATTERN = /^\$\{([A-Z][A-Z0-9_]*)\}$/;
|
||||||
|
|
||||||
|
|||||||
496
src/plugins/provider-auth-input.ts
Normal file
496
src/plugins/provider-auth-input.ts
Normal file
@ -0,0 +1,496 @@
|
|||||||
|
import { resolveEnvApiKey } from "../agents/model-auth.js";
|
||||||
|
import type { OpenClawConfig } from "../config/types.js";
|
||||||
|
import {
|
||||||
|
isValidEnvSecretRefId,
|
||||||
|
type SecretInput,
|
||||||
|
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 { resolveSecretRefString } from "../secrets/resolve.js";
|
||||||
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
|
import type { SecretInputMode } from "./provider-auth-types.js";
|
||||||
|
|
||||||
|
const DEFAULT_KEY_PREVIEW = { head: 4, tail: 4 };
|
||||||
|
const ENV_SOURCE_LABEL_RE = /(?:^|:\s)([A-Z][A-Z0-9_]*)$/;
|
||||||
|
|
||||||
|
type SecretRefChoice = "env" | "provider"; // pragma: allowlist secret
|
||||||
|
|
||||||
|
export type SecretInputModePromptCopy = {
|
||||||
|
modeMessage?: string;
|
||||||
|
plaintextLabel?: string;
|
||||||
|
plaintextHint?: string;
|
||||||
|
refLabel?: string;
|
||||||
|
refHint?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function normalizeApiKeyInput(raw: string): string {
|
||||||
|
const trimmed = String(raw ?? "").trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const assignmentMatch = trimmed.match(/^(?:export\s+)?[A-Za-z_][A-Za-z0-9_]*\s*=\s*(.+)$/);
|
||||||
|
const valuePart = assignmentMatch ? assignmentMatch[1].trim() : trimmed;
|
||||||
|
|
||||||
|
const unquoted =
|
||||||
|
valuePart.length >= 2 &&
|
||||||
|
((valuePart.startsWith('"') && valuePart.endsWith('"')) ||
|
||||||
|
(valuePart.startsWith("'") && valuePart.endsWith("'")) ||
|
||||||
|
(valuePart.startsWith("`") && valuePart.endsWith("`")))
|
||||||
|
? valuePart.slice(1, -1)
|
||||||
|
: valuePart;
|
||||||
|
|
||||||
|
const withoutSemicolon = unquoted.endsWith(";") ? unquoted.slice(0, -1) : unquoted;
|
||||||
|
|
||||||
|
return withoutSemicolon.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateApiKeyInput = (value: string) =>
|
||||||
|
normalizeApiKeyInput(value).length > 0 ? undefined : "Required";
|
||||||
|
|
||||||
|
export function formatApiKeyPreview(
|
||||||
|
raw: string,
|
||||||
|
opts: { head?: number; tail?: number } = {},
|
||||||
|
): string {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return "…";
|
||||||
|
}
|
||||||
|
const head = opts.head ?? DEFAULT_KEY_PREVIEW.head;
|
||||||
|
const tail = opts.tail ?? DEFAULT_KEY_PREVIEW.tail;
|
||||||
|
if (trimmed.length <= head + tail) {
|
||||||
|
const shortHead = Math.min(2, trimmed.length);
|
||||||
|
const shortTail = Math.min(2, trimmed.length - shortHead);
|
||||||
|
if (shortTail <= 0) {
|
||||||
|
return `${trimmed.slice(0, shortHead)}…`;
|
||||||
|
}
|
||||||
|
return `${trimmed.slice(0, shortHead)}…${trimmed.slice(-shortTail)}`;
|
||||||
|
}
|
||||||
|
return `${trimmed.slice(0, head)}…${trimmed.slice(-tail)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatErrorMessage(error: unknown): string {
|
||||||
|
if (error instanceof Error && typeof error.message === "string" && error.message.trim()) {
|
||||||
|
return error.message;
|
||||||
|
}
|
||||||
|
return String(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
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`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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") {
|
||||||
|
const envVarRaw = await params.prompter.text({
|
||||||
|
message: params.copy?.envVarMessage ?? "Environment variable name",
|
||||||
|
initialValue: 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 : defaultEnvVar;
|
||||||
|
if (!envVar) {
|
||||||
|
throw new Error(
|
||||||
|
`No valid environment variable name provided for provider "${params.provider}".`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const ref: SecretRef = {
|
||||||
|
source: "env",
|
||||||
|
provider: resolveDefaultSecretProviderAlias(params.config, "env", {
|
||||||
|
preferFirstProviderForSource: true,
|
||||||
|
}),
|
||||||
|
id: envVar,
|
||||||
|
};
|
||||||
|
const resolvedValue = await resolveSecretRefString(ref, {
|
||||||
|
config: params.config,
|
||||||
|
env: process.env,
|
||||||
|
});
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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",
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
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"
|
||||||
|
: 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 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",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeTokenProviderInput(
|
||||||
|
tokenProvider: string | null | undefined,
|
||||||
|
): string | undefined {
|
||||||
|
const normalized = String(tokenProvider ?? "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
return normalized || undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeSecretInputModeInput(
|
||||||
|
secretInputMode: string | null | undefined,
|
||||||
|
): SecretInputMode | undefined {
|
||||||
|
const normalized = String(secretInputMode ?? "")
|
||||||
|
.trim()
|
||||||
|
.toLowerCase();
|
||||||
|
if (normalized === "plaintext" || normalized === "ref") {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function resolveSecretInputModeForEnvSelection(params: {
|
||||||
|
prompter: WizardPrompter;
|
||||||
|
explicitMode?: SecretInputMode;
|
||||||
|
copy?: SecretInputModePromptCopy;
|
||||||
|
}): Promise<SecretInputMode> {
|
||||||
|
if (params.explicitMode) {
|
||||||
|
return params.explicitMode;
|
||||||
|
}
|
||||||
|
if (typeof params.prompter.select !== "function") {
|
||||||
|
return "plaintext";
|
||||||
|
}
|
||||||
|
const selected = await params.prompter.select<SecretInputMode>({
|
||||||
|
message: params.copy?.modeMessage ?? "How do you want to provide this API key?",
|
||||||
|
initialValue: "plaintext",
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
value: "plaintext",
|
||||||
|
label: params.copy?.plaintextLabel ?? "Paste API key now",
|
||||||
|
hint: params.copy?.plaintextHint ?? "Stores the key directly in OpenClaw config",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: "ref",
|
||||||
|
label: params.copy?.refLabel ?? "Use external secret provider",
|
||||||
|
hint:
|
||||||
|
params.copy?.refHint ??
|
||||||
|
"Stores a reference to env or configured external secret providers",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
return selected === "ref" ? "ref" : "plaintext";
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function maybeApplyApiKeyFromOption(params: {
|
||||||
|
token: string | undefined;
|
||||||
|
tokenProvider: string | undefined;
|
||||||
|
secretInputMode?: SecretInputMode;
|
||||||
|
expectedProviders: string[];
|
||||||
|
normalize: (value: string) => string;
|
||||||
|
setCredential: (apiKey: SecretInput, mode?: SecretInputMode) => Promise<void>;
|
||||||
|
}): Promise<string | undefined> {
|
||||||
|
const tokenProvider = normalizeTokenProviderInput(params.tokenProvider);
|
||||||
|
const expectedProviders = params.expectedProviders
|
||||||
|
.map((provider) => normalizeTokenProviderInput(provider))
|
||||||
|
.filter((provider): provider is string => Boolean(provider));
|
||||||
|
if (!params.token || !tokenProvider || !expectedProviders.includes(tokenProvider)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const apiKey = params.normalize(params.token);
|
||||||
|
await params.setCredential(apiKey, params.secretInputMode);
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureApiKeyFromOptionEnvOrPrompt(params: {
|
||||||
|
token: string | undefined;
|
||||||
|
tokenProvider: string | undefined;
|
||||||
|
secretInputMode?: SecretInputMode;
|
||||||
|
config: OpenClawConfig;
|
||||||
|
expectedProviders: string[];
|
||||||
|
provider: string;
|
||||||
|
envLabel: string;
|
||||||
|
promptMessage: string;
|
||||||
|
normalize: (value: string) => string;
|
||||||
|
validate: (value: string) => string | undefined;
|
||||||
|
prompter: WizardPrompter;
|
||||||
|
setCredential: (apiKey: SecretInput, mode?: SecretInputMode) => Promise<void>;
|
||||||
|
noteMessage?: string;
|
||||||
|
noteTitle?: string;
|
||||||
|
}): Promise<string> {
|
||||||
|
const optionApiKey = await maybeApplyApiKeyFromOption({
|
||||||
|
token: params.token,
|
||||||
|
tokenProvider: params.tokenProvider,
|
||||||
|
secretInputMode: params.secretInputMode,
|
||||||
|
expectedProviders: params.expectedProviders,
|
||||||
|
normalize: params.normalize,
|
||||||
|
setCredential: params.setCredential,
|
||||||
|
});
|
||||||
|
if (optionApiKey) {
|
||||||
|
return optionApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (params.noteMessage) {
|
||||||
|
await params.prompter.note(params.noteMessage, params.noteTitle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await ensureApiKeyFromEnvOrPrompt({
|
||||||
|
config: params.config,
|
||||||
|
provider: params.provider,
|
||||||
|
envLabel: params.envLabel,
|
||||||
|
promptMessage: params.promptMessage,
|
||||||
|
normalize: params.normalize,
|
||||||
|
validate: params.validate,
|
||||||
|
prompter: params.prompter,
|
||||||
|
secretInputMode: params.secretInputMode,
|
||||||
|
setCredential: params.setCredential,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function ensureApiKeyFromEnvOrPrompt(params: {
|
||||||
|
config: OpenClawConfig;
|
||||||
|
provider: string;
|
||||||
|
envLabel: string;
|
||||||
|
promptMessage: string;
|
||||||
|
normalize: (value: string) => string;
|
||||||
|
validate: (value: string) => string | undefined;
|
||||||
|
prompter: WizardPrompter;
|
||||||
|
secretInputMode?: SecretInputMode;
|
||||||
|
setCredential: (apiKey: SecretInput, mode?: SecretInputMode) => Promise<void>;
|
||||||
|
}): Promise<string> {
|
||||||
|
const selectedMode = await resolveSecretInputModeForEnvSelection({
|
||||||
|
prompter: params.prompter,
|
||||||
|
explicitMode: params.secretInputMode,
|
||||||
|
});
|
||||||
|
const envKey = resolveEnvApiKey(params.provider);
|
||||||
|
|
||||||
|
if (selectedMode === "ref") {
|
||||||
|
if (typeof params.prompter.select !== "function") {
|
||||||
|
const fallback = resolveRefFallbackInput({
|
||||||
|
config: params.config,
|
||||||
|
provider: params.provider,
|
||||||
|
preferredEnvVar: envKey?.source ? extractEnvVarFromSourceLabel(envKey.source) : undefined,
|
||||||
|
});
|
||||||
|
await params.setCredential(fallback.ref, selectedMode);
|
||||||
|
return fallback.resolvedValue;
|
||||||
|
}
|
||||||
|
const resolved = await promptSecretRefForSetup({
|
||||||
|
provider: params.provider,
|
||||||
|
config: params.config,
|
||||||
|
prompter: params.prompter,
|
||||||
|
preferredEnvVar: envKey?.source ? extractEnvVarFromSourceLabel(envKey.source) : undefined,
|
||||||
|
});
|
||||||
|
await params.setCredential(resolved.ref, selectedMode);
|
||||||
|
return resolved.resolvedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (envKey && selectedMode === "plaintext") {
|
||||||
|
const useExisting = await params.prompter.confirm({
|
||||||
|
message: `Use existing ${params.envLabel} (${envKey.source}, ${formatApiKeyPreview(envKey.apiKey)})?`,
|
||||||
|
initialValue: true,
|
||||||
|
});
|
||||||
|
if (useExisting) {
|
||||||
|
await params.setCredential(envKey.apiKey, selectedMode);
|
||||||
|
return envKey.apiKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = await params.prompter.text({
|
||||||
|
message: params.promptMessage,
|
||||||
|
validate: params.validate,
|
||||||
|
});
|
||||||
|
const apiKey = params.normalize(String(key ?? ""));
|
||||||
|
await params.setCredential(apiKey, selectedMode);
|
||||||
|
return apiKey;
|
||||||
|
}
|
||||||
38
src/plugins/provider-auth-token.ts
Normal file
38
src/plugins/provider-auth-token.ts
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import { normalizeProviderId } from "../agents/model-selection.js";
|
||||||
|
|
||||||
|
export const ANTHROPIC_SETUP_TOKEN_PREFIX = "sk-ant-oat01-";
|
||||||
|
export const ANTHROPIC_SETUP_TOKEN_MIN_LENGTH = 80;
|
||||||
|
export const DEFAULT_TOKEN_PROFILE_NAME = "default";
|
||||||
|
|
||||||
|
export function normalizeTokenProfileName(raw: string): string {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return DEFAULT_TOKEN_PROFILE_NAME;
|
||||||
|
}
|
||||||
|
const slug = trimmed
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/[^a-z0-9._-]+/g, "-")
|
||||||
|
.replace(/-+/g, "-")
|
||||||
|
.replace(/^-+|-+$/g, "");
|
||||||
|
return slug || DEFAULT_TOKEN_PROFILE_NAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildTokenProfileId(params: { provider: string; name: string }): string {
|
||||||
|
const provider = normalizeProviderId(params.provider);
|
||||||
|
const name = normalizeTokenProfileName(params.name);
|
||||||
|
return `${provider}:${name}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function validateAnthropicSetupToken(raw: string): string | undefined {
|
||||||
|
const trimmed = raw.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
return "Required";
|
||||||
|
}
|
||||||
|
if (!trimmed.startsWith(ANTHROPIC_SETUP_TOKEN_PREFIX)) {
|
||||||
|
return `Expected token starting with ${ANTHROPIC_SETUP_TOKEN_PREFIX}`;
|
||||||
|
}
|
||||||
|
if (trimmed.length < ANTHROPIC_SETUP_TOKEN_MIN_LENGTH) {
|
||||||
|
return "Token looks too short; paste the full setup-token";
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
1
src/plugins/provider-auth-types.ts
Normal file
1
src/plugins/provider-auth-types.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export type SecretInputMode = "plaintext" | "ref"; // pragma: allowlist secret
|
||||||
41
src/plugins/provider-model-allowlist.ts
Normal file
41
src/plugins/provider-model-allowlist.ts
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import { DEFAULT_PROVIDER } from "../agents/defaults.js";
|
||||||
|
import { resolveAllowlistModelKey } from "../agents/model-selection.js";
|
||||||
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
|
||||||
|
export function ensureModelAllowlistEntry(params: {
|
||||||
|
cfg: OpenClawConfig;
|
||||||
|
modelRef: string;
|
||||||
|
defaultProvider?: string;
|
||||||
|
}): OpenClawConfig {
|
||||||
|
const rawModelRef = params.modelRef.trim();
|
||||||
|
if (!rawModelRef) {
|
||||||
|
return params.cfg;
|
||||||
|
}
|
||||||
|
|
||||||
|
const models = { ...params.cfg.agents?.defaults?.models };
|
||||||
|
const keySet = new Set<string>([rawModelRef]);
|
||||||
|
const canonicalKey = resolveAllowlistModelKey(
|
||||||
|
rawModelRef,
|
||||||
|
params.defaultProvider ?? DEFAULT_PROVIDER,
|
||||||
|
);
|
||||||
|
if (canonicalKey) {
|
||||||
|
keySet.add(canonicalKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of keySet) {
|
||||||
|
models[key] = {
|
||||||
|
...models[key],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...params.cfg,
|
||||||
|
agents: {
|
||||||
|
...params.cfg.agents,
|
||||||
|
defaults: {
|
||||||
|
...params.cfg.agents?.defaults,
|
||||||
|
models,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
81
src/plugins/provider-model-defaults.ts
Normal file
81
src/plugins/provider-model-defaults.ts
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import { ensureModelAllowlistEntry } from "./provider-model-allowlist.js";
|
||||||
|
import { applyAgentDefaultPrimaryModel } from "./provider-model-primary.js";
|
||||||
|
|
||||||
|
export const GOOGLE_GEMINI_DEFAULT_MODEL = "google/gemini-3.1-pro-preview";
|
||||||
|
export const OPENAI_DEFAULT_MODEL = "openai/gpt-5.1-codex";
|
||||||
|
export const OPENCODE_GO_DEFAULT_MODEL_REF = "opencode-go/kimi-k2.5";
|
||||||
|
export const OPENCODE_ZEN_DEFAULT_MODEL = "opencode/claude-opus-4-6";
|
||||||
|
|
||||||
|
const LEGACY_OPENCODE_ZEN_DEFAULT_MODELS = new Set([
|
||||||
|
"opencode/claude-opus-4-5",
|
||||||
|
"opencode-zen/claude-opus-4-5",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export function applyGoogleGeminiModelDefault(cfg: OpenClawConfig): {
|
||||||
|
next: OpenClawConfig;
|
||||||
|
changed: boolean;
|
||||||
|
} {
|
||||||
|
return applyAgentDefaultPrimaryModel({ cfg, model: GOOGLE_GEMINI_DEFAULT_MODEL });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyOpenAIProviderConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||||
|
const next = ensureModelAllowlistEntry({
|
||||||
|
cfg,
|
||||||
|
modelRef: OPENAI_DEFAULT_MODEL,
|
||||||
|
});
|
||||||
|
const models = { ...next.agents?.defaults?.models };
|
||||||
|
models[OPENAI_DEFAULT_MODEL] = {
|
||||||
|
...models[OPENAI_DEFAULT_MODEL],
|
||||||
|
alias: models[OPENAI_DEFAULT_MODEL]?.alias ?? "GPT",
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...next,
|
||||||
|
agents: {
|
||||||
|
...next.agents,
|
||||||
|
defaults: {
|
||||||
|
...next.agents?.defaults,
|
||||||
|
models,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyOpenAIConfig(cfg: OpenClawConfig): OpenClawConfig {
|
||||||
|
const next = applyOpenAIProviderConfig(cfg);
|
||||||
|
return {
|
||||||
|
...next,
|
||||||
|
agents: {
|
||||||
|
...next.agents,
|
||||||
|
defaults: {
|
||||||
|
...next.agents?.defaults,
|
||||||
|
model:
|
||||||
|
next.agents?.defaults?.model && typeof next.agents.defaults.model === "object"
|
||||||
|
? {
|
||||||
|
...next.agents.defaults.model,
|
||||||
|
primary: OPENAI_DEFAULT_MODEL,
|
||||||
|
}
|
||||||
|
: { primary: OPENAI_DEFAULT_MODEL },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyOpencodeGoModelDefault(cfg: OpenClawConfig): {
|
||||||
|
next: OpenClawConfig;
|
||||||
|
changed: boolean;
|
||||||
|
} {
|
||||||
|
return applyAgentDefaultPrimaryModel({ cfg, model: OPENCODE_GO_DEFAULT_MODEL_REF });
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyOpencodeZenModelDefault(cfg: OpenClawConfig): {
|
||||||
|
next: OpenClawConfig;
|
||||||
|
changed: boolean;
|
||||||
|
} {
|
||||||
|
return applyAgentDefaultPrimaryModel({
|
||||||
|
cfg,
|
||||||
|
model: OPENCODE_ZEN_DEFAULT_MODEL,
|
||||||
|
legacyModels: LEGACY_OPENCODE_ZEN_DEFAULT_MODELS,
|
||||||
|
});
|
||||||
|
}
|
||||||
72
src/plugins/provider-model-primary.ts
Normal file
72
src/plugins/provider-model-primary.ts
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import type { AgentModelListConfig } from "../config/types.js";
|
||||||
|
|
||||||
|
export function resolvePrimaryModel(model?: AgentModelListConfig | string): string | undefined {
|
||||||
|
if (typeof model === "string") {
|
||||||
|
return model;
|
||||||
|
}
|
||||||
|
if (model && typeof model === "object" && typeof model.primary === "string") {
|
||||||
|
return model.primary;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyAgentDefaultPrimaryModel(params: {
|
||||||
|
cfg: OpenClawConfig;
|
||||||
|
model: string;
|
||||||
|
legacyModels?: Set<string>;
|
||||||
|
}): { next: OpenClawConfig; changed: boolean } {
|
||||||
|
const current = resolvePrimaryModel(params.cfg.agents?.defaults?.model)?.trim();
|
||||||
|
const normalizedCurrent = current && params.legacyModels?.has(current) ? params.model : current;
|
||||||
|
if (normalizedCurrent === params.model) {
|
||||||
|
return { next: params.cfg, changed: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
next: {
|
||||||
|
...params.cfg,
|
||||||
|
agents: {
|
||||||
|
...params.cfg.agents,
|
||||||
|
defaults: {
|
||||||
|
...params.cfg.agents?.defaults,
|
||||||
|
model:
|
||||||
|
params.cfg.agents?.defaults?.model &&
|
||||||
|
typeof params.cfg.agents.defaults.model === "object"
|
||||||
|
? {
|
||||||
|
...params.cfg.agents.defaults.model,
|
||||||
|
primary: params.model,
|
||||||
|
}
|
||||||
|
: { primary: params.model },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
changed: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function applyPrimaryModel(cfg: OpenClawConfig, model: string): OpenClawConfig {
|
||||||
|
const defaults = cfg.agents?.defaults;
|
||||||
|
const existingModel = defaults?.model;
|
||||||
|
const existingModels = defaults?.models;
|
||||||
|
const fallbacks =
|
||||||
|
typeof existingModel === "object" && existingModel !== null && "fallbacks" in existingModel
|
||||||
|
? (existingModel as { fallbacks?: string[] }).fallbacks
|
||||||
|
: undefined;
|
||||||
|
return {
|
||||||
|
...cfg,
|
||||||
|
agents: {
|
||||||
|
...cfg.agents,
|
||||||
|
defaults: {
|
||||||
|
...defaults,
|
||||||
|
model: {
|
||||||
|
...(fallbacks ? { fallbacks } : undefined),
|
||||||
|
primary: model,
|
||||||
|
},
|
||||||
|
models: {
|
||||||
|
...existingModels,
|
||||||
|
[model]: existingModels?.[model] ?? {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
53
src/plugins/provider-oauth-flow.ts
Normal file
53
src/plugins/provider-oauth-flow.ts
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
|
|
||||||
|
export type OAuthPrompt = { message: string; placeholder?: string };
|
||||||
|
|
||||||
|
const validateRequiredInput = (value: string) => (value.trim().length > 0 ? undefined : "Required");
|
||||||
|
|
||||||
|
export function createVpsAwareOAuthHandlers(params: {
|
||||||
|
isRemote: boolean;
|
||||||
|
prompter: WizardPrompter;
|
||||||
|
runtime: RuntimeEnv;
|
||||||
|
spin: ReturnType<WizardPrompter["progress"]>;
|
||||||
|
openUrl: (url: string) => Promise<unknown>;
|
||||||
|
localBrowserMessage: string;
|
||||||
|
manualPromptMessage?: string;
|
||||||
|
}): {
|
||||||
|
onAuth: (event: { url: string }) => Promise<void>;
|
||||||
|
onPrompt: (prompt: OAuthPrompt) => Promise<string>;
|
||||||
|
} {
|
||||||
|
const manualPromptMessage = params.manualPromptMessage ?? "Paste the redirect URL";
|
||||||
|
let manualCodePromise: Promise<string> | undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
onAuth: async ({ url }) => {
|
||||||
|
if (params.isRemote) {
|
||||||
|
params.spin.stop("OAuth URL ready");
|
||||||
|
params.runtime.log(`\nOpen this URL in your LOCAL browser:\n\n${url}\n`);
|
||||||
|
manualCodePromise = params.prompter
|
||||||
|
.text({
|
||||||
|
message: manualPromptMessage,
|
||||||
|
validate: validateRequiredInput,
|
||||||
|
})
|
||||||
|
.then((value) => String(value));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
params.spin.update(params.localBrowserMessage);
|
||||||
|
await params.openUrl(url);
|
||||||
|
params.runtime.log(`Open: ${url}`);
|
||||||
|
},
|
||||||
|
onPrompt: async (prompt) => {
|
||||||
|
if (manualCodePromise) {
|
||||||
|
return manualCodePromise;
|
||||||
|
}
|
||||||
|
const code = await params.prompter.text({
|
||||||
|
message: prompt.message,
|
||||||
|
placeholder: prompt.placeholder,
|
||||||
|
validate: validateRequiredInput,
|
||||||
|
});
|
||||||
|
return String(code);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
164
src/plugins/provider-openai-codex-oauth-tls.ts
Normal file
164
src/plugins/provider-openai-codex-oauth-tls.ts
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { formatCliCommand } from "../cli/command-format.js";
|
||||||
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import { note } from "../terminal/note.js";
|
||||||
|
|
||||||
|
const TLS_CERT_ERROR_CODES = new Set([
|
||||||
|
"UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
|
||||||
|
"UNABLE_TO_VERIFY_LEAF_SIGNATURE",
|
||||||
|
"CERT_HAS_EXPIRED",
|
||||||
|
"DEPTH_ZERO_SELF_SIGNED_CERT",
|
||||||
|
"SELF_SIGNED_CERT_IN_CHAIN",
|
||||||
|
"ERR_TLS_CERT_ALTNAME_INVALID",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const TLS_CERT_ERROR_PATTERNS = [
|
||||||
|
/unable to get local issuer certificate/i,
|
||||||
|
/unable to verify the first certificate/i,
|
||||||
|
/self[- ]signed certificate/i,
|
||||||
|
/certificate has expired/i,
|
||||||
|
];
|
||||||
|
|
||||||
|
const OPENAI_AUTH_PROBE_URL =
|
||||||
|
"https://auth.openai.com/oauth/authorize?response_type=code&client_id=openclaw-preflight&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback&scope=openid+profile+email";
|
||||||
|
|
||||||
|
type PreflightFailureKind = "tls-cert" | "network";
|
||||||
|
|
||||||
|
export type OpenAIOAuthTlsPreflightResult =
|
||||||
|
| { ok: true }
|
||||||
|
| {
|
||||||
|
ok: false;
|
||||||
|
kind: PreflightFailureKind;
|
||||||
|
code?: string;
|
||||||
|
message: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function asRecord(value: unknown): Record<string, unknown> | null {
|
||||||
|
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractFailure(error: unknown): {
|
||||||
|
code?: string;
|
||||||
|
message: string;
|
||||||
|
kind: PreflightFailureKind;
|
||||||
|
} {
|
||||||
|
const root = asRecord(error);
|
||||||
|
const rootCause = asRecord(root?.cause);
|
||||||
|
const code = typeof rootCause?.code === "string" ? rootCause.code : undefined;
|
||||||
|
const message =
|
||||||
|
typeof rootCause?.message === "string"
|
||||||
|
? rootCause.message
|
||||||
|
: typeof root?.message === "string"
|
||||||
|
? root.message
|
||||||
|
: String(error);
|
||||||
|
const isTlsCertError =
|
||||||
|
(code ? TLS_CERT_ERROR_CODES.has(code) : false) ||
|
||||||
|
TLS_CERT_ERROR_PATTERNS.some((pattern) => pattern.test(message));
|
||||||
|
return {
|
||||||
|
code,
|
||||||
|
message,
|
||||||
|
kind: isTlsCertError ? "tls-cert" : "network",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveHomebrewPrefixFromExecPath(execPath: string): string | null {
|
||||||
|
const marker = `${path.sep}Cellar${path.sep}`;
|
||||||
|
const idx = execPath.indexOf(marker);
|
||||||
|
if (idx > 0) {
|
||||||
|
return execPath.slice(0, idx);
|
||||||
|
}
|
||||||
|
const envPrefix = process.env.HOMEBREW_PREFIX?.trim();
|
||||||
|
return envPrefix ? envPrefix : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveCertBundlePath(): string | null {
|
||||||
|
const prefix = resolveHomebrewPrefixFromExecPath(process.execPath);
|
||||||
|
if (!prefix) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return path.join(prefix, "etc", "openssl@3", "cert.pem");
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasOpenAICodexOAuthProfile(cfg: OpenClawConfig): boolean {
|
||||||
|
const profiles = cfg.auth?.profiles;
|
||||||
|
if (!profiles) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Object.values(profiles).some(
|
||||||
|
(profile) => profile.provider === "openai-codex" && profile.mode === "oauth",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldRunOpenAIOAuthTlsPrerequisites(params: {
|
||||||
|
cfg: OpenClawConfig;
|
||||||
|
deep?: boolean;
|
||||||
|
}): boolean {
|
||||||
|
if (params.deep === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return hasOpenAICodexOAuthProfile(params.cfg);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runOpenAIOAuthTlsPreflight(options?: {
|
||||||
|
timeoutMs?: number;
|
||||||
|
fetchImpl?: typeof fetch;
|
||||||
|
}): Promise<OpenAIOAuthTlsPreflightResult> {
|
||||||
|
const timeoutMs = options?.timeoutMs ?? 5000;
|
||||||
|
const fetchImpl = options?.fetchImpl ?? fetch;
|
||||||
|
try {
|
||||||
|
await fetchImpl(OPENAI_AUTH_PROBE_URL, {
|
||||||
|
method: "GET",
|
||||||
|
redirect: "manual",
|
||||||
|
signal: AbortSignal.timeout(timeoutMs),
|
||||||
|
});
|
||||||
|
return { ok: true };
|
||||||
|
} catch (error) {
|
||||||
|
const failure = extractFailure(error);
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
kind: failure.kind,
|
||||||
|
code: failure.code,
|
||||||
|
message: failure.message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatOpenAIOAuthTlsPreflightFix(
|
||||||
|
result: Exclude<OpenAIOAuthTlsPreflightResult, { ok: true }>,
|
||||||
|
): string {
|
||||||
|
if (result.kind !== "tls-cert") {
|
||||||
|
return [
|
||||||
|
"OpenAI OAuth prerequisites check failed due to a network error before the browser flow.",
|
||||||
|
`Cause: ${result.message}`,
|
||||||
|
"Verify DNS/firewall/proxy access to auth.openai.com and retry.",
|
||||||
|
].join("\n");
|
||||||
|
}
|
||||||
|
const certBundlePath = resolveCertBundlePath();
|
||||||
|
const lines = [
|
||||||
|
"OpenAI OAuth prerequisites check failed: Node/OpenSSL cannot validate TLS certificates.",
|
||||||
|
`Cause: ${result.code ? `${result.code} (${result.message})` : result.message}`,
|
||||||
|
"",
|
||||||
|
"Fix (Homebrew Node/OpenSSL):",
|
||||||
|
`- ${formatCliCommand("brew postinstall ca-certificates")}`,
|
||||||
|
`- ${formatCliCommand("brew postinstall openssl@3")}`,
|
||||||
|
];
|
||||||
|
if (certBundlePath) {
|
||||||
|
lines.push(`- Verify cert bundle exists: ${certBundlePath}`);
|
||||||
|
}
|
||||||
|
lines.push("- Retry the OAuth login flow.");
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function noteOpenAIOAuthTlsPrerequisites(params: {
|
||||||
|
cfg: OpenClawConfig;
|
||||||
|
deep?: boolean;
|
||||||
|
}): Promise<void> {
|
||||||
|
if (!shouldRunOpenAIOAuthTlsPrerequisites(params)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const result = await runOpenAIOAuthTlsPreflight({ timeoutMs: 4000 });
|
||||||
|
if (result.ok || result.kind !== "tls-cert") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
note(formatOpenAIOAuthTlsPreflightFix(result), "OAuth TLS prerequisites");
|
||||||
|
}
|
||||||
65
src/plugins/provider-openai-codex-oauth.ts
Normal file
65
src/plugins/provider-openai-codex-oauth.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { loginOpenAICodex, type OAuthCredentials } from "@mariozechner/pi-ai/oauth";
|
||||||
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
|
import { createVpsAwareOAuthHandlers } from "./provider-oauth-flow.js";
|
||||||
|
import {
|
||||||
|
formatOpenAIOAuthTlsPreflightFix,
|
||||||
|
runOpenAIOAuthTlsPreflight,
|
||||||
|
} from "./provider-openai-codex-oauth-tls.js";
|
||||||
|
|
||||||
|
export async function loginOpenAICodexOAuth(params: {
|
||||||
|
prompter: WizardPrompter;
|
||||||
|
runtime: RuntimeEnv;
|
||||||
|
isRemote: boolean;
|
||||||
|
openUrl: (url: string) => Promise<void>;
|
||||||
|
localBrowserMessage?: string;
|
||||||
|
}): Promise<OAuthCredentials | null> {
|
||||||
|
const { prompter, runtime, isRemote, openUrl, localBrowserMessage } = params;
|
||||||
|
const preflight = await runOpenAIOAuthTlsPreflight();
|
||||||
|
if (!preflight.ok && preflight.kind === "tls-cert") {
|
||||||
|
const hint = formatOpenAIOAuthTlsPreflightFix(preflight);
|
||||||
|
runtime.error(hint);
|
||||||
|
await prompter.note(hint, "OAuth prerequisites");
|
||||||
|
throw new Error(preflight.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
await prompter.note(
|
||||||
|
isRemote
|
||||||
|
? [
|
||||||
|
"You are running in a remote/VPS environment.",
|
||||||
|
"A URL will be shown for you to open in your LOCAL browser.",
|
||||||
|
"After signing in, paste the redirect URL back here.",
|
||||||
|
].join("\n")
|
||||||
|
: [
|
||||||
|
"Browser will open for OpenAI authentication.",
|
||||||
|
"If the callback doesn't auto-complete, paste the redirect URL.",
|
||||||
|
"OpenAI OAuth uses localhost:1455 for the callback.",
|
||||||
|
].join("\n"),
|
||||||
|
"OpenAI Codex OAuth",
|
||||||
|
);
|
||||||
|
|
||||||
|
const spin = prompter.progress("Starting OAuth flow…");
|
||||||
|
try {
|
||||||
|
const { onAuth: baseOnAuth, onPrompt } = createVpsAwareOAuthHandlers({
|
||||||
|
isRemote,
|
||||||
|
prompter,
|
||||||
|
runtime,
|
||||||
|
spin,
|
||||||
|
openUrl,
|
||||||
|
localBrowserMessage: localBrowserMessage ?? "Complete sign-in in browser…",
|
||||||
|
});
|
||||||
|
|
||||||
|
const creds = await loginOpenAICodex({
|
||||||
|
onAuth: baseOnAuth,
|
||||||
|
onPrompt,
|
||||||
|
onProgress: (msg: string) => spin.update(msg),
|
||||||
|
});
|
||||||
|
spin.stop("OpenAI OAuth complete");
|
||||||
|
return creds ?? null;
|
||||||
|
} catch (err) {
|
||||||
|
spin.stop("OpenAI OAuth failed");
|
||||||
|
runtime.error(String(err));
|
||||||
|
await prompter.note("Trouble with OAuth? See https://docs.openclaw.ai/start/faq", "OAuth help");
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -17,8 +17,6 @@ import type { AnyAgentTool } from "../agents/tools/common.js";
|
|||||||
import type { ThinkLevel } from "../auto-reply/thinking.js";
|
import type { ThinkLevel } from "../auto-reply/thinking.js";
|
||||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||||
import type { ChannelId, ChannelPlugin } from "../channels/plugins/types.js";
|
import type { ChannelId, ChannelPlugin } from "../channels/plugins/types.js";
|
||||||
import type { createVpsAwareOAuthHandlers } from "../commands/oauth-flow.js";
|
|
||||||
import type { OnboardOptions } from "../commands/onboard-types.js";
|
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import type { ModelProviderConfig } from "../config/types.js";
|
import type { ModelProviderConfig } from "../config/types.js";
|
||||||
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
|
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
|
||||||
@ -39,11 +37,20 @@ import type {
|
|||||||
SpeechVoiceOption,
|
SpeechVoiceOption,
|
||||||
} from "../tts/provider-types.js";
|
} from "../tts/provider-types.js";
|
||||||
import type { WizardPrompter } from "../wizard/prompts.js";
|
import type { WizardPrompter } from "../wizard/prompts.js";
|
||||||
|
import type { SecretInputMode } from "./provider-auth-types.js";
|
||||||
|
import type { createVpsAwareOAuthHandlers } from "./provider-oauth-flow.js";
|
||||||
import type { PluginRuntime } from "./runtime/types.js";
|
import type { PluginRuntime } from "./runtime/types.js";
|
||||||
|
|
||||||
export type { PluginRuntime } from "./runtime/types.js";
|
export type { PluginRuntime } from "./runtime/types.js";
|
||||||
export type { AnyAgentTool } from "../agents/tools/common.js";
|
export type { AnyAgentTool } from "../agents/tools/common.js";
|
||||||
|
|
||||||
|
export type ProviderAuthOptionBag = {
|
||||||
|
token?: string;
|
||||||
|
tokenProvider?: string;
|
||||||
|
secretInputMode?: SecretInputMode;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
export type PluginLogger = {
|
export type PluginLogger = {
|
||||||
debug?: (message: string) => void;
|
debug?: (message: string) => void;
|
||||||
info: (message: string) => void;
|
info: (message: string) => void;
|
||||||
@ -144,7 +151,7 @@ export type ProviderAuthContext = {
|
|||||||
* `--token/--token-provider` pairs. Direct `models auth login` usually
|
* `--token/--token-provider` pairs. Direct `models auth login` usually
|
||||||
* leaves this undefined.
|
* leaves this undefined.
|
||||||
*/
|
*/
|
||||||
opts?: Partial<OnboardOptions>;
|
opts?: ProviderAuthOptionBag;
|
||||||
/**
|
/**
|
||||||
* Onboarding secret persistence preference.
|
* Onboarding secret persistence preference.
|
||||||
*
|
*
|
||||||
@ -152,7 +159,7 @@ export type ProviderAuthContext = {
|
|||||||
* plaintext or env/file/exec ref storage. Ad-hoc `models auth login` flows
|
* plaintext or env/file/exec ref storage. Ad-hoc `models auth login` flows
|
||||||
* usually leave it undefined.
|
* usually leave it undefined.
|
||||||
*/
|
*/
|
||||||
secretInputMode?: OnboardOptions["secretInputMode"];
|
secretInputMode?: SecretInputMode;
|
||||||
/**
|
/**
|
||||||
* Whether the provider auth flow should offer the onboarding secret-storage
|
* Whether the provider auth flow should offer the onboarding secret-storage
|
||||||
* mode picker when `secretInputMode` is unset.
|
* mode picker when `secretInputMode` is unset.
|
||||||
@ -196,7 +203,7 @@ export type ProviderAuthMethodNonInteractiveContext = {
|
|||||||
authChoice: string;
|
authChoice: string;
|
||||||
config: OpenClawConfig;
|
config: OpenClawConfig;
|
||||||
baseConfig: OpenClawConfig;
|
baseConfig: OpenClawConfig;
|
||||||
opts: OnboardOptions;
|
opts: ProviderAuthOptionBag;
|
||||||
runtime: RuntimeEnv;
|
runtime: RuntimeEnv;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
workspaceDir?: string;
|
workspaceDir?: string;
|
||||||
|
|||||||
@ -1,7 +1,3 @@
|
|||||||
import {
|
|
||||||
promptSecretRefForSetup,
|
|
||||||
resolveSecretInputModeForEnvSelection,
|
|
||||||
} from "../commands/auth-choice.apply-helpers.js";
|
|
||||||
import {
|
import {
|
||||||
normalizeGatewayTokenInput,
|
normalizeGatewayTokenInput,
|
||||||
randomToken,
|
randomToken,
|
||||||
@ -23,6 +19,10 @@ import {
|
|||||||
} from "../gateway/gateway-config-prompts.shared.js";
|
} from "../gateway/gateway-config-prompts.shared.js";
|
||||||
import { DEFAULT_DANGEROUS_NODE_COMMANDS } from "../gateway/node-command-policy.js";
|
import { DEFAULT_DANGEROUS_NODE_COMMANDS } from "../gateway/node-command-policy.js";
|
||||||
import { findTailscaleBinary } from "../infra/tailscale.js";
|
import { findTailscaleBinary } from "../infra/tailscale.js";
|
||||||
|
import {
|
||||||
|
promptSecretRefForSetup,
|
||||||
|
resolveSecretInputModeForEnvSelection,
|
||||||
|
} from "../plugins/provider-auth-input.js";
|
||||||
import type { RuntimeEnv } from "../runtime.js";
|
import type { RuntimeEnv } from "../runtime.js";
|
||||||
import { validateIPv4AddressInput } from "../shared/net/ipv4.js";
|
import { validateIPv4AddressInput } from "../shared/net/ipv4.js";
|
||||||
import type { WizardPrompter } from "./prompts.js";
|
import type { WizardPrompter } from "./prompts.js";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user