fix(cli): skip workspace policy loading when in home directory (#20054)

This commit is contained in:
Abhijit Balaji
2026-02-23 14:08:56 -08:00
committed by GitHub
parent 767d80e768
commit cec45a1ebc
5 changed files with 111 additions and 31 deletions

View File

@@ -142,4 +142,48 @@ describe('resolveWorkspacePolicyState', () => {
expect.stringContaining('Automatically accepting and loading'),
);
});
it('should not return workspace policies if cwd is the home directory', async () => {
const policiesDir = path.join(tempDir, '.gemini', 'policies');
fs.mkdirSync(policiesDir, { recursive: true });
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
// Run from HOME directory (tempDir is mocked as HOME in beforeEach)
const result = await resolveWorkspacePolicyState({
cwd: tempDir,
trustedFolder: true,
interactive: true,
});
expect(result.workspacePoliciesDir).toBeUndefined();
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
});
it('should not return workspace policies if cwd is a symlink to the home directory', async () => {
const policiesDir = path.join(tempDir, '.gemini', 'policies');
fs.mkdirSync(policiesDir, { recursive: true });
fs.writeFileSync(path.join(policiesDir, 'policy.toml'), 'rules = []');
// Create a symlink to the home directory
const symlinkDir = path.join(
os.tmpdir(),
`gemini-cli-symlink-${Date.now()}`,
);
fs.symlinkSync(tempDir, symlinkDir, 'dir');
try {
// Run from symlink to HOME directory
const result = await resolveWorkspacePolicyState({
cwd: symlinkDir,
trustedFolder: true,
interactive: true,
});
expect(result.workspacePoliciesDir).toBeUndefined();
expect(result.policyUpdateConfirmationRequest).toBeUndefined();
} finally {
// Clean up symlink
fs.unlinkSync(symlinkDir);
}
});
});

View File

@@ -67,9 +67,15 @@ export async function resolveWorkspacePolicyState(options: {
| undefined;
if (trustedFolder) {
const potentialWorkspacePoliciesDir = new Storage(
cwd,
).getWorkspacePoliciesDir();
const storage = new Storage(cwd);
// If we are in the home directory (or rather, our target Gemini dir is the global one),
// don't treat it as a workspace to avoid loading global policies twice.
if (storage.isWorkspaceHomeDir()) {
return { workspacePoliciesDir: undefined };
}
const potentialWorkspacePoliciesDir = storage.getWorkspacePoliciesDir();
const integrityManager = new PolicyIntegrityManager();
const integrityResult = await integrityManager.checkIntegrity(
'workspace',

View File

@@ -79,6 +79,7 @@ import {
import {
FatalConfigError,
GEMINI_DIR,
Storage,
type MCPServerConfig,
} from '@google/gemini-cli-core';
import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js';
@@ -126,6 +127,30 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
const os = await import('node:os');
const pathMod = await import('node:path');
const fsMod = await import('node:fs');
// Helper to resolve paths using the test's mocked environment
const testResolve = (p: string | undefined) => {
if (!p) return '';
try {
// Use the mocked fs.realpathSync if available, otherwise fallback
return fsMod.realpathSync(pathMod.resolve(p));
} catch {
return pathMod.resolve(p);
}
};
// Create a smarter mock for isWorkspaceHomeDir
vi.spyOn(actual.Storage.prototype, 'isWorkspaceHomeDir').mockImplementation(
function (this: Storage) {
const target = testResolve(pathMod.dirname(this.getGeminiDir()));
// Pick up the mocked home directory specifically from the 'os' mock
const home = testResolve(os.homedir());
return actual.normalizePath(target) === actual.normalizePath(home);
},
);
return {
...actual,
coreEvents: mockCoreEvents,
@@ -1491,20 +1516,29 @@ describe('Settings Loading and Merging', () => {
return pStr;
});
// Force the storage check to return true for this specific test
const isWorkspaceHomeDirSpy = vi
.spyOn(Storage.prototype, 'isWorkspaceHomeDir')
.mockReturnValue(true);
(mockFsExistsSync as Mock).mockImplementation(
(p: string) =>
// Only return true for workspace settings path to see if it gets loaded
p === mockWorkspaceSettingsPath,
);
const settings = loadSettings(mockSymlinkDir);
try {
const settings = loadSettings(mockSymlinkDir);
// Verify that even though the file exists, it was NOT loaded because realpath matched home
expect(fs.readFileSync).not.toHaveBeenCalledWith(
mockWorkspaceSettingsPath,
'utf-8',
);
expect(settings.workspace.settings).toEqual({});
// Verify that even though the file exists, it was NOT loaded because realpath matched home
expect(fs.readFileSync).not.toHaveBeenCalledWith(
mockWorkspaceSettingsPath,
'utf-8',
);
expect(settings.workspace.settings).toEqual({});
} finally {
isWorkspaceHomeDirSpy.mockRestore();
}
});
});

View File

@@ -637,24 +637,8 @@ export function loadSettings(
const systemSettingsPath = getSystemSettingsPath();
const systemDefaultsPath = getSystemDefaultsPath();
// Resolve paths to their canonical representation to handle symlinks
const resolvedWorkspaceDir = path.resolve(workspaceDir);
const resolvedHomeDir = path.resolve(homedir());
let realWorkspaceDir = resolvedWorkspaceDir;
try {
// fs.realpathSync gets the "true" path, resolving any symlinks
realWorkspaceDir = fs.realpathSync(resolvedWorkspaceDir);
} catch (_e) {
// This is okay. The path might not exist yet, and that's a valid state.
}
// We expect homedir to always exist and be resolvable.
const realHomeDir = fs.realpathSync(resolvedHomeDir);
const workspaceSettingsPath = new Storage(
workspaceDir,
).getWorkspaceSettingsPath();
const storage = new Storage(workspaceDir);
const workspaceSettingsPath = storage.getWorkspaceSettingsPath();
const load = (filePath: string): { settings: Settings; rawJson?: string } => {
try {
@@ -712,7 +696,7 @@ export function loadSettings(
settings: {} as Settings,
rawJson: undefined,
};
if (realWorkspaceDir !== realHomeDir) {
if (!storage.isWorkspaceHomeDir()) {
workspaceResult = load(workspaceSettingsPath);
}
@@ -800,11 +784,11 @@ export function loadSettings(
readOnly: false,
},
{
path: realWorkspaceDir === realHomeDir ? '' : workspaceSettingsPath,
path: storage.isWorkspaceHomeDir() ? '' : workspaceSettingsPath,
settings: workspaceSettings,
originalSettings: workspaceOriginalSettings,
rawJson: workspaceResult.rawJson,
readOnly: realWorkspaceDir === realHomeDir,
readOnly: storage.isWorkspaceHomeDir(),
},
isTrusted,
settingsErrors,

View File

@@ -14,6 +14,7 @@ import {
GOOGLE_ACCOUNTS_FILENAME,
isSubpath,
resolveToRealPath,
normalizePath,
} from '../utils/paths.js';
import { ProjectRegistry } from './projectRegistry.js';
import { StorageMigration } from './storageMigration.js';
@@ -142,6 +143,17 @@ export class Storage {
return path.join(this.targetDir, GEMINI_DIR);
}
/**
* Checks if the current workspace storage location is the same as the global/user storage location.
* This handles symlinks and platform-specific path normalization.
*/
isWorkspaceHomeDir(): boolean {
return (
normalizePath(resolveToRealPath(this.targetDir)) ===
normalizePath(resolveToRealPath(homedir()))
);
}
getAgentsDir(): string {
return path.join(this.targetDir, AGENTS_DIR_NAME);
}