diff --git a/packages/cli/src/zed-integration/zedIntegration.test.ts b/packages/cli/src/zed-integration/zedIntegration.test.ts index f1cb22bfda..37da3035c3 100644 --- a/packages/cli/src/zed-integration/zedIntegration.test.ts +++ b/packages/cli/src/zed-integration/zedIntegration.test.ts @@ -177,6 +177,14 @@ describe('GeminiAgent', () => { expect(response.protocolVersion).toBe(acp.PROTOCOL_VERSION); expect(response.authMethods).toHaveLength(3); + const geminiAuth = response.authMethods?.find( + (m) => m.id === AuthType.USE_GEMINI, + ); + expect(geminiAuth?._meta).toEqual({ + 'api-key': { + provider: 'google', + }, + }); expect(response.agentCapabilities?.loadSession).toBe(true); }); @@ -187,6 +195,7 @@ describe('GeminiAgent', () => { expect(mockConfig.refreshAuth).toHaveBeenCalledWith( AuthType.LOGIN_WITH_GOOGLE, + undefined, ); expect(mockSettings.setValue).toHaveBeenCalledWith( SettingScope.User, @@ -195,6 +204,25 @@ describe('GeminiAgent', () => { ); }); + it('should authenticate correctly with api-key in _meta', async () => { + await agent.authenticate({ + methodId: AuthType.USE_GEMINI, + _meta: { + 'api-key': 'test-api-key', + }, + } as unknown as acp.AuthenticateRequest); + + expect(mockConfig.refreshAuth).toHaveBeenCalledWith( + AuthType.USE_GEMINI, + 'test-api-key', + ); + expect(mockSettings.setValue).toHaveBeenCalledWith( + SettingScope.User, + 'security.auth.selectedType', + AuthType.USE_GEMINI, + ); + }); + it('should create a new session', async () => { mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({ apiKey: 'test-key', diff --git a/packages/cli/src/zed-integration/zedIntegration.ts b/packages/cli/src/zed-integration/zedIntegration.ts index f6c0a63349..d4f1b27b92 100644 --- a/packages/cli/src/zed-integration/zedIntegration.ts +++ b/packages/cli/src/zed-integration/zedIntegration.ts @@ -37,12 +37,17 @@ import { partListUnionToString, LlmRole, ApprovalMode, + getVersion, convertSessionToClientHistory, } from '@google/gemini-cli-core'; import * as acp from '@agentclientprotocol/sdk'; import { AcpFileSystemService } from './fileSystemService.js'; import { getAcpErrorMessage } from './acpErrors.js'; import { Readable, Writable } from 'node:stream'; + +function hasMeta(obj: unknown): obj is { _meta?: Record } { + return typeof obj === 'object' && obj !== null && '_meta' in obj; +} import type { Content, Part, FunctionCall } from '@google/genai'; import type { LoadedSettings } from '../config/settings.js'; import { SettingScope, loadSettings } from '../config/settings.js'; @@ -81,6 +86,7 @@ export async function runZedIntegration( export class GeminiAgent { private sessions: Map = new Map(); private clientCapabilities: acp.ClientCapabilities | undefined; + private apiKey: string | undefined; constructor( private config: Config, @@ -97,25 +103,35 @@ export class GeminiAgent { { id: AuthType.LOGIN_WITH_GOOGLE, name: 'Log in with Google', - description: null, + description: 'Log in with your Google account', }, { id: AuthType.USE_GEMINI, - name: 'Use Gemini API key', - description: - 'Requires setting the `GEMINI_API_KEY` environment variable', + name: 'Gemini API key', + description: 'Use an API key with Gemini Developer API', + _meta: { + 'api-key': { + provider: 'google', + }, + }, }, { id: AuthType.USE_VERTEX_AI, name: 'Vertex AI', - description: null, + description: 'Use an API key with Vertex AI GenAI API', }, ]; await this.config.initialize(); + const version = await getVersion(); return { protocolVersion: acp.PROTOCOL_VERSION, authMethods, + agentInfo: { + name: 'gemini-cli', + title: 'Gemini CLI', + version, + }, agentCapabilities: { loadSession: true, promptCapabilities: { @@ -131,7 +147,8 @@ export class GeminiAgent { }; } - async authenticate({ methodId }: acp.AuthenticateRequest): Promise { + async authenticate(req: acp.AuthenticateRequest): Promise { + const { methodId } = req; const method = z.nativeEnum(AuthType).parse(methodId); const selectedAuthType = this.settings.merged.security.auth.selectedType; @@ -139,17 +156,21 @@ export class GeminiAgent { if (selectedAuthType && selectedAuthType !== method) { await clearCachedCredentialFile(); } + // Check for api-key in _meta + const meta = hasMeta(req) ? req._meta : undefined; + const apiKey = + typeof meta?.['api-key'] === 'string' ? meta['api-key'] : undefined; // Refresh auth with the requested method // This will reuse existing credentials if they're valid, // or perform new authentication if needed try { - await this.config.refreshAuth(method); + if (apiKey) { + this.apiKey = apiKey; + } + await this.config.refreshAuth(method, apiKey ?? this.apiKey); } catch (e) { - throw new acp.RequestError( - getErrorStatus(e) || 401, - getAcpErrorMessage(e), - ); + throw new acp.RequestError(-32000, getAcpErrorMessage(e)); } this.settings.setValue( SettingScope.User, @@ -177,7 +198,7 @@ export class GeminiAgent { let isAuthenticated = false; let authErrorMessage = ''; try { - await config.refreshAuth(authType); + await config.refreshAuth(authType, this.apiKey); isAuthenticated = true; // Extra validation for Gemini API key @@ -199,7 +220,7 @@ export class GeminiAgent { if (!isAuthenticated) { throw new acp.RequestError( - 401, + -32000, authErrorMessage || 'Authentication required.', ); } @@ -302,7 +323,7 @@ export class GeminiAgent { // This satisfies the security requirement to verify the user before executing // potentially unsafe server definitions. try { - await config.refreshAuth(selectedAuthType); + await config.refreshAuth(selectedAuthType, this.apiKey); } catch (e) { debugLogger.error(`Authentication failed: ${e}`); throw acp.RequestError.authRequired(); diff --git a/packages/core/src/code_assist/oauth2.test.ts b/packages/core/src/code_assist/oauth2.test.ts index 5726f76451..c1fe162e63 100644 --- a/packages/core/src/code_assist/oauth2.test.ts +++ b/packages/core/src/code_assist/oauth2.test.ts @@ -95,6 +95,7 @@ const mockConfig = { getNoBrowser: () => false, getProxy: () => 'http://test.proxy.com:8080', isBrowserLaunchSuppressed: () => false, + getExperimentalZedIntegration: () => false, } as unknown as Config; // Mock fetch globally diff --git a/packages/core/src/code_assist/oauth2.ts b/packages/core/src/code_assist/oauth2.ts index 14e65f5906..31bc3c0e5e 100644 --- a/packages/core/src/code_assist/oauth2.ts +++ b/packages/core/src/code_assist/oauth2.ts @@ -271,9 +271,12 @@ async function initOauthClient( await triggerPostAuthCallbacks(client.credentials); } else { - const userConsent = await getConsentForOauth(''); - if (!userConsent) { - throw new FatalCancellationError('Authentication cancelled by user.'); + // In Zed integration, we skip the interactive consent and directly open the browser + if (!config.getExperimentalZedIntegration()) { + const userConsent = await getConsentForOauth(''); + if (!userConsent) { + throw new FatalCancellationError('Authentication cancelled by user.'); + } } const webLogin = await authWithWeb(client); diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index a9e9a78415..e92f464fa2 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -499,6 +499,7 @@ describe('Server Config (config.ts)', () => { expect(createContentGeneratorConfig).toHaveBeenCalledWith( config, authType, + undefined, ); // Verify that contentGeneratorConfig is updated expect(config.getContentGeneratorConfig()).toEqual(mockContentConfig); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index dceb65c9a8..7297693b8e 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -1126,7 +1126,7 @@ export class Config { return this.contentGenerator; } - async refreshAuth(authMethod: AuthType) { + async refreshAuth(authMethod: AuthType, apiKey?: string) { // Reset availability service when switching auth this.modelAvailabilityService.reset(); @@ -1152,6 +1152,7 @@ export class Config { const newContentGeneratorConfig = await createContentGeneratorConfig( this, authMethod, + apiKey, ); this.contentGenerator = await createContentGenerator( newContentGeneratorConfig, diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts index 7adae874aa..98d8d50020 100644 --- a/packages/core/src/core/contentGenerator.ts +++ b/packages/core/src/core/contentGenerator.ts @@ -90,9 +90,13 @@ export type ContentGeneratorConfig = { export async function createContentGeneratorConfig( config: Config, authType: AuthType | undefined, + apiKey?: string, ): Promise { const geminiApiKey = - process.env['GEMINI_API_KEY'] || (await loadApiKey()) || undefined; + apiKey || + process.env['GEMINI_API_KEY'] || + (await loadApiKey()) || + undefined; const googleApiKey = process.env['GOOGLE_API_KEY'] || undefined; const googleCloudProject = process.env['GOOGLE_CLOUD_PROJECT'] ||