Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 31 additions & 12 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export interface CliArgs {
rawOutput: boolean | undefined;
acceptRawOutputRisk: boolean | undefined;
isCommand: boolean | undefined;
fast?: boolean;
saveSession?: boolean;
}

/**
Expand Down Expand Up @@ -443,6 +445,16 @@ export async function parseArguments(
.option('accept-raw-output-risk', {
type: 'boolean',
description: 'Suppress the security warning when using --raw-output.',
})
.option('fast', {
type: 'boolean',
description:
'Enable fast mode: minimize request payload and skip preflight requests (quota, experiments).',
})
.option('save-session', {
type: 'boolean',
default: true,
description: 'Persist the chat session to disk.',
}),
)
.version(await getVersion()) // This will enable the --version flag based on package.json
Expand Down Expand Up @@ -534,7 +546,8 @@ export async function loadCliConfig(
}

const memoryImportFormat = settings.context?.importFormat || 'tree';
const includeDirectoryTree = settings.context?.includeDirectoryTree ?? true;
const includeDirectoryTree =
(settings.context?.includeDirectoryTree ?? true) && !argv.fast;

const ideMode = settings.ide?.enabled ?? false;

Expand Down Expand Up @@ -614,7 +627,7 @@ export async function loadCliConfig(
.getExtensions()
.find((ext) => ext.isActive && ext.plan?.directory)?.plan;

const experimentalJitContext = settings.experimental.jitContext;
const experimentalJitContext = settings.experimental.jitContext && !argv.fast;

let extensionRegistryURI =
process.env['GEMINI_CLI_EXTENSION_REGISTRY_URI'] ??
Expand Down Expand Up @@ -887,7 +900,7 @@ export async function loadCliConfig(
const useGeneralistProfile =
settings.experimental?.generalistProfile ?? false;
const useContextManagement =
settings.experimental?.contextManagement ?? false;
(settings.experimental?.contextManagement ?? false) && !argv.fast;
const contextManagement = {
...(useGeneralistProfile ? generalistProfile : {}),
...(useContextManagement ? settings?.contextManagement : {}),
Expand All @@ -913,6 +926,9 @@ export async function loadCliConfig(
debugMode,
question,
worktreeSettings,
minimalPayload: !!argv.fast,
skipPreflightRequests: !!argv.fast,
saveSession: argv.saveSession,

coreTools: settings.tools?.core || undefined,
allowedTools: allowedTools.length > 0 ? allowedTools : undefined,
Expand Down Expand Up @@ -977,20 +993,23 @@ export async function loadCliConfig(
extensionRegistryURI,
enableExtensionReloading: settings.experimental?.extensionReloading,
enableAgents: settings.experimental?.enableAgents,
plan: settings.general?.plan?.enabled ?? true,
tracker: settings.experimental?.taskTracker,
plan: (settings.general?.plan?.enabled ?? true) && !argv.fast,
tracker: (settings.experimental?.taskTracker ?? false) && !argv.fast,
directWebFetch: settings.experimental?.directWebFetch,
planSettings: settings.general?.plan?.directory
? settings.general.plan
: (extensionPlanSettings ?? settings.general?.plan),
enableEventDrivenScheduler: true,
skillsSupport: settings.skills?.enabled ?? true,
skillsSupport: (settings.skills?.enabled ?? true) && !argv.fast,
disabledSkills: settings.skills?.disabled,
experimentalJitContext: settings.experimental?.jitContext,
experimentalMemoryManager: settings.experimental?.memoryManager,
experimentalJitContext: experimentalJitContext,
experimentalMemoryManager:
(settings.experimental?.memoryManager ?? false) && !argv.fast,
contextManagement,
modelSteering: settings.experimental?.modelSteering,
topicUpdateNarration: settings.experimental?.topicUpdateNarration,
modelSteering:
(settings.experimental?.modelSteering ?? false) && !argv.fast,
topicUpdateNarration:
(settings.experimental?.topicUpdateNarration ?? false) && !argv.fast,
noBrowser: !!process.env['NO_BROWSER'],
summarizeToolOutput: settings.model?.summarizeToolOutput,
ideMode,
Expand Down Expand Up @@ -1032,8 +1051,8 @@ export async function loadCliConfig(
dynamicModelConfiguration: settings.experimental?.dynamicModelConfiguration,
modelConfigServiceConfig: settings.modelConfigs,
// TODO: loading of hooks based on workspace trust
enableHooks: settings.hooksConfig.enabled,
enableHooksUI: settings.hooksConfig.enabled,
enableHooks: (settings.hooksConfig.enabled ?? true) && !argv.fast,
enableHooksUI: (settings.hooksConfig.enabled ?? true) && !argv.fast,
Comment on lines +1054 to +1055
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The --fast CLI flag disables the hook system, which is used for security policies such as Data Loss Prevention (DLP). Allowing users to bypass these hooks via a CLI flag undermines the security model. Security-sensitive settings should not allow less-trusted configuration scopes (like a user-provided CLI flag) to completely override or bypass more-trusted security enforcement mechanisms. Ensure security-critical hooks remain active even when performance optimizations are requested.

References
  1. Security checks should be implemented in a 'fail-closed' manner. If an item's validity cannot be verified, it should be rejected by default.
  2. Security-sensitive settings should not use a merge strategy that allows less-trusted configuration scopes to completely override more-trusted scopes.

hooks: settings.hooks || {},
disabledHooks: settings.hooksConfig?.disabled || [],
projectHooks: projectHooks || {},
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/code_assist/experiments/experiments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ let experimentsPromise: Promise<Experiments> | undefined;
export async function getExperiments(
server?: CodeAssistServer,
): Promise<Experiments> {
if (server?.config?.getSkipPreflightRequests()) {
return { flags: {}, experimentIds: [] };
}
if (experimentsPromise) {
return experimentsPromise;
}
Expand Down
11 changes: 8 additions & 3 deletions packages/core/src/code_assist/oauth2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,10 +172,15 @@ async function initOauthClient(
// This will verify locally that the credentials look good.
const { token } = await client.getAccessToken();
if (token) {
// This will check with the server to see if it hasn't been revoked.
await client.getTokenInfo(token);
if (!config.getSkipPreflightRequests()) {
// This will check with the server to see if it hasn't been revoked.
await client.getTokenInfo(token);
}

if (!userAccountManager.getCachedGoogleAccount()) {
if (
!userAccountManager.getCachedGoogleAccount() &&
!config.getSkipPreflightRequests()
) {
try {
await fetchAndCacheUserInfo(client);
} catch (error) {
Expand Down
15 changes: 15 additions & 0 deletions packages/core/src/code_assist/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -404,6 +404,12 @@ export class CodeAssistServer implements ContentGenerator {
signal?: AbortSignal,
retryDelay: number = 100,
): Promise<T> {
if (
this.config?.getSkipPreflightRequests() &&
method !== 'generateContent'
) {
return {} as T;
}
Comment on lines +407 to +412
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-high high

The requestPost method returns an empty object {} when skipPreflightRequests is enabled. This bypasses security-sensitive operations like fetchAdminControls. Security checks should be implemented in a 'fail-closed' manner; returning an empty object instead of performing the request can lead to an insecure default state. Ensure that security-critical requests are always executed or handled with a safe default that does not bypass policy enforcement.

References
  1. Security checks should be implemented in a 'fail-closed' manner. If an item's validity cannot be verified, it should be rejected by default.

const res = await this.client.request<T>({
url: this.getMethodUrl(method),
method: 'POST',
Expand Down Expand Up @@ -432,6 +438,9 @@ export class CodeAssistServer implements ContentGenerator {
url: string,
signal?: AbortSignal,
): Promise<T> {
if (this.config?.getSkipPreflightRequests()) {
return {} as T;
}
const res = await this.client.request<T>({
url,
method: 'GET',
Expand All @@ -458,6 +467,12 @@ export class CodeAssistServer implements ContentGenerator {
req: object,
signal?: AbortSignal,
): Promise<AsyncGenerator<T>> {
if (
this.config?.getSkipPreflightRequests() &&
method !== 'streamGenerateContent'
) {
return (async function* () {})() as AsyncGenerator<T>;
}
const res = await this.client.request<AsyncIterable<unknown>>({
url: this.getMethodUrl(method),
method: 'POST',
Expand Down
45 changes: 44 additions & 1 deletion packages/core/src/code_assist/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ import {
OnboardingSuccessEvent,
} from '../telemetry/index.js';

import { Storage } from '../config/storage.js';
import * as fs from 'node:fs/promises';
import * as path from 'node:path';

export class ProjectIdRequiredError extends Error {
constructor() {
super(
Expand Down Expand Up @@ -89,6 +93,27 @@ export function resetUserDataCacheForTesting() {
});
}

async function loadPersistentUserData(): Promise<UserData | undefined> {
const filePath = path.join(Storage.getGlobalGeminiDir(), 'user_data.json');
try {
const content = await fs.readFile(filePath, 'utf-8');
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
return JSON.parse(content) as UserData;
} catch {
return undefined;
}
}

async function savePersistentUserData(userData: UserData): Promise<void> {
const filePath = path.join(Storage.getGlobalGeminiDir(), 'user_data.json');
try {
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, JSON.stringify(userData, null, 2), 'utf-8');
} catch (e) {
debugLogger.debug('Failed to save persistent user data', e);
}
}

/**
* Sets up the user by loading their Code Assist configuration and onboarding if needed.
*
Expand Down Expand Up @@ -122,16 +147,32 @@ export async function setupUser(
process.env['GOOGLE_CLOUD_PROJECT_ID'] ||
undefined;

if (config.getSkipPreflightRequests()) {
const cached = await loadPersistentUserData();
if (cached) {
return cached;
}
return {
projectId: projectId || 'unknown-project',
userTier: UserTierId.STANDARD,
userTierName: 'Standard',
hasOnboardedPreviously: true,
};
}

const projectCache = userDataCache.getOrCreate(client, () =>
createCache<string | undefined, Promise<UserData>>({
storage: 'map',
defaultTtl: 30000, // 30 seconds
}),
);

return projectCache.getOrCreate(projectId, () =>
const userData = await projectCache.getOrCreate(projectId, () =>
_doSetupUser(client, projectId, config, httpOptions),
);

void savePersistentUserData(userData);
return userData;
}

/**
Expand All @@ -150,6 +191,8 @@ async function _doSetupUser(
'',
undefined,
undefined,
undefined,
config,
);
const coreClientMetadata: ClientMetadata = {
ideType: 'IDE_UNSPECIFIED',
Expand Down
Loading