fix(gemini-vertex): persist Vertex creds, sanitize project fallback, harden env test

Round-3 CodeRabbit findings on #1614:

- provider-bootstrap.ts: carry the active credential into the saved profile.
  PROFILE_ENV_KEYS clears GEMINI_ACCESS_TOKEN / GOOGLE_APPLICATION_CREDENTIALS
  on relaunch, so an access-token profile must persist the bearer (and fail
  fast if absent) and an ADC profile must persist the credentials file —
  otherwise the saved profile relaunches unauthed (thread 3460713116).
- providerProfiles.ts: sanitize each Vertex project candidate with
  trimOrUndefined() + ?? at the startup-profile site so a whitespace-only
  GEMINI_VERTEX_PROJECT no longer shadows a real GOOGLE_CLOUD_PROJECT /
  GCLOUD_PROJECT / GOOGLE_PROJECT_ID (thread 3460713146; the compatibility
  builder site was already fixed in 0501694).
- security-hardening.test.ts: assert env classification via runtime
  isProviderManagedEnvVar() / SAFE_ENV_VARS.has() instead of slicing source
  text, so the policy test tracks behavior, not formatting (thread 3460713129).

Validation: tsc --noEmit, type-tests, security-hardening.test.ts (13/13),
knip — all green. providerProfiles getAPIProvider() timeouts are pre-existing
(identical on the unmodified tree).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
gitdevine
2026-06-26 11:37:40 +02:00
co-authored by Claude Opus 4.8
parent 630ec4cfa0
commit ce14db1865
3 changed files with 31 additions and 16 deletions
+15
View File
@@ -117,6 +117,21 @@ async function main(): Promise<void> {
authMode: vertexAuthMode,
processEnv: process.env,
})
// PROFILE_ENV_KEYS clears GEMINI_ACCESS_TOKEN / GOOGLE_APPLICATION_CREDENTIALS
// on relaunch, so the saved profile must carry the active credential forward
// or it relaunches unauthed — mirrors the compatibility builder in
// providerProfile.ts / applyProviderProfileToProcessEnv.
if (vertexAuthMode === 'access-token') {
const accessToken = process.env.GEMINI_ACCESS_TOKEN?.trim()
if (!accessToken) {
console.error('Gemini Vertex access-token profiles require GEMINI_ACCESS_TOKEN.')
process.exit(1)
}
env.GEMINI_ACCESS_TOKEN = accessToken
} else {
const adcFile = process.env.GOOGLE_APPLICATION_CREDENTIALS?.trim()
if (adcFile) env.GOOGLE_APPLICATION_CREDENTIALS = adcFile
}
} else if (selected === 'mistral') {
const builtEnv = buildMistralProfileEnv({
model: argModel || null,
+8 -10
View File
@@ -106,13 +106,11 @@ describe('SAFE_ENV_VARS excludes credentials', () => {
})
test('Gemini Vertex routing env vars are managed; project ids stay unsafe', async () => {
const content = await file('utils/managedEnvConstants.ts').text()
const managedStart = content.indexOf('const PROVIDER_MANAGED_ENV_VARS')
const managedEnd = content.indexOf('])', managedStart)
const managedSection = content.slice(managedStart, managedEnd)
const safeStart = content.indexOf('export const SAFE_ENV_VARS')
const safeEnd = content.indexOf('])', safeStart)
const safeSection = content.slice(safeStart, safeEnd)
// Assert runtime classification behavior, not source formatting, so the
// test catches real regressions in env policy instead of breaking on edits.
const { isProviderManagedEnvVar, SAFE_ENV_VARS } = await import(
'../utils/managedEnvConstants.ts'
)
for (const envVar of [
'CLAUDE_CODE_USE_GEMINI_VERTEX',
@@ -124,7 +122,7 @@ describe('SAFE_ENV_VARS excludes credentials', () => {
'GCLOUD_PROJECT',
'GOOGLE_PROJECT_ID',
]) {
expect(managedSection).toContain(envVar)
expect(isProviderManagedEnvVar(envVar)).toBe(true)
}
for (const envVar of [
@@ -133,7 +131,7 @@ describe('SAFE_ENV_VARS excludes credentials', () => {
'GEMINI_VERTEX_MODEL',
'GEMINI_VERTEX_LOCATION',
]) {
expect(safeSection).toContain(envVar)
expect(SAFE_ENV_VARS.has(envVar)).toBe(true)
}
// Project ids from untrusted settings would switch Vertex traffic to an
@@ -145,7 +143,7 @@ describe('SAFE_ENV_VARS excludes credentials', () => {
'GCLOUD_PROJECT',
'GOOGLE_PROJECT_ID',
]) {
expect(safeSection).not.toContain(envVar)
expect(SAFE_ENV_VARS.has(envVar)).toBe(false)
}
})
})
+8 -6
View File
@@ -1392,13 +1392,15 @@ function buildStartupProfileFromActiveProfile(
const env = buildGeminiVertexProfileEnv({
model: getPrimaryModel(activeProfile.model),
// baseUrl carries the project id, but the preset can seed it with the
// endpoint URL — never let a URL through as a project.
// endpoint URL — never let a URL through as a project. Sanitize each
// candidate first so a whitespace-only GEMINI_VERTEX_PROJECT doesn't win
// over a real GOOGLE_CLOUD_PROJECT/GCLOUD_PROJECT/GOOGLE_PROJECT_ID.
project:
geminiVertexProjectFromProfile(activeProfile.baseUrl) ||
process.env.GEMINI_VERTEX_PROJECT ||
process.env.GOOGLE_CLOUD_PROJECT ||
process.env.GCLOUD_PROJECT ||
process.env.GOOGLE_PROJECT_ID,
geminiVertexProjectFromProfile(activeProfile.baseUrl) ??
trimOrUndefined(process.env.GEMINI_VERTEX_PROJECT) ??
trimOrUndefined(process.env.GOOGLE_CLOUD_PROJECT) ??
trimOrUndefined(process.env.GCLOUD_PROJECT) ??
trimOrUndefined(process.env.GOOGLE_PROJECT_ID),
authMode: vertexAuthMode,
processEnv: process.env,
})