| |
| |
| |
| |
| |
|
|
| import * as fs from 'node:fs'; |
| import * as path from 'node:path'; |
| import { platform } from 'node:os'; |
| import * as dotenv from 'dotenv'; |
| import process from 'node:process'; |
| import { |
| CoreEvent, |
| FatalConfigError, |
| GEMINI_DIR, |
| getErrorMessage, |
| getFsErrorMessage, |
| Storage, |
| coreEvents, |
| homedir, |
| AuthType, |
| type AdminControlsSettings, |
| createCache, |
| isFileAndDirectorySecureSync, |
| createPathSecurityCache, |
| } from '@google/gemini-cli-core'; |
| import stripJsonComments from 'strip-json-comments'; |
| import { DefaultLight } from '../ui/themes/builtin/light/default-light.js'; |
| import { DefaultDark } from '../ui/themes/builtin/dark/default-dark.js'; |
| import { isWorkspaceTrusted } from './trustedFolders.js'; |
| import { |
| type Settings, |
| type MergedSettings, |
| type MemoryImportFormat, |
| type MergeStrategy, |
| type SettingsSchema, |
| type SettingDefinition, |
| getSettingsSchema, |
| } from './settingsSchema.js'; |
|
|
| export { |
| type Settings, |
| type MergedSettings, |
| type MemoryImportFormat, |
| type MergeStrategy, |
| type SettingsSchema, |
| type SettingDefinition, |
| getSettingsSchema, |
| }; |
|
|
| import { resolveEnvVarsInObject } from '../utils/envVarResolver.js'; |
| import { customDeepMerge } from '../utils/deepMerge.js'; |
| import { updateSettingsFilePreservingFormat } from '../utils/commentJson.js'; |
| import { |
| validateSettings, |
| formatValidationError, |
| } from './settings-validation.js'; |
|
|
| export function getMergeStrategyForPath( |
| path: string[], |
| ): MergeStrategy | undefined { |
| let current: SettingDefinition | undefined = undefined; |
| let currentSchema: SettingsSchema | undefined = getSettingsSchema(); |
| let parent: SettingDefinition | undefined = undefined; |
|
|
| for (const key of path) { |
| if (!currentSchema || !currentSchema[key]) { |
| |
| if (parent?.additionalProperties?.mergeStrategy) { |
| return parent.additionalProperties.mergeStrategy; |
| } |
| return undefined; |
| } |
| parent = current; |
| current = currentSchema[key]; |
| currentSchema = current.properties; |
| } |
|
|
| return current?.mergeStrategy; |
| } |
|
|
| export const USER_SETTINGS_PATH = Storage.getGlobalSettingsPath(); |
| export const USER_SETTINGS_DIR = path.dirname(USER_SETTINGS_PATH); |
| export const DEFAULT_EXCLUDED_ENV_VARS = [ |
| 'DEBUG', |
| 'DEBUG_MODE', |
| 'GEMINI_CLI_IDE_SERVER_STDIO_COMMAND', |
| 'GEMINI_CLI_IDE_SERVER_STDIO_ARGS', |
| ]; |
|
|
| const AUTH_ENV_VAR_WHITELIST = [ |
| 'GEMINI_API_KEY', |
| 'GOOGLE_API_KEY', |
| 'GOOGLE_CLOUD_PROJECT', |
| 'GOOGLE_CLOUD_LOCATION', |
| ]; |
|
|
| |
| |
| |
| |
| export function sanitizeEnvVar(value: string): string { |
| return value.replace(/[^a-zA-Z0-9\-_./]/g, ''); |
| } |
|
|
| export function getSystemSettingsPath(): string { |
| if (process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']) { |
| return process.env['GEMINI_CLI_SYSTEM_SETTINGS_PATH']; |
| } |
| if (platform() === 'darwin') { |
| return '/Library/Application Support/GeminiCli/settings.json'; |
| } else if (platform() === 'win32') { |
| return 'C:\\ProgramData\\gemini-cli\\settings.json'; |
| } else { |
| return '/etc/gemini-cli/settings.json'; |
| } |
| } |
|
|
| export function getSystemDefaultsPath(): string { |
| if (process.env['GEMINI_CLI_SYSTEM_DEFAULTS_PATH']) { |
| return process.env['GEMINI_CLI_SYSTEM_DEFAULTS_PATH']; |
| } |
| return path.join( |
| path.dirname(getSystemSettingsPath()), |
| 'system-defaults.json', |
| ); |
| } |
|
|
| export type { DnsResolutionOrder } from './settingsSchema.js'; |
|
|
| export enum SettingScope { |
| User = 'User', |
| Workspace = 'Workspace', |
| System = 'System', |
| SystemDefaults = 'SystemDefaults', |
| |
| |
| Session = 'Session', |
| } |
|
|
| |
| |
| |
| export type LoadableSettingScope = |
| | SettingScope.User |
| | SettingScope.Workspace |
| | SettingScope.System |
| | SettingScope.SystemDefaults; |
|
|
| |
| |
| |
| const _loadableSettingScopes = [ |
| SettingScope.User, |
| SettingScope.Workspace, |
| SettingScope.System, |
| SettingScope.SystemDefaults, |
| ]; |
|
|
| |
| |
| |
| |
| export function isLoadableSettingScope( |
| scope: SettingScope, |
| ): scope is LoadableSettingScope { |
| return _loadableSettingScopes.includes(scope); |
| } |
|
|
| export interface CheckpointingSettings { |
| enabled?: boolean; |
| } |
|
|
| export interface SummarizeToolOutputSettings { |
| tokenBudget?: number; |
| } |
|
|
| export type LoadingPhrasesMode = 'tips' | 'witty' | 'all' | 'off'; |
|
|
| export interface AccessibilitySettings { |
| |
| enableLoadingPhrases?: boolean; |
| screenReader?: boolean; |
| } |
|
|
| export interface SessionRetentionSettings { |
| |
| enabled?: boolean; |
|
|
| |
| maxAge?: string; |
|
|
| |
| maxCount?: number; |
|
|
| |
| minRetention?: string; |
| } |
|
|
| export interface SettingsError { |
| message: string; |
| path: string; |
| severity: 'error' | 'warning'; |
| } |
|
|
| export interface SettingsFile { |
| settings: Settings; |
| originalSettings: Settings; |
| path: string; |
| rawJson?: string; |
| readOnly?: boolean; |
| } |
|
|
| function setNestedProperty( |
| obj: Record<string, unknown>, |
| path: string, |
| value: unknown, |
| ) { |
| const keys = path.split('.'); |
| const lastKey = keys.pop(); |
| if (!lastKey) return; |
|
|
| let current: Record<string, unknown> = obj; |
| for (const key of keys) { |
| if (current[key] === undefined) { |
| current[key] = {}; |
| } |
| const next = current[key]; |
| if (typeof next === 'object' && next !== null) { |
| |
| current = next as Record<string, unknown>; |
| } else { |
| |
| return; |
| } |
| } |
| current[lastKey] = value; |
| } |
|
|
| export function getDefaultsFromSchema( |
| schema: SettingsSchema = getSettingsSchema(), |
| ): Settings { |
| const defaults: Record<string, unknown> = {}; |
| for (const key in schema) { |
| const definition = schema[key]; |
| if (definition.properties) { |
| defaults[key] = getDefaultsFromSchema(definition.properties); |
| } else if (definition.default !== undefined) { |
| defaults[key] = definition.default; |
| } |
| } |
| return defaults as Settings; |
| } |
|
|
| export function mergeSettings( |
| system: Settings, |
| systemDefaults: Settings, |
| user: Settings, |
| workspace: Settings, |
| isTrusted: boolean, |
| ): MergedSettings { |
| const safeWorkspace = isTrusted ? workspace : ({} as Settings); |
| const schemaDefaults = getDefaultsFromSchema(); |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| return customDeepMerge( |
| getMergeStrategyForPath, |
| schemaDefaults, |
| systemDefaults, |
| user, |
| safeWorkspace, |
| system, |
| ) as MergedSettings; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function createTestMergedSettings( |
| overrides: Partial<Settings> = {}, |
| ): MergedSettings { |
| |
| return customDeepMerge( |
| getMergeStrategyForPath, |
| getDefaultsFromSchema(), |
| overrides, |
| ) as MergedSettings; |
| } |
|
|
| |
| |
| |
| |
| export interface LoadedSettingsSnapshot { |
| system: SettingsFile; |
| systemDefaults: SettingsFile; |
| user: SettingsFile; |
| workspace: SettingsFile; |
| isTrusted: boolean; |
| errors: SettingsError[]; |
| merged: MergedSettings; |
| } |
|
|
| export class LoadedSettings { |
| constructor( |
| system: SettingsFile, |
| systemDefaults: SettingsFile, |
| user: SettingsFile, |
| workspace: SettingsFile, |
| isTrusted: boolean, |
| errors: SettingsError[] = [], |
| ) { |
| this.system = system; |
| this.systemDefaults = systemDefaults; |
| this.user = user; |
| this._workspaceFile = workspace; |
| this.isTrusted = isTrusted; |
| this.workspace = isTrusted |
| ? workspace |
| : this.createEmptyWorkspace(workspace); |
| this.errors = errors; |
| this._merged = this.computeMergedSettings(); |
| this._snapshot = this.computeSnapshot(); |
| } |
|
|
| readonly system: SettingsFile; |
| readonly systemDefaults: SettingsFile; |
| readonly user: SettingsFile; |
| workspace: SettingsFile; |
| isTrusted: boolean; |
| readonly errors: SettingsError[]; |
|
|
| private _workspaceFile: SettingsFile; |
| private _merged: MergedSettings; |
| private _snapshot: LoadedSettingsSnapshot; |
| private _remoteAdminSettings: Partial<Settings> | undefined; |
|
|
| get merged(): MergedSettings { |
| return this._merged; |
| } |
|
|
| |
| |
| |
| |
| |
| getMergedSettingsAsIfTrusted(): MergedSettings { |
| return this.computeMergedSettings(true); |
| } |
|
|
| 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(forceTrusted = false): MergedSettings { |
| const isTrusted = forceTrusted || this.isTrusted; |
| const workspace = forceTrusted ? this._workspaceFile : this.workspace; |
|
|
| const merged = mergeSettings( |
| this.system.settings, |
| this.systemDefaults.settings, |
| this.user.settings, |
| workspace.settings, |
| isTrusted, |
| ); |
|
|
| |
| |
| const adminSettingSchema = getSettingsSchema().admin; |
| if (adminSettingSchema?.properties) { |
| const adminSchema = adminSettingSchema.properties; |
| const adminDefaults = getDefaultsFromSchema(adminSchema); |
|
|
| |
| |
| |
| merged.admin = customDeepMerge( |
| (path: string[]) => getMergeStrategyForPath(['admin', ...path]), |
| adminDefaults, |
| this._remoteAdminSettings?.admin ?? {}, |
| ) as MergedSettings['admin']; |
| } |
| return merged; |
| } |
|
|
| private computeSnapshot(): LoadedSettingsSnapshot { |
| const cloneSettingsFile = (file: SettingsFile): SettingsFile => ({ |
| ...file, |
| settings: structuredClone(file.settings), |
| originalSettings: structuredClone(file.originalSettings), |
| }); |
| return { |
| system: cloneSettingsFile(this.system), |
| systemDefaults: cloneSettingsFile(this.systemDefaults), |
| user: cloneSettingsFile(this.user), |
| workspace: cloneSettingsFile(this.workspace), |
| isTrusted: this.isTrusted, |
| errors: [...this.errors], |
| merged: structuredClone(this._merged), |
| }; |
| } |
|
|
| |
| |
| |
| subscribe(listener: () => void): () => void { |
| coreEvents.on(CoreEvent.SettingsChanged, listener); |
| return () => coreEvents.off(CoreEvent.SettingsChanged, listener); |
| } |
|
|
| getSnapshot(): LoadedSettingsSnapshot { |
| return this._snapshot; |
| } |
|
|
| forScope(scope: LoadableSettingScope): SettingsFile { |
| switch (scope) { |
| case SettingScope.User: |
| return this.user; |
| case SettingScope.Workspace: |
| return this.workspace; |
| case SettingScope.System: |
| return this.system; |
| case SettingScope.SystemDefaults: |
| return this.systemDefaults; |
| default: |
| throw new Error(`Invalid scope: ${scope}`); |
| } |
| } |
|
|
| private isPersistable(settingsFile: SettingsFile): boolean { |
| return !settingsFile.readOnly; |
| } |
|
|
| setValue(scope: LoadableSettingScope, key: string, value: unknown): void { |
| const settingsFile = this.forScope(scope); |
|
|
| |
| const valueToSet = |
| typeof value === 'object' && value !== null |
| ? structuredClone(value) |
| : value; |
|
|
| setNestedProperty(settingsFile.settings, key, valueToSet); |
|
|
| if (this.isPersistable(settingsFile)) { |
| |
| setNestedProperty( |
| settingsFile.originalSettings, |
| key, |
| structuredClone(valueToSet), |
| ); |
| saveSettings(settingsFile); |
| } |
|
|
| this._merged = this.computeMergedSettings(); |
| this._snapshot = this.computeSnapshot(); |
| coreEvents.emitSettingsChanged(); |
| } |
|
|
| setRemoteAdminSettings(remoteSettings: AdminControlsSettings): void { |
| const admin: Settings['admin'] = {}; |
| const { strictModeDisabled, mcpSetting, cliFeatureSetting } = |
| remoteSettings; |
|
|
| if (Object.keys(remoteSettings).length === 0) { |
| this._remoteAdminSettings = { admin }; |
| this._merged = this.computeMergedSettings(); |
| return; |
| } |
|
|
| admin.secureModeEnabled = !strictModeDisabled; |
| admin.mcp = { |
| enabled: mcpSetting?.mcpEnabled, |
| config: mcpSetting?.mcpConfig?.mcpServers, |
| requiredConfig: mcpSetting?.requiredMcpConfig, |
| }; |
| admin.extensions = { |
| enabled: cliFeatureSetting?.extensionsSetting?.extensionsEnabled, |
| }; |
| admin.skills = { |
| enabled: cliFeatureSetting?.unmanagedCapabilitiesEnabled, |
| }; |
|
|
| this._remoteAdminSettings = { admin }; |
| this._merged = this.computeMergedSettings(); |
| } |
|
|
| |
| |
| |
| getConsolidatedExcludedMcpServers(): string[] { |
| const scopes = [ |
| this.system, |
| this.systemDefaults, |
| this.user, |
| this.workspace, |
| ]; |
| return scopes.flatMap((scope) => { |
| const excluded = scope?.settings?.mcp?.excluded; |
| return Array.isArray(excluded) ? excluded : []; |
| }); |
| } |
|
|
| |
| |
| |
| getConsolidatedAllowedMcpServers(): string[] | undefined { |
| const scopes = [ |
| this.system, |
| this.systemDefaults, |
| this.user, |
| this.workspace, |
| ]; |
| const definedAllowlists = scopes.flatMap((scope) => { |
| const allowed = scope?.settings?.mcp?.allowed; |
| return Array.isArray(allowed) ? [allowed] : []; |
| }); |
|
|
| if (definedAllowlists.length === 0) { |
| return undefined; |
| } |
|
|
| return definedAllowlists.reduce((acc, current) => { |
| const normalizedCurrent = new Set( |
| current.map((item) => item.toLowerCase().trim()), |
| ); |
| return acc.filter((item) => |
| normalizedCurrent.has(item.toLowerCase().trim()), |
| ); |
| }); |
| } |
| } |
|
|
| function findEnvFile( |
| startDir: string, |
| isTrusted: boolean, |
| ignoreLocalEnv: boolean, |
| ): string | null { |
| let currentDir = path.resolve(startDir); |
| while (true) { |
| |
| if (isTrusted) { |
| const geminiEnvPath = path.join(currentDir, GEMINI_DIR, '.env'); |
| if (fs.existsSync(geminiEnvPath)) { |
| return geminiEnvPath; |
| } |
| } |
| const envPath = path.join(currentDir, '.env'); |
| if (fs.existsSync(envPath)) { |
| if (!ignoreLocalEnv || currentDir === homedir()) { |
| return envPath; |
| } |
| } |
| const parentDir = path.dirname(currentDir); |
| if (parentDir === currentDir || !parentDir) { |
| |
| if (isTrusted) { |
| const homeGeminiEnvPath = path.join(homedir(), GEMINI_DIR, '.env'); |
| if (fs.existsSync(homeGeminiEnvPath)) { |
| return homeGeminiEnvPath; |
| } |
| } |
| const homeEnvPath = path.join(homedir(), '.env'); |
| if (fs.existsSync(homeEnvPath)) { |
| return homeEnvPath; |
| } |
| return null; |
| } |
| currentDir = parentDir; |
| } |
| } |
|
|
| |
| |
| |
| const USER_GCP_PROJECT = '_GEMINI_USER_GCP_PROJECT'; |
|
|
| export function setUpCloudShellEnvironment( |
| envFilePath: string | null, |
| isTrusted: boolean, |
| isSandboxed: boolean, |
| selectedAuthType?: string, |
| ): void { |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| if (!process.env[USER_GCP_PROJECT]) { |
| const current = process.env['GOOGLE_CLOUD_PROJECT']; |
| if (current && current !== 'cloudshell-gca') { |
| process.env[USER_GCP_PROJECT] = current; |
| } |
| } |
|
|
| let value: string | undefined = 'cloudshell-gca'; |
|
|
| if (selectedAuthType === AuthType.USE_VERTEX_AI) { |
| value = process.env[USER_GCP_PROJECT]; |
| } |
|
|
| if (envFilePath && fs.existsSync(envFilePath)) { |
| const envFileContent = fs.readFileSync(envFilePath); |
| const parsedEnv = dotenv.parse(envFileContent); |
| if (parsedEnv['GOOGLE_CLOUD_PROJECT']) { |
| |
| value = parsedEnv['GOOGLE_CLOUD_PROJECT']; |
| if (!isTrusted && isSandboxed) { |
| value = sanitizeEnvVar(value); |
| } |
| } |
| } |
|
|
| if (value !== undefined) { |
| process.env['GOOGLE_CLOUD_PROJECT'] = value; |
| } else if (process.env['GOOGLE_CLOUD_PROJECT'] === 'cloudshell-gca') { |
| delete process.env['GOOGLE_CLOUD_PROJECT']; |
| } |
| } |
|
|
| export function loadEnvironment( |
| settings: Settings, |
| workspaceDir: string, |
| isWorkspaceTrustedFn = isWorkspaceTrusted, |
| ): void { |
| const trustResult = isWorkspaceTrustedFn(settings, workspaceDir); |
| const isTrusted = trustResult.isTrusted ?? false; |
|
|
| |
| |
| |
| |
| |
| const args = process.argv.slice(2); |
| const doubleDashIndex = args.indexOf('--'); |
| const relevantArgs = |
| doubleDashIndex === -1 ? args : args.slice(0, doubleDashIndex); |
|
|
| const isSandboxed = |
| !!settings.tools?.sandbox || |
| relevantArgs.includes('-s') || |
| relevantArgs.includes('--sandbox'); |
|
|
| const shouldIgnoreEnv = |
| !!settings.advanced?.ignoreLocalEnv || |
| relevantArgs.includes('--ignore-env'); |
|
|
| const envFilePath = findEnvFile(workspaceDir, isTrusted, shouldIgnoreEnv); |
|
|
| |
| if (process.env['CLOUD_SHELL'] === 'true') { |
| const selectedAuthType = settings.security?.auth?.selectedType; |
| setUpCloudShellEnvironment( |
| envFilePath, |
| isTrusted, |
| isSandboxed, |
| selectedAuthType, |
| ); |
| } |
|
|
| if (envFilePath) { |
| |
| |
| try { |
| const envFileContent = fs.readFileSync(envFilePath, 'utf-8'); |
| const parsedEnv = dotenv.parse(envFileContent); |
|
|
| const excludedVars = |
| settings?.advanced?.excludedEnvVars || DEFAULT_EXCLUDED_ENV_VARS; |
| const isProjectEnvFile = !envFilePath.includes(GEMINI_DIR); |
|
|
| for (const key in parsedEnv) { |
| if (Object.hasOwn(parsedEnv, key)) { |
| let value = parsedEnv[key]; |
| |
| if (!isTrusted) { |
| if (!AUTH_ENV_VAR_WHITELIST.includes(key)) { |
| continue; |
| } |
| |
| value = sanitizeEnvVar(value); |
| } |
|
|
| |
| if (isProjectEnvFile && excludedVars.includes(key)) { |
| continue; |
| } |
|
|
| |
| if (!Object.hasOwn(process.env, key)) { |
| process.env[key] = value; |
| } |
| } |
| } |
| } catch { |
| |
| } |
| } |
| } |
|
|
| |
| const settingsCache = createCache<string, LoadedSettings>({ |
| storage: 'map', |
| defaultTtl: 10000, |
| }); |
|
|
| |
| |
| |
| |
| export function resetSettingsCacheForTesting() { |
| settingsCache.clear(); |
| } |
|
|
| export function isWorktreeEnabled(settings: LoadedSettings): boolean { |
| return settings.merged.experimental.worktrees; |
| } |
|
|
| |
| |
| |
| |
| export function loadSettings( |
| workspaceDir: string = process.cwd(), |
| ): LoadedSettings { |
| const normalizedWorkspaceDir = path.resolve(workspaceDir); |
| return settingsCache.getOrCreate(normalizedWorkspaceDir, () => |
| _doLoadSettings(normalizedWorkspaceDir), |
| ); |
| } |
|
|
| |
| |
| |
| function _doLoadSettings(workspaceDir: string): LoadedSettings { |
| let systemSettings: Settings = {}; |
| let systemDefaultSettings: Settings = {}; |
| let userSettings: Settings = {}; |
| let workspaceSettings: Settings = {}; |
| const settingsErrors: SettingsError[] = []; |
| const systemSettingsPath = getSystemSettingsPath(); |
| const systemDefaultsPath = getSystemDefaultsPath(); |
|
|
| const storage = new Storage(workspaceDir); |
| const workspaceSettingsPath = storage.getWorkspaceSettingsPath(); |
|
|
| const load = ( |
| filePath: string, |
| ): { settings: Settings; rawSettings: Settings; rawJson?: string } => { |
| try { |
| if (fs.existsSync(filePath)) { |
| const content = fs.readFileSync(filePath, 'utf-8'); |
| const rawSettings: unknown = JSON.parse(stripJsonComments(content)); |
|
|
| if ( |
| typeof rawSettings !== 'object' || |
| rawSettings === null || |
| Array.isArray(rawSettings) |
| ) { |
| settingsErrors.push({ |
| message: 'Settings file is not a valid JSON object.', |
| path: filePath, |
| severity: 'error', |
| }); |
| return { settings: {}, rawSettings: {} }; |
| } |
|
|
| |
| const settingsObject = rawSettings as Record<string, unknown>; |
|
|
| |
| const expandedSettings = resolveEnvVarsInObject( |
| settingsObject as Settings, |
| ); |
|
|
| |
| const validationResult = validateSettings(expandedSettings); |
| if (!validationResult.success && validationResult.error) { |
| const errorMessage = formatValidationError( |
| validationResult.error, |
| filePath, |
| ); |
| settingsErrors.push({ |
| message: errorMessage, |
| path: filePath, |
| severity: 'warning', |
| }); |
| return { |
| settings: expandedSettings, |
| rawSettings: settingsObject as Settings, |
| rawJson: content, |
| }; |
| } |
|
|
| |
| return { |
| |
| |
| |
| settings: (validationResult.data as Settings) ?? expandedSettings, |
| rawSettings: settingsObject as Settings, |
| rawJson: content, |
| }; |
| } |
| } catch (error: unknown) { |
| settingsErrors.push({ |
| message: getErrorMessage(error), |
| path: filePath, |
| severity: 'error', |
| }); |
| } |
| return { settings: {}, rawSettings: {} }; |
| }; |
|
|
| const securityCache = createPathSecurityCache(); |
|
|
| const loadSystemFile = ( |
| filePath: string, |
| fileLabel: string, |
| ): { settings: Settings; rawSettings: Settings; rawJson?: string } => { |
| if (!fs.existsSync(filePath)) { |
| return { settings: {}, rawSettings: {} }; |
| } |
|
|
| const check = isFileAndDirectorySecureSync(filePath, securityCache); |
| if (!check.secure) { |
| settingsErrors.push({ |
| message: `Security Warning: Skipping ${fileLabel} file '${filePath}': ${check.reason}`, |
| path: filePath, |
| severity: 'warning', |
| }); |
| return { settings: {}, rawSettings: {} }; |
| } |
|
|
| return load(filePath); |
| }; |
|
|
| const systemResult = loadSystemFile(systemSettingsPath, 'system settings'); |
| const systemDefaultsResult = loadSystemFile( |
| systemDefaultsPath, |
| 'system defaults', |
| ); |
| const userResult = load(USER_SETTINGS_PATH); |
|
|
| let workspaceResult: { |
| settings: Settings; |
| rawSettings: Settings; |
| rawJson?: string; |
| } = { |
| settings: {} as Settings, |
| rawSettings: {} as Settings, |
| rawJson: undefined, |
| }; |
| if (!storage.isWorkspaceHomeDir()) { |
| workspaceResult = load(workspaceSettingsPath); |
| } |
|
|
| const systemOriginalSettings = structuredClone(systemResult.rawSettings); |
| const systemDefaultsOriginalSettings = structuredClone( |
| systemDefaultsResult.rawSettings, |
| ); |
| const userOriginalSettings = structuredClone(userResult.rawSettings); |
| const workspaceOriginalSettings = structuredClone( |
| workspaceResult.rawSettings, |
| ); |
|
|
| |
| systemSettings = systemResult.settings; |
| systemDefaultSettings = systemDefaultsResult.settings; |
| userSettings = userResult.settings; |
| workspaceSettings = workspaceResult.settings; |
|
|
| |
| if (userSettings.ui?.theme === 'VS') { |
| userSettings.ui.theme = DefaultLight.name; |
| } else if (userSettings.ui?.theme === 'VS2015') { |
| userSettings.ui.theme = DefaultDark.name; |
| } |
| if (workspaceSettings.ui?.theme === 'VS') { |
| workspaceSettings.ui.theme = DefaultLight.name; |
| } else if (workspaceSettings.ui?.theme === 'VS2015') { |
| workspaceSettings.ui.theme = DefaultDark.name; |
| } |
|
|
| |
| const initialTrustCheckSettings = customDeepMerge( |
| getMergeStrategyForPath, |
| getDefaultsFromSchema(), |
| systemDefaultSettings, |
| userSettings, |
| systemSettings, |
| ); |
| const isTrusted = |
| isWorkspaceTrusted(initialTrustCheckSettings as Settings, workspaceDir) |
| ?.isTrusted ?? false; |
|
|
| |
| const tempMergedSettings = mergeSettings( |
| systemSettings, |
| systemDefaultSettings, |
| userSettings, |
| workspaceSettings, |
| isTrusted, |
| ); |
|
|
| |
| |
| loadEnvironment(tempMergedSettings, workspaceDir); |
|
|
| |
| const fatalErrors = settingsErrors.filter((e) => e.severity === 'error'); |
| if (fatalErrors.length > 0) { |
| const errorMessages = fatalErrors.map( |
| (error) => `Error in ${error.path}: ${error.message}`, |
| ); |
| throw new FatalConfigError( |
| `${errorMessages.join('\n')}\nPlease fix the configuration file(s) and try again.`, |
| ); |
| } |
|
|
| const loadedSettings = new LoadedSettings( |
| { |
| path: systemSettingsPath, |
| settings: systemSettings, |
| originalSettings: systemOriginalSettings, |
| rawJson: systemResult.rawJson, |
| readOnly: true, |
| }, |
| { |
| path: systemDefaultsPath, |
| settings: systemDefaultSettings, |
| originalSettings: systemDefaultsOriginalSettings, |
| rawJson: systemDefaultsResult.rawJson, |
| readOnly: true, |
| }, |
| { |
| path: USER_SETTINGS_PATH, |
| settings: userSettings, |
| originalSettings: userOriginalSettings, |
| rawJson: userResult.rawJson, |
| readOnly: false, |
| }, |
| { |
| path: storage.isWorkspaceHomeDir() ? '' : workspaceSettingsPath, |
| settings: workspaceSettings, |
| originalSettings: workspaceOriginalSettings, |
| rawJson: workspaceResult.rawJson, |
| readOnly: storage.isWorkspaceHomeDir(), |
| }, |
| isTrusted, |
| settingsErrors, |
| ); |
|
|
| |
| migrateDeprecatedSettings(loadedSettings); |
|
|
| return loadedSettings; |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| export function migrateDeprecatedSettings( |
| loadedSettings: LoadedSettings, |
| removeDeprecated = true, |
| ): boolean { |
| let anyModified = false; |
| const systemWarnings: Map<LoadableSettingScope, string[]> = new Map(); |
|
|
| |
| |
| |
| const migrateBoolean = ( |
| settings: Record<string, unknown>, |
| oldKey: string, |
| newKey: string, |
| prefix: string, |
| foundDeprecated?: string[], |
| ): boolean => { |
| let modified = false; |
| const oldValue = settings[oldKey]; |
| const newValue = settings[newKey]; |
|
|
| if (typeof oldValue === 'boolean') { |
| if (foundDeprecated) { |
| foundDeprecated.push(prefix ? `${prefix}.${oldKey}` : oldKey); |
| } |
| if (typeof newValue === 'boolean') { |
| |
| if (removeDeprecated) { |
| delete settings[oldKey]; |
| modified = true; |
| } |
| } else { |
| |
| settings[newKey] = !oldValue; |
| if (removeDeprecated) { |
| delete settings[oldKey]; |
| } |
| modified = true; |
| } |
| } |
| return modified; |
| }; |
|
|
| const processScope = (scope: LoadableSettingScope) => { |
| const settingsFile = loadedSettings.forScope(scope); |
| const settings = settingsFile.settings; |
| const foundDeprecated: string[] = []; |
|
|
| |
| const generalSettings = settings.general as |
| | Record<string, unknown> |
| | undefined; |
| if (generalSettings) { |
| const newGeneral = { ...generalSettings }; |
| let modified = false; |
|
|
| modified = |
| migrateBoolean( |
| newGeneral, |
| 'disableAutoUpdate', |
| 'enableAutoUpdate', |
| 'general', |
| foundDeprecated, |
| ) || modified; |
| modified = |
| migrateBoolean( |
| newGeneral, |
| 'disableUpdateNag', |
| 'enableAutoUpdateNotification', |
| 'general', |
| foundDeprecated, |
| ) || modified; |
|
|
| if (modified) { |
| loadedSettings.setValue(scope, 'general', newGeneral); |
| if (!settingsFile.readOnly) { |
| anyModified = true; |
| } |
| } |
| } |
|
|
| |
| const uiSettings = settings.ui as Record<string, unknown> | undefined; |
| if (uiSettings) { |
| const newUi = { ...uiSettings }; |
| |
| const accessibilitySettings = newUi['accessibility'] as |
| | Record<string, unknown> |
| | undefined; |
|
|
| if (accessibilitySettings) { |
| const newAccessibility = { ...accessibilitySettings }; |
| if ( |
| migrateBoolean( |
| newAccessibility, |
| 'disableLoadingPhrases', |
| 'enableLoadingPhrases', |
| 'ui.accessibility', |
| foundDeprecated, |
| ) |
| ) { |
| newUi['accessibility'] = newAccessibility; |
| loadedSettings.setValue(scope, 'ui', newUi); |
| if (!settingsFile.readOnly) { |
| anyModified = true; |
| } |
| } |
|
|
| |
| const enableLP = newAccessibility['enableLoadingPhrases']; |
| if ( |
| typeof enableLP === 'boolean' && |
| newUi['loadingPhrases'] === undefined |
| ) { |
| if (!enableLP) { |
| newUi['loadingPhrases'] = 'off'; |
| loadedSettings.setValue(scope, 'ui', newUi); |
| if (!settingsFile.readOnly) { |
| anyModified = true; |
| } |
| } |
| foundDeprecated.push('ui.accessibility.enableLoadingPhrases'); |
| } |
| } |
| } |
|
|
| |
| const contextSettings = settings.context as |
| | Record<string, unknown> |
| | undefined; |
| if (contextSettings) { |
| const newContext = { ...contextSettings }; |
| |
| const fileFilteringSettings = newContext['fileFiltering'] as |
| | Record<string, unknown> |
| | undefined; |
|
|
| if (fileFilteringSettings) { |
| const newFileFiltering = { ...fileFilteringSettings }; |
| if ( |
| migrateBoolean( |
| newFileFiltering, |
| 'disableFuzzySearch', |
| 'enableFuzzySearch', |
| 'context.fileFiltering', |
| foundDeprecated, |
| ) |
| ) { |
| newContext['fileFiltering'] = newFileFiltering; |
| loadedSettings.setValue(scope, 'context', newContext); |
| if (!settingsFile.readOnly) { |
| anyModified = true; |
| } |
| } |
| } |
| } |
|
|
| |
| const toolsSettings = settings.tools as Record<string, unknown> | undefined; |
| if (toolsSettings) { |
| if (toolsSettings['approvalMode'] !== undefined) { |
| foundDeprecated.push('tools.approvalMode'); |
|
|
| const generalSettings = |
| (settings.general as Record<string, unknown> | undefined) || {}; |
| const newGeneral = { ...generalSettings }; |
|
|
| |
| if (newGeneral['defaultApprovalMode'] === undefined) { |
| newGeneral['defaultApprovalMode'] = toolsSettings['approvalMode']; |
| loadedSettings.setValue(scope, 'general', newGeneral); |
| if (!settingsFile.readOnly) { |
| anyModified = true; |
| } |
| } |
|
|
| if (removeDeprecated) { |
| const newTools = { ...toolsSettings }; |
| delete newTools['approvalMode']; |
| loadedSettings.setValue(scope, 'tools', newTools); |
| if (!settingsFile.readOnly) { |
| anyModified = true; |
| } |
| } |
| } |
| } |
|
|
| |
| const experimentalModified = migrateExperimentalSettings( |
| settings, |
| loadedSettings, |
| scope, |
| removeDeprecated, |
| foundDeprecated, |
| ); |
|
|
| if (experimentalModified) { |
| if (!settingsFile.readOnly) { |
| anyModified = true; |
| } |
| } |
|
|
| if (settingsFile.readOnly && foundDeprecated.length > 0) { |
| systemWarnings.set(scope, foundDeprecated); |
| } |
| }; |
|
|
| processScope(SettingScope.User); |
| processScope(SettingScope.Workspace); |
| processScope(SettingScope.System); |
| processScope(SettingScope.SystemDefaults); |
|
|
| if (systemWarnings.size > 0) { |
| for (const [scope, flags] of systemWarnings) { |
| const scopeName = |
| scope === SettingScope.SystemDefaults |
| ? 'system default' |
| : scope.toLowerCase(); |
| coreEvents.emitFeedback( |
| 'warning', |
| `The ${scopeName} configuration contains deprecated settings: [${flags.join(', ')}]. These could not be migrated automatically as system settings are read-only. Please update the system configuration manually.`, |
| ); |
| } |
| } |
|
|
| return anyModified; |
| } |
|
|
| export function saveSettings(settingsFile: SettingsFile): void { |
| |
| settingsCache.clear(); |
|
|
| try { |
| |
| const dirPath = path.dirname(settingsFile.path); |
| if (!fs.existsSync(dirPath)) { |
| fs.mkdirSync(dirPath, { recursive: true }); |
| } |
|
|
| const settingsToSave = settingsFile.originalSettings; |
|
|
| |
| updateSettingsFilePreservingFormat( |
| settingsFile.path, |
| settingsToSave as Record<string, unknown>, |
| ); |
| } catch (error) { |
| const detailedErrorMessage = getFsErrorMessage(error); |
| coreEvents.emitFeedback( |
| 'error', |
| `Failed to save settings: ${detailedErrorMessage}`, |
| error, |
| ); |
| } |
| } |
|
|
| export function saveModelChange( |
| loadedSettings: LoadedSettings, |
| model: string, |
| ): void { |
| try { |
| loadedSettings.setValue(SettingScope.User, 'model.name', model); |
| } catch (error) { |
| const detailedErrorMessage = getFsErrorMessage(error); |
| coreEvents.emitFeedback( |
| 'error', |
| `Failed to save preferred model: ${detailedErrorMessage}`, |
| error, |
| ); |
| } |
| } |
|
|
| function migrateExperimentalSettings( |
| settings: Settings, |
| loadedSettings: LoadedSettings, |
| scope: LoadableSettingScope, |
| removeDeprecated: boolean, |
| foundDeprecated?: string[], |
| ): boolean { |
| const experimentalSettings = settings.experimental as |
| | Record<string, unknown> |
| | undefined; |
|
|
| if (experimentalSettings) { |
| const agentsSettings = { |
| ...(settings.agents as Record<string, unknown> | undefined), |
| }; |
| const agentsOverrides = { |
| |
| ...((agentsSettings['overrides'] as Record<string, unknown>) || {}), |
| }; |
| let modified = false; |
|
|
| const migrateExperimental = <T = Record<string, unknown>>( |
| oldKey: string, |
| migrateFn: (oldValue: T) => void, |
| ) => { |
| const old = experimentalSettings[oldKey]; |
| if (old !== undefined) { |
| foundDeprecated?.push(`experimental.${oldKey}`); |
| |
| migrateFn(old as T); |
| modified = true; |
| } |
| }; |
|
|
| |
| migrateExperimental('codebaseInvestigatorSettings', (old) => { |
| const override = { |
| |
| ...(agentsOverrides['codebase_investigator'] as |
| | Record<string, unknown> |
| | undefined), |
| }; |
|
|
| if (old['enabled'] !== undefined) override['enabled'] = old['enabled']; |
|
|
| const runConfig = { |
| |
| ...(override['runConfig'] as Record<string, unknown> | undefined), |
| }; |
| if (old['maxNumTurns'] !== undefined) |
| runConfig['maxTurns'] = old['maxNumTurns']; |
| if (old['maxTimeMinutes'] !== undefined) |
| runConfig['maxTimeMinutes'] = old['maxTimeMinutes']; |
| if (Object.keys(runConfig).length > 0) override['runConfig'] = runConfig; |
|
|
| if (old['model'] !== undefined || old['thinkingBudget'] !== undefined) { |
| const modelConfig = { |
| |
| ...(override['modelConfig'] as Record<string, unknown> | undefined), |
| }; |
| if (old['model'] !== undefined) modelConfig['model'] = old['model']; |
| if (old['thinkingBudget'] !== undefined) { |
| const generateContentConfig = { |
| |
| ...(modelConfig['generateContentConfig'] as |
| | Record<string, unknown> |
| | undefined), |
| }; |
| const thinkingConfig = { |
| |
| ...(generateContentConfig['thinkingConfig'] as |
| | Record<string, unknown> |
| | undefined), |
| }; |
| thinkingConfig['thinkingBudget'] = old['thinkingBudget']; |
| generateContentConfig['thinkingConfig'] = thinkingConfig; |
| modelConfig['generateContentConfig'] = generateContentConfig; |
| } |
| override['modelConfig'] = modelConfig; |
| } |
|
|
| agentsOverrides['codebase_investigator'] = override; |
| }); |
|
|
| |
| migrateExperimental('cliHelpAgentSettings', (old) => { |
| const override = { |
| |
| ...(agentsOverrides['cli_help'] as Record<string, unknown> | undefined), |
| }; |
| if (old['enabled'] !== undefined) override['enabled'] = old['enabled']; |
| agentsOverrides['cli_help'] = override; |
| }); |
|
|
| |
| migrateExperimental<boolean>('plan', (planValue) => { |
| const generalSettings = |
| (settings.general as Record<string, unknown> | undefined) || {}; |
| const newGeneral = { ...generalSettings }; |
| const planSettings = |
| |
| (newGeneral['plan'] as Record<string, unknown> | undefined) || {}; |
| const newPlan = { ...planSettings }; |
|
|
| if (newPlan['enabled'] === undefined) { |
| newPlan['enabled'] = planValue; |
| newGeneral['plan'] = newPlan; |
| loadedSettings.setValue(scope, 'general', newGeneral); |
| modified = true; |
| } |
| }); |
|
|
| if (modified) { |
| agentsSettings['overrides'] = agentsOverrides; |
| loadedSettings.setValue(scope, 'agents', agentsSettings); |
|
|
| if (removeDeprecated) { |
| const newExperimental = { ...experimentalSettings }; |
| delete newExperimental['codebaseInvestigatorSettings']; |
| delete newExperimental['cliHelpAgentSettings']; |
| delete newExperimental['plan']; |
| loadedSettings.setValue(scope, 'experimental', newExperimental); |
| } |
| return true; |
| } |
| } |
| return false; |
| } |
|
|