fix(acp): update auth handshake to spec (#19725)

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