fix: enforce folder trust for workspace settings, skills, and context (#17596)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
Gal Zahavi
2026-02-03 14:53:31 -08:00
committed by GitHub
parent d63c34b6e1
commit 71f46f1160
18 changed files with 1310 additions and 788 deletions
+124 -119
View File
@@ -277,8 +277,11 @@ export class LoadedSettings {
this.system = system;
this.systemDefaults = systemDefaults;
this.user = user;
this.workspace = workspace;
this._workspaceFile = workspace;
this.isTrusted = isTrusted;
this.workspace = isTrusted
? workspace
: this.createEmptyWorkspace(workspace);
this.errors = errors;
this._merged = this.computeMergedSettings();
}
@@ -286,10 +289,11 @@ export class LoadedSettings {
readonly system: SettingsFile;
readonly systemDefaults: SettingsFile;
readonly user: SettingsFile;
readonly workspace: SettingsFile;
readonly isTrusted: boolean;
workspace: SettingsFile;
isTrusted: boolean;
readonly errors: SettingsError[];
private _workspaceFile: SettingsFile;
private _merged: MergedSettings;
private _remoteAdminSettings: Partial<Settings> | undefined;
@@ -297,6 +301,26 @@ export class LoadedSettings {
return this._merged;
}
setTrusted(isTrusted: boolean): void {
if (this.isTrusted === isTrusted) {
return;
}
this.isTrusted = isTrusted;
this.workspace = isTrusted
? this._workspaceFile
: this.createEmptyWorkspace(this._workspaceFile);
this._merged = this.computeMergedSettings();
coreEvents.emitSettingsChanged();
}
private createEmptyWorkspace(workspace: SettingsFile): SettingsFile {
return {
...workspace,
settings: {},
originalSettings: {},
};
}
private computeMergedSettings(): MergedSettings {
const merged = mergeSettings(
this.system.settings,
@@ -341,8 +365,21 @@ export class LoadedSettings {
setValue(scope: LoadableSettingScope, key: string, value: unknown): void {
const settingsFile = this.forScope(scope);
setNestedProperty(settingsFile.settings, key, value);
setNestedProperty(settingsFile.originalSettings, key, value);
// Clone value to prevent reference sharing between settings and originalSettings
const valueToSet =
typeof value === 'object' && value !== null
? structuredClone(value)
: value;
setNestedProperty(settingsFile.settings, key, valueToSet);
// Use a fresh clone for originalSettings to ensure total independence
setNestedProperty(
settingsFile.originalSettings,
key,
structuredClone(valueToSet),
);
this._merged = this.computeMergedSettings();
saveSettings(settingsFile);
coreEvents.emitSettingsChanged();
@@ -592,9 +629,10 @@ export function loadSettings(
// For the initial trust check, we can only use user and system settings.
const initialTrustCheckSettings = customDeepMerge(
getMergeStrategyForPath,
{},
systemSettings,
getDefaultsFromSchema(),
systemDefaultSettings,
userSettings,
systemSettings,
);
const isTrusted =
isWorkspaceTrusted(initialTrustCheckSettings as Settings, workspaceDir)
@@ -672,57 +710,55 @@ export function migrateDeprecatedSettings(
removeDeprecated = false,
): boolean {
let anyModified = false;
const migrateBoolean = (
settings: Record<string, unknown>,
oldKey: string,
newKey: string,
): boolean => {
let modified = false;
const oldValue = settings[oldKey];
const newValue = settings[newKey];
if (typeof oldValue === 'boolean') {
if (typeof newValue === 'boolean') {
// Both exist, trust the new one
if (removeDeprecated) {
delete settings[oldKey];
modified = true;
}
} else {
// Only old exists, migrate to new (inverted)
settings[newKey] = !oldValue;
if (removeDeprecated) {
delete settings[oldKey];
}
modified = true;
}
}
return modified;
};
const processScope = (scope: LoadableSettingScope) => {
const settings = loadedSettings.forScope(scope).settings;
// Migrate inverted boolean settings (disableX -> enableX)
// These settings were renamed and their boolean logic inverted
// Migrate general settings
const generalSettings = settings.general as
| Record<string, unknown>
| undefined;
const uiSettings = settings.ui as Record<string, unknown> | undefined;
const contextSettings = settings.context as
| Record<string, unknown>
| undefined;
// Migrate general settings (disableAutoUpdate, disableUpdateNag)
if (generalSettings) {
const newGeneral: Record<string, unknown> = { ...generalSettings };
const newGeneral = { ...generalSettings };
let modified = false;
if (typeof newGeneral['disableAutoUpdate'] === 'boolean') {
if (typeof newGeneral['enableAutoUpdate'] === 'boolean') {
// Both exist, trust the new one
if (removeDeprecated) {
delete newGeneral['disableAutoUpdate'];
modified = true;
}
} else {
const oldValue = newGeneral['disableAutoUpdate'];
newGeneral['enableAutoUpdate'] = !oldValue;
if (removeDeprecated) {
delete newGeneral['disableAutoUpdate'];
}
modified = true;
}
}
if (typeof newGeneral['disableUpdateNag'] === 'boolean') {
if (typeof newGeneral['enableAutoUpdateNotification'] === 'boolean') {
// Both exist, trust the new one
if (removeDeprecated) {
delete newGeneral['disableUpdateNag'];
modified = true;
}
} else {
const oldValue = newGeneral['disableUpdateNag'];
newGeneral['enableAutoUpdateNotification'] = !oldValue;
if (removeDeprecated) {
delete newGeneral['disableUpdateNag'];
}
modified = true;
}
}
modified =
migrateBoolean(newGeneral, 'disableAutoUpdate', 'enableAutoUpdate') ||
modified;
modified =
migrateBoolean(
newGeneral,
'disableUpdateNag',
'enableAutoUpdateNotification',
) || modified;
if (modified) {
loadedSettings.setValue(scope, 'general', newGeneral);
@@ -731,94 +767,63 @@ export function migrateDeprecatedSettings(
}
// Migrate ui settings
const uiSettings = settings.ui as Record<string, unknown> | undefined;
if (uiSettings) {
const newUi: Record<string, unknown> = { ...uiSettings };
let modified = false;
// Migrate ui.accessibility.disableLoadingPhrases -> ui.accessibility.enableLoadingPhrases
const newUi = { ...uiSettings };
const accessibilitySettings = newUi['accessibility'] as
| Record<string, unknown>
| undefined;
if (
accessibilitySettings &&
typeof accessibilitySettings['disableLoadingPhrases'] === 'boolean'
) {
const newAccessibility: Record<string, unknown> = {
...accessibilitySettings,
};
if (
typeof accessibilitySettings['enableLoadingPhrases'] === 'boolean'
) {
// Both exist, trust the new one
if (removeDeprecated) {
delete newAccessibility['disableLoadingPhrases'];
newUi['accessibility'] = newAccessibility;
modified = true;
}
} else {
const oldValue = accessibilitySettings['disableLoadingPhrases'];
newAccessibility['enableLoadingPhrases'] = !oldValue;
if (removeDeprecated) {
delete newAccessibility['disableLoadingPhrases'];
}
newUi['accessibility'] = newAccessibility;
modified = true;
}
}
if (modified) {
loadedSettings.setValue(scope, 'ui', newUi);
anyModified = true;
if (accessibilitySettings) {
const newAccessibility = { ...accessibilitySettings };
if (
migrateBoolean(
newAccessibility,
'disableLoadingPhrases',
'enableLoadingPhrases',
)
) {
newUi['accessibility'] = newAccessibility;
loadedSettings.setValue(scope, 'ui', newUi);
anyModified = true;
}
}
}
// Migrate context settings
const contextSettings = settings.context as
| Record<string, unknown>
| undefined;
if (contextSettings) {
const newContext: Record<string, unknown> = { ...contextSettings };
let modified = false;
// Migrate context.fileFiltering.disableFuzzySearch -> context.fileFiltering.enableFuzzySearch
const newContext = { ...contextSettings };
const fileFilteringSettings = newContext['fileFiltering'] as
| Record<string, unknown>
| undefined;
if (
fileFilteringSettings &&
typeof fileFilteringSettings['disableFuzzySearch'] === 'boolean'
) {
const newFileFiltering: Record<string, unknown> = {
...fileFilteringSettings,
};
if (typeof fileFilteringSettings['enableFuzzySearch'] === 'boolean') {
// Both exist, trust the new one
if (removeDeprecated) {
delete newFileFiltering['disableFuzzySearch'];
newContext['fileFiltering'] = newFileFiltering;
modified = true;
}
} else {
const oldValue = fileFilteringSettings['disableFuzzySearch'];
newFileFiltering['enableFuzzySearch'] = !oldValue;
if (removeDeprecated) {
delete newFileFiltering['disableFuzzySearch'];
}
newContext['fileFiltering'] = newFileFiltering;
modified = true;
}
}
if (modified) {
loadedSettings.setValue(scope, 'context', newContext);
anyModified = true;
if (fileFilteringSettings) {
const newFileFiltering = { ...fileFilteringSettings };
if (
migrateBoolean(
newFileFiltering,
'disableFuzzySearch',
'enableFuzzySearch',
)
) {
newContext['fileFiltering'] = newFileFiltering;
loadedSettings.setValue(scope, 'context', newContext);
anyModified = true;
}
}
}
// Migrate experimental agent settings
anyModified ||= migrateExperimentalSettings(
settings,
loadedSettings,
scope,
removeDeprecated,
);
anyModified =
migrateExperimentalSettings(
settings,
loadedSettings,
scope,
removeDeprecated,
) || anyModified;
};
processScope(SettingScope.User);