Add BYOID experiment flag and skeleton for BYOID auth flow.

This commit is contained in:
davidapierce
2026-05-29 01:33:01 +00:00
parent c82e2b5976
commit ada5848c69
12 changed files with 216 additions and 17 deletions
+115 -10
View File
@@ -7,6 +7,7 @@
import { AuthType } from '@google/gemini-cli-core';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { validateAuthMethod } from './auth.js';
import fs from 'node:fs';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
@@ -17,11 +18,30 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
};
});
const { mockSettings } = vi.hoisted(() => ({
mockSettings: {
merged: {
security: {
auth: {
byoidConfigPath: undefined as string | undefined,
},
},
experimental: {
byoid: false as boolean,
},
},
},
}));
vi.mock('./settings.js', () => ({
loadEnvironment: vi.fn(),
loadSettings: vi.fn().mockReturnValue({
merged: vi.fn().mockReturnValue({}),
}),
loadSettings: vi.fn(() => mockSettings),
}));
vi.mock('node:fs', () => ({
default: {
existsSync: vi.fn(),
},
}));
describe('validateAuthMethod', () => {
@@ -30,10 +50,13 @@ describe('validateAuthMethod', () => {
vi.stubEnv('GOOGLE_CLOUD_PROJECT', undefined);
vi.stubEnv('GOOGLE_CLOUD_LOCATION', undefined);
vi.stubEnv('GOOGLE_API_KEY', undefined);
mockSettings.merged.security.auth.byoidConfigPath = undefined;
mockSettings.merged.experimental.byoid = false;
});
afterEach(() => {
vi.unstubAllEnvs();
vi.clearAllMocks();
});
it.each([
@@ -92,17 +115,99 @@ describe('validateAuthMethod', () => {
'• GOOGLE_API_KEY environment variable (if using express mode).\n' +
'Update your environment and try again (no reload needed if using .env)!',
},
{
description:
'should return null for BYOID if experimental.byoid is true, and byoidConfigPath is set and exists',
authType: AuthType.BYOID,
envs: {},
byoidEnabled: true,
byoidConfigPath: '/path/to/config',
fsExists: true,
expected: null,
},
{
description:
'should return error for BYOID if experimental.byoid is false',
authType: AuthType.BYOID,
envs: {},
byoidEnabled: false,
expected:
'BYOID authentication is experimental and must be enabled via experimental.byoid in settings.',
},
{
description:
'should return error for BYOID if byoidConfigPath is not set',
authType: AuthType.BYOID,
envs: {},
byoidEnabled: true,
expected:
'When using BYOID, you must specify the security.auth.byoidConfigPath setting.\n' +
'Update your settings and try again!',
},
{
description:
'should return error for BYOID if byoidConfigPath does not exist',
authType: AuthType.BYOID,
envs: {},
byoidEnabled: true,
byoidConfigPath: '/non/existent/path',
fsExists: false,
expected: 'BYOID configuration file not found at: /non/existent/path',
},
{
description:
'should return null for BYOID if experimentalByoid argument is true, even if settings are false',
authType: AuthType.BYOID,
envs: {},
experimentalByoidArg: true,
byoidEnabled: false,
byoidConfigPath: '/path/to/config',
fsExists: true,
expected: null,
},
{
description:
'should return error for BYOID if experimentalByoid argument is false and settings are false',
authType: AuthType.BYOID,
envs: {},
experimentalByoidArg: false,
byoidEnabled: false,
expected:
'BYOID authentication is experimental and must be enabled via experimental.byoid in settings.',
},
{
description: 'should return an error message for an invalid auth method',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
authType: 'invalid-method' as any,
authType: 'invalid' as any,
envs: {},
expected: 'Invalid auth method selected.',
},
])('$description', async ({ authType, envs, expected }) => {
for (const [key, value] of Object.entries(envs)) {
vi.stubEnv(key, value as string);
}
expect(await validateAuthMethod(authType)).toBe(expected);
});
])(
'$description',
async ({
authType,
envs,
expected,
byoidConfigPath,
fsExists,
byoidEnabled,
experimentalByoidArg,
}) => {
for (const [key, value] of Object.entries(envs)) {
vi.stubEnv(key, value as string);
}
if (byoidConfigPath !== undefined) {
mockSettings.merged.security.auth.byoidConfigPath = byoidConfigPath;
}
if (byoidEnabled !== undefined) {
mockSettings.merged.experimental.byoid = byoidEnabled;
}
if (fsExists !== undefined) {
vi.mocked(fs.existsSync).mockReturnValue(fsExists);
}
expect(await validateAuthMethod(authType, experimentalByoidArg)).toBe(
expected,
);
},
);
});
+24 -1
View File
@@ -6,11 +6,14 @@
import { AuthType, loadApiKey } from '@google/gemini-cli-core';
import { loadEnvironment, loadSettings } from './settings.js';
import fs from 'node:fs';
export async function validateAuthMethod(
authMethod: string,
experimentalByoid?: boolean,
): Promise<string | null> {
loadEnvironment(loadSettings().merged, process.cwd());
const settings = loadSettings();
loadEnvironment(settings.merged, process.cwd());
if (
authMethod === AuthType.LOGIN_WITH_GOOGLE ||
authMethod === AuthType.COMPUTE_ADC
@@ -45,5 +48,25 @@ export async function validateAuthMethod(
return null;
}
if (authMethod === AuthType.BYOID) {
const isByoidEnabled =
experimentalByoid || settings.merged.experimental?.byoid;
if (!isByoidEnabled) {
return 'BYOID authentication is experimental and must be enabled via experimental.byoid in settings.';
}
const configPath = settings.merged.security.auth.byoidConfigPath;
if (!configPath) {
return (
'When using BYOID, you must specify the security.auth.byoidConfigPath setting.\n' +
'Update your settings and try again!'
);
}
if (!fs.existsSync(configPath)) {
return `BYOID configuration file not found at: ${configPath}`;
}
return null;
}
return 'Invalid auth method selected.';
}
+7
View File
@@ -91,6 +91,7 @@ export interface CliArgs {
allowedTools: string[] | undefined;
acp?: boolean;
experimentalAcp?: boolean;
experimentalByoid?: boolean;
extensions: string[] | undefined;
listExtensions: boolean | undefined;
resume: string | typeof RESUME_LATEST | undefined;
@@ -368,6 +369,10 @@ export async function parseArguments(
description:
'Starts the agent in ACP mode (deprecated, use --acp instead)',
})
.option('experimental-byoid', {
type: 'boolean',
description: 'Enable experimental support for BYOID authentication',
})
.option('allowed-mcp-server-names', {
type: 'array',
string: true,
@@ -1055,6 +1060,7 @@ export async function loadCliConfig(
disabledSkills: settings.skills?.disabled,
experimentalAutoMemory: settings.experimental?.autoMemory,
experimentalGemma: settings.experimental?.gemma,
experimentalByoid: argv.experimentalByoid || settings.experimental?.byoid,
contextManagement,
modelSteering: settings.experimental?.modelSteering,
topicUpdateNarration:
@@ -1121,6 +1127,7 @@ export async function loadCliConfig(
};
},
enableConseca: settings.security?.enableConseca,
byoidConfigPath: settings.security?.auth?.byoidConfigPath,
});
}
@@ -131,6 +131,7 @@ export class ExtensionManager extends ExtensionLoader {
cwd: options.workspaceDir,
model: '',
debugMode: false,
byoidConfigPath: options.settings.security?.auth?.byoidConfigPath,
});
this.requestConsent = options.requestConsent;
this.requestSetting = options.requestSetting ?? undefined;
+20
View File
@@ -2002,6 +2002,16 @@ const SETTINGS_SCHEMA = {
description: 'Whether to use an external authentication flow.',
showInDialog: false,
},
byoidConfigPath: {
type: 'string',
label: 'BYOID Configuration Path',
category: 'Security',
requiresRestart: true,
default: undefined as string | undefined,
description:
'Path to the BYOID (Bring Your Own IDentifier) configuration file.',
showInDialog: true,
},
},
},
enableConseca: {
@@ -2467,6 +2477,16 @@ const SETTINGS_SCHEMA = {
description: 'Enable logic for context management.',
showInDialog: true,
},
byoid: {
type: 'boolean',
label: 'Enable BYOID Auth',
category: 'Experimental',
requiresRestart: true,
default: false,
description:
'Enable experimental support for BYOID (Bring Your Own IDentifier) authentication.',
showInDialog: true,
},
topicUpdateNarration: {
type: 'boolean',
label: 'Topic & Update Narration',
+1
View File
@@ -516,6 +516,7 @@ export async function main() {
) {
const err = await validateAuthMethod(
settings.merged.security.auth.selectedType,
partialConfig.isExperimentalByoidEnabled(),
);
if (err) {
throw new Error(err);
+5 -1
View File
@@ -912,7 +912,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
const authMethod = settings.merged.security.auth.selectedType;
void (async () => {
try {
const error = await validateAuthMethod(authMethod);
const error = await validateAuthMethod(
authMethod,
config.isExperimentalByoidEnabled(),
);
if (
error &&
authMethod === settings.merged.security.auth.selectedType
@@ -931,6 +934,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
settings.merged.security.auth.enforcedType,
settings.merged.security.auth.useExternal,
onAuthError,
config,
]);
const { isModelDialogOpen, openModelDialog, closeModelDialog } =
+3 -1
View File
@@ -21,6 +21,7 @@ import { validateAuthMethod } from '../../config/auth.js';
export async function validateAuthMethodWithSettings(
authType: AuthType,
settings: LoadedSettings,
config?: Config,
): Promise<string | null> {
const enforcedType = settings.merged.security.auth.enforcedType;
if (enforcedType && enforcedType !== authType) {
@@ -33,7 +34,7 @@ export async function validateAuthMethodWithSettings(
if (authType === AuthType.USE_GEMINI) {
return null;
}
return validateAuthMethod(authType);
return validateAuthMethod(authType, config?.isExperimentalByoidEnabled());
}
import type { AccountSuspensionInfo } from '../contexts/UIStateContext.js';
@@ -114,6 +115,7 @@ export const useAuthCommand = (
const error = await validateAuthMethodWithSettings(
authType,
settings,
config,
).catch((e: unknown) => getErrorMessage(e));
if (error) {
@@ -42,7 +42,10 @@ export async function validateNonInteractiveAuth(
const authType: AuthType = effectiveAuthType;
if (!useExternalAuth) {
const err = await validateAuthMethod(String(authType));
const err = await validateAuthMethod(
String(authType),
nonInteractiveConfig.isExperimentalByoidEnabled(),
);
if (err != null) {
throw new Error(err);
}
+2 -1
View File
@@ -19,7 +19,8 @@ export async function createCodeAssistContentGenerator(
): Promise<ContentGenerator> {
if (
authType === AuthType.LOGIN_WITH_GOOGLE ||
authType === AuthType.COMPUTE_ADC
authType === AuthType.COMPUTE_ADC ||
authType === AuthType.BYOID
) {
const authClient = await getOauthClient(authType, config);
const userData = await setupUser(authClient, config, httpOptions);
+18
View File
@@ -715,6 +715,7 @@ export interface ConfigParameters {
autoDistillation?: boolean;
experimentalAutoMemory?: boolean;
experimentalGemma?: boolean;
experimentalByoid?: boolean;
experimentalContextManagementConfig?: string;
experimentalAgentHistoryTruncation?: boolean;
experimentalAgentHistoryTruncationThreshold?: number;
@@ -744,6 +745,7 @@ export interface ConfigParameters {
};
vertexAiRouting?: VertexAiRoutingConfig;
logRagSnippets?: boolean;
byoidConfigPath?: string;
}
export class Config implements McpContext, AgentLoopContext {
@@ -956,6 +958,7 @@ export class Config implements McpContext, AgentLoopContext {
overageStrategy: OverageStrategy;
};
private readonly vertexAiRouting: VertexAiRoutingConfig | undefined;
private readonly byoidConfigPath: string | undefined;
private readonly enableAgents: boolean;
private agents: AgentSettings;
@@ -965,6 +968,7 @@ export class Config implements McpContext, AgentLoopContext {
private readonly adminSkillsEnabled: boolean;
private readonly experimentalAutoMemory: boolean;
private readonly experimentalGemma: boolean;
private readonly experimentalByoid: boolean;
private readonly experimentalContextManagementConfig?: string;
private readonly memoryBoundaryMarkers: readonly string[];
private readonly topicUpdateNarration: boolean;
@@ -1186,6 +1190,7 @@ export class Config implements McpContext, AgentLoopContext {
this.experimentalAutoMemory = params.experimentalAutoMemory ?? false;
this.experimentalGemma = params.experimentalGemma ?? true;
this.experimentalByoid = params.experimentalByoid ?? false;
this.experimentalContextManagementConfig =
params.experimentalContextManagementConfig;
this.memoryBoundaryMarkers = params.memoryBoundaryMarkers ?? ['.git'];
@@ -1598,6 +1603,7 @@ export class Config implements McpContext, AgentLoopContext {
baseUrl,
customHeaders,
this.vertexAiRouting,
this.byoidConfigPath,
);
this.contentGenerator = await createContentGenerator(
newContentGeneratorConfig,
@@ -2652,6 +2658,10 @@ export class Config implements McpContext, AgentLoopContext {
return this.modelSteering;
}
isExperimentalByoidEnabled(): boolean {
return this.experimentalByoid;
}
async getToolOutputMaskingConfig(): Promise<ToolOutputMaskingConfig> {
await this.ensureExperimentsLoaded();
@@ -4136,6 +4146,14 @@ export class Config implements McpContext, AgentLoopContext {
await this.mcpClientManager.stop();
}
}
isExperimentalByoidEnabled(): boolean {
return this.experimentalByoid;
}
getByoidConfigPath(): string | undefined {
return this.byoidConfigPath;
}
}
// Export model constants for use in CLI
export { DEFAULT_GEMINI_FLASH_MODEL };
+16 -2
View File
@@ -65,6 +65,7 @@ export enum AuthType {
LEGACY_CLOUD_SHELL = 'cloud-shell',
COMPUTE_ADC = 'compute-default-credentials',
GATEWAY = 'gateway',
BYOID = 'byoid',
}
/**
@@ -105,6 +106,7 @@ export type ContentGeneratorConfig = {
baseUrl?: string;
customHeaders?: Record<string, string>;
vertexAiRouting?: VertexAiRoutingConfig;
byoidConfigPath?: string;
};
export type VertexAiRequestType = 'dedicated' | 'shared';
@@ -134,6 +136,7 @@ export async function createContentGeneratorConfig(
baseUrl?: string,
customHeaders?: Record<string, string>,
vertexAiRouting?: VertexAiRoutingConfig,
byoidConfigPath?: string,
): Promise<ContentGeneratorConfig> {
const contentGeneratorConfig: ContentGeneratorConfig = {
authType,
@@ -141,6 +144,7 @@ export async function createContentGeneratorConfig(
baseUrl,
customHeaders,
vertexAiRouting,
byoidConfigPath,
};
// If we are using Google auth or we are in Cloud Shell, there is nothing else to validate for now.
@@ -148,7 +152,8 @@ export async function createContentGeneratorConfig(
// (WSL/SSH/Docker/CI) keytar can block indefinitely on its functional probe.
if (
authType === AuthType.LOGIN_WITH_GOOGLE ||
authType === AuthType.COMPUTE_ADC
authType === AuthType.COMPUTE_ADC ||
authType === AuthType.BYOID
) {
return contentGeneratorConfig;
}
@@ -277,8 +282,17 @@ export async function createContentGenerator(
}
if (
config.authType === AuthType.LOGIN_WITH_GOOGLE ||
config.authType === AuthType.COMPUTE_ADC
config.authType === AuthType.COMPUTE_ADC ||
config.authType === AuthType.BYOID
) {
if (
config.authType === AuthType.BYOID &&
!gcConfig.isExperimentalByoidEnabled()
) {
throw new Error(
'BYOID authentication is experimental and must be enabled via the experimentalByoid flag.',
);
}
const httpOptions = { headers: baseHeaders };
return new LoggingContentGenerator(
await createCodeAssistContentGenerator(