import fs from "node:fs/promises"; import path from "node:path"; import type { CliBackendPreparedExecution } from "openclaw/plugin-sdk/cli-backend"; import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path"; import { GOOGLE_GEMINI_CLI_PROVIDER_ID, resolveGeminiCliProfileHome as resolveGeminiCliProfileHomePath, } from "./gemini-cli-auth-home.js"; const GEMINI_CLI_PROVIDER_ID = GOOGLE_GEMINI_CLI_PROVIDER_ID; const GOOGLE_PROVIDER_ID = "google"; const VERCEL_AI_GATEWAY_PROVIDER_ID = "vercel-ai-gateway"; const GEMINI_CLI_CREDENTIALS_FILENAME = "gemini-credentials.json"; const GEMINI_CLI_GCA_AUTH_ENV = [ "GOOGLE_GENAI_USE_GCA", "GOOGLE_CLOUD_ACCESS_TOKEN", "GOOGLE_APPLICATION_CREDENTIALS", "GEMINI_FORCE_ENCRYPTED_FILE_STORAGE", "GEMINI_FORCE_FILE_STORAGE", ]; const GEMINI_CLI_API_KEY_AUTH_ENV = [ ...GEMINI_CLI_GCA_AUTH_ENV, "GOOGLE_GENAI_USE_VERTEXAI", "GOOGLE_API_KEY", "GOOGLE_CLOUD_PROJECT", "GOOGLE_CLOUD_PROJECT_ID", "GOOGLE_CLOUD_QUOTA_PROJECT", "GOOGLE_CLOUD_LOCATION", "GOOGLE_GEMINI_BASE_URL", "GEMINI_CLI_CUSTOM_HEADERS", "GEMINI_API_KEY_AUTH_MECHANISM", ]; const GEMINI_CLI_PROFILE_AUTH_ENV = [...GEMINI_CLI_API_KEY_AUTH_ENV, "GEMINI_API_KEY"]; const GEMINI_CLI_PROFILE_SETTINGS_ENV = ["GEMINI_CLI_SYSTEM_SETTINGS_PATH"]; type GeminiAuthProfileCredential = { type: "api_key" | "oauth" | "token"; provider: string; key?: string; token?: string; access?: string; refresh?: string; expires?: number; idToken?: string; projectId?: string; }; type GeminiOAuthCredential = GeminiAuthProfileCredential & { type: "oauth"; provider: typeof GEMINI_CLI_PROVIDER_ID; access: string; refresh: string; expires: number; }; type GeminiApiKeyCredential = GeminiAuthProfileCredential & { type: "api_key"; provider: typeof GEMINI_CLI_PROVIDER_ID | typeof GOOGLE_PROVIDER_ID; key: string; }; type GeminiCliAuthHomeContext = { agentDir?: string; authProfileId?: string; systemSettingsPath?: string; }; type GeminiCliAuthSelectedType = "oauth-personal" | "gemini-api-key"; function normalizeString(value: string | undefined): string | undefined { const trimmed = value?.trim(); return trimmed ? trimmed : undefined; } function throwUnsupportedGeminiCredential(credential: GeminiAuthProfileCredential): never { if (credential.provider === VERCEL_AI_GATEWAY_PROVIDER_ID) { throw new Error( "Gemini CLI execution cannot use a vercel-ai-gateway auth profile. Use the OpenClaw vercel-ai-gateway provider instead.", ); } throw new Error("Gemini CLI execution requires a google-gemini-cli auth profile."); } function throwUnstageableSelectedGeminiProfile( ctx: GeminiCliAuthHomeContext, credential: GeminiAuthProfileCredential | undefined, ): never { const authProfileId = normalizeString(ctx.authProfileId); if (!authProfileId) { throw new Error("Gemini CLI execution requires a selected auth profile."); } if (!credential) { throw new Error( "Gemini CLI auth profile was selected but no credential material was found. Re-authenticate with `openclaw models auth login --provider google-gemini-cli --force`.", ); } if (credential.provider !== GEMINI_CLI_PROVIDER_ID) { throwUnsupportedGeminiCredential(credential); } throw new Error( "Gemini CLI execution supports google-gemini-cli OAuth or API-key auth profiles. Re-authenticate with `openclaw models auth login --provider google-gemini-cli --force`.", ); } function requireGeminiOAuthCredential( credential: GeminiAuthProfileCredential | undefined, ): GeminiOAuthCredential | null { if (!credential) { return null; } if (credential.type !== "oauth") { return null; } if (credential.provider !== GEMINI_CLI_PROVIDER_ID) { throwUnsupportedGeminiCredential(credential); } const access = normalizeString(credential.access); const refresh = normalizeString(credential.refresh); if ( !access || !refresh || typeof credential.expires !== "number" || !Number.isFinite(credential.expires) ) { throw new Error( "Gemini CLI OAuth profile is missing usable token material. Re-authenticate with `openclaw models auth login --provider google-gemini-cli --force`.", ); } return { ...credential, type: "oauth", provider: GEMINI_CLI_PROVIDER_ID, access, refresh, expires: credential.expires, idToken: normalizeString(credential.idToken), projectId: normalizeString(credential.projectId), }; } function requireGeminiApiKeyCredential( credential: GeminiAuthProfileCredential | undefined, ): GeminiApiKeyCredential | null { if (!credential) { return null; } if (credential.type !== "api_key") { return null; } if ( credential.provider !== GEMINI_CLI_PROVIDER_ID && credential.provider !== GOOGLE_PROVIDER_ID ) { throwUnsupportedGeminiCredential(credential); } const key = normalizeString(credential.key); if (!key) { throw new Error("Gemini CLI API-key profile is missing usable key material."); } return { ...credential, type: "api_key", provider: credential.provider, key, }; } function resolveGeminiCliProfileHome(ctx: GeminiCliAuthHomeContext): { home: string; geminiDir: string; } { const agentDir = normalizeString(ctx.agentDir); if (!agentDir) { throw new Error("Gemini CLI auth profile execution requires an agent directory."); } const authProfileId = normalizeString(ctx.authProfileId); if (!authProfileId) { throw new Error("Gemini CLI auth profile execution requires a selected auth profile."); } const home = resolveGeminiCliProfileHomePath(agentDir, authProfileId); return { home, geminiDir: path.join(home, ".gemini") }; } function isRecord(value: unknown): value is Record { return value !== null && typeof value === "object" && !Array.isArray(value); } function readGeminiAuthProfileCredential( credential: unknown, ): GeminiAuthProfileCredential | undefined { if (!isRecord(credential)) { return undefined; } return credential as GeminiAuthProfileCredential; } async function readGeminiCliJsonObject( filePath: string | undefined, ): Promise> { const normalized = normalizeString(filePath); if (!normalized) { return {}; } try { const parsed = JSON.parse(await fs.readFile(normalized, "utf8")) as unknown; if (!isRecord(parsed)) { throw new Error(`Gemini CLI system settings must be a JSON object: ${normalized}`); } return { ...parsed }; } catch (error) { if ( error && typeof error === "object" && "code" in error && (error as { code?: unknown }).code === "ENOENT" ) { return {}; } throw error; } } function buildGeminiCliAuthSettings( selectedType: GeminiCliAuthSelectedType, ): Record { return { security: { auth: { selectedType } } }; } async function buildGeminiCliSystemSettings( ctx: GeminiCliAuthHomeContext, selectedType: GeminiCliAuthSelectedType, ): Promise> { const base = await readGeminiCliJsonObject(ctx.systemSettingsPath); const security = isRecord(base.security) ? { ...base.security } : {}; const auth = isRecord(security.auth) ? { ...security.auth } : {}; const enforcedType = normalizeString( typeof auth.enforcedType === "string" ? auth.enforcedType : undefined, ); if (enforcedType && enforcedType !== selectedType) { throw new Error( `Gemini CLI system settings enforce ${enforcedType} auth, but the selected OpenClaw profile requires ${selectedType}.`, ); } security.auth = { ...auth, selectedType }; return { ...base, security, }; } async function writeGeminiCliJson(filePath: string, value: unknown): Promise { await fs.writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600, }); await fs.chmod(filePath, 0o600); } async function prepareGeminiCliProfileHome( ctx: GeminiCliAuthHomeContext, selectedType: GeminiCliAuthSelectedType, ): Promise<{ home: string; geminiDir: string; systemSettingsPath: string; cleanup: () => Promise; }> { const { home, geminiDir } = resolveGeminiCliProfileHome(ctx); await fs.mkdir(geminiDir, { recursive: true, mode: 0o700 }); await fs.chmod(home, 0o700); await fs.chmod(geminiDir, 0o700); const settings = buildGeminiCliAuthSettings(selectedType); const systemSettings = await buildGeminiCliSystemSettings(ctx, selectedType); const systemSettingsDir = await fs.mkdtemp( path.join(resolvePreferredOpenClawTmpDir(), "openclaw-gemini-cli-"), ); await fs.chmod(systemSettingsDir, 0o700); const systemSettingsPath = path.join(systemSettingsDir, "settings.json"); try { await Promise.all([ writeGeminiCliJson(path.join(geminiDir, "settings.json"), settings), writeGeminiCliJson(path.join(home, "settings.json"), settings), writeGeminiCliJson(systemSettingsPath, systemSettings), ]); } catch (error) { await fs.rm(systemSettingsDir, { recursive: true, force: true }); throw error; } return { home, geminiDir, systemSettingsPath, cleanup: async () => { await fs.rm(systemSettingsDir, { recursive: true, force: true }); }, }; } async function clearGeminiCliCachedCredentials(geminiDir: string): Promise { // Gemini prefers its token store over oauth_creds.json. Rebuild that store // from the selected OpenClaw profile each run so stale CLI auth cannot win. await fs.rm(path.join(geminiDir, GEMINI_CLI_CREDENTIALS_FILENAME), { force: true }); } function buildGeminiCliProjectEnv(projectId: string | undefined): Record { const normalized = normalizeString(projectId); if (!normalized) { return {}; } return { GOOGLE_CLOUD_PROJECT: normalized, GOOGLE_CLOUD_PROJECT_ID: normalized, GOOGLE_CLOUD_QUOTA_PROJECT: normalized, }; } async function prepareGeminiCliOAuthHome( ctx: GeminiCliAuthHomeContext, credential: GeminiAuthProfileCredential | undefined, ): Promise { const oauth = requireGeminiOAuthCredential(credential); if (!oauth) { return null; } const { home, geminiDir, systemSettingsPath, cleanup } = await prepareGeminiCliProfileHome( ctx, "oauth-personal", ); await clearGeminiCliCachedCredentials(geminiDir); const idToken = normalizeString(oauth.idToken); const oauthCreds: Record = { access_token: oauth.access, refresh_token: oauth.refresh, expiry_date: oauth.expires, token_type: "Bearer", }; if (idToken) { oauthCreds.id_token = idToken; } await writeGeminiCliJson(path.join(geminiDir, "oauth_creds.json"), oauthCreds); return { env: { GEMINI_CLI_HOME: home, GEMINI_CLI_SYSTEM_SETTINGS_PATH: systemSettingsPath, GEMINI_FORCE_FILE_STORAGE: "true", ...buildGeminiCliProjectEnv(oauth.projectId), }, clearEnv: [...GEMINI_CLI_PROFILE_AUTH_ENV, ...GEMINI_CLI_PROFILE_SETTINGS_ENV], cleanup, }; } async function prepareGeminiCliApiKeyHome( ctx: GeminiCliAuthHomeContext, credential: GeminiAuthProfileCredential | undefined, ): Promise { const apiKey = requireGeminiApiKeyCredential(credential); if (!apiKey) { return null; } const { home, geminiDir, systemSettingsPath, cleanup } = await prepareGeminiCliProfileHome( ctx, "gemini-api-key", ); await Promise.all([ fs.rm(path.join(geminiDir, "oauth_creds.json"), { force: true }), clearGeminiCliCachedCredentials(geminiDir), ]); return { env: { GEMINI_CLI_HOME: home, GEMINI_CLI_SYSTEM_SETTINGS_PATH: systemSettingsPath, GEMINI_FORCE_FILE_STORAGE: "true", GEMINI_API_KEY: apiKey.key, }, clearEnv: [...GEMINI_CLI_PROFILE_AUTH_ENV, ...GEMINI_CLI_PROFILE_SETTINGS_ENV], cleanup, }; } export async function prepareGeminiCliAuthHome( ctx: GeminiCliAuthHomeContext, credential: unknown, ): Promise { const authCredential = readGeminiAuthProfileCredential(credential); const prepared = (await prepareGeminiCliOAuthHome(ctx, authCredential)) ?? (await prepareGeminiCliApiKeyHome(ctx, authCredential)); if (prepared) { return prepared; } if (normalizeString(ctx.authProfileId)) { throwUnstageableSelectedGeminiProfile(ctx, authCredential); } return null; }