Files
openclaw/src/commands/onboard-auth.credentials.ts
mudrii 7ecfc1d93c fix(auth): bidirectional mode/type compat + sync OAuth to all agents (#12692)
Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 2dee8e1174e637e50d10bf7020f1de2990b804dc
Co-authored-by: mudrii <220262+mudrii@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
2026-02-20 16:01:09 +05:30

363 lines
9.9 KiB
TypeScript

import fs from "node:fs";
import path from "node:path";
import type { OAuthCredentials } from "@mariozechner/pi-ai";
import { resolveOpenClawAgentDir } from "../agents/agent-paths.js";
import { upsertAuthProfile } from "../agents/auth-profiles.js";
import { resolveStateDir } from "../config/paths.js";
export { CLOUDFLARE_AI_GATEWAY_DEFAULT_MODEL_REF } from "../agents/cloudflare-ai-gateway.js";
export { XAI_DEFAULT_MODEL_REF } from "./onboard-auth.models.js";
const resolveAuthAgentDir = (agentDir?: string) => agentDir ?? resolveOpenClawAgentDir();
export type WriteOAuthCredentialsOptions = {
syncSiblingAgents?: boolean;
};
/** Resolve real path, returning null if the target doesn't exist. */
function safeRealpathSync(dir: string): string | null {
try {
return fs.realpathSync(path.resolve(dir));
} catch {
return null;
}
}
function resolveSiblingAgentDirs(primaryAgentDir: string): string[] {
const normalized = path.resolve(primaryAgentDir);
// Derive agentsRoot from primaryAgentDir when it matches the standard
// layout (.../agents/<name>/agent). Falls back to global state dir.
const parentOfAgent = path.dirname(normalized);
const candidateAgentsRoot = path.dirname(parentOfAgent);
const looksLikeStandardLayout =
path.basename(normalized) === "agent" && path.basename(candidateAgentsRoot) === "agents";
const agentsRoot = looksLikeStandardLayout
? candidateAgentsRoot
: path.join(resolveStateDir(), "agents");
const entries = (() => {
try {
return fs.readdirSync(agentsRoot, { withFileTypes: true });
} catch {
return [];
}
})();
// Include both directories and symlinks-to-directories.
const discovered = entries
.filter((entry) => entry.isDirectory() || entry.isSymbolicLink())
.map((entry) => path.join(agentsRoot, entry.name, "agent"));
// Deduplicate via realpath to handle symlinks and path normalization.
const seen = new Set<string>();
const result: string[] = [];
for (const dir of [normalized, ...discovered]) {
const real = safeRealpathSync(dir);
if (real && !seen.has(real)) {
seen.add(real);
result.push(real);
}
}
return result;
}
export async function writeOAuthCredentials(
provider: string,
creds: OAuthCredentials,
agentDir?: string,
options?: WriteOAuthCredentialsOptions,
): Promise<string> {
const email =
typeof creds.email === "string" && creds.email.trim() ? creds.email.trim() : "default";
const profileId = `${provider}:${email}`;
const resolvedAgentDir = path.resolve(resolveAuthAgentDir(agentDir));
const targetAgentDirs = options?.syncSiblingAgents
? resolveSiblingAgentDirs(resolvedAgentDir)
: [resolvedAgentDir];
const credential = {
type: "oauth" as const,
provider,
...creds,
};
// Primary write must succeed — let it throw on failure.
upsertAuthProfile({
profileId,
credential,
agentDir: resolvedAgentDir,
});
// Sibling sync is best-effort — log and ignore individual failures.
if (options?.syncSiblingAgents) {
const primaryReal = safeRealpathSync(resolvedAgentDir);
for (const targetAgentDir of targetAgentDirs) {
const targetReal = safeRealpathSync(targetAgentDir);
if (targetReal && primaryReal && targetReal === primaryReal) {
continue;
}
try {
upsertAuthProfile({
profileId,
credential,
agentDir: targetAgentDir,
});
} catch {
// Best-effort: sibling sync failure must not block primary onboarding.
}
}
}
return profileId;
}
export async function setAnthropicApiKey(key: string, agentDir?: string) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "anthropic:default",
credential: {
type: "api_key",
provider: "anthropic",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setGeminiApiKey(key: string, agentDir?: string) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "google:default",
credential: {
type: "api_key",
provider: "google",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setMinimaxApiKey(
key: string,
agentDir?: string,
profileId: string = "minimax:default",
) {
const provider = profileId.split(":")[0] ?? "minimax";
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId,
credential: {
type: "api_key",
provider,
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setMoonshotApiKey(key: string, agentDir?: string) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "moonshot:default",
credential: {
type: "api_key",
provider: "moonshot",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setKimiCodingApiKey(key: string, agentDir?: string) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "kimi-coding:default",
credential: {
type: "api_key",
provider: "kimi-coding",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setSyntheticApiKey(key: string, agentDir?: string) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "synthetic:default",
credential: {
type: "api_key",
provider: "synthetic",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setVeniceApiKey(key: string, agentDir?: string) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "venice:default",
credential: {
type: "api_key",
provider: "venice",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export const ZAI_DEFAULT_MODEL_REF = "zai/glm-5";
export const XIAOMI_DEFAULT_MODEL_REF = "xiaomi/mimo-v2-flash";
export const OPENROUTER_DEFAULT_MODEL_REF = "openrouter/auto";
export const HUGGINGFACE_DEFAULT_MODEL_REF = "huggingface/deepseek-ai/DeepSeek-R1";
export const TOGETHER_DEFAULT_MODEL_REF = "together/moonshotai/Kimi-K2.5";
export const LITELLM_DEFAULT_MODEL_REF = "litellm/claude-opus-4-6";
export const VERCEL_AI_GATEWAY_DEFAULT_MODEL_REF = "vercel-ai-gateway/anthropic/claude-opus-4.6";
export async function setZaiApiKey(key: string, agentDir?: string) {
// Write to resolved agent dir so gateway finds credentials on startup.
upsertAuthProfile({
profileId: "zai:default",
credential: {
type: "api_key",
provider: "zai",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setXiaomiApiKey(key: string, agentDir?: string) {
upsertAuthProfile({
profileId: "xiaomi:default",
credential: {
type: "api_key",
provider: "xiaomi",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setOpenrouterApiKey(key: string, agentDir?: string) {
// Never persist the literal "undefined" (e.g. when prompt returns undefined and caller used String(key)).
const safeKey = key === "undefined" ? "" : key;
upsertAuthProfile({
profileId: "openrouter:default",
credential: {
type: "api_key",
provider: "openrouter",
key: safeKey,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setCloudflareAiGatewayConfig(
accountId: string,
gatewayId: string,
apiKey: string,
agentDir?: string,
) {
const normalizedAccountId = accountId.trim();
const normalizedGatewayId = gatewayId.trim();
const normalizedKey = apiKey.trim();
upsertAuthProfile({
profileId: "cloudflare-ai-gateway:default",
credential: {
type: "api_key",
provider: "cloudflare-ai-gateway",
key: normalizedKey,
metadata: {
accountId: normalizedAccountId,
gatewayId: normalizedGatewayId,
},
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setLitellmApiKey(key: string, agentDir?: string) {
upsertAuthProfile({
profileId: "litellm:default",
credential: {
type: "api_key",
provider: "litellm",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setVercelAiGatewayApiKey(key: string, agentDir?: string) {
upsertAuthProfile({
profileId: "vercel-ai-gateway:default",
credential: {
type: "api_key",
provider: "vercel-ai-gateway",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setOpencodeZenApiKey(key: string, agentDir?: string) {
upsertAuthProfile({
profileId: "opencode:default",
credential: {
type: "api_key",
provider: "opencode",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setTogetherApiKey(key: string, agentDir?: string) {
upsertAuthProfile({
profileId: "together:default",
credential: {
type: "api_key",
provider: "together",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export async function setHuggingfaceApiKey(key: string, agentDir?: string) {
upsertAuthProfile({
profileId: "huggingface:default",
credential: {
type: "api_key",
provider: "huggingface",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export function setQianfanApiKey(key: string, agentDir?: string) {
upsertAuthProfile({
profileId: "qianfan:default",
credential: {
type: "api_key",
provider: "qianfan",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}
export function setXaiApiKey(key: string, agentDir?: string) {
upsertAuthProfile({
profileId: "xai:default",
credential: {
type: "api_key",
provider: "xai",
key,
},
agentDir: resolveAuthAgentDir(agentDir),
});
}