fix: sync google-gemini-cli-auth tokens from external CLI (#3803)
This commit is contained in:
parent
0761652701
commit
2a4f44851a
@ -7,6 +7,7 @@ export const LEGACY_AUTH_FILENAME = "auth.json";
|
||||
export const CLAUDE_CLI_PROFILE_ID = "anthropic:claude-cli";
|
||||
export const CODEX_CLI_PROFILE_ID = "openai-codex:codex-cli";
|
||||
export const QWEN_CLI_PROFILE_ID = "qwen-portal:qwen-cli";
|
||||
export const GEMINI_CLI_PROFILE_ID = "google-gemini-cli:google-gemini-cli";
|
||||
|
||||
export const AUTH_STORE_LOCK_OPTIONS = {
|
||||
retries: {
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { readQwenCliCredentialsCached } from "../cli-credentials.js";
|
||||
import {
|
||||
readGeminiCliCredentialsCached,
|
||||
readQwenCliCredentialsCached,
|
||||
} from "../cli-credentials.js";
|
||||
import {
|
||||
EXTERNAL_CLI_NEAR_EXPIRY_MS,
|
||||
EXTERNAL_CLI_SYNC_TTL_MS,
|
||||
GEMINI_CLI_PROFILE_ID,
|
||||
QWEN_CLI_PROFILE_ID,
|
||||
log,
|
||||
} from "./constants.js";
|
||||
@ -25,7 +29,7 @@ function shallowEqualOAuthCredentials(a: OAuthCredential | undefined, b: OAuthCr
|
||||
function isExternalProfileFresh(cred: AuthProfileCredential | undefined, now: number): boolean {
|
||||
if (!cred) return false;
|
||||
if (cred.type !== "oauth" && cred.type !== "token") return false;
|
||||
if (cred.provider !== "qwen-portal") {
|
||||
if (cred.provider !== "qwen-portal" && cred.provider !== "google-gemini-cli") {
|
||||
return false;
|
||||
}
|
||||
if (typeof cred.expires !== "number") return true;
|
||||
@ -69,5 +73,34 @@ export function syncExternalCliCredentials(store: AuthProfileStore): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
// Sync from Gemini CLI
|
||||
const geminiCreds = readGeminiCliCredentialsCached({ ttlMs: EXTERNAL_CLI_SYNC_TTL_MS });
|
||||
if (geminiCreds) {
|
||||
// We sync to both the generic "google-gemini-cli" profile and the email-specific one
|
||||
const profileIds = [GEMINI_CLI_PROFILE_ID];
|
||||
if (geminiCreds.email) {
|
||||
profileIds.push(`google-gemini-cli:${geminiCreds.email}`);
|
||||
}
|
||||
|
||||
for (const profileId of profileIds) {
|
||||
const existing = store.profiles[profileId];
|
||||
const existingOAuth = existing?.type === "oauth" ? existing : undefined;
|
||||
const shouldUpdate =
|
||||
!existingOAuth ||
|
||||
existingOAuth.provider !== "google-gemini-cli" ||
|
||||
existingOAuth.expires <= now ||
|
||||
geminiCreds.expires > existingOAuth.expires;
|
||||
|
||||
if (shouldUpdate && !shallowEqualOAuthCredentials(existingOAuth, geminiCreds)) {
|
||||
store.profiles[profileId] = geminiCreds;
|
||||
mutated = true;
|
||||
log.info("synced gemini credentials from gemini cli", {
|
||||
profileId,
|
||||
expires: new Date(geminiCreds.expires).toISOString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mutated;
|
||||
}
|
||||
|
||||
@ -8,6 +8,7 @@ import { AUTH_STORE_LOCK_OPTIONS, log } from "./constants.js";
|
||||
import { formatAuthDoctorHint } from "./doctor.js";
|
||||
import { ensureAuthStoreFile, resolveAuthStorePath } from "./paths.js";
|
||||
import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js";
|
||||
import { syncExternalCliCredentials } from "./external-cli-sync.js";
|
||||
import { ensureAuthProfileStore, saveAuthProfileStore } from "./store.js";
|
||||
import type { AuthProfileStore } from "./types.js";
|
||||
|
||||
@ -35,6 +36,15 @@ async function refreshOAuthTokenWithLock(params: {
|
||||
});
|
||||
|
||||
const store = ensureAuthProfileStore(params.agentDir);
|
||||
|
||||
// Try syncing from external CLI first if this is a Gemini CLI profile
|
||||
if (params.profileId.startsWith("google-gemini-cli")) {
|
||||
const mutated = syncExternalCliCredentials(store);
|
||||
if (mutated) {
|
||||
saveAuthProfileStore(store, params.agentDir);
|
||||
}
|
||||
}
|
||||
|
||||
const cred = store.profiles[params.profileId];
|
||||
if (!cred || cred.type !== "oauth") return null;
|
||||
|
||||
|
||||
@ -14,6 +14,7 @@ const log = createSubsystemLogger("agents/auth-profiles");
|
||||
const CLAUDE_CLI_CREDENTIALS_RELATIVE_PATH = ".claude/.credentials.json";
|
||||
const CODEX_CLI_AUTH_FILENAME = "auth.json";
|
||||
const QWEN_CLI_CREDENTIALS_RELATIVE_PATH = ".qwen/oauth_creds.json";
|
||||
const GEMINI_CLI_CREDENTIALS_RELATIVE_PATH = ".gemini-cli/credentials.json";
|
||||
|
||||
const CLAUDE_CLI_KEYCHAIN_SERVICE = "Claude Code-credentials";
|
||||
const CLAUDE_CLI_KEYCHAIN_ACCOUNT = "Claude Code";
|
||||
@ -27,11 +28,13 @@ type CachedValue<T> = {
|
||||
let claudeCliCache: CachedValue<ClaudeCliCredential> | null = null;
|
||||
let codexCliCache: CachedValue<CodexCliCredential> | null = null;
|
||||
let qwenCliCache: CachedValue<QwenCliCredential> | null = null;
|
||||
let geminiCliCache: CachedValue<GeminiCliCredential> | null = null;
|
||||
|
||||
export function resetCliCredentialCachesForTest(): void {
|
||||
claudeCliCache = null;
|
||||
codexCliCache = null;
|
||||
qwenCliCache = null;
|
||||
geminiCliCache = null;
|
||||
}
|
||||
|
||||
export type ClaudeCliCredential =
|
||||
@ -66,6 +69,16 @@ export type QwenCliCredential = {
|
||||
expires: number;
|
||||
};
|
||||
|
||||
export type GeminiCliCredential = {
|
||||
type: "oauth";
|
||||
provider: "google-gemini-cli";
|
||||
access: string;
|
||||
refresh: string;
|
||||
expires: number;
|
||||
email?: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
type ClaudeCliFileOptions = {
|
||||
homeDir?: string;
|
||||
};
|
||||
@ -102,6 +115,11 @@ function resolveQwenCliCredentialsPath(homeDir?: string) {
|
||||
return path.join(baseDir, QWEN_CLI_CREDENTIALS_RELATIVE_PATH);
|
||||
}
|
||||
|
||||
function resolveGeminiCliCredentialsPath(homeDir?: string) {
|
||||
const baseDir = homeDir ?? resolveUserPath("~");
|
||||
return path.join(baseDir, GEMINI_CLI_CREDENTIALS_RELATIVE_PATH);
|
||||
}
|
||||
|
||||
function computeCodexKeychainAccount(codexHome: string) {
|
||||
const hash = createHash("sha256").update(codexHome).digest("hex");
|
||||
return `cli|${hash.slice(0, 16)}`;
|
||||
@ -186,6 +204,33 @@ function readQwenCliCredentials(options?: { homeDir?: string }): QwenCliCredenti
|
||||
};
|
||||
}
|
||||
|
||||
function readGeminiCliCredentials(options?: { homeDir?: string }): GeminiCliCredential | null {
|
||||
const credPath = resolveGeminiCliCredentialsPath(options?.homeDir);
|
||||
const raw = loadJsonFile(credPath);
|
||||
if (!raw || typeof raw !== "object") return null;
|
||||
const data = raw as Record<string, unknown>;
|
||||
const accessToken = data.access_token;
|
||||
const refreshToken = data.refresh_token;
|
||||
const expiresAt = data.expiry_date;
|
||||
const projectId = data.project_id;
|
||||
const email = data.email;
|
||||
|
||||
if (typeof accessToken !== "string" || !accessToken) return null;
|
||||
if (typeof refreshToken !== "string" || !refreshToken) return null;
|
||||
if (typeof expiresAt !== "number" || !Number.isFinite(expiresAt)) return null;
|
||||
if (typeof projectId !== "string" || !projectId) return null;
|
||||
|
||||
return {
|
||||
type: "oauth",
|
||||
provider: "google-gemini-cli",
|
||||
access: accessToken,
|
||||
refresh: refreshToken,
|
||||
expires: expiresAt,
|
||||
projectId,
|
||||
email: typeof email === "string" ? email : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
function readClaudeCliKeychainCredentials(
|
||||
execSyncImpl: ExecSyncFn = execSync,
|
||||
): ClaudeCliCredential | null {
|
||||
@ -497,3 +542,25 @@ export function readQwenCliCredentialsCached(options?: {
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function readGeminiCliCredentialsCached(options?: {
|
||||
ttlMs?: number;
|
||||
homeDir?: string;
|
||||
}): GeminiCliCredential | null {
|
||||
const ttlMs = options?.ttlMs ?? 0;
|
||||
const now = Date.now();
|
||||
const cacheKey = resolveGeminiCliCredentialsPath(options?.homeDir);
|
||||
if (
|
||||
ttlMs > 0 &&
|
||||
geminiCliCache &&
|
||||
geminiCliCache.cacheKey === cacheKey &&
|
||||
now - geminiCliCache.readAt < ttlMs
|
||||
) {
|
||||
return geminiCliCache.value;
|
||||
}
|
||||
const value = readGeminiCliCredentials({ homeDir: options?.homeDir });
|
||||
if (ttlMs > 0) {
|
||||
geminiCliCache = { value, readAt: now, cacheKey };
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user