fix(acp/auth): support optional API keys natively for enterprise gateways and unify gateway inference

Summary:
Configures the `GATEWAY` authentication mode to natively supply an empty string for `apiKey` instead of injecting a dummy placeholder, avoiding client instantiation errors while directly suppressing conflicting `x-goog-api-key` network headers. Additionally, unifies environmental inference by updating `getAuthTypeFromEnv` to route any `GOOGLE_GEMINI_BASE_URL` usage to `GATEWAY` auth, guaranteeing absolute consistency across background CLI execution, subagents, and ACP sidecars.

Details:

Why this happened:
Initializing a session with the `GATEWAY` auth method without specifying an API key previously caused the underlying `@google/genai` SDK to throw a constructor instantiation error. To circumvent this, a placeholder key (`'gateway-placeholder-key'`) was automatically injected. However, the SDK's internal `WebAuth` module unconditionally appended this placeholder as an `x-goog-api-key` header to outgoing network requests. When enterprise AI API Gateways received both an OIDC authentication header (e.g., `Authorization: Bearer <token>`) and the invalid placeholder key, they prioritized validating the API key and rejected the requests. Furthermore, background CLI execution or embedded SDK scenarios could crash or fail to authenticate consistently when custom proxy URLs were active without explicit auth settings.

Solution Implemented:
1. Pristine SDK Instantiation: Configured `createContentGeneratorConfig` to fall back to `process.env['GEMINI_API_KEY']` to preserve any injected placeholder values from existing partner integrations, while supplying an empty string (`apiKey ?? ''`) when no key is provided. The `GoogleGenAI` constructor options explicitly preserve this empty string mapping under `GATEWAY` auth to satisfy internal null checks safely.
2. Network Header Suppression: Updated `createContentGenerator` to pre-clear the `x-goog-api-key` base header (`headers['x-goog-api-key'] = ''`) whenever `GATEWAY` auth runs with an empty string key. This triggers an early return inside the SDK's `WebAuth` interceptor, guaranteeing that outgoing requests heading to enterprise Gateways remain pristine.
3. Unified Environmental Mapping: Centralized `AuthType.GATEWAY` inference mapping inside `getAuthTypeFromEnv` when `GOOGLE_GEMINI_BASE_URL` is configured. This cleanly bypasses default `USE_GEMINI` startup validations consistently across all application entry points.
This commit is contained in:
Sri Pasumarthi
2026-05-13 16:20:25 -07:00
parent 7cd228f5af
commit 88650f320d
3 changed files with 114 additions and 11 deletions
+10 -2
View File
@@ -69,7 +69,10 @@ export class AcpSessionManager {
);
const authType =
loadedSettings.merged.security.auth.selectedType || AuthType.USE_GEMINI;
loadedSettings.merged.security.auth.selectedType ||
(authDetails.baseUrl || process.env['GOOGLE_GEMINI_BASE_URL']
? AuthType.GATEWAY
: AuthType.USE_GEMINI);
let isAuthenticated = false;
let authErrorMessage = '';
@@ -231,7 +234,12 @@ export class AcpSessionManager {
mcpServers: acp.McpServer[],
authDetails: AuthDetails,
): Promise<Config> {
const selectedAuthType = this.settings.merged.security.auth.selectedType;
const selectedAuthType =
this.settings.merged.security.auth.selectedType ||
(authDetails.baseUrl || process.env['GOOGLE_GEMINI_BASE_URL']
? AuthType.GATEWAY
: undefined);
if (!selectedAuthType) {
throw acp.RequestError.authRequired();
}
@@ -9,6 +9,7 @@ import {
createContentGenerator,
AuthType,
createContentGeneratorConfig,
getAuthTypeFromEnv,
type ContentGenerator,
} from './contentGenerator.js';
import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js';
@@ -35,6 +36,45 @@ const mockConfig = {
getClientName: vi.fn().mockReturnValue(undefined),
} as unknown as Config;
describe('getAuthTypeFromEnv', () => {
beforeEach(() => {
vi.stubEnv('GEMINI_API_KEY', '');
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('should detect LOGIN_WITH_GOOGLE when GOOGLE_GENAI_USE_GCA is true', () => {
vi.stubEnv('GOOGLE_GENAI_USE_GCA', 'true');
expect(getAuthTypeFromEnv()).toBe(AuthType.LOGIN_WITH_GOOGLE);
});
it('should detect USE_VERTEX_AI when GOOGLE_GENAI_USE_VERTEXAI is true', () => {
vi.stubEnv('GOOGLE_GENAI_USE_VERTEXAI', 'true');
expect(getAuthTypeFromEnv()).toBe(AuthType.USE_VERTEX_AI);
});
it('should detect GATEWAY when GOOGLE_GEMINI_BASE_URL is present', () => {
vi.stubEnv('GOOGLE_GEMINI_BASE_URL', 'https://gateway.example.com');
expect(getAuthTypeFromEnv()).toBe(AuthType.GATEWAY);
});
it('should detect USE_GEMINI when GEMINI_API_KEY is present', () => {
vi.stubEnv('GEMINI_API_KEY', 'fake-key');
expect(getAuthTypeFromEnv()).toBe(AuthType.USE_GEMINI);
});
it('should detect COMPUTE_ADC when CLOUD_SHELL is true', () => {
vi.stubEnv('CLOUD_SHELL', 'true');
expect(getAuthTypeFromEnv()).toBe(AuthType.COMPUTE_ADC);
});
it('should return undefined when no matching env variables are set', () => {
expect(getAuthTypeFromEnv()).toBeUndefined();
});
});
describe('createContentGenerator', () => {
beforeEach(() => {
resetVersionCache();
@@ -851,6 +891,40 @@ describe('createContentGenerator', () => {
),
).rejects.toThrow('Invalid custom base URL: not-a-url');
});
it('should set empty x-goog-api-key header for GATEWAY auth when apiKey is empty string', async () => {
const mockConfig = {
getModel: vi.fn().mockReturnValue('gemini-pro'),
getProxy: vi.fn().mockReturnValue(undefined),
getUsageStatisticsEnabled: () => false,
getClientName: vi.fn().mockReturnValue(undefined),
} as unknown as Config;
const mockGenerator = {
models: {},
} as unknown as GoogleGenAI;
vi.mocked(GoogleGenAI).mockImplementation(() => mockGenerator as never);
await createContentGenerator(
{
apiKey: '',
authType: AuthType.GATEWAY,
baseUrl: 'https://gateway.test.local',
},
mockConfig,
);
expect(GoogleGenAI).toHaveBeenCalledWith(
expect.objectContaining({
apiKey: '',
httpOptions: expect.objectContaining({
headers: expect.objectContaining({
'x-goog-api-key': '',
}),
}),
}),
);
});
});
describe('createContentGeneratorConfig', () => {
@@ -955,24 +1029,33 @@ describe('createContentGeneratorConfig', () => {
expect(config.apiKey).toBeUndefined();
expect(config.vertexai).toBeUndefined();
});
it('should configure for GATEWAY using dummy placeholder if GEMINI_API_KEY is set', async () => {
vi.stubEnv('GEMINI_API_KEY', 'env-gemini-key');
it('should configure for GATEWAY using provided apiKey if available', async () => {
const config = await createContentGeneratorConfig(
mockConfig,
AuthType.GATEWAY,
'custom-gateway-key',
);
expect(config.apiKey).toBe('gateway-placeholder-key');
expect(config.apiKey).toBe('custom-gateway-key');
expect(config.vertexai).toBe(false);
});
it('should configure for GATEWAY using dummy placeholder if GEMINI_API_KEY is not set', async () => {
vi.stubEnv('GEMINI_API_KEY', '');
vi.mocked(loadApiKey).mockResolvedValue(null);
it('should configure for GATEWAY using GEMINI_API_KEY from environment if set', async () => {
vi.stubEnv('GEMINI_API_KEY', 'env-gateway-key');
const config = await createContentGeneratorConfig(
mockConfig,
AuthType.GATEWAY,
);
expect(config.apiKey).toBe('gateway-placeholder-key');
expect(config.apiKey).toBe('env-gateway-key');
expect(config.vertexai).toBe(false);
});
it('should configure for GATEWAY using empty string if no apiKey is provided', async () => {
vi.stubEnv('GEMINI_API_KEY', '');
const config = await createContentGeneratorConfig(
mockConfig,
AuthType.GATEWAY,
);
expect(config.apiKey).toBe('');
expect(config.vertexai).toBe(false);
});
});
+14 -2
View File
@@ -80,6 +80,9 @@ export function getAuthTypeFromEnv(): AuthType | undefined {
if (process.env['GOOGLE_GENAI_USE_VERTEXAI'] === 'true') {
return AuthType.USE_VERTEX_AI;
}
if (process.env['GOOGLE_GEMINI_BASE_URL']) {
return AuthType.GATEWAY;
}
if (process.env['GEMINI_API_KEY']) {
return AuthType.USE_GEMINI;
}
@@ -178,7 +181,8 @@ export async function createContentGeneratorConfig(
}
if (authType === AuthType.GATEWAY) {
contentGeneratorConfig.apiKey = apiKey || 'gateway-placeholder-key';
contentGeneratorConfig.apiKey =
apiKey || process.env['GEMINI_API_KEY'] || '';
contentGeneratorConfig.vertexai = false;
return contentGeneratorConfig;
@@ -313,6 +317,9 @@ export async function createContentGenerator(
'x-gemini-api-privileged-user-id': `${installationId}`,
};
}
if (config.authType === AuthType.GATEWAY && config.apiKey === '') {
headers['x-goog-api-key'] = '';
}
let baseUrl = config.baseUrl;
if (!baseUrl) {
const envBaseUrl =
@@ -337,7 +344,12 @@ export async function createContentGenerator(
}
const googleGenAI = new GoogleGenAI({
apiKey: config.apiKey === '' ? undefined : config.apiKey,
apiKey:
config.authType === AuthType.GATEWAY
? config.apiKey
: config.apiKey === ''
? undefined
: config.apiKey,
vertexai: config.vertexai ?? config.authType === AuthType.USE_VERTEX_AI,
httpOptions,
...(apiVersionEnv && { apiVersion: apiVersionEnv }),