mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-03 14:46:46 -07:00
fix(acp): update auth handshake to spec (#19725)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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'] ||
|
||||||
|
|||||||
Reference in New Issue
Block a user