@@ -199,6 +199,11 @@ export const DEFAULT_PLUGIN_CONFIG: PluginConfig = {
199199 preemptiveQuotaMaxDeferralMs : 2 * 60 * 60_000 ,
200200} ;
201201
202+ const PLUGIN_CONFIG_FIELD_SCHEMAS = PluginConfigSchema . shape ;
203+ const PLUGIN_CONFIG_KEYS = Object . keys ( DEFAULT_PLUGIN_CONFIG ) as Array <
204+ keyof PluginConfig
205+ > ;
206+
202207/**
203208 * Return a shallow copy of the default plugin configuration.
204209 *
@@ -238,6 +243,10 @@ export function loadPluginConfig(): PluginConfig {
238243 sourceKind = "file" ;
239244 }
240245
246+ const normalizedUserConfig = sanitizePluginConfigRecord ( userConfig , {
247+ warnOnInvalid : true ,
248+ } ) ;
249+
241250 const hasFallbackEnvOverride =
242251 process . env . CODEX_AUTH_FALLBACK_UNSUPPORTED_MODEL !== undefined ||
243252 process . env . CODEX_AUTH_FALLBACK_GPT53_TO_GPT52 !== undefined ;
@@ -255,16 +264,9 @@ export function loadPluginConfig(): PluginConfig {
255264 }
256265 }
257266
258- const schemaErrors = getValidationErrors ( PluginConfigSchema , userConfig ) ;
259- if ( schemaErrors . length > 0 ) {
260- logConfigWarnOnce (
261- `Plugin config validation warnings: ${ schemaErrors . slice ( 0 , 3 ) . join ( ", " ) } ` ,
262- ) ;
263- }
264-
265267 if (
266268 sourceKind === "file" &&
267- isRecord ( userConfig ) &&
269+ normalizedUserConfig !== null &&
268270 ( process . env . CODEX_MULTI_AUTH_CONFIG_PATH ?? "" ) . trim ( ) . length === 0
269271 ) {
270272 logConfigWarnOnce (
@@ -274,7 +276,7 @@ export function loadPluginConfig(): PluginConfig {
274276
275277 return {
276278 ...DEFAULT_PLUGIN_CONFIG ,
277- ...( userConfig as Partial < PluginConfig > ) ,
279+ ...( normalizedUserConfig ?? { } ) ,
278280 } ;
279281 } catch ( error ) {
280282 const configPath = resolvePluginConfigPath ( ) ?? CONFIG_PATH ;
@@ -285,6 +287,62 @@ export function loadPluginConfig(): PluginConfig {
285287 }
286288}
287289
290+ function sanitizePluginConfigRecord (
291+ data : unknown ,
292+ options ?: { warnOnInvalid ?: boolean } ,
293+ ) : Partial < PluginConfig > | null {
294+ if ( ! isRecord ( data ) ) {
295+ return null ;
296+ }
297+
298+ if ( options ?. warnOnInvalid ) {
299+ const schemaErrors = getValidationErrors ( PluginConfigSchema , data ) ;
300+ if ( schemaErrors . length > 0 ) {
301+ logConfigWarnOnce (
302+ `Plugin config validation warnings: ${ schemaErrors . slice ( 0 , 3 ) . join ( ", " ) } ` ,
303+ ) ;
304+ }
305+ }
306+
307+ const sanitized : Record < string , unknown > = { } ;
308+ for ( const key of PLUGIN_CONFIG_KEYS ) {
309+ const value = data [ key ] ;
310+ if ( value === undefined ) {
311+ continue ;
312+ }
313+ const schema = PLUGIN_CONFIG_FIELD_SCHEMAS [ key ] ;
314+ const result = schema . safeParse ( value ) ;
315+ if ( result . success ) {
316+ sanitized [ String ( key ) ] = result . data ;
317+ }
318+ }
319+
320+ return sanitized as Partial < PluginConfig > ;
321+ }
322+
323+ function sanitizeStoredPluginConfigRecord (
324+ data : unknown ,
325+ ) : Record < string , unknown > {
326+ if ( ! isRecord ( data ) ) {
327+ return { } ;
328+ }
329+
330+ const sanitized : Record < string , unknown > = { } ;
331+ for ( const [ key , value ] of Object . entries ( data ) ) {
332+ if ( ! PLUGIN_CONFIG_KEYS . includes ( key as keyof PluginConfig ) ) {
333+ sanitized [ key ] = value ;
334+ continue ;
335+ }
336+ const schema = PLUGIN_CONFIG_FIELD_SCHEMAS [ key as keyof PluginConfig ] ;
337+ const result = schema . safeParse ( value ) ;
338+ if ( result . success ) {
339+ sanitized [ key ] = result . data ;
340+ }
341+ }
342+
343+ return sanitized ;
344+ }
345+
288346/**
289347 * Remove a leading UTF‑8 byte order mark (BOM) from the given string if present.
290348 *
@@ -443,7 +501,8 @@ function resolveStoredPluginConfigRecord(): {
443501function sanitizePluginConfigForSave (
444502 config : Partial < PluginConfig > ,
445503) : Record < string , unknown > {
446- const entries = Object . entries ( config as Record < string , unknown > ) ;
504+ const normalized = sanitizePluginConfigRecord ( config as Record < string , unknown > ) ;
505+ const entries = Object . entries ( ( normalized ?? { } ) as Record < string , unknown > ) ;
447506 const sanitized : Record < string , unknown > = { } ;
448507 for ( const [ key , value ] of entries ) {
449508 if ( value === undefined ) continue ;
@@ -478,8 +537,11 @@ export async function savePluginConfig(
478537
479538 if ( envPath . length > 0 ) {
480539 await withConfigSaveLock ( envPath , async ( ) => {
540+ const existingConfig = sanitizeStoredPluginConfigRecord (
541+ readConfigRecordFromPath ( envPath ) ,
542+ ) ;
481543 const merged = {
482- ...( readConfigRecordFromPath ( envPath ) ?? { } ) ,
544+ ...( existingConfig ?? { } ) ,
483545 ...sanitizedPatch ,
484546 } ;
485547 await writeJsonFileAtomicWithRetry ( envPath , merged ) ;
@@ -489,11 +551,16 @@ export async function savePluginConfig(
489551
490552 const unifiedPath = getUnifiedSettingsPath ( ) ;
491553 await withConfigSaveLock ( unifiedPath , async ( ) => {
492- const unifiedConfig = loadUnifiedPluginConfigSync ( ) ;
554+ const unifiedConfig = sanitizeStoredPluginConfigRecord (
555+ loadUnifiedPluginConfigSync ( ) ,
556+ ) ;
493557 const legacyPath = unifiedConfig ? null : resolvePluginConfigPath ( ) ;
558+ const legacyConfig = legacyPath
559+ ? sanitizeStoredPluginConfigRecord ( readConfigRecordFromPath ( legacyPath ) )
560+ : null ;
494561 const merged = {
495562 ...( unifiedConfig ??
496- ( legacyPath ? readConfigRecordFromPath ( legacyPath ) : null ) ??
563+ legacyConfig ??
497564 { } ) ,
498565 ...sanitizedPatch ,
499566 } ;
@@ -510,12 +577,30 @@ export async function savePluginConfig(
510577 */
511578function parseBooleanEnv ( value : string | undefined ) : boolean | undefined {
512579 if ( value === undefined ) return undefined ;
513- return value === "1" ;
580+ const normalized = value . trim ( ) . toLowerCase ( ) ;
581+ if ( normalized . length === 0 ) return undefined ;
582+ if (
583+ normalized === "1" ||
584+ normalized === "true" ||
585+ normalized === "yes"
586+ ) {
587+ return true ;
588+ }
589+ if (
590+ normalized === "0" ||
591+ normalized === "false" ||
592+ normalized === "no"
593+ ) {
594+ return false ;
595+ }
596+ return undefined ;
514597}
515598
516599function parseNumberEnv ( value : string | undefined ) : number | undefined {
517600 if ( value === undefined ) return undefined ;
518- const parsed = Number ( value ) ;
601+ const trimmed = value . trim ( ) ;
602+ if ( trimmed . length === 0 ) return undefined ;
603+ const parsed = Number ( trimmed ) ;
519604 if ( ! Number . isFinite ( parsed ) ) return undefined ;
520605 return parsed ;
521606}
@@ -544,7 +629,13 @@ function resolveBooleanSetting(
544629 configValue : boolean | undefined ,
545630 defaultValue : boolean ,
546631) : boolean {
547- const envValue = parseBooleanEnv ( process . env [ envName ] ) ;
632+ const rawEnvValue = process . env [ envName ] ;
633+ const envValue = parseBooleanEnv ( rawEnvValue ) ;
634+ if ( rawEnvValue !== undefined && envValue === undefined ) {
635+ logConfigWarnOnce (
636+ `Ignoring invalid boolean env ${ envName } =${ JSON . stringify ( rawEnvValue ) } . Expected 0/1, true/false, or yes/no.` ,
637+ ) ;
638+ }
548639 if ( envValue !== undefined ) return envValue ;
549640 return configValue ?? defaultValue ;
550641}
@@ -566,7 +657,13 @@ function resolveNumberSetting(
566657 defaultValue : number ,
567658 options ?: { min ?: number ; max ?: number } ,
568659) : number {
569- const envValue = parseNumberEnv ( process . env [ envName ] ) ;
660+ const rawEnvValue = process . env [ envName ] ;
661+ const envValue = parseNumberEnv ( rawEnvValue ) ;
662+ if ( rawEnvValue !== undefined && envValue === undefined ) {
663+ logConfigWarnOnce (
664+ `Ignoring invalid numeric env ${ envName } =${ JSON . stringify ( rawEnvValue ) } . Expected a finite number.` ,
665+ ) ;
666+ }
570667 const candidate = envValue ?? configValue ?? defaultValue ;
571668 const min = options ?. min ?? Number . NEGATIVE_INFINITY ;
572669 const max = options ?. max ?? Number . POSITIVE_INFINITY ;
0 commit comments