






















@@ -13,6 +13,7 @@ const log = createSubsystemLogger("agents/auth-profiles");
1313const CLAUDE_CLI_CREDENTIALS_RELATIVE_PATH = ".claude/.credentials.json";
1414const CODEX_CLI_AUTH_FILENAME = "auth.json";
1515const MINIMAX_CLI_CREDENTIALS_RELATIVE_PATH = ".minimax/oauth_creds.json";
16+const GEMINI_CLI_CREDENTIALS_RELATIVE_PATH = ".gemini/oauth_creds.json";
16171718const CLAUDE_CLI_KEYCHAIN_SERVICE = "Claude Code-credentials";
1819const CLAUDE_CLI_KEYCHAIN_ACCOUNT = "Claude Code";
@@ -27,11 +28,13 @@ type CachedValue<T> = {
2728let claudeCliCache: CachedValue<ClaudeCliCredential> | null = null;
2829let codexCliCache: CachedValue<CodexCliCredential> | null = null;
2930let minimaxCliCache: CachedValue<MiniMaxCliCredential> | null = null;
31+let geminiCliCache: CachedValue<GeminiCliCredential> | null = null;
30323133export function resetCliCredentialCachesForTest(): void {
3234claudeCliCache = null;
3335codexCliCache = null;
3436minimaxCliCache = null;
37+geminiCliCache = null;
3538}
36393740export type ClaudeCliCredential =
@@ -67,6 +70,16 @@ export type MiniMaxCliCredential = {
6770expires: number;
6871};
697273+export type GeminiCliCredential = {
74+type: "oauth";
75+provider: "google-gemini-cli";
76+access: string;
77+refresh: string;
78+expires: number;
79+accountId?: string;
80+email?: string;
81+};
82+7083type ClaudeCliFileOptions = {
7184homeDir?: string;
7285};
@@ -131,6 +144,11 @@ function resolveMiniMaxCliCredentialsPath(homeDir?: string) {
131144return path.join(baseDir, MINIMAX_CLI_CREDENTIALS_RELATIVE_PATH);
132145}
133146147+function resolveGeminiCliCredentialsPath(homeDir?: string) {
148+const baseDir = homeDir ?? resolveUserPath("~");
149+return path.join(baseDir, GEMINI_CLI_CREDENTIALS_RELATIVE_PATH);
150+}
151+134152function readFileMtimeMs(filePath: string): number | null {
135153try {
136154return fs.statSync(filePath).mtimeMs;
@@ -211,6 +229,22 @@ function decodeJwtExpiryMs(token: string): number | null {
211229}
212230}
213231232+function decodeJwtIdentityClaims(token: string): { sub?: string; email?: string } {
233+const parts = token.split(".");
234+if (parts.length < 2) {
235+return {};
236+}
237+try {
238+const payloadRaw = Buffer.from(parts[1], "base64url").toString("utf8");
239+const payload = JSON.parse(payloadRaw) as { sub?: unknown; email?: unknown };
240+const sub = typeof payload.sub === "string" && payload.sub ? payload.sub : undefined;
241+const email = typeof payload.email === "string" && payload.email ? payload.email : undefined;
242+return { sub, email };
243+} catch {
244+return {};
245+}
246+}
247+214248function readCodexKeychainAuthRecord(options?: {
215249codexHome?: string;
216250platform?: NodeJS.Platform;
@@ -328,6 +362,49 @@ function readMiniMaxCliCredentials(options?: { homeDir?: string }): MiniMaxCliCr
328362return readPortalCliOauthCredentials(credPath, "minimax-portal");
329363}
330364365+function readGeminiCliCredentials(options?: { homeDir?: string }): GeminiCliCredential | null {
366+const credPath = resolveGeminiCliCredentialsPath(options?.homeDir);
367+const raw = loadJsonFile(credPath);
368+if (!raw || typeof raw !== "object") {
369+return null;
370+}
371+const data = raw as Record<string, unknown>;
372+const accessToken = data.access_token;
373+const refreshToken = data.refresh_token;
374+const expiresAt = data.expiry_date;
375+376+if (typeof accessToken !== "string" || !accessToken) {
377+return null;
378+}
379+if (typeof refreshToken !== "string" || !refreshToken) {
380+return null;
381+}
382+if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) {
383+return null;
384+}
385+386+// Gemini CLI's login flow stores the openid id_token alongside the OAuth
387+// tokens. Decode it once here to lift the Google account identity (sub,
388+// email) onto the credential so the shared OAuth-identity encoder can key
389+// the auth epoch on stable, non-secret identity material — matching the
390+// Claude/Codex contract that #70132 codifies. Without this lift the encoder
391+// collapses to a provider-keyed constant and stale bindings can survive a
392+// re-login under a different Google account.
393+const idTokenRaw = data.id_token;
394+const identity =
395+typeof idTokenRaw === "string" && idTokenRaw ? decodeJwtIdentityClaims(idTokenRaw) : {};
396+397+return {
398+type: "oauth",
399+provider: "google-gemini-cli",
400+access: accessToken,
401+refresh: refreshToken,
402+expires: expiresAt,
403+ ...(identity.email ? { email: identity.email } : {}),
404+ ...(identity.sub ? { accountId: identity.sub } : {}),
405+};
406+}
407+331408function readClaudeCliKeychainCredentials(
332409execSyncImpl: ExecSyncFn = execSync,
333410): ClaudeCliCredential | null {
@@ -609,3 +686,20 @@ export function readMiniMaxCliCredentialsCached(options?: {
609686readSourceFingerprint: () => readFileMtimeMs(credPath),
610687});
611688}
689+690+export function readGeminiCliCredentialsCached(options?: {
691+ttlMs?: number;
692+homeDir?: string;
693+}): GeminiCliCredential | null {
694+const credPath = resolveGeminiCliCredentialsPath(options?.homeDir);
695+return readCachedCliCredential({
696+ttlMs: options?.ttlMs ?? 0,
697+cache: geminiCliCache,
698+cacheKey: credPath,
699+read: () => readGeminiCliCredentials({ homeDir: options?.homeDir }),
700+setCache: (next) => {
701+geminiCliCache = next;
702+},
703+readSourceFingerprint: () => readFileMtimeMs(credPath),
704+});
705+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。