fix(core): improve headless mode detection for flags and query args (#18855)

This commit is contained in:
Gal Zahavi
2026-02-11 16:20:54 -08:00
committed by GitHub
parent 941691ce72
commit 08e8eeab84
5 changed files with 94 additions and 25 deletions

View File

@@ -445,7 +445,11 @@ export async function loadCliConfig(
process.env['VITEST'] === 'true' process.env['VITEST'] === 'true'
? false ? false
: (settings.security?.folderTrust?.enabled ?? false); : (settings.security?.folderTrust?.enabled ?? false);
const trustedFolder = isWorkspaceTrusted(settings, cwd)?.isTrusted ?? false; const trustedFolder =
isWorkspaceTrusted(settings, cwd, undefined, {
prompt: argv.prompt,
query: argv.query,
})?.isTrusted ?? false;
// Set the context filename in the server's memoryTool module BEFORE loading memory // Set the context filename in the server's memoryTool module BEFORE loading memory
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed // TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
@@ -602,8 +606,7 @@ export async function loadCliConfig(
const interactive = const interactive =
!!argv.promptInteractive || !!argv.promptInteractive ||
!!argv.experimentalAcp || !!argv.experimentalAcp ||
(!isHeadlessMode({ prompt: argv.prompt }) && (!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
!argv.query &&
!argv.isCommand); !argv.isCommand);
const allowedTools = argv.allowedTools || settings.tools?.allowed || []; const allowedTools = argv.allowedTools || settings.tools?.allowed || [];

View File

@@ -449,6 +449,14 @@ describe('Trusted Folders', () => {
false, false,
); );
}); });
it('should return true for isPathTrusted when isHeadlessMode is true', async () => {
const geminiCore = await import('@google/gemini-cli-core');
vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(true);
const folders = loadTrustedFolders();
expect(folders.isPathTrusted('/any-untrusted-path')).toBe(true);
});
}); });
describe('Trusted Folders Caching', () => { describe('Trusted Folders Caching', () => {

View File

@@ -17,6 +17,7 @@ import {
homedir, homedir,
isHeadlessMode, isHeadlessMode,
coreEvents, coreEvents,
type HeadlessModeOptions,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import type { Settings } from './settings.js'; import type { Settings } from './settings.js';
import stripJsonComments from 'strip-json-comments'; import stripJsonComments from 'strip-json-comments';
@@ -128,7 +129,11 @@ export class LoadedTrustedFolders {
isPathTrusted( isPathTrusted(
location: string, location: string,
config?: Record<string, TrustLevel>, config?: Record<string, TrustLevel>,
headlessOptions?: HeadlessModeOptions,
): boolean | undefined { ): boolean | undefined {
if (isHeadlessMode(headlessOptions)) {
return true;
}
const configToUse = config ?? this.user.config; const configToUse = config ?? this.user.config;
// Resolve location to its realpath for canonical comparison // Resolve location to its realpath for canonical comparison
@@ -333,6 +338,7 @@ export function isFolderTrustEnabled(settings: Settings): boolean {
function getWorkspaceTrustFromLocalConfig( function getWorkspaceTrustFromLocalConfig(
workspaceDir: string, workspaceDir: string,
trustConfig?: Record<string, TrustLevel>, trustConfig?: Record<string, TrustLevel>,
headlessOptions?: HeadlessModeOptions,
): TrustResult { ): TrustResult {
const folders = loadTrustedFolders(); const folders = loadTrustedFolders();
const configToUse = trustConfig ?? folders.user.config; const configToUse = trustConfig ?? folders.user.config;
@@ -346,7 +352,11 @@ function getWorkspaceTrustFromLocalConfig(
); );
} }
const isTrusted = folders.isPathTrusted(workspaceDir, configToUse); const isTrusted = folders.isPathTrusted(
workspaceDir,
configToUse,
headlessOptions,
);
return { return {
isTrusted, isTrusted,
source: isTrusted !== undefined ? 'file' : undefined, source: isTrusted !== undefined ? 'file' : undefined,
@@ -357,8 +367,9 @@ export function isWorkspaceTrusted(
settings: Settings, settings: Settings,
workspaceDir: string = process.cwd(), workspaceDir: string = process.cwd(),
trustConfig?: Record<string, TrustLevel>, trustConfig?: Record<string, TrustLevel>,
headlessOptions?: HeadlessModeOptions,
): TrustResult { ): TrustResult {
if (isHeadlessMode()) { if (isHeadlessMode(headlessOptions)) {
return { isTrusted: true, source: undefined }; return { isTrusted: true, source: undefined };
} }
@@ -372,5 +383,9 @@ export function isWorkspaceTrusted(
} }
// Fall back to the local user configuration // Fall back to the local user configuration
return getWorkspaceTrustFromLocalConfig(workspaceDir, trustConfig); return getWorkspaceTrustFromLocalConfig(
workspaceDir,
trustConfig,
headlessOptions,
);
} }

View File

@@ -99,16 +99,50 @@ describe('isHeadlessMode', () => {
expect(isHeadlessMode({ prompt: true })).toBe(true); expect(isHeadlessMode({ prompt: true })).toBe(true);
}); });
it('should return false if query is provided but it is still a TTY', () => { it('should return true if query is provided', () => {
// Note: per current logic, query alone doesn't force headless if TTY expect(isHeadlessMode({ query: 'test query' })).toBe(true);
// This matches the existing behavior in packages/cli/src/config/config.ts });
expect(isHeadlessMode({ query: 'test query' })).toBe(false);
it('should return true if -p or --prompt is in process.argv as a fallback', () => {
const originalArgv = process.argv;
process.argv = ['node', 'index.js', '-p', 'hello'];
try {
expect(isHeadlessMode()).toBe(true);
} finally {
process.argv = originalArgv;
}
process.argv = ['node', 'index.js', '--prompt', 'hello'];
try {
expect(isHeadlessMode()).toBe(true);
} finally {
process.argv = originalArgv;
}
});
it('should return true if -y or --yolo is in process.argv as a fallback', () => {
const originalArgv = process.argv;
process.argv = ['node', 'index.js', '-y'];
try {
expect(isHeadlessMode()).toBe(true);
} finally {
process.argv = originalArgv;
}
process.argv = ['node', 'index.js', '--yolo'];
try {
expect(isHeadlessMode()).toBe(true);
} finally {
process.argv = originalArgv;
}
}); });
it('should handle undefined process.stdout gracefully', () => { it('should handle undefined process.stdout gracefully', () => {
const originalStdout = process.stdout; const originalStdout = process.stdout;
// @ts-expect-error - testing edge case Object.defineProperty(process, 'stdout', {
delete process.stdout; value: undefined,
configurable: true,
});
try { try {
expect(isHeadlessMode()).toBe(false); expect(isHeadlessMode()).toBe(false);
@@ -122,8 +156,10 @@ describe('isHeadlessMode', () => {
it('should handle undefined process.stdin gracefully', () => { it('should handle undefined process.stdin gracefully', () => {
const originalStdin = process.stdin; const originalStdin = process.stdin;
// @ts-expect-error - testing edge case Object.defineProperty(process, 'stdin', {
delete process.stdin; value: undefined,
configurable: true,
});
try { try {
expect(isHeadlessMode()).toBe(false); expect(isHeadlessMode()).toBe(false);

View File

@@ -28,18 +28,25 @@ export interface HeadlessModeOptions {
* @returns true if the environment is considered headless. * @returns true if the environment is considered headless.
*/ */
export function isHeadlessMode(options?: HeadlessModeOptions): boolean { export function isHeadlessMode(options?: HeadlessModeOptions): boolean {
if (process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true') { if (process.env['GEMINI_CLI_INTEGRATION_TEST'] !== 'true') {
return ( const isCI =
!!options?.prompt || process.env['CI'] === 'true' || process.env['GITHUB_ACTIONS'] === 'true';
if (isCI) {
return true;
}
}
const isNotTTY =
(!!process.stdin && !process.stdin.isTTY) || (!!process.stdin && !process.stdin.isTTY) ||
(!!process.stdout && !process.stdout.isTTY) (!!process.stdout && !process.stdout.isTTY);
);
} if (isNotTTY || !!options?.prompt || !!options?.query) {
return ( return true;
process.env['CI'] === 'true' || }
process.env['GITHUB_ACTIONS'] === 'true' ||
!!options?.prompt || // Fallback: check process.argv for flags that imply headless or auto-approve mode.
(!!process.stdin && !process.stdin.isTTY) || return process.argv.some(
(!!process.stdout && !process.stdout.isTTY) (arg) =>
arg === '-p' || arg === '--prompt' || arg === '-y' || arg === '--yolo',
); );
} }