From ba2bb119014a4ca04130bcc8894da465a7b48fff Mon Sep 17 00:00:00 2001 From: gitdevine <210788251+gitdevine@users.noreply.github.com> Date: Tue, 16 Jun 2026 18:52:45 +0200 Subject: [PATCH] =?UTF-8?q?fix(gemini-vertex):=20address=20CodeRabbit=20fo?= =?UTF-8?q?llow-up=20=E2=80=94=20credentials,=20URL=20guards,=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - providerProfiles.ts / providerProfile.ts: carry the active credential forward when applying a saved Gemini Vertex profile. buildCompatibilityProcessEnv clears every managed key (incl. GEMINI_ACCESS_TOKEN / GOOGLE_APPLICATION_CREDENTIALS), so a profile switch could relaunch unauthed; re-add the access token (access-token mode) or ADC file (adc mode). - providerProfiles.ts (startup-profile path) and provider-bootstrap.ts: extend the endpoint-URL guard to the remaining sites that fed baseUrl into the Vertex project — drop URL-shaped values via geminiVertexProjectFromProfile() and reject http(s) --base-url in the bootstrap script. - Tests: add applyModelFlagFromArgs GEMINI_VERTEX_MODEL coverage; lock access-token / ADC credential preservation on profile apply; isolate every provider-selection flag in the cache-scope test via PROVIDER_SELECTION_FLAGS. Co-Authored-By: Claude Fable 5 --- scripts/provider-bootstrap.ts | 7 ++++- src/services/api/providerConfig.local.test.ts | 24 ++++++++++----- src/utils/providerFlag.test.ts | 6 ++++ src/utils/providerProfile.ts | 8 +++++ src/utils/providerProfiles.test.ts | 28 +++++++++++++++++ src/utils/providerProfiles.ts | 30 +++++++++++++++---- 6 files changed, 90 insertions(+), 13 deletions(-) diff --git a/scripts/provider-bootstrap.ts b/scripts/provider-bootstrap.ts index c70764645..bbc52bfeb 100644 --- a/scripts/provider-bootstrap.ts +++ b/scripts/provider-bootstrap.ts @@ -97,7 +97,12 @@ async function main(): Promise { } else if (selected === 'gemini-vertex') { // Reuse the runtime project resolver (same normalization as the provider // validator) rather than an ad-hoc OR chain. - const project = argBaseUrl?.trim() || getGeminiVertexProjectId(process.env) || null + const projectFromArg = argBaseUrl?.trim() + if (projectFromArg && /^https?:\/\//i.test(projectFromArg)) { + console.error('Gemini Vertex --base-url expects a Google Cloud project id, not an API endpoint URL.') + process.exit(1) + } + const project = projectFromArg || getGeminiVertexProjectId(process.env) || null if (!project) { console.error('Gemini Vertex profile requires a project. Use --base-url or set GEMINI_VERTEX_PROJECT/GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT/GOOGLE_PROJECT_ID.') process.exit(1) diff --git a/src/services/api/providerConfig.local.test.ts b/src/services/api/providerConfig.local.test.ts index a90f5275e..859da47a1 100644 --- a/src/services/api/providerConfig.local.test.ts +++ b/src/services/api/providerConfig.local.test.ts @@ -8,6 +8,7 @@ import { resolveProviderRequest, shouldAttemptLocalToollessRetry, } from './providerConfig.js' +import { PROVIDER_SELECTION_FLAGS } from '../../utils/providerSelectionFlags.js' const originalEnv = { CLAUDE_CODE_USE_OPENAI: process.env.CLAUDE_CODE_USE_OPENAI, @@ -330,17 +331,26 @@ test('does not classify Gemini Vertex sessions as first-party bootstrap traffic' // Maintainer P2: with only the Gemini Vertex flag set, the cache scope // must be null (third-party), not 'firstParty' — otherwise bootstrap // options are fetched through the Anthropic first-party path. - const priorUseOpenAI = process.env.CLAUDE_CODE_USE_OPENAI - // A leaked CLAUDE_CODE_USE_OPENAI from the runner env would take the OpenAI - // branch and make this pass/fail for the wrong reason. - delete process.env.CLAUDE_CODE_USE_OPENAI + // + // Isolate every provider-selection flag (not just CLAUDE_CODE_USE_OPENAI) so a + // leaked flag from the runner env can't take another branch and make this + // pass/fail for the wrong reason — the null result must be attributable to + // Gemini Vertex selection alone. + const priorFlags = new Map() + for (const flag of PROVIDER_SELECTION_FLAGS) { + priorFlags.set(flag, process.env[flag]) + delete process.env[flag] + } process.env.CLAUDE_CODE_USE_GEMINI_VERTEX = '1' try { expect(getAdditionalModelOptionsCacheScope()).toBeNull() } finally { - delete process.env.CLAUDE_CODE_USE_GEMINI_VERTEX - if (priorUseOpenAI !== undefined) { - process.env.CLAUDE_CODE_USE_OPENAI = priorUseOpenAI + for (const [flag, value] of priorFlags) { + if (value === undefined) { + delete process.env[flag] + } else { + process.env[flag] = value + } } } }) diff --git a/src/utils/providerFlag.test.ts b/src/utils/providerFlag.test.ts index 663a559c7..550ab78d8 100644 --- a/src/utils/providerFlag.test.ts +++ b/src/utils/providerFlag.test.ts @@ -855,6 +855,12 @@ describe('applyModelFlagFromArgs', () => { expect(process.env.GEMINI_MODEL).toBe('gemini-2.0-flash') }) + test('sets GEMINI_VERTEX_MODEL when CLAUDE_CODE_USE_GEMINI_VERTEX is active', () => { + process.env.CLAUDE_CODE_USE_GEMINI_VERTEX = '1' + applyModelFlagFromArgs(['--model', 'gemini-2.5-flash']) + expect(process.env.GEMINI_VERTEX_MODEL).toBe('gemini-2.5-flash') + }) + test('sets MISTRAL_MODEL when CLAUDE_CODE_USE_MISTRAL is active', () => { process.env.CLAUDE_CODE_USE_MISTRAL = '1' applyModelFlagFromArgs(['--model', 'devstral-latest']) diff --git a/src/utils/providerProfile.ts b/src/utils/providerProfile.ts index 15f6d96ac..212063648 100644 --- a/src/utils/providerProfile.ts +++ b/src/utils/providerProfile.ts @@ -1654,6 +1654,14 @@ export async function buildLaunchEnv(options: { if (googleApplicationCredentials) { env.GOOGLE_APPLICATION_CREDENTIALS = googleApplicationCredentials } + // PROFILE_ENV_KEYS clears GEMINI_ACCESS_TOKEN on relaunch; an access-token + // Vertex profile must carry its bearer forward or it relaunches unauthed. + if (env.GEMINI_VERTEX_AUTH_MODE === 'access-token') { + const geminiAccessToken = sanitizeApiKey( + processEnv.GEMINI_ACCESS_TOKEN || persistedEnv.GEMINI_ACCESS_TOKEN, + ) + if (geminiAccessToken) env.GEMINI_ACCESS_TOKEN = geminiAccessToken + } return buildCompatibilityProcessEnv({ processEnv, diff --git a/src/utils/providerProfiles.test.ts b/src/utils/providerProfiles.test.ts index 0d0a739e4..bd101aac9 100644 --- a/src/utils/providerProfiles.test.ts +++ b/src/utils/providerProfiles.test.ts @@ -369,6 +369,34 @@ describe('applyProviderProfileToProcessEnv', () => { expect(process.env.GEMINI_VERTEX_PROJECT).toBe('my-gcp-project') }) + test('gemini vertex access-token profile preserves GEMINI_ACCESS_TOKEN', async () => { + const { applyProviderProfileToProcessEnv } = + await importFreshProviderProfileModules() + // buildCompatibilityProcessEnv clears managed keys; the access token must + // survive a profile switch or the first request relaunches unauthed. + process.env.GEMINI_VERTEX_AUTH_MODE = 'access-token' + process.env.GEMINI_ACCESS_TOKEN = 'ya29.test-bearer' + + applyProviderProfileToProcessEnv( + buildGeminiVertexProfile({ baseUrl: 'my-gcp-project' }), + ) + + expect(process.env.GEMINI_ACCESS_TOKEN).toBe('ya29.test-bearer') + }) + + test('gemini vertex adc profile preserves GOOGLE_APPLICATION_CREDENTIALS', async () => { + const { applyProviderProfileToProcessEnv } = + await importFreshProviderProfileModules() + process.env.GEMINI_VERTEX_AUTH_MODE = 'adc' + process.env.GOOGLE_APPLICATION_CREDENTIALS = '/tmp/adc.json' + + applyProviderProfileToProcessEnv( + buildGeminiVertexProfile({ baseUrl: 'my-gcp-project' }), + ) + + expect(process.env.GOOGLE_APPLICATION_CREDENTIALS).toBe('/tmp/adc.json') + }) + test('bedrock profile sets CLAUDE_CODE_USE_BEDROCK and preserves anthropic model routing', async () => { const { applyProviderProfileToProcessEnv } = await importFreshProviderProfileModules() diff --git a/src/utils/providerProfiles.ts b/src/utils/providerProfiles.ts index 15f599124..ae8c47137 100644 --- a/src/utils/providerProfiles.ts +++ b/src/utils/providerProfiles.ts @@ -844,6 +844,11 @@ export function applyProviderProfileToProcessEnv( ...(profile.apiKey ? { GEMINI_API_KEY: profile.apiKey } : {}), } } else if (compatibilityMode === 'gemini-vertex') { + const vertexAuthMode = + process.env.GEMINI_VERTEX_AUTH_MODE === 'access-token' || + process.env.GEMINI_VERTEX_AUTH_MODE === 'adc' + ? process.env.GEMINI_VERTEX_AUTH_MODE + : 'adc' profileEnv = buildGeminiVertexProfileEnv({ model: primaryModel, project: @@ -853,11 +858,19 @@ export function applyProviderProfileToProcessEnv( process.env.GCLOUD_PROJECT || process.env.GOOGLE_PROJECT_ID, location: process.env.GEMINI_VERTEX_LOCATION, - authMode: - process.env.GEMINI_VERTEX_AUTH_MODE === 'access-token' || process.env.GEMINI_VERTEX_AUTH_MODE === 'adc' - ? process.env.GEMINI_VERTEX_AUTH_MODE - : 'adc', + authMode: vertexAuthMode, }) + // buildCompatibilityProcessEnv clears every managed key (incl. + // GEMINI_ACCESS_TOKEN / GOOGLE_APPLICATION_CREDENTIALS) before applying + // profileEnv, so carry the active credential forward or selecting a Vertex + // profile would erase it before the first request. + if (vertexAuthMode === 'access-token') { + const accessToken = trimOrUndefined(process.env.GEMINI_ACCESS_TOKEN) + if (accessToken) profileEnv.GEMINI_ACCESS_TOKEN = accessToken + } else { + const adcFile = trimOrUndefined(process.env.GOOGLE_APPLICATION_CREDENTIALS) + if (adcFile) profileEnv.GOOGLE_APPLICATION_CREDENTIALS = adcFile + } } else if (isGithubCompatibilityMode(compatibilityMode)) { profileEnv = buildGithubCompatibleProfileEnv({ model: primaryModel, @@ -1359,7 +1372,14 @@ function buildStartupProfileFromActiveProfile( case 'gemini-vertex': { const env = buildGeminiVertexProfileEnv({ model: getPrimaryModel(activeProfile.model), - project: activeProfile.baseUrl, + // baseUrl carries the project id, but the preset can seed it with the + // endpoint URL — never let a URL through as a project. + project: + geminiVertexProjectFromProfile(activeProfile.baseUrl) || + process.env.GEMINI_VERTEX_PROJECT || + process.env.GOOGLE_CLOUD_PROJECT || + process.env.GCLOUD_PROJECT || + process.env.GOOGLE_PROJECT_ID, processEnv: process.env, }) return { profile: 'gemini-vertex', env: applySupportedProfileCustomHeaders(activeProfile, env) }