mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-11 22:51:00 -07:00
Handle untrusted folders on extension install and link (#12322)
Co-authored-by: Jacob MacDonald <jakemac@google.com>
This commit is contained in:
@@ -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 });
|
||||
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user