mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-07-22 15:51:18 -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 { type Settings, SettingScope } from './settings.js';
|
||||||
import { createHash, randomUUID } from 'node:crypto';
|
import { createHash, randomUUID } from 'node:crypto';
|
||||||
import { loadInstallMetadata, type ExtensionConfig } from './extension.js';
|
import { loadInstallMetadata, type ExtensionConfig } from './extension.js';
|
||||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
import {
|
||||||
|
isWorkspaceTrusted,
|
||||||
|
loadTrustedFolders,
|
||||||
|
TrustLevel,
|
||||||
|
} from './trustedFolders.js';
|
||||||
import {
|
import {
|
||||||
cloneFromGit,
|
cloneFromGit,
|
||||||
downloadFromGitHubRelease,
|
downloadFromGitHubRelease,
|
||||||
@@ -136,11 +140,19 @@ export class ExtensionManager implements ExtensionLoader {
|
|||||||
let extension: GeminiCLIExtension | null;
|
let extension: GeminiCLIExtension | null;
|
||||||
try {
|
try {
|
||||||
if (!isWorkspaceTrusted(this.settings).isTrusted) {
|
if (!isWorkspaceTrusted(this.settings).isTrusted) {
|
||||||
throw new Error(
|
if (
|
||||||
`Could not install extension from untrusted folder at ${installMetadata.source}`,
|
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();
|
const extensionsDir = ExtensionStorage.getUserExtensionsDir();
|
||||||
await fs.promises.mkdir(extensionsDir, { recursive: true });
|
await fs.promises.mkdir(extensionsDir, { recursive: true });
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ import {
|
|||||||
KeychainTokenStorage,
|
KeychainTokenStorage,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { loadSettings, SettingScope } from './settings.js';
|
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 { createExtension } from '../test-utils/createExtension.js';
|
||||||
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
import { ExtensionEnablementManager } from './extensions/extensionEnablement.js';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
@@ -182,6 +185,7 @@ describe('extension tests', () => {
|
|||||||
requestSetting: mockPromptForSettings,
|
requestSetting: mockPromptForSettings,
|
||||||
settings: loadSettings(tempWorkspaceDir).merged,
|
settings: loadSettings(tempWorkspaceDir).merged,
|
||||||
});
|
});
|
||||||
|
resetTrustedFoldersForTesting();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -872,6 +876,87 @@ describe('extension tests', () => {
|
|||||||
fs.rmSync(targetExtDir, { recursive: true, force: true });
|
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])(
|
describe.each([true, false])(
|
||||||
'with previous extension config: %s',
|
'with previous extension config: %s',
|
||||||
(isUpdate: boolean) => {
|
(isUpdate: boolean) => {
|
||||||
|
|||||||
@@ -18,13 +18,16 @@ import type { Settings } from './settings.js';
|
|||||||
import stripJsonComments from 'strip-json-comments';
|
import stripJsonComments from 'strip-json-comments';
|
||||||
|
|
||||||
export const TRUSTED_FOLDERS_FILENAME = 'trustedFolders.json';
|
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 {
|
export function getTrustedFoldersPath(): string {
|
||||||
if (process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH']) {
|
if (process.env['GEMINI_CLI_TRUSTED_FOLDERS_PATH']) {
|
||||||
return 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 {
|
export enum TrustLevel {
|
||||||
|
|||||||
Reference in New Issue
Block a user