From 3d2c66ad14e7652134ac30abebd6f5ad684b52a9 Mon Sep 17 00:00:00 2001 From: Zirkonium88 Date: Mon, 22 Jun 2026 08:51:19 +0200 Subject: [PATCH 1/3] Changed from hardcoded config schema to a flexible one for YAML-Imports --- PR_DESCRIPTION.md | 53 ++++++++++++++++++++++++++++++++++++++++++++ src/server/config.ts | 49 +++++++++++++++++++++++++++++++++------- 2 files changed, 94 insertions(+), 8 deletions(-) create mode 100644 PR_DESCRIPTION.md diff --git a/PR_DESCRIPTION.md b/PR_DESCRIPTION.md new file mode 100644 index 0000000..9c76d64 --- /dev/null +++ b/PR_DESCRIPTION.md @@ -0,0 +1,53 @@ +## Summary + +Fixes #72 and #73. + +The admin panel uses `configSchema` from `librechat-data-provider` to validate imported YAML configs and individual field edits. This schema contains `z.nativeEnum()` validators that reject values not present in the bundled enum at build time. When LibreChat adds new enum values (e.g. `subagents`, `skills` in agent capabilities, or any future addition to endpoint types, OCR strategies, etc.), the admin panel rejects otherwise valid configs — blocking users from using new features. + +This PR makes validation lenient for enum mismatches: if the only validation errors are `invalid_enum_value`, the config is accepted as-is. Structural and type errors still block import. This keeps the admin panel forward-compatible with newer LibreChat versions without requiring a synchronized release. + +### Changes + +- `parseImportedYaml`: When `configSchema.safeParse()` fails exclusively with enum errors, accept the raw config (LibreChat validates at runtime anyway) +- `validateFieldValue`: Filter out enum errors from field-level validation so the UI doesn't block edits containing new upstream values + +## Change Type + +- Bug fix (non-breaking change which fixes an issue) + +## Testing + +1. Create a `librechat.yaml` with agent capabilities that include `subagents` and `skills`: + ```yaml + version: 1.3.12 + endpoints: + agents: + capabilities: + - 'execute_code' + - 'file_search' + - 'web_search' + - 'artifacts' + - 'subagents' + - 'skills' + - 'tools' + - 'chain' + - 'ocr' + ``` +2. Import via the admin panel config editor +3. **Before this fix**: Validation error on `subagents` and `skills` +4. **After this fix**: Config imports successfully, all values preserved + +Also verified that configs with actual structural errors (wrong types, missing required fields) still fail validation as expected. + +### **Test Configuration**: + +- LibreChat config version: 1.3.12 +- `librechat-data-provider`: 0.8.505 (locked version that already includes `subagents`/`skills` in the enum — but the fix is generic and protects against future additions too) + +## Checklist + +- [x] My code adheres to this project's style guidelines +- [x] I have performed a self-review of my own code +- [x] I have commented in any complex areas of my code +- [x] My changes do not introduce new warnings +- [x] Local unit tests pass with my changes diff --git a/src/server/config.ts b/src/server/config.ts index b46abc4..5aaf73d 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -552,6 +552,8 @@ export function resolveSubSchema( return current; } +const ENUM_ERROR_CODE = 'invalid_enum_value'; + export function validateFieldValue( fieldPath: string, value: unknown, @@ -570,12 +572,18 @@ export function validateFieldValue( subSchema as { safeParse: (v: unknown) => { success: boolean; - error?: { issues: Array<{ message: string; path: (string | number)[] }> }; + error?: { + issues: Array<{ code?: string; message: string; path: (string | number)[] }>; + }; }; } ).safeParse(value); if (!result.success && result.error) { - const messages = result.error.issues.map((i) => i.message); + const nonEnumIssues = result.error.issues.filter( + (i) => i.code !== ENUM_ERROR_CODE, + ); + if (nonEnumIssues.length === 0) return { success: true }; + const messages = nonEnumIssues.map((i) => i.message); return { success: false, error: messages.join('; ') || 'Validation failed' }; } } @@ -610,6 +618,18 @@ export const getConfigSchemaFields = createServerFn({ method: 'GET' }).handler(a } }); +/** + * Returns true when every Zod issue is an enum validation failure. + * These failures indicate that the config uses values not yet known to this + * admin panel build (e.g. new agent capabilities, new endpoint types) but + * that are valid in the corresponding LibreChat version. + */ +function isOnlyEnumErrors( + errors: Array<{ code?: string; message: string; path: (string | number)[] }>, +): boolean { + return errors.length > 0 && errors.every((e) => e.code === ENUM_ERROR_CODE); +} + export const parseImportedYaml = createServerFn({ method: 'POST' }) .inputValidator(z.object({ yamlContent: z.string() })) .handler(async ({ data }: { data: { yamlContent: string } }) => { @@ -638,15 +658,28 @@ export const parseImportedYaml = createServerFn({ method: 'POST' }) const result = configSchema.safeParse(rawConfig); if (!result.success) { + const errors = result.error.errors as Array<{ + code?: string; + message: string; + path: (string | number)[]; + }>; + + if (isOnlyEnumErrors(errors)) { + return { + success: true, + error: undefined, + validationErrors: undefined, + appConfig: rawConfig as Record, + }; + } + return { success: false, error: 'Config validation failed', - validationErrors: result.error.errors.map( - (e: { path: (string | number)[]; message: string }) => ({ - path: e.path.join('.'), - message: e.message, - }), - ), + validationErrors: errors.map((e) => ({ + path: e.path.join('.'), + message: e.message, + })), appConfig: null, }; } From a33b74751c6c223775b2ae878fbfb4745ef5ec72 Mon Sep 17 00:00:00 2001 From: Zirkonium88 Date: Mon, 22 Jun 2026 09:57:41 +0200 Subject: [PATCH 2/3] Removed PR description markdown --- PR_DESCRIPTION.md | 53 --------------------------------------- src/server/config.test.ts | 14 +++++++++++ 2 files changed, 14 insertions(+), 53 deletions(-) delete mode 100644 PR_DESCRIPTION.md diff --git a/PR_DESCRIPTION.md b/PR_DESCRIPTION.md deleted file mode 100644 index 9c76d64..0000000 --- a/PR_DESCRIPTION.md +++ /dev/null @@ -1,53 +0,0 @@ -## Summary - -Fixes #72 and #73. - -The admin panel uses `configSchema` from `librechat-data-provider` to validate imported YAML configs and individual field edits. This schema contains `z.nativeEnum()` validators that reject values not present in the bundled enum at build time. When LibreChat adds new enum values (e.g. `subagents`, `skills` in agent capabilities, or any future addition to endpoint types, OCR strategies, etc.), the admin panel rejects otherwise valid configs — blocking users from using new features. - -This PR makes validation lenient for enum mismatches: if the only validation errors are `invalid_enum_value`, the config is accepted as-is. Structural and type errors still block import. This keeps the admin panel forward-compatible with newer LibreChat versions without requiring a synchronized release. - -### Changes - -- `parseImportedYaml`: When `configSchema.safeParse()` fails exclusively with enum errors, accept the raw config (LibreChat validates at runtime anyway) -- `validateFieldValue`: Filter out enum errors from field-level validation so the UI doesn't block edits containing new upstream values - -## Change Type - -- Bug fix (non-breaking change which fixes an issue) - -## Testing - -1. Create a `librechat.yaml` with agent capabilities that include `subagents` and `skills`: - ```yaml - version: 1.3.12 - endpoints: - agents: - capabilities: - - 'execute_code' - - 'file_search' - - 'web_search' - - 'artifacts' - - 'subagents' - - 'skills' - - 'tools' - - 'chain' - - 'ocr' - ``` -2. Import via the admin panel config editor -3. **Before this fix**: Validation error on `subagents` and `skills` -4. **After this fix**: Config imports successfully, all values preserved - -Also verified that configs with actual structural errors (wrong types, missing required fields) still fail validation as expected. - -### **Test Configuration**: - -- LibreChat config version: 1.3.12 -- `librechat-data-provider`: 0.8.505 (locked version that already includes `subagents`/`skills` in the enum — but the fix is generic and protects against future additions too) - -## Checklist - -- [x] My code adheres to this project's style guidelines -- [x] I have performed a self-review of my own code -- [x] I have commented in any complex areas of my code -- [x] My changes do not introduce new warnings -- [x] Local unit tests pass with my changes diff --git a/src/server/config.test.ts b/src/server/config.test.ts index 67a04a1..f0437e9 100644 --- a/src/server/config.test.ts +++ b/src/server/config.test.ts @@ -683,6 +683,20 @@ describe('validateFieldValue', () => { const bad = validateFieldValue('mcpServers.foo.headers.Authorization', 42); expect(bad.success).toBe(false); }); + + it('accepts unknown enum values for agent capabilities (forward-compatibility)', () => { + const result = validateFieldValue('endpoints.agents.capabilities', [ + 'execute_code', + 'future_capability_not_yet_in_enum', + 'web_search', + ]); + expect(result).toEqual({ success: true }); + }); + + it('still rejects type errors even alongside enum arrays', () => { + const result = validateFieldValue('version', 123); + expect(result.success).toBe(false); + }); }); /* --------------------------------------------------------------------------- From 1bba590c8b50f23b032020484dbcffceb6569b7c Mon Sep 17 00:00:00 2001 From: Malte Polley Date: Mon, 22 Jun 2026 16:03:31 +0200 Subject: [PATCH 3/3] Address review: reject non-string enum values, preserve parsed shape --- src/server/config.test.ts | 8 ++++ src/server/config.ts | 88 +++++++++++++++++++++++++++++++++++---- 2 files changed, 87 insertions(+), 9 deletions(-) diff --git a/src/server/config.test.ts b/src/server/config.test.ts index f0437e9..1bc9b2e 100644 --- a/src/server/config.test.ts +++ b/src/server/config.test.ts @@ -693,6 +693,14 @@ describe('validateFieldValue', () => { expect(result).toEqual({ success: true }); }); + it('rejects numeric values in enum arrays (not forward-compatible)', () => { + const result = validateFieldValue('endpoints.agents.capabilities', [ + 'execute_code', + 123, + ]); + expect(result.success).toBe(false); + }); + it('still rejects type errors even alongside enum arrays', () => { const result = validateFieldValue('version', 123); expect(result.success).toBe(false); diff --git a/src/server/config.ts b/src/server/config.ts index 5aaf73d..0ccdbdb 100644 --- a/src/server/config.ts +++ b/src/server/config.ts @@ -573,14 +573,19 @@ export function validateFieldValue( safeParse: (v: unknown) => { success: boolean; error?: { - issues: Array<{ code?: string; message: string; path: (string | number)[] }>; + issues: Array<{ + code?: string; + received?: unknown; + message: string; + path: (string | number)[]; + }>; }; }; } ).safeParse(value); if (!result.success && result.error) { const nonEnumIssues = result.error.issues.filter( - (i) => i.code !== ENUM_ERROR_CODE, + (i) => !(i.code === ENUM_ERROR_CODE && typeof i.received === 'string'), ); if (nonEnumIssues.length === 0) return { success: true }; const messages = nonEnumIssues.map((i) => i.message); @@ -619,15 +624,75 @@ export const getConfigSchemaFields = createServerFn({ method: 'GET' }).handler(a }); /** - * Returns true when every Zod issue is an enum validation failure. - * These failures indicate that the config uses values not yet known to this - * admin panel build (e.g. new agent capabilities, new endpoint types) but - * that are valid in the corresponding LibreChat version. + * Returns true when every Zod issue is an enum validation failure with a + * string received value. Numeric inputs to z.nativeEnum are also reported as + * `invalid_enum_value` but should NOT be bypassed — future enum members are + * always strings for config fields. */ function isOnlyEnumErrors( - errors: Array<{ code?: string; message: string; path: (string | number)[] }>, + errors: Array<{ code?: string; received?: unknown; message: string; path: (string | number)[] }>, ): boolean { - return errors.length > 0 && errors.every((e) => e.code === ENUM_ERROR_CODE); + return ( + errors.length > 0 && + errors.every((e) => e.code === ENUM_ERROR_CODE && typeof e.received === 'string') + ); +} + +/** Re-parses rawConfig with enum-failing paths removed, then overlays the + * original raw values at those paths. This preserves schema defaults, transforms, + * and unknown-key stripping while keeping the forward-compatible enum values. */ +function parseWithEnumBypass( + rawConfig: Record, + enumErrors: Array<{ path: (string | number)[] }>, +): Record { + const clone = structuredClone(rawConfig); + const savedValues: Array<{ path: (string | number)[]; value: unknown }> = []; + + for (const err of enumErrors) { + let parent: Record | unknown[] = clone; + for (let i = 0; i < err.path.length - 1; i++) { + const seg = err.path[i]; + parent = (parent as Record)[seg as string] as + | Record + | unknown[]; + if (!parent || typeof parent !== 'object') break; + } + if (parent && typeof parent === 'object') { + const lastSeg = err.path[err.path.length - 1]; + savedValues.push({ path: err.path, value: (parent as Record)[lastSeg as string] }); + if (Array.isArray(parent)) { + parent[lastSeg as number] = undefined; + } else { + delete (parent as Record)[lastSeg as string]; + } + } + } + + const reParsed = configSchema.safeParse(clone); + const base = (reParsed.success ? reParsed.data : clone) as Record; + + for (const { path, value } of savedValues) { + let target: Record | unknown[] = base; + for (let i = 0; i < path.length - 1; i++) { + const seg = path[i]; + const next = (target as Record)[seg as string]; + if (!next || typeof next !== 'object') { + const container: Record = {}; + (target as Record)[seg as string] = container; + target = container; + } else { + target = next as Record | unknown[]; + } + } + const lastSeg = path[path.length - 1]; + if (Array.isArray(target)) { + target[lastSeg as number] = value; + } else { + (target as Record)[lastSeg as string] = value; + } + } + + return base as Record; } export const parseImportedYaml = createServerFn({ method: 'POST' }) @@ -660,16 +725,21 @@ export const parseImportedYaml = createServerFn({ method: 'POST' }) if (!result.success) { const errors = result.error.errors as Array<{ code?: string; + received?: unknown; message: string; path: (string | number)[]; }>; if (isOnlyEnumErrors(errors)) { + const appConfig = parseWithEnumBypass( + rawConfig as Record, + errors, + ); return { success: true, error: undefined, validationErrors: undefined, - appConfig: rawConfig as Record, + appConfig, }; }