diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index 040fed0e9a6..bcee8cfef4b 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -130,18 +130,6 @@ export async function createContentGeneratorConfig( customHeaders?: Record, vertexAiRouting?: VertexAiRoutingConfig, ): Promise { - const geminiApiKey = - apiKey || - process.env['GEMINI_API_KEY'] || - (await loadApiKey()) || - undefined; - const googleApiKey = process.env['GOOGLE_API_KEY'] || undefined; - const googleCloudProject = - process.env['GOOGLE_CLOUD_PROJECT'] || - process.env['GOOGLE_CLOUD_PROJECT_ID'] || - undefined; - const googleCloudLocation = process.env['GOOGLE_CLOUD_LOCATION'] || undefined; - const contentGeneratorConfig: ContentGeneratorConfig = { authType, proxy: config?.getProxy(), @@ -150,7 +138,9 @@ export async function createContentGeneratorConfig( vertexAiRouting, }; - // If we are using Google auth or we are in Cloud Shell, there is nothing else to validate for now + // If we are using Google auth or we are in Cloud Shell, there is nothing else to validate for now. + // Return before touching the API-key keychain: on Linux without a Secret Service + // (WSL/SSH/Docker/CI) keytar can block indefinitely on its functional probe. if ( authType === AuthType.LOGIN_WITH_GOOGLE || authType === AuthType.COMPUTE_ADC @@ -158,6 +148,18 @@ export async function createContentGeneratorConfig( return contentGeneratorConfig; } + const geminiApiKey = + apiKey || + process.env['GEMINI_API_KEY'] || + (await loadApiKey()) || + undefined; + const googleApiKey = process.env['GOOGLE_API_KEY'] || undefined; + const googleCloudProject = + process.env['GOOGLE_CLOUD_PROJECT'] || + process.env['GOOGLE_CLOUD_PROJECT_ID'] || + undefined; + const googleCloudLocation = process.env['GOOGLE_CLOUD_LOCATION'] || undefined; + if (authType === AuthType.USE_GEMINI && geminiApiKey) { contentGeneratorConfig.apiKey = geminiApiKey; contentGeneratorConfig.vertexai = false; diff --git a/packages/core/src/services/keychainService.ts b/packages/core/src/services/keychainService.ts index 0d12e68ffba..2227166aec7 100644 --- a/packages/core/src/services/keychainService.ts +++ b/packages/core/src/services/keychainService.ts @@ -140,7 +140,7 @@ export class KeychainService { return keychainModule; } - debugLogger.debug('Keychain functional verification failed'); + debugLogger.debug('Keychain functional verification failed or timed out'); return null; } catch (error) { // Avoid logging full error objects to prevent PII exposure. @@ -173,18 +173,32 @@ export class KeychainService { } // Performs a set-get-delete cycle to verify keychain functionality. + // Capped with a 2s timeout so a non-responsive Secret Service (common on + // headless Linux: WSL/SSH/Docker without gnome-keyring or D-Bus) falls back + // to FileKeychain instead of hanging the CLI indefinitely. private async isKeychainFunctional(keychain: Keychain): Promise { const testAccount = `${KEYCHAIN_TEST_PREFIX}${crypto.randomBytes(8).toString('hex')}`; const testPassword = 'test'; - await keychain.setPassword(this.serviceName, testAccount, testPassword); - const retrieved = await keychain.getPassword(this.serviceName, testAccount); - const deleted = await keychain.deletePassword( - this.serviceName, - testAccount, - ); - - return deleted && retrieved === testPassword; + const probe = async (): Promise => { + await keychain.setPassword(this.serviceName, testAccount, testPassword); + const retrieved = await keychain.getPassword( + this.serviceName, + testAccount, + ); + const deleted = await keychain.deletePassword( + this.serviceName, + testAccount, + ); + return deleted && retrieved === testPassword; + }; + + return Promise.race([ + probe(), + new Promise((resolve) => + setTimeout(() => resolve(false), 2000).unref(), + ), + ]); } /**