Skip to content

Commit 08e8eea

Browse files
authored
fix(core): improve headless mode detection for flags and query args (#18855)
1 parent 941691c commit 08e8eea

File tree

5 files changed

+94
-25
lines changed

5 files changed

+94
-25
lines changed

packages/cli/src/config/config.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,11 @@ export async function loadCliConfig(
445445
process.env['VITEST'] === 'true'
446446
? false
447447
: (settings.security?.folderTrust?.enabled ?? false);
448-
const trustedFolder = isWorkspaceTrusted(settings, cwd)?.isTrusted ?? false;
448+
const trustedFolder =
449+
isWorkspaceTrusted(settings, cwd, undefined, {
450+
prompt: argv.prompt,
451+
query: argv.query,
452+
})?.isTrusted ?? false;
449453

450454
// Set the context filename in the server's memoryTool module BEFORE loading memory
451455
// TODO(b/343434939): This is a bit of a hack. The contextFileName should ideally be passed
@@ -602,8 +606,7 @@ export async function loadCliConfig(
602606
const interactive =
603607
!!argv.promptInteractive ||
604608
!!argv.experimentalAcp ||
605-
(!isHeadlessMode({ prompt: argv.prompt }) &&
606-
!argv.query &&
609+
(!isHeadlessMode({ prompt: argv.prompt, query: argv.query }) &&
607610
!argv.isCommand);
608611

609612
const allowedTools = argv.allowedTools || settings.tools?.allowed || [];

packages/cli/src/config/trustedFolders.test.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,14 @@ describe('Trusted Folders', () => {
449449
false,
450450
);
451451
});
452+
453+
it('should return true for isPathTrusted when isHeadlessMode is true', async () => {
454+
const geminiCore = await import('@google/gemini-cli-core');
455+
vi.spyOn(geminiCore, 'isHeadlessMode').mockReturnValue(true);
456+
457+
const folders = loadTrustedFolders();
458+
expect(folders.isPathTrusted('/any-untrusted-path')).toBe(true);
459+
});
452460
});
453461

454462
describe('Trusted Folders Caching', () => {

packages/cli/src/config/trustedFolders.ts

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
homedir,
1818
isHeadlessMode,
1919
coreEvents,
20+
type HeadlessModeOptions,
2021
} from '@google/gemini-cli-core';
2122
import type { Settings } from './settings.js';
2223
import stripJsonComments from 'strip-json-comments';
@@ -128,7 +129,11 @@ export class LoadedTrustedFolders {
128129
isPathTrusted(
129130
location: string,
130131
config?: Record<string, TrustLevel>,
132+
headlessOptions?: HeadlessModeOptions,
131133
): boolean | undefined {
134+
if (isHeadlessMode(headlessOptions)) {
135+
return true;
136+
}
132137
const configToUse = config ?? this.user.config;
133138

134139
// Resolve location to its realpath for canonical comparison
@@ -333,6 +338,7 @@ export function isFolderTrustEnabled(settings: Settings): boolean {
333338
function getWorkspaceTrustFromLocalConfig(
334339
workspaceDir: string,
335340
trustConfig?: Record<string, TrustLevel>,
341+
headlessOptions?: HeadlessModeOptions,
336342
): TrustResult {
337343
const folders = loadTrustedFolders();
338344
const configToUse = trustConfig ?? folders.user.config;
@@ -346,7 +352,11 @@ function getWorkspaceTrustFromLocalConfig(
346352
);
347353
}
348354

349-
const isTrusted = folders.isPathTrusted(workspaceDir, configToUse);
355+
const isTrusted = folders.isPathTrusted(
356+
workspaceDir,
357+
configToUse,
358+
headlessOptions,
359+
);
350360
return {
351361
isTrusted,
352362
source: isTrusted !== undefined ? 'file' : undefined,
@@ -357,8 +367,9 @@ export function isWorkspaceTrusted(
357367
settings: Settings,
358368
workspaceDir: string = process.cwd(),
359369
trustConfig?: Record<string, TrustLevel>,
370+
headlessOptions?: HeadlessModeOptions,
360371
): TrustResult {
361-
if (isHeadlessMode()) {
372+
if (isHeadlessMode(headlessOptions)) {
362373
return { isTrusted: true, source: undefined };
363374
}
364375

@@ -372,5 +383,9 @@ export function isWorkspaceTrusted(
372383
}
373384

374385
// Fall back to the local user configuration
375-
return getWorkspaceTrustFromLocalConfig(workspaceDir, trustConfig);
386+
return getWorkspaceTrustFromLocalConfig(
387+
workspaceDir,
388+
trustConfig,
389+
headlessOptions,
390+
);
376391
}

packages/core/src/utils/headless.test.ts

Lines changed: 44 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,16 +99,50 @@ describe('isHeadlessMode', () => {
9999
expect(isHeadlessMode({ prompt: true })).toBe(true);
100100
});
101101

102-
it('should return false if query is provided but it is still a TTY', () => {
103-
// Note: per current logic, query alone doesn't force headless if TTY
104-
// This matches the existing behavior in packages/cli/src/config/config.ts
105-
expect(isHeadlessMode({ query: 'test query' })).toBe(false);
102+
it('should return true if query is provided', () => {
103+
expect(isHeadlessMode({ query: 'test query' })).toBe(true);
104+
});
105+
106+
it('should return true if -p or --prompt is in process.argv as a fallback', () => {
107+
const originalArgv = process.argv;
108+
process.argv = ['node', 'index.js', '-p', 'hello'];
109+
try {
110+
expect(isHeadlessMode()).toBe(true);
111+
} finally {
112+
process.argv = originalArgv;
113+
}
114+
115+
process.argv = ['node', 'index.js', '--prompt', 'hello'];
116+
try {
117+
expect(isHeadlessMode()).toBe(true);
118+
} finally {
119+
process.argv = originalArgv;
120+
}
121+
});
122+
123+
it('should return true if -y or --yolo is in process.argv as a fallback', () => {
124+
const originalArgv = process.argv;
125+
process.argv = ['node', 'index.js', '-y'];
126+
try {
127+
expect(isHeadlessMode()).toBe(true);
128+
} finally {
129+
process.argv = originalArgv;
130+
}
131+
132+
process.argv = ['node', 'index.js', '--yolo'];
133+
try {
134+
expect(isHeadlessMode()).toBe(true);
135+
} finally {
136+
process.argv = originalArgv;
137+
}
106138
});
107139

108140
it('should handle undefined process.stdout gracefully', () => {
109141
const originalStdout = process.stdout;
110-
// @ts-expect-error - testing edge case
111-
delete process.stdout;
142+
Object.defineProperty(process, 'stdout', {
143+
value: undefined,
144+
configurable: true,
145+
});
112146

113147
try {
114148
expect(isHeadlessMode()).toBe(false);
@@ -122,8 +156,10 @@ describe('isHeadlessMode', () => {
122156

123157
it('should handle undefined process.stdin gracefully', () => {
124158
const originalStdin = process.stdin;
125-
// @ts-expect-error - testing edge case
126-
delete process.stdin;
159+
Object.defineProperty(process, 'stdin', {
160+
value: undefined,
161+
configurable: true,
162+
});
127163

128164
try {
129165
expect(isHeadlessMode()).toBe(false);

packages/core/src/utils/headless.ts

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,18 +28,25 @@ export interface HeadlessModeOptions {
2828
* @returns true if the environment is considered headless.
2929
*/
3030
export function isHeadlessMode(options?: HeadlessModeOptions): boolean {
31-
if (process.env['GEMINI_CLI_INTEGRATION_TEST'] === 'true') {
32-
return (
33-
!!options?.prompt ||
34-
(!!process.stdin && !process.stdin.isTTY) ||
35-
(!!process.stdout && !process.stdout.isTTY)
36-
);
31+
if (process.env['GEMINI_CLI_INTEGRATION_TEST'] !== 'true') {
32+
const isCI =
33+
process.env['CI'] === 'true' || process.env['GITHUB_ACTIONS'] === 'true';
34+
if (isCI) {
35+
return true;
36+
}
3737
}
38-
return (
39-
process.env['CI'] === 'true' ||
40-
process.env['GITHUB_ACTIONS'] === 'true' ||
41-
!!options?.prompt ||
38+
39+
const isNotTTY =
4240
(!!process.stdin && !process.stdin.isTTY) ||
43-
(!!process.stdout && !process.stdout.isTTY)
41+
(!!process.stdout && !process.stdout.isTTY);
42+
43+
if (isNotTTY || !!options?.prompt || !!options?.query) {
44+
return true;
45+
}
46+
47+
// Fallback: check process.argv for flags that imply headless or auto-approve mode.
48+
return process.argv.some(
49+
(arg) =>
50+
arg === '-p' || arg === '--prompt' || arg === '-y' || arg === '--yolo',
4451
);
4552
}

0 commit comments

Comments
 (0)