@@ -336,33 +336,26 @@ export class Config {
336336 if ( userVal === undefined ) continue ;
337337
338338 const systemVal = systemRecord [ key ] ;
339-
340- // Type guard: reject user values whose JSON type fundamentally differs
341- // from the system default. Without this, a user value like "oops" for a
342- // boolean field would overwrite the valid system boolean, only to be
343- // stripped to undefined by normalization — silently losing the baseline.
344- if ( systemVal !== undefined ) {
345- const sysIsArr = Array . isArray ( systemVal ) ;
346- const usrIsArr = Array . isArray ( userVal ) ;
347- if ( sysIsArr !== usrIsArr || ( ! sysIsArr && typeof systemVal !== typeof userVal ) ) {
348- continue ;
339+ // For scalar/primitive fields, no type guard here — fb()/fbArray() in
340+ // the normalization section reject invalid merged values and fall back
341+ // to the raw system value. A blanket type guard would block valid user
342+ // overrides when the system config itself has a typo (e.g., system
343+ // "true" string vs user true boolean).
344+ //
345+ // For object fields, we protect the system object from being replaced
346+ // by a non-object user value (e.g., runtimeEnablement: "oops"). This
347+ // is safe because a non-object can never be a valid override for an
348+ // object-valued setting.
349+ if ( systemVal != null && typeof systemVal === "object" && ! Array . isArray ( systemVal ) ) {
350+ if ( userVal != null && typeof userVal === "object" && ! Array . isArray ( userVal ) ) {
351+ // LIMITATION: This is a shallow key-level merge — user sub-values replace
352+ // system sub-values without per-value validation. E.g., a user setting
353+ // featureFlagOverrides.flagA = "bogus" overwrites the admin's "on". This
354+ // is accepted because validation lives in consumers (getFeatureFlagOverride),
355+ // not the config merge layer, and the set of valid values varies per key.
356+ mergedRecord [ key ] = { ...systemVal , ...userVal } ;
349357 }
350- }
351-
352- if (
353- systemVal != null &&
354- typeof systemVal === "object" &&
355- ! Array . isArray ( systemVal ) &&
356- userVal != null &&
357- typeof userVal === "object" &&
358- ! Array . isArray ( userVal )
359- ) {
360- // LIMITATION: This is a shallow key-level merge — user sub-values replace
361- // system sub-values without per-value validation. E.g., a user setting
362- // featureFlagOverrides.flagA = "bogus" overwrites the admin's "on". This
363- // is accepted because validation lives in consumers (getFeatureFlagOverride),
364- // not the config merge layer, and the set of valid values varies per key.
365- mergedRecord [ key ] = { ...systemVal , ...userVal } ;
358+ // else: user value is non-object — skip it, system object survives.
366359 } else {
367360 mergedRecord [ key ] = userVal ;
368361 }
@@ -618,7 +611,11 @@ export class Config {
618611 parsed . mdnsServiceName ,
619612 systemParsed ?. mdnsServiceName
620613 ) ,
621- serverSshHost : parsed . serverSshHost ,
614+ serverSshHost : fb (
615+ parseOptionalNonEmptyString ,
616+ parsed . serverSshHost ,
617+ systemParsed ?. serverSshHost
618+ ) ,
622619 serverAuthGithubOwner : fb (
623620 parseOptionalNonEmptyString ,
624621 parsed . serverAuthGithubOwner ,
@@ -629,7 +626,11 @@ export class Config {
629626 parsed . defaultProjectDir ,
630627 systemParsed ?. defaultProjectDir
631628 ) ,
632- viewedSplashScreens : parsed . viewedSplashScreens ,
629+ viewedSplashScreens : fbArray (
630+ parseOptionalStringArray ,
631+ parsed . viewedSplashScreens ,
632+ systemParsed ?. viewedSplashScreens
633+ ) ,
633634 layoutPresets,
634635 taskSettings,
635636 muxGatewayEnabled,
@@ -639,7 +640,14 @@ export class Config {
639640 agentAiDefaults,
640641 // Legacy fields are still parsed and returned for downgrade compatibility.
641642 subagentAiDefaults : legacySubagentAiDefaults ,
642- featureFlagOverrides : parsed . featureFlagOverrides ,
643+ // featureFlagOverrides is a raw pass-through (no normalizer) — validate
644+ // that the merged value is actually a plain object before accepting it.
645+ featureFlagOverrides :
646+ parsed . featureFlagOverrides != null &&
647+ typeof parsed . featureFlagOverrides === "object" &&
648+ ! Array . isArray ( parsed . featureFlagOverrides )
649+ ? parsed . featureFlagOverrides
650+ : systemParsed ?. featureFlagOverrides ,
643651 useSSH2Transport : fb (
644652 parseOptionalBoolean ,
645653 parsed . useSSH2Transport ,
0 commit comments