summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorshrutip90 <[email protected]>2025-08-13 11:06:31 -0700
committerGitHub <[email protected]>2025-08-13 18:06:31 +0000
commit38876b738f4c9ef8bd1b839d5e33580486e9a089 (patch)
tree485acb25444b45b12250146cbefaf39e508c5e83
parentb61a63aef4bcce9cb56fe46f10f0dc90b8fd6597 (diff)
Add support for trustedFolders.json config file (#6073)
-rw-r--r--packages/cli/src/config/config.test.ts123
-rw-r--r--packages/cli/src/config/config.ts6
-rw-r--r--packages/cli/src/config/trustedFolders.test.ts203
-rw-r--r--packages/cli/src/config/trustedFolders.ts158
-rw-r--r--packages/cli/src/ui/App.tsx6
-rw-r--r--packages/cli/src/ui/hooks/useFolderTrust.test.ts145
-rw-r--r--packages/cli/src/ui/hooks/useFolderTrust.ts41
-rw-r--r--packages/core/src/config/config.ts7
8 files changed, 635 insertions, 54 deletions
diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts
index fc4d24bd..69985867 100644
--- a/packages/cli/src/config/config.test.ts
+++ b/packages/cli/src/config/config.test.ts
@@ -13,6 +13,11 @@ import { loadCliConfig, parseArguments } from './config.js';
import { Settings } from './settings.js';
import { Extension } from './extension.js';
import * as ServerConfig from '@google/gemini-cli-core';
+import { isWorkspaceTrusted } from './trustedFolders.js';
+
+vi.mock('./trustedFolders.js', () => ({
+ isWorkspaceTrusted: vi.fn(),
+}));
vi.mock('os', async (importOriginal) => {
const actualOs = await importOriginal<typeof os>();
@@ -1628,6 +1633,7 @@ describe('loadCliConfig approval mode', () => {
vi.resetAllMocks();
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
process.env.GEMINI_API_KEY = 'test-api-key';
+ process.argv = ['node', 'script.js']; // Reset argv for each test
});
afterEach(() => {
@@ -1696,3 +1702,120 @@ describe('loadCliConfig approval mode', () => {
expect(config.getApprovalMode()).toBe(ServerConfig.ApprovalMode.YOLO);
});
});
+
+describe('loadCliConfig trustedFolder', () => {
+ const originalArgv = process.argv;
+ const originalEnv = { ...process.env };
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+ vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
+ process.env.GEMINI_API_KEY = 'test-api-key';
+ process.argv = ['node', 'script.js']; // Reset argv for each test
+ });
+
+ afterEach(() => {
+ process.argv = originalArgv;
+ process.env = originalEnv;
+ vi.restoreAllMocks();
+ });
+
+ const testCases = [
+ // Cases where folderTrustFeature is false (feature disabled)
+ {
+ folderTrustFeature: false,
+ folderTrust: true,
+ isWorkspaceTrusted: true,
+ expectedFolderTrust: false,
+ expectedIsTrustedFolder: true,
+ description:
+ 'feature disabled, folderTrust true, workspace trusted -> behave as trusted',
+ },
+ {
+ folderTrustFeature: false,
+ folderTrust: true,
+ isWorkspaceTrusted: false,
+ expectedFolderTrust: false,
+ expectedIsTrustedFolder: true,
+ description:
+ 'feature disabled, folderTrust true, workspace not trusted -> behave as trusted',
+ },
+ {
+ folderTrustFeature: false,
+ folderTrust: false,
+ isWorkspaceTrusted: true,
+ expectedFolderTrust: false,
+ expectedIsTrustedFolder: true,
+ description:
+ 'feature disabled, folderTrust false, workspace trusted -> behave as trusted',
+ },
+
+ // Cases where folderTrustFeature is true but folderTrust setting is false
+ {
+ folderTrustFeature: true,
+ folderTrust: false,
+ isWorkspaceTrusted: true,
+ expectedFolderTrust: false,
+ expectedIsTrustedFolder: true,
+ description:
+ 'feature on, folderTrust false, workspace trusted -> behave as trusted',
+ },
+ {
+ folderTrustFeature: true,
+ folderTrust: false,
+ isWorkspaceTrusted: false,
+ expectedFolderTrust: false,
+ expectedIsTrustedFolder: true,
+ description:
+ 'feature on, folderTrust false, workspace not trusted -> behave as trusted',
+ },
+
+ // Cases where feature is fully enabled (folderTrustFeature and folderTrust are true)
+ {
+ folderTrustFeature: true,
+ folderTrust: true,
+ isWorkspaceTrusted: true,
+ expectedFolderTrust: true,
+ expectedIsTrustedFolder: true,
+ description:
+ 'feature on, folderTrust on, workspace trusted -> is trusted',
+ },
+ {
+ folderTrustFeature: true,
+ folderTrust: true,
+ isWorkspaceTrusted: false,
+ expectedFolderTrust: true,
+ expectedIsTrustedFolder: false,
+ description:
+ 'feature on, folderTrust on, workspace NOT trusted -> is NOT trusted',
+ },
+ {
+ folderTrustFeature: true,
+ folderTrust: true,
+ isWorkspaceTrusted: undefined,
+ expectedFolderTrust: true,
+ expectedIsTrustedFolder: undefined,
+ description:
+ 'feature on, folderTrust on, workspace trust unknown -> is unknown',
+ },
+ ];
+
+ for (const {
+ folderTrustFeature,
+ folderTrust,
+ isWorkspaceTrusted: mockTrustValue,
+ expectedFolderTrust,
+ expectedIsTrustedFolder,
+ description,
+ } of testCases) {
+ it(`should be correct for: ${description}`, async () => {
+ (isWorkspaceTrusted as vi.Mock).mockReturnValue(mockTrustValue);
+ const argv = await parseArguments();
+ const settings: Settings = { folderTrustFeature, folderTrust };
+ const config = await loadCliConfig(settings, [], 'test-session', argv);
+
+ expect(config.getFolderTrust()).toBe(expectedFolderTrust);
+ expect(config.isTrustedFolder()).toBe(expectedIsTrustedFolder);
+ });
+ }
+});
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts
index 636696fa..296d140d 100644
--- a/packages/cli/src/config/config.ts
+++ b/packages/cli/src/config/config.ts
@@ -35,6 +35,8 @@ import { getCliVersion } from '../utils/version.js';
import { loadSandboxConfig } from './sandboxConfig.js';
import { resolvePath } from '../utils/resolvePath.js';
+import { isWorkspaceTrusted } from './trustedFolders.js';
+
// Simple console logger for now - replace with actual logger if available
const logger = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -317,8 +319,9 @@ export async function loadCliConfig(
const ideMode = settings.ideMode ?? false;
const folderTrustFeature = settings.folderTrustFeature ?? false;
- const folderTrustSetting = settings.folderTrust ?? false;
+ const folderTrustSetting = settings.folderTrust ?? true;
const folderTrust = folderTrustFeature && folderTrustSetting;
+ const trustedFolder = folderTrust ? isWorkspaceTrusted() : true;
const allExtensions = annotateActiveExtensions(
extensions,
@@ -523,6 +526,7 @@ export async function loadCliConfig(
folderTrustFeature,
folderTrust,
interactive,
+ trustedFolder,
});
}
diff --git a/packages/cli/src/config/trustedFolders.test.ts b/packages/cli/src/config/trustedFolders.test.ts
new file mode 100644
index 00000000..67bf9cfc
--- /dev/null
+++ b/packages/cli/src/config/trustedFolders.test.ts
@@ -0,0 +1,203 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+// Mock 'os' first.
+import * as osActual from 'os';
+vi.mock('os', async (importOriginal) => {
+ const actualOs = await importOriginal<typeof osActual>();
+ return {
+ ...actualOs,
+ homedir: vi.fn(() => '/mock/home/user'),
+ platform: vi.fn(() => 'linux'),
+ };
+});
+
+import {
+ describe,
+ it,
+ expect,
+ vi,
+ beforeEach,
+ afterEach,
+ type Mocked,
+ type Mock,
+} from 'vitest';
+import * as fs from 'fs';
+import stripJsonComments from 'strip-json-comments';
+import * as path from 'path';
+
+import {
+ loadTrustedFolders,
+ USER_TRUSTED_FOLDERS_PATH,
+ TrustLevel,
+ isWorkspaceTrusted,
+} from './trustedFolders.js';
+
+vi.mock('fs', async (importOriginal) => {
+ const actualFs = await importOriginal<typeof fs>();
+ return {
+ ...actualFs,
+ existsSync: vi.fn(),
+ readFileSync: vi.fn(),
+ writeFileSync: vi.fn(),
+ mkdirSync: vi.fn(),
+ };
+});
+
+vi.mock('strip-json-comments', () => ({
+ default: vi.fn((content) => content),
+}));
+
+describe('Trusted Folders Loading', () => {
+ let mockFsExistsSync: Mocked<typeof fs.existsSync>;
+ let mockStripJsonComments: Mocked<typeof stripJsonComments>;
+ let mockFsWriteFileSync: Mocked<typeof fs.writeFileSync>;
+
+ beforeEach(() => {
+ vi.resetAllMocks();
+ mockFsExistsSync = vi.mocked(fs.existsSync);
+ mockStripJsonComments = vi.mocked(stripJsonComments);
+ mockFsWriteFileSync = vi.mocked(fs.writeFileSync);
+ vi.mocked(osActual.homedir).mockReturnValue('/mock/home/user');
+ (mockStripJsonComments as unknown as Mock).mockImplementation(
+ (jsonString: string) => jsonString,
+ );
+ (mockFsExistsSync as Mock).mockReturnValue(false);
+ (fs.readFileSync as Mock).mockReturnValue('{}');
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
+ it('should load empty rules if no files exist', () => {
+ const { rules, errors } = loadTrustedFolders();
+ expect(rules).toEqual([]);
+ expect(errors).toEqual([]);
+ });
+
+ it('should load user rules if only user file exists', () => {
+ const userPath = USER_TRUSTED_FOLDERS_PATH;
+ (mockFsExistsSync as Mock).mockImplementation((p) => p === userPath);
+ const userContent = {
+ '/user/folder': TrustLevel.TRUST_FOLDER,
+ };
+ (fs.readFileSync as Mock).mockImplementation((p) => {
+ if (p === userPath) return JSON.stringify(userContent);
+ return '{}';
+ });
+
+ const { rules, errors } = loadTrustedFolders();
+ expect(rules).toEqual([
+ { path: '/user/folder', trustLevel: TrustLevel.TRUST_FOLDER },
+ ]);
+ expect(errors).toEqual([]);
+ });
+
+ it('should handle JSON parsing errors gracefully', () => {
+ const userPath = USER_TRUSTED_FOLDERS_PATH;
+ (mockFsExistsSync as Mock).mockImplementation((p) => p === userPath);
+ (fs.readFileSync as Mock).mockImplementation((p) => {
+ if (p === userPath) return 'invalid json';
+ return '{}';
+ });
+
+ const { rules, errors } = loadTrustedFolders();
+ expect(rules).toEqual([]);
+ expect(errors.length).toBe(1);
+ expect(errors[0].path).toBe(userPath);
+ expect(errors[0].message).toContain('Unexpected token');
+ });
+
+ it('setValue should update the user config and save it', () => {
+ const loadedFolders = loadTrustedFolders();
+ loadedFolders.setValue('/new/path', TrustLevel.TRUST_FOLDER);
+
+ expect(loadedFolders.user.config['/new/path']).toBe(
+ TrustLevel.TRUST_FOLDER,
+ );
+ expect(mockFsWriteFileSync).toHaveBeenCalledWith(
+ USER_TRUSTED_FOLDERS_PATH,
+ JSON.stringify({ '/new/path': TrustLevel.TRUST_FOLDER }, null, 2),
+ 'utf-8',
+ );
+ });
+});
+
+describe('isWorkspaceTrusted', () => {
+ let mockCwd: string;
+ const mockRules: Record<string, TrustLevel> = {};
+
+ beforeEach(() => {
+ vi.spyOn(process, 'cwd').mockImplementation(() => mockCwd);
+ vi.spyOn(fs, 'readFileSync').mockImplementation((p) => {
+ if (p === USER_TRUSTED_FOLDERS_PATH) {
+ return JSON.stringify(mockRules);
+ }
+ return '{}';
+ });
+ vi.spyOn(fs, 'existsSync').mockImplementation(
+ (p) => p === USER_TRUSTED_FOLDERS_PATH,
+ );
+ });
+
+ afterEach(() => {
+ vi.restoreAllMocks();
+ // Clear the object
+ Object.keys(mockRules).forEach((key) => delete mockRules[key]);
+ });
+
+ it('should return true for a directly trusted folder', () => {
+ mockCwd = '/home/user/projectA';
+ mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
+ expect(isWorkspaceTrusted()).toBe(true);
+ });
+
+ it('should return true for a child of a trusted folder', () => {
+ mockCwd = '/home/user/projectA/src';
+ mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
+ expect(isWorkspaceTrusted()).toBe(true);
+ });
+
+ it('should return true for a child of a trusted parent folder', () => {
+ mockCwd = '/home/user/projectB';
+ mockRules['/home/user/projectB/somefile.txt'] = TrustLevel.TRUST_PARENT;
+ expect(isWorkspaceTrusted()).toBe(true);
+ });
+
+ it('should return false for a directly untrusted folder', () => {
+ mockCwd = '/home/user/untrusted';
+ mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST;
+ expect(isWorkspaceTrusted()).toBe(false);
+ });
+
+ it('should return undefined for a child of an untrusted folder', () => {
+ mockCwd = '/home/user/untrusted/src';
+ mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST;
+ expect(isWorkspaceTrusted()).toBeUndefined();
+ });
+
+ it('should return undefined when no rules match', () => {
+ mockCwd = '/home/user/other';
+ mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
+ mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST;
+ expect(isWorkspaceTrusted()).toBeUndefined();
+ });
+
+ it('should prioritize trust over distrust', () => {
+ mockCwd = '/home/user/projectA/untrusted';
+ mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
+ mockRules['/home/user/projectA/untrusted'] = TrustLevel.DO_NOT_TRUST;
+ expect(isWorkspaceTrusted()).toBe(true);
+ });
+
+ it('should handle path normalization', () => {
+ mockCwd = '/home/user/projectA';
+ mockRules[`/home/user/../user/${path.basename('/home/user/projectA')}`] =
+ TrustLevel.TRUST_FOLDER;
+ expect(isWorkspaceTrusted()).toBe(true);
+ });
+});
diff --git a/packages/cli/src/config/trustedFolders.ts b/packages/cli/src/config/trustedFolders.ts
new file mode 100644
index 00000000..9da27c80
--- /dev/null
+++ b/packages/cli/src/config/trustedFolders.ts
@@ -0,0 +1,158 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import * as fs from 'fs';
+import * as path from 'path';
+import { homedir } from 'os';
+import { getErrorMessage, isWithinRoot } from '@google/gemini-cli-core';
+import stripJsonComments from 'strip-json-comments';
+
+export const TRUSTED_FOLDERS_FILENAME = 'trustedFolders.json';
+export const SETTINGS_DIRECTORY_NAME = '.gemini';
+export const USER_SETTINGS_DIR = path.join(homedir(), SETTINGS_DIRECTORY_NAME);
+export const USER_TRUSTED_FOLDERS_PATH = path.join(
+ USER_SETTINGS_DIR,
+ TRUSTED_FOLDERS_FILENAME,
+);
+
+export enum TrustLevel {
+ TRUST_FOLDER = 'TRUST_FOLDER',
+ TRUST_PARENT = 'TRUST_PARENT',
+ DO_NOT_TRUST = 'DO_NOT_TRUST',
+}
+
+export interface TrustRule {
+ path: string;
+ trustLevel: TrustLevel;
+}
+
+export interface TrustedFoldersError {
+ message: string;
+ path: string;
+}
+
+export interface TrustedFoldersFile {
+ config: Record<string, TrustLevel>;
+ path: string;
+}
+
+export class LoadedTrustedFolders {
+ constructor(
+ public user: TrustedFoldersFile,
+ public errors: TrustedFoldersError[],
+ ) {}
+
+ get rules(): TrustRule[] {
+ return Object.entries(this.user.config).map(([path, trustLevel]) => ({
+ path,
+ trustLevel,
+ }));
+ }
+
+ setValue(path: string, trustLevel: TrustLevel): void {
+ this.user.config[path] = trustLevel;
+ saveTrustedFolders(this.user);
+ }
+}
+
+export function loadTrustedFolders(): LoadedTrustedFolders {
+ const errors: TrustedFoldersError[] = [];
+ const userConfig: Record<string, TrustLevel> = {};
+
+ const userPath = USER_TRUSTED_FOLDERS_PATH;
+
+ // Load user trusted folders
+ try {
+ if (fs.existsSync(userPath)) {
+ const content = fs.readFileSync(userPath, 'utf-8');
+ const parsed = JSON.parse(stripJsonComments(content)) as Record<
+ string,
+ TrustLevel
+ >;
+ if (parsed) {
+ Object.assign(userConfig, parsed);
+ }
+ }
+ } catch (error: unknown) {
+ errors.push({
+ message: getErrorMessage(error),
+ path: userPath,
+ });
+ }
+
+ return new LoadedTrustedFolders(
+ { path: userPath, config: userConfig },
+ errors,
+ );
+}
+
+export function saveTrustedFolders(
+ trustedFoldersFile: TrustedFoldersFile,
+): void {
+ try {
+ // Ensure the directory exists
+ const dirPath = path.dirname(trustedFoldersFile.path);
+ if (!fs.existsSync(dirPath)) {
+ fs.mkdirSync(dirPath, { recursive: true });
+ }
+
+ fs.writeFileSync(
+ trustedFoldersFile.path,
+ JSON.stringify(trustedFoldersFile.config, null, 2),
+ 'utf-8',
+ );
+ } catch (error) {
+ console.error('Error saving trusted folders file:', error);
+ }
+}
+
+export function isWorkspaceTrusted(): boolean | undefined {
+ const { rules, errors } = loadTrustedFolders();
+
+ if (errors.length > 0) {
+ for (const error of errors) {
+ console.error(
+ `Error loading trusted folders config from ${error.path}: ${error.message}`,
+ );
+ }
+ }
+
+ const trustedPaths: string[] = [];
+ const untrustedPaths: string[] = [];
+
+ for (const rule of rules) {
+ switch (rule.trustLevel) {
+ case TrustLevel.TRUST_FOLDER:
+ trustedPaths.push(rule.path);
+ break;
+ case TrustLevel.TRUST_PARENT:
+ trustedPaths.push(path.dirname(rule.path));
+ break;
+ case TrustLevel.DO_NOT_TRUST:
+ untrustedPaths.push(rule.path);
+ break;
+ default:
+ // Do nothing for unknown trust levels.
+ break;
+ }
+ }
+
+ const cwd = process.cwd();
+
+ for (const trustedPath of trustedPaths) {
+ if (isWithinRoot(cwd, trustedPath)) {
+ return true;
+ }
+ }
+
+ for (const untrustedPath of untrustedPaths) {
+ if (path.normalize(cwd) === path.normalize(untrustedPath)) {
+ return false;
+ }
+ }
+
+ return undefined;
+}
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index e8aca549..5d4643e5 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -252,8 +252,10 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
const { isSettingsDialogOpen, openSettingsDialog, closeSettingsDialog } =
useSettingsCommand();
- const { isFolderTrustDialogOpen, handleFolderTrustSelect } =
- useFolderTrust(settings);
+ const { isFolderTrustDialogOpen, handleFolderTrustSelect } = useFolderTrust(
+ settings,
+ config,
+ );
const {
isAuthDialogOpen,
diff --git a/packages/cli/src/ui/hooks/useFolderTrust.test.ts b/packages/cli/src/ui/hooks/useFolderTrust.test.ts
index 61552af0..e565ab05 100644
--- a/packages/cli/src/ui/hooks/useFolderTrust.test.ts
+++ b/packages/cli/src/ui/hooks/useFolderTrust.test.ts
@@ -4,15 +4,33 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { renderHook, act } from '@testing-library/react';
import { vi } from 'vitest';
+import { renderHook, act } from '@testing-library/react';
import { useFolderTrust } from './useFolderTrust.js';
-import { LoadedSettings, SettingScope } from '../../config/settings.js';
+import { type Config } from '@google/gemini-cli-core';
+import { LoadedSettings } from '../../config/settings.js';
import { FolderTrustChoice } from '../components/FolderTrustDialog.js';
+import {
+ LoadedTrustedFolders,
+ TrustLevel,
+} from '../../config/trustedFolders.js';
+import * as process from 'process';
+
+import * as trustedFolders from '../../config/trustedFolders.js';
+
+vi.mock('process', () => ({
+ cwd: vi.fn(),
+ platform: 'linux',
+}));
describe('useFolderTrust', () => {
- it('should set isFolderTrustDialogOpen to true when folderTrustFeature is true and folderTrust is undefined', () => {
- const settings = {
+ let mockSettings: LoadedSettings;
+ let mockConfig: Config;
+ let mockTrustedFolders: LoadedTrustedFolders;
+ let loadTrustedFoldersSpy: vi.SpyInstance;
+
+ beforeEach(() => {
+ mockSettings = {
merged: {
folderTrustFeature: true,
folderTrust: undefined,
@@ -20,59 +38,110 @@ describe('useFolderTrust', () => {
setValue: vi.fn(),
} as unknown as LoadedSettings;
- const { result } = renderHook(() => useFolderTrust(settings));
+ mockConfig = {
+ isTrustedFolder: vi.fn().mockReturnValue(undefined),
+ } as unknown as Config;
- expect(result.current.isFolderTrustDialogOpen).toBe(true);
+ mockTrustedFolders = {
+ setValue: vi.fn(),
+ } as unknown as LoadedTrustedFolders;
+
+ loadTrustedFoldersSpy = vi
+ .spyOn(trustedFolders, 'loadTrustedFolders')
+ .mockReturnValue(mockTrustedFolders);
+ (process.cwd as vi.Mock).mockReturnValue('/test/path');
});
- it('should set isFolderTrustDialogOpen to false when folderTrustFeature is false', () => {
- const settings = {
- merged: {
- folderTrustFeature: false,
- folderTrust: undefined,
- },
- setValue: vi.fn(),
- } as unknown as LoadedSettings;
+ afterEach(() => {
+ vi.clearAllMocks();
+ });
- const { result } = renderHook(() => useFolderTrust(settings));
+ it('should not open dialog when folder is already trusted', () => {
+ (mockConfig.isTrustedFolder as vi.Mock).mockReturnValue(true);
+ const { result } = renderHook(() =>
+ useFolderTrust(mockSettings, mockConfig),
+ );
+ expect(result.current.isFolderTrustDialogOpen).toBe(false);
+ });
+ it('should not open dialog when folder is already untrusted', () => {
+ (mockConfig.isTrustedFolder as vi.Mock).mockReturnValue(false);
+ const { result } = renderHook(() =>
+ useFolderTrust(mockSettings, mockConfig),
+ );
expect(result.current.isFolderTrustDialogOpen).toBe(false);
});
- it('should set isFolderTrustDialogOpen to false when folderTrust is defined', () => {
- const settings = {
- merged: {
- folderTrustFeature: true,
- folderTrust: true,
- },
- setValue: vi.fn(),
- } as unknown as LoadedSettings;
+ it('should open dialog when folder trust is undefined', () => {
+ (mockConfig.isTrustedFolder as vi.Mock).mockReturnValue(undefined);
+ const { result } = renderHook(() =>
+ useFolderTrust(mockSettings, mockConfig),
+ );
+ expect(result.current.isFolderTrustDialogOpen).toBe(true);
+ });
+
+ it('should handle TRUST_FOLDER choice', () => {
+ const { result } = renderHook(() =>
+ useFolderTrust(mockSettings, mockConfig),
+ );
- const { result } = renderHook(() => useFolderTrust(settings));
+ act(() => {
+ result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_FOLDER);
+ });
+ expect(loadTrustedFoldersSpy).toHaveBeenCalled();
+ expect(mockTrustedFolders.setValue).toHaveBeenCalledWith(
+ '/test/path',
+ TrustLevel.TRUST_FOLDER,
+ );
expect(result.current.isFolderTrustDialogOpen).toBe(false);
});
- it('should call setValue and set isFolderTrustDialogOpen to false on handleFolderTrustSelect', () => {
- const settings = {
- merged: {
- folderTrustFeature: true,
- folderTrust: undefined,
- },
- setValue: vi.fn(),
- } as unknown as LoadedSettings;
+ it('should handle TRUST_PARENT choice', () => {
+ const { result } = renderHook(() =>
+ useFolderTrust(mockSettings, mockConfig),
+ );
+
+ act(() => {
+ result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_PARENT);
+ });
- const { result } = renderHook(() => useFolderTrust(settings));
+ expect(mockTrustedFolders.setValue).toHaveBeenCalledWith(
+ '/test/path',
+ TrustLevel.TRUST_PARENT,
+ );
+ expect(result.current.isFolderTrustDialogOpen).toBe(false);
+ });
+
+ it('should handle DO_NOT_TRUST choice', () => {
+ const { result } = renderHook(() =>
+ useFolderTrust(mockSettings, mockConfig),
+ );
act(() => {
- result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_FOLDER);
+ result.current.handleFolderTrustSelect(FolderTrustChoice.DO_NOT_TRUST);
});
- expect(settings.setValue).toHaveBeenCalledWith(
- SettingScope.User,
- 'folderTrust',
- true,
+ expect(mockTrustedFolders.setValue).toHaveBeenCalledWith(
+ '/test/path',
+ TrustLevel.DO_NOT_TRUST,
);
expect(result.current.isFolderTrustDialogOpen).toBe(false);
});
+
+ it('should do nothing for default choice', () => {
+ const { result } = renderHook(() =>
+ useFolderTrust(mockSettings, mockConfig),
+ );
+
+ act(() => {
+ result.current.handleFolderTrustSelect(
+ 'invalid_choice' as FolderTrustChoice,
+ );
+ });
+
+ expect(mockTrustedFolders.setValue).not.toHaveBeenCalled();
+ expect(mockSettings.setValue).not.toHaveBeenCalled();
+ expect(result.current.isFolderTrustDialogOpen).toBe(true);
+ });
});
diff --git a/packages/cli/src/ui/hooks/useFolderTrust.ts b/packages/cli/src/ui/hooks/useFolderTrust.ts
index 90a69132..6458d4aa 100644
--- a/packages/cli/src/ui/hooks/useFolderTrust.ts
+++ b/packages/cli/src/ui/hooks/useFolderTrust.ts
@@ -5,24 +5,39 @@
*/
import { useState, useCallback } from 'react';
-import { LoadedSettings, SettingScope } from '../../config/settings.js';
+import { type Config } from '@google/gemini-cli-core';
+import { LoadedSettings } from '../../config/settings.js';
import { FolderTrustChoice } from '../components/FolderTrustDialog.js';
+import { loadTrustedFolders, TrustLevel } from '../../config/trustedFolders.js';
+import * as process from 'process';
-export const useFolderTrust = (settings: LoadedSettings) => {
+export const useFolderTrust = (settings: LoadedSettings, config: Config) => {
const [isFolderTrustDialogOpen, setIsFolderTrustDialogOpen] = useState(
- !!settings.merged.folderTrustFeature &&
- // TODO: Update to avoid showing dialog for folders that are trusted.
- settings.merged.folderTrust === undefined,
+ config.isTrustedFolder() === undefined,
);
- const handleFolderTrustSelect = useCallback(
- (_choice: FolderTrustChoice) => {
- // TODO: Store folderPath in the trusted folders config file based on the choice.
- settings.setValue(SettingScope.User, 'folderTrust', true);
- setIsFolderTrustDialogOpen(false);
- },
- [settings],
- );
+ const handleFolderTrustSelect = useCallback((choice: FolderTrustChoice) => {
+ const trustedFolders = loadTrustedFolders();
+ const cwd = process.cwd();
+ let trustLevel: TrustLevel;
+
+ switch (choice) {
+ case FolderTrustChoice.TRUST_FOLDER:
+ trustLevel = TrustLevel.TRUST_FOLDER;
+ break;
+ case FolderTrustChoice.TRUST_PARENT:
+ trustLevel = TrustLevel.TRUST_PARENT;
+ break;
+ case FolderTrustChoice.DO_NOT_TRUST:
+ trustLevel = TrustLevel.DO_NOT_TRUST;
+ break;
+ default:
+ return;
+ }
+
+ trustedFolders.setValue(cwd, trustLevel);
+ setIsFolderTrustDialogOpen(false);
+ }, []);
return {
isFolderTrustDialogOpen,
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index 069a486d..7c61f239 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -197,6 +197,7 @@ export interface ConfigParameters {
loadMemoryFromIncludeDirectories?: boolean;
chatCompression?: ChatCompressionSettings;
interactive?: boolean;
+ trustedFolder?: boolean;
}
export class Config {
@@ -260,6 +261,7 @@ export class Config {
private readonly loadMemoryFromIncludeDirectories: boolean = false;
private readonly chatCompression: ChatCompressionSettings | undefined;
private readonly interactive: boolean;
+ private readonly trustedFolder: boolean | undefined;
private initialized: boolean = false;
constructor(params: ConfigParameters) {
@@ -324,6 +326,7 @@ export class Config {
params.loadMemoryFromIncludeDirectories ?? false;
this.chatCompression = params.chatCompression;
this.interactive = params.interactive ?? false;
+ this.trustedFolder = params.trustedFolder;
if (params.contextFileName) {
setGeminiMdFilename(params.contextFileName);
@@ -664,6 +667,10 @@ export class Config {
return this.folderTrust;
}
+ isTrustedFolder(): boolean | undefined {
+ return this.trustedFolder;
+ }
+
setIdeMode(value: boolean): void {
this.ideMode = value;
}