diff --git a/packages/cli/src/config/settings.test.ts b/packages/cli/src/config/settings.test.ts index c82cb1a9ac..09655499e5 100644 --- a/packages/cli/src/config/settings.test.ts +++ b/packages/cli/src/config/settings.test.ts @@ -34,7 +34,7 @@ vi.mock('./trustedFolders.js', () => ({ })); // NOW import everything else, including the (now effectively re-exported) settings.js -import * as pathActual from 'node:path'; // Restored for MOCK_WORKSPACE_SETTINGS_PATH +import path, * as pathActual from 'node:path'; // Restored for MOCK_WORKSPACE_SETTINGS_PATH import { describe, it, @@ -58,7 +58,9 @@ import { SETTINGS_DIRECTORY_NAME, // This is from the original module, but used by the mock. migrateSettingsToV1, type Settings, + loadEnvironment, } from './settings.js'; +import { GEMINI_DIR } from '@google/gemini-cli-core'; const MOCK_WORKSPACE_DIR = '/mock/workspace'; // Use the (mocked) SETTINGS_DIRECTORY_NAME for consistency @@ -2363,4 +2365,54 @@ describe('Settings Loading and Merging', () => { }); }); }); + + describe('loadEnvironment', () => { + function setup({ + isFolderTrustEnabled = true, + isWorkspaceTrustedValue = true, + }) { + delete process.env['TESTTEST']; // reset + const geminiEnvPath = path.resolve(path.join(GEMINI_DIR, '.env')); + + vi.mocked(isWorkspaceTrusted).mockReturnValue(isWorkspaceTrustedValue); + (mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) => + [USER_SETTINGS_PATH, geminiEnvPath].includes(p.toString()), + ); + const userSettingsContent: Settings = { + ui: { + theme: 'dark', + }, + security: { + folderTrust: { + enabled: isFolderTrustEnabled, + }, + }, + context: { + fileName: 'USER_CONTEXT.md', + }, + }; + (fs.readFileSync as Mock).mockImplementation( + (p: fs.PathOrFileDescriptor) => { + if (p === USER_SETTINGS_PATH) + return JSON.stringify(userSettingsContent); + if (p === geminiEnvPath) return 'TESTTEST=1234'; + return '{}'; + }, + ); + } + + it('sets environment variables from .env files', () => { + setup({ isFolderTrustEnabled: false, isWorkspaceTrustedValue: true }); + loadEnvironment(loadSettings(MOCK_WORKSPACE_DIR).merged); + + expect(process.env['TESTTEST']).toEqual('1234'); + }); + + it('does not load env files from untrusted spaces', () => { + setup({ isFolderTrustEnabled: true, isWorkspaceTrustedValue: false }); + loadEnvironment(loadSettings(MOCK_WORKSPACE_DIR).merged); + + expect(process.env['TESTTEST']).not.toEqual('1234'); + }); + }); }); diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 1fc2c60f6c..4058c8a35e 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -556,6 +556,10 @@ export function setUpCloudShellEnvironment(envFilePath: string | null): void { export function loadEnvironment(settings: Settings): void { const envFilePath = findEnvFile(process.cwd()); + if (!isWorkspaceTrusted(settings)) { + return; + } + // Cloud Shell environment variable handling if (process.env['CLOUD_SHELL'] === 'true') { setUpCloudShellEnvironment(envFilePath); diff --git a/packages/cli/src/config/trustedFolders.test.ts b/packages/cli/src/config/trustedFolders.test.ts index b6583a8362..bf03682f59 100644 --- a/packages/cli/src/config/trustedFolders.test.ts +++ b/packages/cli/src/config/trustedFolders.test.ts @@ -80,6 +80,52 @@ describe('Trusted Folders Loading', () => { expect(errors).toEqual([]); }); + describe('isPathTrusted', () => { + function setup({ config = {} as Record } = {}) { + (mockFsExistsSync as Mock).mockImplementation( + (p) => p === USER_TRUSTED_FOLDERS_PATH, + ); + (fs.readFileSync as Mock).mockImplementation((p) => { + if (p === USER_TRUSTED_FOLDERS_PATH) return JSON.stringify(config); + return '{}'; + }); + + const folders = loadTrustedFolders(); + + return { folders }; + } + + it('provides a method to determine if a path is trusted', () => { + const { folders } = setup({ + config: { + './myfolder': TrustLevel.TRUST_FOLDER, + '/trustedparent/trustme': TrustLevel.TRUST_PARENT, + '/user/folder': TrustLevel.TRUST_FOLDER, + '/secret': TrustLevel.DO_NOT_TRUST, + '/secret/publickeys': TrustLevel.TRUST_FOLDER, + }, + }); + expect(folders.isPathTrusted('/secret')).toBe(false); + expect(folders.isPathTrusted('/user/folder')).toBe(true); + expect(folders.isPathTrusted('/secret/publickeys/public.pem')).toBe(true); + expect(folders.isPathTrusted('/user/folder/harhar')).toBe(true); + expect(folders.isPathTrusted('myfolder/somefile.jpg')).toBe(true); + expect(folders.isPathTrusted('/trustedparent/someotherfolder')).toBe( + true, + ); + expect(folders.isPathTrusted('/trustedparent/trustme')).toBe(true); + + // No explicit rule covers this file + expect(folders.isPathTrusted('/secret/bankaccounts.json')).toBe( + undefined, + ); + expect(folders.isPathTrusted('/secret/mine/privatekey.pem')).toBe( + undefined, + ); + expect(folders.isPathTrusted('/user/someotherfolder')).toBe(undefined); + }); + }); + it('should load user rules if only user file exists', () => { const userPath = USER_TRUSTED_FOLDERS_PATH; (mockFsExistsSync as Mock).mockImplementation((p) => p === userPath); diff --git a/packages/cli/src/config/trustedFolders.ts b/packages/cli/src/config/trustedFolders.ts index 8763c769f7..e698822186 100644 --- a/packages/cli/src/config/trustedFolders.ts +++ b/packages/cli/src/config/trustedFolders.ts @@ -42,8 +42,8 @@ export interface TrustedFoldersFile { export class LoadedTrustedFolders { constructor( - public user: TrustedFoldersFile, - public errors: TrustedFoldersError[], + readonly user: TrustedFoldersFile, + readonly errors: TrustedFoldersError[], ) {} get rules(): TrustRule[] { @@ -53,6 +53,49 @@ export class LoadedTrustedFolders { })); } + /** + * Returns true or false if the path should be "trusted". This function + * should only be invoked when the folder trust setting is active. + * + * @param location path + * @returns + */ + isPathTrusted(location: string): boolean | undefined { + const trustedPaths: string[] = []; + const untrustedPaths: string[] = []; + + for (const rule of this.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; + } + } + + for (const trustedPath of trustedPaths) { + if (isWithinRoot(location, trustedPath)) { + return true; + } + } + + for (const untrustedPath of untrustedPaths) { + if (path.normalize(location) === path.normalize(untrustedPath)) { + return false; + } + } + + return undefined; + } + setValue(path: string, trustLevel: TrustLevel): void { this.user.config[path] = trustLevel; saveTrustedFolders(this.user); @@ -110,59 +153,28 @@ export function saveTrustedFolders( } } -export function isWorkspaceTrusted(settings: Settings): boolean | undefined { +/** Is folder trust feature enabled per the current applied settings */ +export function isFolderTrustEnabled(settings: Settings): boolean { const folderTrustFeature = settings.security?.folderTrust?.featureEnabled ?? false; const folderTrustSetting = settings.security?.folderTrust?.enabled ?? true; - const folderTrustEnabled = folderTrustFeature && folderTrustSetting; + return folderTrustFeature && folderTrustSetting; +} - if (!folderTrustEnabled) { +export function isWorkspaceTrusted(settings: Settings): boolean | undefined { + if (!isFolderTrustEnabled(settings)) { return true; } - const { rules, errors } = loadTrustedFolders(); + const folders = loadTrustedFolders(); - if (errors.length > 0) { - for (const error of errors) { + if (folders.errors.length > 0) { + for (const error of folders.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; + return folders.isPathTrusted(process.cwd()); }