basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 *6 * Adapter that lets core's `applyProviderInstallPlan` write through7 * `LoadedSettings` while preserving CLI-specific guarantees:8 * - scope resolution via `getPersistScopeForModelSelection`9 * - on-disk `.orig` backup of the target settings file10 * - in-memory snapshot of `settings` / `originalSettings` for rollback11 * - merged-settings recomputation after restore12 */13 14import type {15 ModelProvidersConfig,16 ProviderSettingsAdapter,17} from '@qwen-code/qwen-code-core';18import type { LoadedSettings, SettingScope } from './settings.js';19import { getPersistScopeForModelSelection } from './modelProvidersScope.js';20import {21 backupSettingsFile,22 cleanupSettingsBackup,23 restoreSettingsFromBackup,24 getNestedProperty,25} from '../utils/settingsUtils.js';26 27export function createLoadedSettingsAdapter(28 settings: LoadedSettings,29 scope?: SettingScope,30): ProviderSettingsAdapter {31 const persistScope = scope ?? getPersistScopeForModelSelection(settings);32 const settingsFile = settings.forScope(persistScope);33 34 let settingsSnapshot: object | null = null;35 let originalSnapshot: object | null = null;36 37 return {38 getValue(key: string): unknown {39 return getNestedProperty(settings.merged as Record<string, unknown>, key);40 },41 42 setValue(key: string, value: unknown): void {43 // Defense in depth: refuse prototype-chain segments before delegating to44 // LoadedSettings.setValue, which goes through setNestedPropertySafe and45 // doesn't enforce this itself. Inline literal === comparisons (rather46 // than Set.has) are what CodeQL's prototype-pollution sanitiser47 // recognises — keep this list in sync with the matching guard in48 // `packages/vscode-ide-companion/src/services/settingsWriter.ts`.49 for (const part of key.split('.')) {50 if (51 part === '__proto__' ||52 part === 'constructor' ||53 part === 'prototype'54 ) {55 throw new Error(56 `Refusing to write settings key with reserved segment: ${key}`,57 );58 }59 }60 settings.setValue(persistScope, key, value);61 },62 63 getModelProviders(): ModelProvidersConfig {64 return (settings.merged.modelProviders ?? {}) as ModelProvidersConfig;65 },66 67 persist(): void {68 // LoadedSettings.setValue already persists on each write.69 },70 71 backup(): void {72 backupSettingsFile(settingsFile.path);73 settingsSnapshot = structuredClone(settingsFile.settings);74 originalSnapshot = structuredClone(settingsFile.originalSettings);75 },76 77 restore(): void {78 // restoreSettingsFromBackup returns false (rather than throwing) when79 // the .orig copy can't be restored (EACCES, disk full, missing .orig).80 // Log loudly so a user staring at the next CLI session knows the81 // on-disk file may be inconsistent with the recovered in-memory state.82 const restored = restoreSettingsFromBackup(settingsFile.path);83 if (!restored) {84 // eslint-disable-next-line no-console -- best-effort rollback path85 console.error(86 `[loadedSettingsAdapter] On-disk rollback of ${settingsFile.path} failed; ` +87 `in-memory state was restored but the file may be inconsistent. ` +88 `Re-run /auth or inspect the file directly to recover.`,89 );90 }91 if (settingsSnapshot !== null) {92 settingsFile.settings =93 settingsSnapshot as typeof settingsFile.settings;94 }95 if (originalSnapshot !== null) {96 settingsFile.originalSettings =97 originalSnapshot as typeof settingsFile.originalSettings;98 }99 settings.recomputeMerged();100 },101 102 cleanupBackup(): void {103 cleanupSettingsBackup(settingsFile.path);104 settingsSnapshot = null;105 originalSnapshot = null;106 },107 };108}109 