Handle untrusted folders on extension install and link (#12322)

Co-authored-by: Jacob MacDonald <jakemac@google.com>
This commit is contained in:
kevinjwang1
2025-10-31 19:17:01 +00:00
committed by GitHub
parent 35f091bb01
commit adddafe6d0
3 changed files with 108 additions and 8 deletions

View File

@@ -12,7 +12,11 @@ import { ExtensionEnablementManager } from './extensions/extensionEnablement.js'
import { type Settings, SettingScope } from './settings.js';
import { createHash, randomUUID } from 'node:crypto';
import { loadInstallMetadata, type ExtensionConfig } from './extension.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import {
isWorkspaceTrusted,
loadTrustedFolders,
TrustLevel,
} from './trustedFolders.js';
import {
cloneFromGit,
downloadFromGitHubRelease,
@@ -136,11 +140,19 @@ export class ExtensionManager implements ExtensionLoader {
let extension: GeminiCLIExtension | null;
try {
if (!isWorkspaceTrusted(this.settings).isTrusted) {
throw new Error(
`Could not install extension from untrusted folder at ${installMetadata.source}`,
);
if (
await this.requestConsent(
`The current workspace at "${this.workspaceDir}" is not trusted. Do you want to trust this workspace to install extensions?`,
)
) {
const trustedFolders = loadTrustedFolders();
trustedFolders.setValue(this.workspaceDir, TrustLevel.TRUST_FOLDER);
} else {
throw new Error(
`Could not install extension because the current workspace at ${this.workspaceDir} is not trusted.`,
);
}
}
const extensionsDir = ExtensionStorage.getUserExtensionsDir();
await fs.promises.mkdir(extensionsDir, { recursive: true });

View File

@@ -16,7 +16,10 @@ import {
KeychainTokenStorage,
} from '@google/gemini-cli-core';
import { loadSettings, SettingScope } from './settings.js';
import { isWorkspaceTrusted } from './trustedFolders.js';
import {
isWorkspaceTrusted,
resetTrustedFoldersForTesting,
} from './trustedFolders.js';
import { createExtension } from '../test-utils/createExtension.js';
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
import { join } from 'node:path';
@@ -182,6 +185,7 @@ describe('extension tests', () => {
requestSetting: mockPromptForSettings,
settings: loadSettings(tempWorkspaceDir).merged,
});
resetTrustedFoldersForTesting();
});
afterEach(() => {
@@ -872,6 +876,87 @@ describe('extension tests', () => {
fs.rmSync(targetExtDir, { recursive: true, force: true });
});
it('should prompt for trust if workspace is not trusted', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: false,
source: undefined,
});
const sourceExtDir = createExtension({
extensionsDir: tempHomeDir,
name: 'my-local-extension',
version: '1.0.0',
});
await extensionManager.loadExtensions();
await extensionManager.installOrUpdateExtension({
source: sourceExtDir,
type: 'local',
});
expect(mockRequestConsent).toHaveBeenCalledWith(
`The current workspace at "${tempWorkspaceDir}" is not trusted. Do you want to trust this workspace to install extensions?`,
);
});
it('should not install if user denies trust', async () => {
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: false,
source: undefined,
});
mockRequestConsent.mockImplementation(async (message) => {
if (
message.includes(
'is not trusted. Do you want to trust this workspace to install extensions?',
)
) {
return false;
}
return true;
});
const sourceExtDir = createExtension({
extensionsDir: tempHomeDir,
name: 'my-local-extension',
version: '1.0.0',
});
await extensionManager.loadExtensions();
await expect(
extensionManager.installOrUpdateExtension({
source: sourceExtDir,
type: 'local',
}),
).rejects.toThrow(
`Could not install extension because the current workspace at ${tempWorkspaceDir} is not trusted.`,
);
});
it('should add the workspace to trusted folders if user consents', async () => {
const trustedFoldersPath = path.join(
tempHomeDir,
'.gemini',
'trustedFolders.json',
);
vi.mocked(isWorkspaceTrusted).mockReturnValue({
isTrusted: false,
source: undefined,
});
const sourceExtDir = createExtension({
extensionsDir: tempHomeDir,
name: 'my-local-extension',
version: '1.0.0',
});
await extensionManager.loadExtensions();
await extensionManager.installOrUpdateExtension({
source: sourceExtDir,
type: 'local',
});
expect(fs.existsSync(trustedFoldersPath)).toBe(true);
const trustedFolders = JSON.parse(
fs.readFileSync(trustedFoldersPath, 'utf-8'),
);
expect(trustedFolders[tempWorkspaceDir]).toBe('TRUST_FOLDER');
});
describe.each([true, false])(
'with previous extension config: %s',
(isUpdate: boolean) => {

View File

@@ -18,13 +18,16 @@ import type { Settings } from './settings.js';
import stripJsonComments from 'strip-json-comments';
export const TRUSTED_FOLDERS_FILENAME = 'trustedFolders.json';
export const USER_SETTINGS_DIR = path.join(homedir(), GEMINI_DIR);
export function getUserSettingsDir(): string {
return path.join(homedir(), GEMINI_DIR);
}
export function getTrustedFoldersPath(): string {
if (process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH']) {
return process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH'];
}
return path.join(USER_SETTINGS_DIR, TRUSTED_FOLDERS_FILENAME);
return path.join(getUserSettingsDir(), TRUSTED_FOLDERS_FILENAME);
}
export enum TrustLevel {