Compare commits

..

4 Commits

Author SHA1 Message Date
cynthialong0-0 96e5fc1663 Merge branch 'main' into fix/setup-github-command 2026-02-25 07:26:44 -08:00
Cynthia Long 3815de798f sort the workflow paths as suggested by bot 2026-02-25 15:11:07 +00:00
Shreya Keshive 50947c57ce fix(acp): update auth handshake to spec (#19725) 2026-02-25 15:04:42 +00:00
Cynthia Long ac3c643035 Fix: update SetupGithubCommand for plan-execute 2026-02-25 15:00:30 +00:00
14 changed files with 95 additions and 38 deletions
-2
View File
@@ -2150,7 +2150,6 @@
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz",
"integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@hono/node-server": "^1.19.9",
"ajv": "^8.17.1",
@@ -17143,7 +17142,6 @@
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
"@google/gemini-cli-core": "file:../core",
"@google/gemini-cli-devtools": "file:../devtools",
"@google/genai": "1.41.0",
"@iarna/toml": "^2.2.5",
"@modelcontextprotocol/sdk": "^1.23.0",
-1
View File
@@ -31,7 +31,6 @@
"dependencies": {
"@agentclientprotocol/sdk": "^0.12.0",
"@google/gemini-cli-core": "file:../core",
"@google/gemini-cli-devtools": "file:../devtools",
"@google/genai": "1.41.0",
"@iarna/toml": "^2.2.5",
"@modelcontextprotocol/sdk": "^1.23.0",
@@ -23,15 +23,17 @@ import { getUrlOpenCommand } from '../../ui/utils/commandUtils.js';
import { debugLogger } from '@google/gemini-cli-core';
export const GITHUB_WORKFLOW_PATHS = [
'gemini-dispatch/gemini-dispatch.yml',
'gemini-assistant/gemini-invoke.yml',
'issue-triage/gemini-triage.yml',
'gemini-assistant/gemini-plan-execute.yml',
'gemini-dispatch/gemini-dispatch.yml',
'issue-triage/gemini-scheduled-triage.yml',
'issue-triage/gemini-triage.yml',
'pr-review/gemini-review.yml',
];
export const GITHUB_COMMANDS_PATHS = [
'gemini-assistant/gemini-invoke.toml',
'gemini-assistant/gemini-plan-execute.toml',
'issue-triage/gemini-scheduled-triage.toml',
'issue-triage/gemini-triage.toml',
'pr-review/gemini-review.toml',
@@ -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',
@@ -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<string, unknown> } {
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<string, Session> = 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<void> {
async authenticate(req: acp.AuthenticateRequest): Promise<void> {
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();
+1 -1
View File
@@ -14,5 +14,5 @@
"./package.json"
],
"exclude": ["node_modules", "dist"],
"references": [{ "path": "../core" }, { "path": "../devtools" }]
"references": [{ "path": "../core" }]
}
@@ -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
+6 -3
View File
@@ -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);
+1
View File
@@ -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);
+2 -1
View File
@@ -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,
+5 -1
View File
@@ -90,9 +90,13 @@ export type ContentGeneratorConfig = {
export async function createContentGeneratorConfig(
config: Config,
authType: AuthType | undefined,
apiKey?: string,
): Promise<ContentGeneratorConfig> {
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'] ||
+8 -10
View File
@@ -5,7 +5,7 @@
*/
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { useDevToolsData, type ConsoleLog, type NetworkLog } from './hooks.js';
import { useDevToolsData, type ConsoleLog, type NetworkLog } from './hooks';
type ThemeMode = 'light' | 'dark' | null; // null means follow system
@@ -115,6 +115,7 @@ export default function App() {
if (!networkMap.has(id)) {
networkMap.set(id, {
...payload,
type,
timestamp,
id,
} as NetworkLog);
@@ -124,7 +125,8 @@ export default function App() {
networkMap.set(id, {
...existing,
...payload,
// Ensure we don't overwrite the original timestamp
// Ensure we don't overwrite the original timestamp or type
type: existing.type,
timestamp: existing.timestamp,
} as NetworkLog);
}
@@ -156,7 +158,7 @@ export default function App() {
const entries: Array<{ timestamp: number; data: object }> = [];
// Export console logs
filteredConsoleLogs.forEach((log: ConsoleLog) => {
filteredConsoleLogs.forEach((log) => {
entries.push({
timestamp: log.timestamp,
data: {
@@ -169,7 +171,7 @@ export default function App() {
});
// Export network logs
filteredNetworkLogs.forEach((log: NetworkLog) => {
filteredNetworkLogs.forEach((log) => {
entries.push({
timestamp: log.timestamp,
data: {
@@ -228,9 +230,7 @@ export default function App() {
if (selectedSessionId === importedSessionId && importedLogs) {
return importedLogs.console;
}
return consoleLogs.filter(
(l: ConsoleLog) => l.sessionId === selectedSessionId,
);
return consoleLogs.filter((l) => l.sessionId === selectedSessionId);
}, [consoleLogs, selectedSessionId, importedSessionId, importedLogs]);
const filteredNetworkLogs = useMemo(() => {
@@ -238,9 +238,7 @@ export default function App() {
if (selectedSessionId === importedSessionId && importedLogs) {
return importedLogs.network;
}
return networkLogs.filter(
(l: NetworkLog) => l.sessionId === selectedSessionId,
);
return networkLogs.filter((l) => l.sessionId === selectedSessionId);
}, [networkLogs, selectedSessionId, importedSessionId, importedLogs]);
return (
+1 -1
View File
@@ -6,7 +6,7 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App.js';
import App from './App';
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
+3 -2
View File
@@ -1,9 +1,10 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "dist",
"lib": ["DOM", "DOM.Iterable", "ES2023"],
"jsx": "react-jsx"
"jsx": "react-jsx",
"allowImportingTsExtensions": true,
"noEmit": true
},
"include": ["src", "client/src"]
}