feat(cli): Add permissions command to modify trust settings (#8792)

This commit is contained in:
shrutip90
2025-09-22 11:45:02 -07:00
committed by GitHub
parent c0c7ad10ca
commit 6c559e2338
26 changed files with 991 additions and 53 deletions
+7 -2
View File
@@ -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 () => {
+1 -1
View File
@@ -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());
});
+15 -4
View File
@@ -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()),
);
+2 -2
View File
@@ -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(
+42 -12
View File
@@ -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,
});
});
});
+24 -6
View File
@@ -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);
}