diff --git a/packages/a2a-server/src/config.ts b/packages/a2a-server/src/config.ts index 0aacdbe15f..cfbee1bcc8 100644 --- a/packages/a2a-server/src/config.ts +++ b/packages/a2a-server/src/config.ts @@ -79,10 +79,10 @@ export async function loadConfig( false, fileService, extensionContextFilePaths, + true, /// TODO: Wire up folder trust logic here. ); configParams.userMemory = memoryContent; configParams.geminiMdFileCount = fileCount; - const config = new Config({ ...configParams, }); diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 7403277f35..79cf6d29c7 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -723,6 +723,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => { '/path/to/ext3/context1.md', '/path/to/ext3/context2.md', ], + true, 'tree', { respectGitIgnore: false, diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 9fab419108..2703517819 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -333,6 +333,7 @@ export async function loadHierarchicalGeminiMemory( fileService: FileDiscoveryService, settings: Settings, extensionContextFilePaths: string[] = [], + folderTrust: boolean, memoryImportFormat: 'flat' | 'tree' = 'tree', fileFilteringOptions?: FileFilteringOptions, ): Promise<{ memoryContent: string; fileCount: number }> { @@ -358,6 +359,7 @@ export async function loadHierarchicalGeminiMemory( debugMode, fileService, extensionContextFilePaths, + folderTrust, memoryImportFormat, fileFilteringOptions, settings.context?.discoveryMaxDirs, @@ -385,7 +387,7 @@ export async function loadCliConfig( settings.security?.folderTrust?.featureEnabled ?? false; const folderTrustSetting = settings.security?.folderTrust?.enabled ?? true; const folderTrust = folderTrustFeature && folderTrustSetting; - const trustedFolder = isWorkspaceTrusted(settings); + const trustedFolder = isWorkspaceTrusted(settings) ?? true; const allExtensions = annotateActiveExtensions( extensions, @@ -433,6 +435,7 @@ export async function loadCliConfig( fileService, settings, extensionContextFilePaths, + trustedFolder, memoryImportFormat, fileFiltering, ); diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index 42323fa129..8cb4eed67c 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -368,6 +368,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { config.getFileService(), settings.merged, config.getExtensionContextFilePaths(), + config.getFolderTrust(), settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree' config.getFileFilteringOptions(), ); diff --git a/packages/cli/src/ui/commands/directoryCommand.tsx b/packages/cli/src/ui/commands/directoryCommand.tsx index cb9b4fc4a6..966ad2eb1d 100644 --- a/packages/cli/src/ui/commands/directoryCommand.tsx +++ b/packages/cli/src/ui/commands/directoryCommand.tsx @@ -104,6 +104,7 @@ export const directoryCommand: SlashCommand = { config.getDebugMode(), config.getFileService(), config.getExtensionContextFilePaths(), + config.getFolderTrust(), context.services.settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree' config.getFileFilteringOptions(), diff --git a/packages/cli/src/ui/commands/memoryCommand.test.ts b/packages/cli/src/ui/commands/memoryCommand.test.ts index 6b27d23a6b..418320c231 100644 --- a/packages/cli/src/ui/commands/memoryCommand.test.ts +++ b/packages/cli/src/ui/commands/memoryCommand.test.ts @@ -16,6 +16,7 @@ import { loadServerHierarchicalMemory, type FileDiscoveryService, } from '@google/gemini-cli-core'; +import type { LoadServerHierarchicalMemoryResponse } from '@google/gemini-cli-core/index.js'; vi.mock('@google/gemini-cli-core', async (importOriginal) => { const original = @@ -170,6 +171,7 @@ describe('memoryCommand', () => { ignore: [], include: [], }), + getFolderTrust: () => false, }; mockContext = createMockCommandContext({ @@ -188,7 +190,7 @@ describe('memoryCommand', () => { it('should display success message when memory is refreshed with content', async () => { if (!refreshCommand.action) throw new Error('Command has no action'); - const refreshResult = { + const refreshResult: LoadServerHierarchicalMemoryResponse = { memoryContent: 'new memory content', fileCount: 2, }; diff --git a/packages/cli/src/ui/commands/memoryCommand.ts b/packages/cli/src/ui/commands/memoryCommand.ts index f00c08b0e7..6c418cdb80 100644 --- a/packages/cli/src/ui/commands/memoryCommand.ts +++ b/packages/cli/src/ui/commands/memoryCommand.ts @@ -92,6 +92,7 @@ export const memoryCommand: SlashCommand = { config.getDebugMode(), config.getFileService(), config.getExtensionContextFilePaths(), + config.getFolderTrust(), context.services.settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree' config.getFileFilteringOptions(), diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 2f052e30bf..2a127ef0c3 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -726,6 +726,10 @@ export class Config { return this.folderTrustFeature; } + /** + * Returns 'true' if the workspace is considered "trusted". + * 'false' for untrusted. + */ getFolderTrust(): boolean { return this.folderTrust; } diff --git a/packages/core/src/utils/memoryDiscovery.test.ts b/packages/core/src/utils/memoryDiscovery.test.ts index eea137aa11..67b6d4936e 100644 --- a/packages/core/src/utils/memoryDiscovery.test.ts +++ b/packages/core/src/utils/memoryDiscovery.test.ts @@ -10,11 +10,11 @@ import * as os from 'node:os'; import * as path from 'node:path'; import { loadServerHierarchicalMemory } from './memoryDiscovery.js'; import { - GEMINI_CONFIG_DIR, setGeminiMdFilename, DEFAULT_CONTEXT_FILENAME, } from '../tools/memoryTool.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; +import { GEMINI_DIR } from './paths.js'; vi.mock('os', async (importOriginal) => { const actualOs = await importOriginal(); @@ -25,6 +25,7 @@ vi.mock('os', async (importOriginal) => { }); describe('loadServerHierarchicalMemory', () => { + const DEFAULT_FOLDER_TRUST = true; let testRootDir: string; let cwd: string; let projectRoot: string; @@ -65,12 +66,62 @@ describe('loadServerHierarchicalMemory', () => { await fsPromises.rm(testRootDir, { recursive: true, force: true }); }); + describe('when untrusted', () => { + it('does not load context files from untrusted workspaces', async () => { + await createTestFile( + path.join(projectRoot, DEFAULT_CONTEXT_FILENAME), + 'Project root memory', + ); + await createTestFile( + path.join(cwd, DEFAULT_CONTEXT_FILENAME), + 'Src directory memory', + ); + const { fileCount } = await loadServerHierarchicalMemory( + cwd, + [], + false, + new FileDiscoveryService(projectRoot), + [], + false, // untrusted + ); + + expect(fileCount).toEqual(0); + }); + + it('loads context from outside the untrusted workspace', async () => { + await createTestFile( + path.join(projectRoot, DEFAULT_CONTEXT_FILENAME), + 'Project root memory', + ); // Untrusted + await createTestFile( + path.join(cwd, DEFAULT_CONTEXT_FILENAME), + 'Src directory memory', + ); // Untrusted + + const filepath = path.join(homedir, GEMINI_DIR, DEFAULT_CONTEXT_FILENAME); + await createTestFile(filepath, 'default context content'); // In user home dir (outside untrusted space). + const { fileCount, memoryContent } = await loadServerHierarchicalMemory( + cwd, + [], + false, + new FileDiscoveryService(projectRoot), + [], + false, // untrusted + ); + + expect(fileCount).toEqual(1); + expect(memoryContent).toContain(path.relative(cwd, filepath).toString()); + }); + }); + it('should return empty memory and count if no context files are found', async () => { const result = await loadServerHierarchicalMemory( cwd, [], false, new FileDiscoveryService(projectRoot), + [], + DEFAULT_FOLDER_TRUST, ); expect(result).toEqual({ @@ -81,7 +132,7 @@ describe('loadServerHierarchicalMemory', () => { it('should load only the global context file if present and others are not (default filename)', async () => { const defaultContextFile = await createTestFile( - path.join(homedir, GEMINI_CONFIG_DIR, DEFAULT_CONTEXT_FILENAME), + path.join(homedir, GEMINI_DIR, DEFAULT_CONTEXT_FILENAME), 'default context content', ); @@ -90,6 +141,8 @@ describe('loadServerHierarchicalMemory', () => { [], false, new FileDiscoveryService(projectRoot), + [], + DEFAULT_FOLDER_TRUST, ); expect(result).toEqual({ @@ -103,7 +156,7 @@ describe('loadServerHierarchicalMemory', () => { setGeminiMdFilename(customFilename); const customContextFile = await createTestFile( - path.join(homedir, GEMINI_CONFIG_DIR, customFilename), + path.join(homedir, GEMINI_DIR, customFilename), 'custom context content', ); @@ -112,6 +165,8 @@ describe('loadServerHierarchicalMemory', () => { [], false, new FileDiscoveryService(projectRoot), + [], + DEFAULT_FOLDER_TRUST, ); expect(result).toEqual({ @@ -138,6 +193,8 @@ describe('loadServerHierarchicalMemory', () => { [], false, new FileDiscoveryService(projectRoot), + [], + DEFAULT_FOLDER_TRUST, ); expect(result).toEqual({ @@ -161,6 +218,8 @@ describe('loadServerHierarchicalMemory', () => { [], false, new FileDiscoveryService(projectRoot), + [], + DEFAULT_FOLDER_TRUST, ); expect(result).toEqual({ @@ -184,6 +243,8 @@ describe('loadServerHierarchicalMemory', () => { [], false, new FileDiscoveryService(projectRoot), + [], + DEFAULT_FOLDER_TRUST, ); expect(result).toEqual({ @@ -207,6 +268,8 @@ describe('loadServerHierarchicalMemory', () => { [], false, new FileDiscoveryService(projectRoot), + [], + DEFAULT_FOLDER_TRUST, ); expect(result).toEqual({ @@ -217,7 +280,7 @@ describe('loadServerHierarchicalMemory', () => { it('should load and correctly order global, upward, and downward ORIGINAL_GEMINI_MD_FILENAME files', async () => { const defaultContextFile = await createTestFile( - path.join(homedir, GEMINI_CONFIG_DIR, DEFAULT_CONTEXT_FILENAME), + path.join(homedir, GEMINI_DIR, DEFAULT_CONTEXT_FILENAME), 'default context content', ); const rootGeminiFile = await createTestFile( @@ -242,6 +305,8 @@ describe('loadServerHierarchicalMemory', () => { [], false, new FileDiscoveryService(projectRoot), + [], + DEFAULT_FOLDER_TRUST, ); expect(result).toEqual({ @@ -269,6 +334,7 @@ describe('loadServerHierarchicalMemory', () => { false, new FileDiscoveryService(projectRoot), [], + DEFAULT_FOLDER_TRUST, 'tree', { respectGitIgnore: true, @@ -299,6 +365,7 @@ describe('loadServerHierarchicalMemory', () => { true, new FileDiscoveryService(projectRoot), [], + DEFAULT_FOLDER_TRUST, 'tree', // importFormat { respectGitIgnore: true, @@ -319,6 +386,8 @@ describe('loadServerHierarchicalMemory', () => { [], false, new FileDiscoveryService(projectRoot), + [], + DEFAULT_FOLDER_TRUST, ); expect(result).toEqual({ @@ -339,6 +408,7 @@ describe('loadServerHierarchicalMemory', () => { false, new FileDiscoveryService(projectRoot), [extensionFilePath], + DEFAULT_FOLDER_TRUST, ); expect(result).toEqual({ @@ -361,6 +431,8 @@ describe('loadServerHierarchicalMemory', () => { [includedDir], false, new FileDiscoveryService(projectRoot), + [], + DEFAULT_FOLDER_TRUST, ); expect(result).toEqual({ @@ -391,6 +463,8 @@ describe('loadServerHierarchicalMemory', () => { createdFiles.map((f) => path.dirname(f)), false, new FileDiscoveryService(projectRoot), + [], + DEFAULT_FOLDER_TRUST, ); // Should have loaded all files @@ -422,6 +496,8 @@ describe('loadServerHierarchicalMemory', () => { [childDir, parentDir], // Deliberately include duplicates false, new FileDiscoveryService(projectRoot), + [], + DEFAULT_FOLDER_TRUST, ); // Should have both files without duplicates diff --git a/packages/core/src/utils/memoryDiscovery.ts b/packages/core/src/utils/memoryDiscovery.ts index c76dd70e6b..dc7ce0c22a 100644 --- a/packages/core/src/utils/memoryDiscovery.ts +++ b/packages/core/src/utils/memoryDiscovery.ts @@ -9,14 +9,12 @@ import * as fsSync from 'node:fs'; import * as path from 'node:path'; import { homedir } from 'node:os'; import { bfsFileSearch } from './bfsFileSearch.js'; -import { - GEMINI_CONFIG_DIR, - getAllGeminiMdFilenames, -} from '../tools/memoryTool.js'; +import { getAllGeminiMdFilenames } from '../tools/memoryTool.js'; import type { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import { processImports } from './memoryImportProcessor.js'; import type { FileFilteringOptions } from '../config/config.js'; import { DEFAULT_MEMORY_FILE_FILTERING_OPTIONS } from '../config/config.js'; +import { GEMINI_DIR } from './paths.js'; // Simple console logger, similar to the one previously in CLI's config.ts // TODO: Integrate with a more robust server-side logger if available/appropriate. @@ -87,6 +85,7 @@ async function getGeminiMdFilePathsInternal( debugMode: boolean, fileService: FileDiscoveryService, extensionContextFilePaths: string[] = [], + folderTrust: boolean, fileFilteringOptions: FileFilteringOptions, maxDirs: number, ): Promise { @@ -109,6 +108,7 @@ async function getGeminiMdFilePathsInternal( debugMode, fileService, extensionContextFilePaths, + folderTrust, fileFilteringOptions, maxDirs, ), @@ -138,6 +138,7 @@ async function getGeminiMdFilePathsInternalForEachDir( debugMode: boolean, fileService: FileDiscoveryService, extensionContextFilePaths: string[] = [], + folderTrust: boolean, fileFilteringOptions: FileFilteringOptions, maxDirs: number, ): Promise { @@ -148,7 +149,7 @@ async function getGeminiMdFilePathsInternalForEachDir( const resolvedHome = path.resolve(userHomePath); const globalMemoryPath = path.join( resolvedHome, - GEMINI_CONFIG_DIR, + GEMINI_DIR, geminiMdFilename, ); @@ -166,7 +167,7 @@ async function getGeminiMdFilePathsInternalForEachDir( // FIX: Only perform the workspace search (upward and downward scans) // if a valid currentWorkingDirectory is provided. - if (dir) { + if (dir && folderTrust) { const resolvedCwd = path.resolve(dir); if (debugMode) logger.debug( @@ -184,7 +185,7 @@ async function getGeminiMdFilePathsInternalForEachDir( : path.dirname(resolvedHome); while (currentDir && currentDir !== path.dirname(currentDir)) { - if (currentDir === path.join(resolvedHome, GEMINI_CONFIG_DIR)) { + if (currentDir === path.join(resolvedHome, GEMINI_DIR)) { break; } @@ -206,7 +207,7 @@ async function getGeminiMdFilePathsInternalForEachDir( } upwardPaths.forEach((p) => allPaths.add(p)); - const mergedOptions = { + const mergedOptions: FileFilteringOptions = { ...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, ...fileFilteringOptions, }; @@ -327,6 +328,11 @@ function concatenateInstructions( .join('\n\n'); } +export interface LoadServerHierarchicalMemoryResponse { + memoryContent: string; + fileCount: number; +} + /** * Loads hierarchical GEMINI.md files and concatenates their content. * This function is intended for use by the server. @@ -337,10 +343,11 @@ export async function loadServerHierarchicalMemory( debugMode: boolean, fileService: FileDiscoveryService, extensionContextFilePaths: string[] = [], + folderTrust: boolean, importFormat: 'flat' | 'tree' = 'tree', fileFilteringOptions?: FileFilteringOptions, maxDirs: number = 200, -): Promise<{ memoryContent: string; fileCount: number }> { +): Promise { if (debugMode) logger.debug( `Loading server hierarchical memory for CWD: ${currentWorkingDirectory} (importFormat: ${importFormat})`, @@ -356,6 +363,7 @@ export async function loadServerHierarchicalMemory( debugMode, fileService, extensionContextFilePaths, + folderTrust, fileFilteringOptions || DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, maxDirs, );