mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-24 18:27:01 -07:00
Add BYOID experiment flag and skeleton for BYOID auth flow.
This commit is contained in:
@@ -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,
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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.';
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 } =
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user