fix(trust): Respect folder trust setting when reading GEMINI.md (#7409)

This commit is contained in:
Richie Foreman
2025-08-29 14:12:36 -04:00
committed by GitHub
parent ea844857a2
commit 5e5f2dffc0
10 changed files with 113 additions and 16 deletions
+1 -1
View File
@@ -79,10 +79,10 @@ export async function loadConfig(
false, false,
fileService, fileService,
extensionContextFilePaths, extensionContextFilePaths,
true, /// TODO: Wire up folder trust logic here.
); );
configParams.userMemory = memoryContent; configParams.userMemory = memoryContent;
configParams.geminiMdFileCount = fileCount; configParams.geminiMdFileCount = fileCount;
const config = new Config({ const config = new Config({
...configParams, ...configParams,
}); });
+1
View File
@@ -723,6 +723,7 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => {
'/path/to/ext3/context1.md', '/path/to/ext3/context1.md',
'/path/to/ext3/context2.md', '/path/to/ext3/context2.md',
], ],
true,
'tree', 'tree',
{ {
respectGitIgnore: false, respectGitIgnore: false,
+4 -1
View File
@@ -333,6 +333,7 @@ export async function loadHierarchicalGeminiMemory(
fileService: FileDiscoveryService, fileService: FileDiscoveryService,
settings: Settings, settings: Settings,
extensionContextFilePaths: string[] = [], extensionContextFilePaths: string[] = [],
folderTrust: boolean,
memoryImportFormat: 'flat' | 'tree' = 'tree', memoryImportFormat: 'flat' | 'tree' = 'tree',
fileFilteringOptions?: FileFilteringOptions, fileFilteringOptions?: FileFilteringOptions,
): Promise<{ memoryContent: string; fileCount: number }> { ): Promise<{ memoryContent: string; fileCount: number }> {
@@ -358,6 +359,7 @@ export async function loadHierarchicalGeminiMemory(
debugMode, debugMode,
fileService, fileService,
extensionContextFilePaths, extensionContextFilePaths,
folderTrust,
memoryImportFormat, memoryImportFormat,
fileFilteringOptions, fileFilteringOptions,
settings.context?.discoveryMaxDirs, settings.context?.discoveryMaxDirs,
@@ -385,7 +387,7 @@ export async function loadCliConfig(
settings.security?.folderTrust?.featureEnabled ?? false; settings.security?.folderTrust?.featureEnabled ?? false;
const folderTrustSetting = settings.security?.folderTrust?.enabled ?? true; const folderTrustSetting = settings.security?.folderTrust?.enabled ?? true;
const folderTrust = folderTrustFeature && folderTrustSetting; const folderTrust = folderTrustFeature && folderTrustSetting;
const trustedFolder = isWorkspaceTrusted(settings); const trustedFolder = isWorkspaceTrusted(settings) ?? true;
const allExtensions = annotateActiveExtensions( const allExtensions = annotateActiveExtensions(
extensions, extensions,
@@ -433,6 +435,7 @@ export async function loadCliConfig(
fileService, fileService,
settings, settings,
extensionContextFilePaths, extensionContextFilePaths,
trustedFolder,
memoryImportFormat, memoryImportFormat,
fileFiltering, fileFiltering,
); );
+1
View File
@@ -368,6 +368,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
config.getFileService(), config.getFileService(),
settings.merged, settings.merged,
config.getExtensionContextFilePaths(), config.getExtensionContextFilePaths(),
config.getFolderTrust(),
settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree' settings.merged.context?.importFormat || 'tree', // Use setting or default to 'tree'
config.getFileFilteringOptions(), config.getFileFilteringOptions(),
); );
@@ -104,6 +104,7 @@ export const directoryCommand: SlashCommand = {
config.getDebugMode(), config.getDebugMode(),
config.getFileService(), config.getFileService(),
config.getExtensionContextFilePaths(), config.getExtensionContextFilePaths(),
config.getFolderTrust(),
context.services.settings.merged.context?.importFormat || context.services.settings.merged.context?.importFormat ||
'tree', // Use setting or default to 'tree' 'tree', // Use setting or default to 'tree'
config.getFileFilteringOptions(), config.getFileFilteringOptions(),
@@ -16,6 +16,7 @@ import {
loadServerHierarchicalMemory, loadServerHierarchicalMemory,
type FileDiscoveryService, type FileDiscoveryService,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import type { LoadServerHierarchicalMemoryResponse } from '@google/gemini-cli-core/index.js';
vi.mock('@google/gemini-cli-core', async (importOriginal) => { vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const original = const original =
@@ -170,6 +171,7 @@ describe('memoryCommand', () => {
ignore: [], ignore: [],
include: [], include: [],
}), }),
getFolderTrust: () => false,
}; };
mockContext = createMockCommandContext({ mockContext = createMockCommandContext({
@@ -188,7 +190,7 @@ describe('memoryCommand', () => {
it('should display success message when memory is refreshed with content', async () => { it('should display success message when memory is refreshed with content', async () => {
if (!refreshCommand.action) throw new Error('Command has no action'); if (!refreshCommand.action) throw new Error('Command has no action');
const refreshResult = { const refreshResult: LoadServerHierarchicalMemoryResponse = {
memoryContent: 'new memory content', memoryContent: 'new memory content',
fileCount: 2, fileCount: 2,
}; };
@@ -92,6 +92,7 @@ export const memoryCommand: SlashCommand = {
config.getDebugMode(), config.getDebugMode(),
config.getFileService(), config.getFileService(),
config.getExtensionContextFilePaths(), config.getExtensionContextFilePaths(),
config.getFolderTrust(),
context.services.settings.merged.context?.importFormat || context.services.settings.merged.context?.importFormat ||
'tree', // Use setting or default to 'tree' 'tree', // Use setting or default to 'tree'
config.getFileFilteringOptions(), config.getFileFilteringOptions(),
+4
View File
@@ -726,6 +726,10 @@ export class Config {
return this.folderTrustFeature; return this.folderTrustFeature;
} }
/**
* Returns 'true' if the workspace is considered "trusted".
* 'false' for untrusted.
*/
getFolderTrust(): boolean { getFolderTrust(): boolean {
return this.folderTrust; return this.folderTrust;
} }
@@ -10,11 +10,11 @@ import * as os from 'node:os';
import * as path from 'node:path'; import * as path from 'node:path';
import { loadServerHierarchicalMemory } from './memoryDiscovery.js'; import { loadServerHierarchicalMemory } from './memoryDiscovery.js';
import { import {
GEMINI_CONFIG_DIR,
setGeminiMdFilename, setGeminiMdFilename,
DEFAULT_CONTEXT_FILENAME, DEFAULT_CONTEXT_FILENAME,
} from '../tools/memoryTool.js'; } from '../tools/memoryTool.js';
import { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { GEMINI_DIR } from './paths.js';
vi.mock('os', async (importOriginal) => { vi.mock('os', async (importOriginal) => {
const actualOs = await importOriginal<typeof os>(); const actualOs = await importOriginal<typeof os>();
@@ -25,6 +25,7 @@ vi.mock('os', async (importOriginal) => {
}); });
describe('loadServerHierarchicalMemory', () => { describe('loadServerHierarchicalMemory', () => {
const DEFAULT_FOLDER_TRUST = true;
let testRootDir: string; let testRootDir: string;
let cwd: string; let cwd: string;
let projectRoot: string; let projectRoot: string;
@@ -65,12 +66,62 @@ describe('loadServerHierarchicalMemory', () => {
await fsPromises.rm(testRootDir, { recursive: true, force: true }); 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 () => { it('should return empty memory and count if no context files are found', async () => {
const result = await loadServerHierarchicalMemory( const result = await loadServerHierarchicalMemory(
cwd, cwd,
[], [],
false, false,
new FileDiscoveryService(projectRoot), new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
); );
expect(result).toEqual({ 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 () => { it('should load only the global context file if present and others are not (default filename)', async () => {
const defaultContextFile = await createTestFile( const defaultContextFile = await createTestFile(
path.join(homedir, GEMINI_CONFIG_DIR, DEFAULT_CONTEXT_FILENAME), path.join(homedir, GEMINI_DIR, DEFAULT_CONTEXT_FILENAME),
'default context content', 'default context content',
); );
@@ -90,6 +141,8 @@ describe('loadServerHierarchicalMemory', () => {
[], [],
false, false,
new FileDiscoveryService(projectRoot), new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
); );
expect(result).toEqual({ expect(result).toEqual({
@@ -103,7 +156,7 @@ describe('loadServerHierarchicalMemory', () => {
setGeminiMdFilename(customFilename); setGeminiMdFilename(customFilename);
const customContextFile = await createTestFile( const customContextFile = await createTestFile(
path.join(homedir, GEMINI_CONFIG_DIR, customFilename), path.join(homedir, GEMINI_DIR, customFilename),
'custom context content', 'custom context content',
); );
@@ -112,6 +165,8 @@ describe('loadServerHierarchicalMemory', () => {
[], [],
false, false,
new FileDiscoveryService(projectRoot), new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
); );
expect(result).toEqual({ expect(result).toEqual({
@@ -138,6 +193,8 @@ describe('loadServerHierarchicalMemory', () => {
[], [],
false, false,
new FileDiscoveryService(projectRoot), new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
); );
expect(result).toEqual({ expect(result).toEqual({
@@ -161,6 +218,8 @@ describe('loadServerHierarchicalMemory', () => {
[], [],
false, false,
new FileDiscoveryService(projectRoot), new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
); );
expect(result).toEqual({ expect(result).toEqual({
@@ -184,6 +243,8 @@ describe('loadServerHierarchicalMemory', () => {
[], [],
false, false,
new FileDiscoveryService(projectRoot), new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
); );
expect(result).toEqual({ expect(result).toEqual({
@@ -207,6 +268,8 @@ describe('loadServerHierarchicalMemory', () => {
[], [],
false, false,
new FileDiscoveryService(projectRoot), new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
); );
expect(result).toEqual({ 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 () => { it('should load and correctly order global, upward, and downward ORIGINAL_GEMINI_MD_FILENAME files', async () => {
const defaultContextFile = await createTestFile( const defaultContextFile = await createTestFile(
path.join(homedir, GEMINI_CONFIG_DIR, DEFAULT_CONTEXT_FILENAME), path.join(homedir, GEMINI_DIR, DEFAULT_CONTEXT_FILENAME),
'default context content', 'default context content',
); );
const rootGeminiFile = await createTestFile( const rootGeminiFile = await createTestFile(
@@ -242,6 +305,8 @@ describe('loadServerHierarchicalMemory', () => {
[], [],
false, false,
new FileDiscoveryService(projectRoot), new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
); );
expect(result).toEqual({ expect(result).toEqual({
@@ -269,6 +334,7 @@ describe('loadServerHierarchicalMemory', () => {
false, false,
new FileDiscoveryService(projectRoot), new FileDiscoveryService(projectRoot),
[], [],
DEFAULT_FOLDER_TRUST,
'tree', 'tree',
{ {
respectGitIgnore: true, respectGitIgnore: true,
@@ -299,6 +365,7 @@ describe('loadServerHierarchicalMemory', () => {
true, true,
new FileDiscoveryService(projectRoot), new FileDiscoveryService(projectRoot),
[], [],
DEFAULT_FOLDER_TRUST,
'tree', // importFormat 'tree', // importFormat
{ {
respectGitIgnore: true, respectGitIgnore: true,
@@ -319,6 +386,8 @@ describe('loadServerHierarchicalMemory', () => {
[], [],
false, false,
new FileDiscoveryService(projectRoot), new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
); );
expect(result).toEqual({ expect(result).toEqual({
@@ -339,6 +408,7 @@ describe('loadServerHierarchicalMemory', () => {
false, false,
new FileDiscoveryService(projectRoot), new FileDiscoveryService(projectRoot),
[extensionFilePath], [extensionFilePath],
DEFAULT_FOLDER_TRUST,
); );
expect(result).toEqual({ expect(result).toEqual({
@@ -361,6 +431,8 @@ describe('loadServerHierarchicalMemory', () => {
[includedDir], [includedDir],
false, false,
new FileDiscoveryService(projectRoot), new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
); );
expect(result).toEqual({ expect(result).toEqual({
@@ -391,6 +463,8 @@ describe('loadServerHierarchicalMemory', () => {
createdFiles.map((f) => path.dirname(f)), createdFiles.map((f) => path.dirname(f)),
false, false,
new FileDiscoveryService(projectRoot), new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
); );
// Should have loaded all files // Should have loaded all files
@@ -422,6 +496,8 @@ describe('loadServerHierarchicalMemory', () => {
[childDir, parentDir], // Deliberately include duplicates [childDir, parentDir], // Deliberately include duplicates
false, false,
new FileDiscoveryService(projectRoot), new FileDiscoveryService(projectRoot),
[],
DEFAULT_FOLDER_TRUST,
); );
// Should have both files without duplicates // Should have both files without duplicates
+17 -9
View File
@@ -9,14 +9,12 @@ import * as fsSync from 'node:fs';
import * as path from 'node:path'; import * as path from 'node:path';
import { homedir } from 'node:os'; import { homedir } from 'node:os';
import { bfsFileSearch } from './bfsFileSearch.js'; import { bfsFileSearch } from './bfsFileSearch.js';
import { import { getAllGeminiMdFilenames } from '../tools/memoryTool.js';
GEMINI_CONFIG_DIR,
getAllGeminiMdFilenames,
} from '../tools/memoryTool.js';
import type { FileDiscoveryService } from '../services/fileDiscoveryService.js'; import type { FileDiscoveryService } from '../services/fileDiscoveryService.js';
import { processImports } from './memoryImportProcessor.js'; import { processImports } from './memoryImportProcessor.js';
import type { FileFilteringOptions } from '../config/config.js'; import type { FileFilteringOptions } from '../config/config.js';
import { DEFAULT_MEMORY_FILE_FILTERING_OPTIONS } 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 // 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. // TODO: Integrate with a more robust server-side logger if available/appropriate.
@@ -87,6 +85,7 @@ async function getGeminiMdFilePathsInternal(
debugMode: boolean, debugMode: boolean,
fileService: FileDiscoveryService, fileService: FileDiscoveryService,
extensionContextFilePaths: string[] = [], extensionContextFilePaths: string[] = [],
folderTrust: boolean,
fileFilteringOptions: FileFilteringOptions, fileFilteringOptions: FileFilteringOptions,
maxDirs: number, maxDirs: number,
): Promise<string[]> { ): Promise<string[]> {
@@ -109,6 +108,7 @@ async function getGeminiMdFilePathsInternal(
debugMode, debugMode,
fileService, fileService,
extensionContextFilePaths, extensionContextFilePaths,
folderTrust,
fileFilteringOptions, fileFilteringOptions,
maxDirs, maxDirs,
), ),
@@ -138,6 +138,7 @@ async function getGeminiMdFilePathsInternalForEachDir(
debugMode: boolean, debugMode: boolean,
fileService: FileDiscoveryService, fileService: FileDiscoveryService,
extensionContextFilePaths: string[] = [], extensionContextFilePaths: string[] = [],
folderTrust: boolean,
fileFilteringOptions: FileFilteringOptions, fileFilteringOptions: FileFilteringOptions,
maxDirs: number, maxDirs: number,
): Promise<string[]> { ): Promise<string[]> {
@@ -148,7 +149,7 @@ async function getGeminiMdFilePathsInternalForEachDir(
const resolvedHome = path.resolve(userHomePath); const resolvedHome = path.resolve(userHomePath);
const globalMemoryPath = path.join( const globalMemoryPath = path.join(
resolvedHome, resolvedHome,
GEMINI_CONFIG_DIR, GEMINI_DIR,
geminiMdFilename, geminiMdFilename,
); );
@@ -166,7 +167,7 @@ async function getGeminiMdFilePathsInternalForEachDir(
// FIX: Only perform the workspace search (upward and downward scans) // FIX: Only perform the workspace search (upward and downward scans)
// if a valid currentWorkingDirectory is provided. // if a valid currentWorkingDirectory is provided.
if (dir) { if (dir && folderTrust) {
const resolvedCwd = path.resolve(dir); const resolvedCwd = path.resolve(dir);
if (debugMode) if (debugMode)
logger.debug( logger.debug(
@@ -184,7 +185,7 @@ async function getGeminiMdFilePathsInternalForEachDir(
: path.dirname(resolvedHome); : path.dirname(resolvedHome);
while (currentDir && currentDir !== path.dirname(currentDir)) { while (currentDir && currentDir !== path.dirname(currentDir)) {
if (currentDir === path.join(resolvedHome, GEMINI_CONFIG_DIR)) { if (currentDir === path.join(resolvedHome, GEMINI_DIR)) {
break; break;
} }
@@ -206,7 +207,7 @@ async function getGeminiMdFilePathsInternalForEachDir(
} }
upwardPaths.forEach((p) => allPaths.add(p)); upwardPaths.forEach((p) => allPaths.add(p));
const mergedOptions = { const mergedOptions: FileFilteringOptions = {
...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, ...DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
...fileFilteringOptions, ...fileFilteringOptions,
}; };
@@ -327,6 +328,11 @@ function concatenateInstructions(
.join('\n\n'); .join('\n\n');
} }
export interface LoadServerHierarchicalMemoryResponse {
memoryContent: string;
fileCount: number;
}
/** /**
* Loads hierarchical GEMINI.md files and concatenates their content. * Loads hierarchical GEMINI.md files and concatenates their content.
* This function is intended for use by the server. * This function is intended for use by the server.
@@ -337,10 +343,11 @@ export async function loadServerHierarchicalMemory(
debugMode: boolean, debugMode: boolean,
fileService: FileDiscoveryService, fileService: FileDiscoveryService,
extensionContextFilePaths: string[] = [], extensionContextFilePaths: string[] = [],
folderTrust: boolean,
importFormat: 'flat' | 'tree' = 'tree', importFormat: 'flat' | 'tree' = 'tree',
fileFilteringOptions?: FileFilteringOptions, fileFilteringOptions?: FileFilteringOptions,
maxDirs: number = 200, maxDirs: number = 200,
): Promise<{ memoryContent: string; fileCount: number }> { ): Promise<LoadServerHierarchicalMemoryResponse> {
if (debugMode) if (debugMode)
logger.debug( logger.debug(
`Loading server hierarchical memory for CWD: ${currentWorkingDirectory} (importFormat: ${importFormat})`, `Loading server hierarchical memory for CWD: ${currentWorkingDirectory} (importFormat: ${importFormat})`,
@@ -356,6 +363,7 @@ export async function loadServerHierarchicalMemory(
debugMode, debugMode,
fileService, fileService,
extensionContextFilePaths, extensionContextFilePaths,
folderTrust,
fileFilteringOptions || DEFAULT_MEMORY_FILE_FILTERING_OPTIONS, fileFilteringOptions || DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
maxDirs, maxDirs,
); );