diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 87eb1e8fa7d..f164ce77d8d 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -445,7 +445,11 @@ export async function loadCliConfig( process.env['VITEST'] === 'true' ? 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 // 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 = !!argv.promptInteractive || !!argv.experimentalAcp || - (!isHeadlessMode({ prompt: argv.prompt }) && - !argv.query && + (!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) && !argv.isCommand); const allowedTools = argv.allowedTools || settings.tools?.allowed || []; diff --git a/packages/cli/src/config/trustedFolders.test.ts b/packages/cli/src/config/trustedFolders.test.ts index dff4610b907..892cd86e4b2 100644 --- a/packages/cli/src/config/trustedFolders.test.ts +++ b/packages/cli/src/config/trustedFolders.test.ts @@ -449,6 +449,14 @@ describe('Trusted Folders', () => { 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', () => { diff --git a/packages/cli/src/config/trustedFolders.ts b/packages/cli/src/config/trustedFolders.ts index 1f85684900c..761bc368d3f 100644 --- a/packages/cli/src/config/trustedFolders.ts +++ b/packages/cli/src/config/trustedFolders.ts @@ -17,6 +17,7 @@ import { homedir, isHeadlessMode, coreEvents, + type HeadlessModeOptions, } from '@google/gemini-cli-core'; import type { Settings } from './settings.js'; import stripJsonComments from 'strip-json-comments'; @@ -128,7 +129,11 @@ export class LoadedTrustedFolders { isPathTrusted( location: string, config?: Record, + headlessOptions?: HeadlessModeOptions, ): boolean | undefined { + if (isHeadlessMode(headlessOptions)) { + return true; + } const configToUse = config ?? this.user.config; // Resolve location to its realpath for canonical comparison @@ -333,6 +338,7 @@ export function isFolderTrustEnabled(settings: Settings): boolean { function getWorkspaceTrustFromLocalConfig( workspaceDir: string, trustConfig?: Record, + headlessOptions?: HeadlessModeOptions, ): TrustResult { const folders = loadTrustedFolders(); 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 { isTrusted, source: isTrusted !== undefined ? 'file' : undefined, @@ -357,8 +367,9 @@ export function isWorkspaceTrusted( settings: Settings, workspaceDir: string = process.cwd(), trustConfig?: Record, + headlessOptions?: HeadlessModeOptions, ): TrustResult { - if (isHeadlessMode()) { + if (isHeadlessMode(headlessOptions)) { return { isTrusted: true, source: undefined }; } @@ -372,5 +383,9 @@ export function isWorkspaceTrusted( } // Fall back to the local user configuration - return getWorkspaceTrustFromLocalConfig(workspaceDir, trustConfig); + return getWorkspaceTrustFromLocalConfig( + workspaceDir, + trustConfig, + headlessOptions, + ); } diff --git a/packages/core/src/utils/headless.test.ts b/packages/core/src/utils/headless.test.ts index 89f42ffcd60..4708c79969d 100644 --- a/packages/core/src/utils/headless.test.ts +++ b/packages/core/src/utils/headless.test.ts @@ -99,16 +99,50 @@ describe('isHeadlessMode', () => { expect(isHeadlessMode({ prompt: true })).toBe(true); }); - it('should return false if query is provided but it is still a TTY', () => { - // Note: per current logic, query alone doesn't force headless if TTY - // This matches the existing behavior in packages/cli/src/config/config.ts - expect(isHeadlessMode({ query: 'test query' })).toBe(false); + it('should return true if query is provided', () => { + expect(isHeadlessMode({ query: 'test query' })).toBe(true); + }); + + 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', () => { const originalStdout = process.stdout; - // @ts-expect-error - testing edge case - delete process.stdout; + Object.defineProperty(process, 'stdout', { + value: undefined, + configurable: true, + }); try { expect(isHeadlessMode()).toBe(false); @@ -122,8 +156,10 @@ describe('isHeadlessMode', () => { it('should handle undefined process.stdin gracefully', () => { const originalStdin = process.stdin; - // @ts-expect-error - testing edge case - delete process.stdin; + Object.defineProperty(process, 'stdin', { + value: undefined, + configurable: true, + }); try { expect(isHeadlessMode()).toBe(false); diff --git a/packages/core/src/utils/headless.ts b/packages/core/src/utils/headless.ts index 27ea5f9cbfa..5a46b90d6d4 100644 --- a/packages/core/src/utils/headless.ts +++ b/packages/core/src/utils/headless.ts @@ -28,18 +28,25 @@ export interface HeadlessModeOptions { * @returns true if the environment is considered headless. */ export function isHeadlessMode(options?: HeadlessModeOptions): boolean { - if (process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true') { - return ( - !!options?.prompt || - (!!process.stdin && !process.stdin.isTTY) || - (!!process.stdout && !process.stdout.isTTY) - ); + if (process.env['GEMINI_CLI_INTEGRATION_TEST'] !== 'true') { + const isCI = + process.env['CI'] === 'true' || process.env['GITHUB_ACTIONS'] === 'true'; + if (isCI) { + return true; + } } - return ( - process.env['CI'] === 'true' || - process.env['GITHUB_ACTIONS'] === 'true' || - !!options?.prompt || + + const isNotTTY = (!!process.stdin && !process.stdin.isTTY) || - (!!process.stdout && !process.stdout.isTTY) + (!!process.stdout && !process.stdout.isTTY); + + if (isNotTTY || !!options?.prompt || !!options?.query) { + return true; + } + + // Fallback: check process.argv for flags that imply headless or auto-approve mode. + return process.argv.some( + (arg) => + arg === '-p' || arg === '--prompt' || arg === '-y' || arg === '--yolo', ); }