mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-29 22:44:45 -07:00
feat(cli): Add permissions command to modify trust settings (#8792)
This commit is contained in:
@@ -22,7 +22,9 @@ import * as ServerConfig from '@google/gemini-cli-core';
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
|
||||
vi.mock('./trustedFolders.js', () => ({
|
||||
isWorkspaceTrusted: vi.fn().mockReturnValue(true), // Default to trusted
|
||||
isWorkspaceTrusted: vi
|
||||
.fn()
|
||||
.mockReturnValue({ isTrusted: true, source: 'file' }), // Default to trusted
|
||||
}));
|
||||
|
||||
vi.mock('fs', async (importOriginal) => {
|
||||
@@ -2098,7 +2100,10 @@ describe('loadCliConfig approval mode', () => {
|
||||
// --- Untrusted Folder Scenarios ---
|
||||
describe('when folder is NOT trusted', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue(false);
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: false,
|
||||
source: 'file',
|
||||
});
|
||||
});
|
||||
|
||||
it('should override --approval-mode=yolo to DEFAULT', async () => {
|
||||
|
||||
@@ -414,7 +414,7 @@ export async function loadCliConfig(
|
||||
const ideMode = settings.ide?.enabled ?? false;
|
||||
|
||||
const folderTrust = settings.security?.folderTrust?.enabled ?? false;
|
||||
const trustedFolder = isWorkspaceTrusted(settings) ?? true;
|
||||
const trustedFolder = isWorkspaceTrusted(settings)?.isTrusted ?? true;
|
||||
|
||||
const allExtensions = annotateActiveExtensions(
|
||||
extensions,
|
||||
|
||||
@@ -87,7 +87,10 @@ describe('update tests', () => {
|
||||
// Clean up before each test
|
||||
fs.rmSync(userExtensionsDir, { recursive: true, force: true });
|
||||
fs.mkdirSync(userExtensionsDir, { recursive: true });
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue(true);
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
vi.spyOn(process, 'cwd').mockReturnValue(tempWorkspaceDir);
|
||||
Object.values(mockGit).forEach((fn) => fn.mockReset());
|
||||
});
|
||||
|
||||
@@ -30,7 +30,9 @@ vi.mock('./settings.js', async (importActual) => {
|
||||
|
||||
// Mock trustedFolders
|
||||
vi.mock('./trustedFolders.js', () => ({
|
||||
isWorkspaceTrusted: vi.fn(),
|
||||
isWorkspaceTrusted: vi
|
||||
.fn()
|
||||
.mockReturnValue({ isTrusted: true, source: 'file' }),
|
||||
}));
|
||||
|
||||
// NOW import everything else, including the (now effectively re-exported) settings.js
|
||||
@@ -120,7 +122,10 @@ describe('Settings Loading and Merging', () => {
|
||||
(mockFsExistsSync as Mock).mockReturnValue(false);
|
||||
(fs.readFileSync as Mock).mockReturnValue('{}'); // Return valid empty JSON
|
||||
(mockFsMkdirSync as Mock).mockImplementation(() => undefined);
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue(true);
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -1843,7 +1848,10 @@ describe('Settings Loading and Merging', () => {
|
||||
});
|
||||
|
||||
it('should NOT merge workspace settings when workspace is not trusted', () => {
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue(false);
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: false,
|
||||
source: 'file',
|
||||
});
|
||||
(mockFsExistsSync as Mock).mockReturnValue(true);
|
||||
const userSettingsContent = {
|
||||
ui: { theme: 'dark' },
|
||||
@@ -2222,7 +2230,10 @@ describe('Settings Loading and Merging', () => {
|
||||
delete process.env['TESTTEST']; // reset
|
||||
const geminiEnvPath = path.resolve(path.join(GEMINI_DIR, '.env'));
|
||||
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue(isWorkspaceTrustedValue);
|
||||
vi.mocked(isWorkspaceTrusted).mockReturnValue({
|
||||
isTrusted: isWorkspaceTrustedValue,
|
||||
source: 'file',
|
||||
});
|
||||
(mockFsExistsSync as Mock).mockImplementation((p: fs.PathLike) =>
|
||||
[USER_SETTINGS_PATH, geminiEnvPath].includes(p.toString()),
|
||||
);
|
||||
|
||||
@@ -492,7 +492,7 @@ export function setUpCloudShellEnvironment(envFilePath: string | null): void {
|
||||
export function loadEnvironment(settings: Settings): void {
|
||||
const envFilePath = findEnvFile(process.cwd());
|
||||
|
||||
if (!isWorkspaceTrusted(settings)) {
|
||||
if (!isWorkspaceTrusted(settings).isTrusted) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -674,7 +674,7 @@ export function loadSettings(
|
||||
userSettings,
|
||||
);
|
||||
const isTrusted =
|
||||
isWorkspaceTrusted(initialTrustCheckSettings as Settings) ?? true;
|
||||
isWorkspaceTrusted(initialTrustCheckSettings as Settings).isTrusted ?? true;
|
||||
|
||||
// Create a temporary merged settings object to pass to loadEnvironment.
|
||||
const tempMergedSettings = mergeSettings(
|
||||
|
||||
@@ -229,52 +229,70 @@ describe('isWorkspaceTrusted', () => {
|
||||
it('should return true for a directly trusted folder', () => {
|
||||
mockCwd = '/home/user/projectA';
|
||||
mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
|
||||
expect(isWorkspaceTrusted(mockSettings)).toBe(true);
|
||||
expect(isWorkspaceTrusted(mockSettings)).toEqual({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return true for a child of a trusted folder', () => {
|
||||
mockCwd = '/home/user/projectA/src';
|
||||
mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
|
||||
expect(isWorkspaceTrusted(mockSettings)).toBe(true);
|
||||
expect(isWorkspaceTrusted(mockSettings)).toEqual({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return true for a child of a trusted parent folder', () => {
|
||||
mockCwd = '/home/user/projectB';
|
||||
mockRules['/home/user/projectB/somefile.txt'] = TrustLevel.TRUST_PARENT;
|
||||
expect(isWorkspaceTrusted(mockSettings)).toBe(true);
|
||||
expect(isWorkspaceTrusted(mockSettings)).toEqual({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false for a directly untrusted folder', () => {
|
||||
mockCwd = '/home/user/untrusted';
|
||||
mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST;
|
||||
expect(isWorkspaceTrusted(mockSettings)).toBe(false);
|
||||
expect(isWorkspaceTrusted(mockSettings)).toEqual({
|
||||
isTrusted: false,
|
||||
source: 'file',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return undefined for a child of an untrusted folder', () => {
|
||||
mockCwd = '/home/user/untrusted/src';
|
||||
mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST;
|
||||
expect(isWorkspaceTrusted(mockSettings)).toBeUndefined();
|
||||
expect(isWorkspaceTrusted(mockSettings).isTrusted).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return undefined when no rules match', () => {
|
||||
mockCwd = '/home/user/other';
|
||||
mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
|
||||
mockRules['/home/user/untrusted'] = TrustLevel.DO_NOT_TRUST;
|
||||
expect(isWorkspaceTrusted(mockSettings)).toBeUndefined();
|
||||
expect(isWorkspaceTrusted(mockSettings).isTrusted).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should prioritize trust over distrust', () => {
|
||||
mockCwd = '/home/user/projectA/untrusted';
|
||||
mockRules['/home/user/projectA'] = TrustLevel.TRUST_FOLDER;
|
||||
mockRules['/home/user/projectA/untrusted'] = TrustLevel.DO_NOT_TRUST;
|
||||
expect(isWorkspaceTrusted(mockSettings)).toBe(true);
|
||||
expect(isWorkspaceTrusted(mockSettings)).toEqual({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle path normalization', () => {
|
||||
mockCwd = '/home/user/projectA';
|
||||
mockRules[`/home/user/../user/${path.basename('/home/user/projectA')}`] =
|
||||
TrustLevel.TRUST_FOLDER;
|
||||
expect(isWorkspaceTrusted(mockSettings)).toBe(true);
|
||||
expect(isWorkspaceTrusted(mockSettings)).toEqual({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -297,7 +315,10 @@ describe('isWorkspaceTrusted with IDE override', () => {
|
||||
vi.spyOn(fs, 'readFileSync').mockReturnValue(
|
||||
JSON.stringify({ [process.cwd()]: TrustLevel.DO_NOT_TRUST }),
|
||||
);
|
||||
expect(isWorkspaceTrusted(mockSettings)).toBe(true);
|
||||
expect(isWorkspaceTrusted(mockSettings)).toEqual({
|
||||
isTrusted: true,
|
||||
source: 'ide',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false when ideTrust is false, ignoring config', () => {
|
||||
@@ -306,7 +327,10 @@ describe('isWorkspaceTrusted with IDE override', () => {
|
||||
vi.spyOn(fs, 'readFileSync').mockReturnValue(
|
||||
JSON.stringify({ [process.cwd()]: TrustLevel.TRUST_FOLDER }),
|
||||
);
|
||||
expect(isWorkspaceTrusted(mockSettings)).toBe(false);
|
||||
expect(isWorkspaceTrusted(mockSettings)).toEqual({
|
||||
isTrusted: false,
|
||||
source: 'ide',
|
||||
});
|
||||
});
|
||||
|
||||
it('should fall back to config when ideTrust is undefined', () => {
|
||||
@@ -314,7 +338,10 @@ describe('isWorkspaceTrusted with IDE override', () => {
|
||||
vi.spyOn(fs, 'readFileSync').mockReturnValue(
|
||||
JSON.stringify({ [process.cwd()]: TrustLevel.TRUST_FOLDER }),
|
||||
);
|
||||
expect(isWorkspaceTrusted(mockSettings)).toBe(true);
|
||||
expect(isWorkspaceTrusted(mockSettings)).toEqual({
|
||||
isTrusted: true,
|
||||
source: 'file',
|
||||
});
|
||||
});
|
||||
|
||||
it('should always return true if folderTrust setting is disabled', () => {
|
||||
@@ -326,6 +353,9 @@ describe('isWorkspaceTrusted with IDE override', () => {
|
||||
},
|
||||
};
|
||||
ideContextStore.set({ workspaceState: { isTrusted: false } });
|
||||
expect(isWorkspaceTrusted(settings)).toBe(true);
|
||||
expect(isWorkspaceTrusted(settings)).toEqual({
|
||||
isTrusted: true,
|
||||
source: undefined,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -47,6 +47,11 @@ export interface TrustedFoldersFile {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface TrustResult {
|
||||
isTrusted: boolean | undefined;
|
||||
source: 'ide' | 'file' | undefined;
|
||||
}
|
||||
|
||||
export class LoadedTrustedFolders {
|
||||
constructor(
|
||||
readonly user: TrustedFoldersFile,
|
||||
@@ -166,9 +171,15 @@ export function isFolderTrustEnabled(settings: Settings): boolean {
|
||||
return folderTrustSetting;
|
||||
}
|
||||
|
||||
function getWorkspaceTrustFromLocalConfig(): boolean | undefined {
|
||||
function getWorkspaceTrustFromLocalConfig(
|
||||
trustConfig?: Record<string, TrustLevel>,
|
||||
): TrustResult {
|
||||
const folders = loadTrustedFolders();
|
||||
|
||||
if (trustConfig) {
|
||||
folders.user.config = trustConfig;
|
||||
}
|
||||
|
||||
if (folders.errors.length > 0) {
|
||||
for (const error of folders.errors) {
|
||||
console.error(
|
||||
@@ -177,19 +188,26 @@ function getWorkspaceTrustFromLocalConfig(): boolean | undefined {
|
||||
}
|
||||
}
|
||||
|
||||
return folders.isPathTrusted(process.cwd());
|
||||
const isTrusted = folders.isPathTrusted(process.cwd());
|
||||
return {
|
||||
isTrusted,
|
||||
source: isTrusted !== undefined ? 'file' : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function isWorkspaceTrusted(settings: Settings): boolean | undefined {
|
||||
export function isWorkspaceTrusted(
|
||||
settings: Settings,
|
||||
trustConfig?: Record<string, TrustLevel>,
|
||||
): TrustResult {
|
||||
if (!isFolderTrustEnabled(settings)) {
|
||||
return true;
|
||||
return { isTrusted: true, source: undefined };
|
||||
}
|
||||
|
||||
const ideTrust = ideContextStore.get()?.workspaceState?.isTrusted;
|
||||
if (ideTrust !== undefined) {
|
||||
return ideTrust;
|
||||
return { isTrusted: ideTrust, source: 'ide' };
|
||||
}
|
||||
|
||||
// Fall back to the local user configuration
|
||||
return getWorkspaceTrustFromLocalConfig();
|
||||
return getWorkspaceTrustFromLocalConfig(trustConfig);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user