mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
fix(cli): skip workspace policy loading when in home directory (#20054)
This commit is contained in:
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user