diff --git a/packages/core/src/code_assist/server.test.ts b/packages/core/src/code_assist/server.test.ts index 67c2cab67d..9ea5ae6ce4 100644 --- a/packages/core/src/code_assist/server.test.ts +++ b/packages/core/src/code_assist/server.test.ts @@ -347,7 +347,7 @@ describe('CodeAssistServer', () => { }); it('should construct the default URL correctly', () => { - const server = new CodeAssistServer({} as never); + const server = new CodeAssistServer({} as never, 'test-project'); const url = server.getMethodUrl('testMethod'); expect(url).toBe( 'https://cloudcode-pa.googleapis.com/v1internal:testMethod', @@ -356,26 +356,79 @@ describe('CodeAssistServer', () => { it('should use the CODE_ASSIST_ENDPOINT environment variable if set', () => { process.env['CODE_ASSIST_ENDPOINT'] = 'https://custom-endpoint.com'; - const server = new CodeAssistServer({} as never); + const server = new CodeAssistServer({} as never, 'test-project'); const url = server.getMethodUrl('testMethod'); expect(url).toBe('https://custom-endpoint.com/v1internal:testMethod'); }); it('should use the CODE_ASSIST_API_VERSION environment variable if set', () => { process.env['CODE_ASSIST_API_VERSION'] = 'v2beta'; - const server = new CodeAssistServer({} as never); + const server = new CodeAssistServer({} as never, 'test-project'); const url = server.getMethodUrl('testMethod'); expect(url).toBe('https://cloudcode-pa.googleapis.com/v2beta:testMethod'); }); it('should use default value if CODE_ASSIST_API_VERSION env var is empty', () => { process.env['CODE_ASSIST_API_VERSION'] = ''; - const server = new CodeAssistServer({} as never); + const server = new CodeAssistServer({} as never, 'test-project'); const url = server.getMethodUrl('testMethod'); expect(url).toBe( 'https://cloudcode-pa.googleapis.com/v1internal:testMethod', ); }); + + it('should use standard API endpoint when no projectId is provided', () => { + const server = new CodeAssistServer({} as never); + const url = server.getMethodUrl('testMethod'); + expect(url).toBe( + 'https://generativelanguage.googleapis.com/v1beta:testMethod', + ); + }); + }); + + describe('loadCodeAssist project hijacking prevention', () => { + it('should strip cloudaicompanionProject from response when no projectId is set', async () => { + const mockRequest = vi.fn(); + const client = { request: mockRequest } as unknown as OAuth2Client; + const server = new CodeAssistServer(client, undefined, {}, undefined); + + mockRequest.mockResolvedValue({ + data: { + cloudaicompanionProject: 'ghost-project-id', + currentTier: { id: 'standard-tier' }, + }, + }); + + const result = await server.loadCodeAssist({ + metadata: { ideType: 'IDE_UNSPECIFIED' }, + }); + + expect(result.cloudaicompanionProject).toBeUndefined(); + }); + + it('should preserve cloudaicompanionProject when projectId is explicitly set', async () => { + const mockRequest = vi.fn(); + const client = { request: mockRequest } as unknown as OAuth2Client; + const server = new CodeAssistServer( + client, + 'explicit-project', + {}, + undefined, + ); + + mockRequest.mockResolvedValue({ + data: { + cloudaicompanionProject: 'server-project-id', + currentTier: { id: 'standard-tier' }, + }, + }); + + const result = await server.loadCodeAssist({ + metadata: { ideType: 'IDE_UNSPECIFIED' }, + }); + + expect(result.cloudaicompanionProject).toBe('server-project-id'); + }); }); it('should call the generateContentStream endpoint and parse SSE', async () => { diff --git a/packages/core/src/code_assist/server.ts b/packages/core/src/code_assist/server.ts index 4ed8328f3d..7ae5498e96 100644 --- a/packages/core/src/code_assist/server.ts +++ b/packages/core/src/code_assist/server.ts @@ -264,10 +264,29 @@ export class CodeAssistServer implements ContentGenerator { req: LoadCodeAssistRequest, ): Promise { try { - return await this.requestPost( + const res = await this.requestPost( 'loadCodeAssist', req, ); + + /** + * FIX: Prevent automatic project hijacking for personal users. + * For Google One AI Premium subscribers, the backend may return a + * "shadow" project ID (e.g., master-impulse-jrkws) in + * cloudaicompanionProject. If adopted, this forces the CLI to route + * through the Enterprise endpoint (cloudcode-pa.googleapis.com), + * which rejects personal OAuth tokens with 403 PERMISSION_DENIED. + * + * By stripping this field when no explicit projectId was provided, + * we preserve the Personal/Standard flow routing. + * See: https://github.com/google-gemini/gemini-cli/issues/25189 + * See: https://github.com/google-gemini/gemini-cli/issues/24517 + */ + if (res.cloudaicompanionProject && !this.projectId) { + delete res.cloudaicompanionProject; + } + + return res; } catch (e) { if (isVpcScAffectedUser(e)) { return { @@ -508,6 +527,18 @@ export class CodeAssistServer implements ContentGenerator { } private getBaseUrl(): string { + /** + * FIX: Endpoint fallback logic. + * If no explicit projectId is provided via CLI flags or config, default + * to the standard Generative Language API. This prevents accidental + * routing to the Enterprise Cloud Code PA endpoint for personal users. + * See: https://github.com/google-gemini/gemini-cli/issues/25189 + * See: https://github.com/google-gemini/gemini-cli/issues/24517 + */ + if (!this.projectId) { + return 'https://generativelanguage.googleapis.com/v1beta'; + } + const endpoint = process.env['CODE_ASSIST_ENDPOINT'] ?? CODE_ASSIST_ENDPOINT; const version =