From 38876b738f4c9ef8bd1b839d5e33580486e9a089 Mon Sep 17 00:00:00 2001 From: shrutip90 Date: Wed, 13 Aug 2025 11:06:31 -0700 Subject: [PATCH] Add support for trustedFolders.json config file (#6073) --- packages/cli/src/config/config.test.ts | 123 +++++++++++ packages/cli/src/config/config.ts | 6 +- .../cli/src/config/trustedFolders.test.ts | 203 ++++++++++++++++++ packages/cli/src/config/trustedFolders.ts | 158 ++++++++++++++ packages/cli/src/ui/App.tsx | 6 +- .../cli/src/ui/hooks/useFolderTrust.test.ts | 163 ++++++++++---- packages/cli/src/ui/hooks/useFolderTrust.ts | 41 ++-- packages/core/src/config/config.ts | 7 + 8 files changed, 644 insertions(+), 63 deletions(-) create mode 100644 packages/cli/src/config/trustedFolders.test.ts create mode 100644 packages/cli/src/config/trustedFolders.ts 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(); @@ -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(); + 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(); + 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; + let mockStripJsonComments: Mocked; + let mockFsWriteFileSync: Mocked; + + 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 = {}; + + 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; + 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 = {}; + + 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; + mockTrustedFolders = { + setValue: vi.fn(), + } as unknown as LoadedTrustedFolders; + + loadTrustedFoldersSpy = vi + .spyOn(trustedFolders, 'loadTrustedFolders') + .mockReturnValue(mockTrustedFolders); + (process.cwd as vi.Mock).mockReturnValue('/test/path'); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + 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 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 set isFolderTrustDialogOpen to false when folderTrustFeature is false', () => { - const settings = { - merged: { - folderTrustFeature: false, - folderTrust: undefined, - }, - setValue: vi.fn(), - } as unknown as LoadedSettings; - - const { result } = renderHook(() => useFolderTrust(settings)); - - 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; - - const { result } = renderHook(() => useFolderTrust(settings)); - - 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; - - const { result } = renderHook(() => useFolderTrust(settings)); + it('should handle TRUST_FOLDER choice', () => { + const { result } = renderHook(() => + useFolderTrust(mockSettings, mockConfig), + ); act(() => { result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_FOLDER); }); - expect(settings.setValue).toHaveBeenCalledWith( - SettingScope.User, - 'folderTrust', - true, + expect(loadTrustedFoldersSpy).toHaveBeenCalled(); + expect(mockTrustedFolders.setValue).toHaveBeenCalledWith( + '/test/path', + TrustLevel.TRUST_FOLDER, ); expect(result.current.isFolderTrustDialogOpen).toBe(false); }); + + it('should handle TRUST_PARENT choice', () => { + const { result } = renderHook(() => + useFolderTrust(mockSettings, mockConfig), + ); + + act(() => { + result.current.handleFolderTrustSelect(FolderTrustChoice.TRUST_PARENT); + }); + + 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.DO_NOT_TRUST); + }); + + 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; }