Download src/utils/settings/validation.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 7.95 kB
-
https://huggingface.co/chenbhao/codev/resolve/714ab7d15ffd78992a4c3d4e2bf153c2f62f4841/src/utils/settings/validation.ts
- Command line
-
hf download hf://chenbhao/codev@714ab7d15ffd78992a4c3d4e2bf153c2f62f4841/src/utils/settings/validation.ts
-
curl -L -o validation.ts https://huggingface.co/chenbhao/codev/resolve/714ab7d15ffd78992a4c3d4e2bf153c2f62f4841/src/utils/settings/validation.ts
7.95 kB
| import type { ConfigScope } from 'src/services/mcp/types.js' | |
| import type { ZodError, ZodIssue } from 'zod/v4' | |
| import { jsonParse } from '../slowOperations.js' | |
| import { plural } from '../stringUtils.js' | |
| import { validatePermissionRule } from './permissionValidation.js' | |
| import { generateSettingsJSONSchema } from './schemaOutput.js' | |
| import type { SettingsJson } from './types.js' | |
| import { SettingsSchema } from './types.js' | |
| import { getValidationTip } from './validationTips.js' | |
| /** | |
| * Helper type guards for specific Zod v4 issue types | |
| * In v4, issue types have different structures than v3 | |
| */ | |
| function isInvalidTypeIssue(issue: ZodIssue): issue is ZodIssue & { | |
| code: 'invalid_type' | |
| expected: string | |
| input: unknown | |
| } { | |
| return issue.code === 'invalid_type' | |
| } | |
| function isInvalidValueIssue(issue: ZodIssue): issue is ZodIssue & { | |
| code: 'invalid_value' | |
| values: unknown[] | |
| input: unknown | |
| } { | |
| return issue.code === 'invalid_value' | |
| } | |
| function isUnrecognizedKeysIssue( | |
| issue: ZodIssue, | |
| ): issue is ZodIssue & { code: 'unrecognized_keys'; keys: string[] } { | |
| return issue.code === 'unrecognized_keys' | |
| } | |
| function isTooSmallIssue(issue: ZodIssue): issue is ZodIssue & { | |
| code: 'too_small' | |
| minimum: number | bigint | |
| origin: string | |
| } { | |
| return issue.code === 'too_small' | |
| } | |
| /** Field path in dot notation (e.g., "permissions.defaultMode", "env.DEBUG") */ | |
| export type FieldPath = string | |
| export type ValidationError = { | |
| /** Relative file path */ | |
| file?: string | |
| /** Field path in dot notation */ | |
| path: FieldPath | |
| /** Human-readable error message */ | |
| message: string | |
| /** Expected value or type */ | |
| expected?: string | |
| /** The actual invalid value that was provided */ | |
| invalidValue?: unknown | |
| /** Suggestion for fixing the error */ | |
| suggestion?: string | |
| /** Link to relevant documentation */ | |
| docLink?: string | |
| /** MCP-specific metadata - only present for MCP configuration errors */ | |
| mcpErrorMetadata?: { | |
| /** Which configuration scope this error came from */ | |
| scope: ConfigScope | |
| /** The server name if error is specific to a server */ | |
| serverName?: string | |
| /** Severity of the error */ | |
| severity?: 'fatal' | 'warning' | |
| } | |
| } | |
| export type SettingsWithErrors = { | |
| settings: SettingsJson | |
| errors: ValidationError[] | |
| } | |
| /** | |
| * Format a Zod validation error into human-readable validation errors | |
| */ | |
| /** | |
| * Get the type string for an unknown value (for error messages) | |
| */ | |
| function getReceivedType(value: unknown): string { | |
| if (value === null) return 'null' | |
| if (value === undefined) return 'undefined' | |
| if (Array.isArray(value)) return 'array' | |
| return typeof value | |
| } | |
| function extractReceivedFromMessage(msg: string): string | undefined { | |
| const match = msg.match(/received (\w+)/) | |
| return match ? match[1] : undefined | |
| } | |
| export function formatZodError( | |
| error: ZodError, | |
| filePath: string, | |
| ): ValidationError[] { | |
| return error.issues.map((issue): ValidationError => { | |
| const path = issue.path.map(String).join('.') | |
| let message = issue.message | |
| let expected: string | undefined | |
| let enumValues: string[] | undefined | |
| let expectedValue: string | undefined | |
| let receivedValue: unknown | |
| let invalidValue: unknown | |
| if (isInvalidValueIssue(issue)) { | |
| enumValues = issue.values.map(v => String(v)) | |
| expectedValue = enumValues.join(' | ') | |
| receivedValue = undefined | |
| invalidValue = undefined | |
| } else if (isInvalidTypeIssue(issue)) { | |
| expectedValue = issue.expected | |
| const receivedType = extractReceivedFromMessage(issue.message) | |
| receivedValue = receivedType ?? getReceivedType(issue.input) | |
| invalidValue = receivedType ?? getReceivedType(issue.input) | |
| } else if (isTooSmallIssue(issue)) { | |
| expectedValue = String(issue.minimum) | |
| } else if (issue.code === 'custom' && 'params' in issue) { | |
| const params = issue.params as { received?: unknown } | |
| receivedValue = params.received | |
| invalidValue = receivedValue | |
| } | |
| const tip = getValidationTip({ | |
| path, | |
| code: issue.code, | |
| expected: expectedValue, | |
| received: receivedValue, | |
| enumValues, | |
| message: issue.message, | |
| value: receivedValue, | |
| }) | |
| if (isInvalidValueIssue(issue)) { | |
| expected = enumValues?.map(v => `"${v}"`).join(', ') | |
| message = `Invalid value. Expected one of: ${expected}` | |
| } else if (isInvalidTypeIssue(issue)) { | |
| const receivedType = | |
| extractReceivedFromMessage(issue.message) ?? | |
| getReceivedType(issue.input) | |
| if ( | |
| issue.expected === 'object' && | |
| receivedType === 'null' && | |
| path === '' | |
| ) { | |
| message = 'Invalid or malformed JSON' | |
| } else { | |
| message = `Expected ${issue.expected}, but received ${receivedType}` | |
| } | |
| } else if (isUnrecognizedKeysIssue(issue)) { | |
| const keys = issue.keys.join(', ') | |
| message = `Unrecognized ${plural(issue.keys.length, 'field')}: ${keys}` | |
| } else if (isTooSmallIssue(issue)) { | |
| message = `Number must be greater than or equal to ${issue.minimum}` | |
| expected = String(issue.minimum) | |
| } | |
| return { | |
| file: filePath, | |
| path, | |
| message, | |
| expected, | |
| invalidValue, | |
| suggestion: tip?.suggestion, | |
| docLink: tip?.docLink, | |
| } | |
| }) | |
| } | |
| /** | |
| * Validates that settings file content conforms to the SettingsSchema. | |
| * This is used during file edits to ensure the resulting file is valid. | |
| */ | |
| export function validateSettingsFileContent(content: string): | |
| | { | |
| isValid: true | |
| } | |
| | { | |
| isValid: false | |
| error: string | |
| fullSchema: string | |
| } { | |
| try { | |
| // Parse the JSON first | |
| const jsonData = jsonParse(content) | |
| // Validate against SettingsSchema in strict mode | |
| const result = SettingsSchema().strict().safeParse(jsonData) | |
| if (result.success) { | |
| return { isValid: true } | |
| } | |
| // Format the validation error in a helpful way | |
| const errors = formatZodError(result.error, 'settings') | |
| const errorMessage = | |
| 'Settings validation failed:\n' + | |
| errors.map(err => `- ${err.path}: ${err.message}`).join('\n') | |
| return { | |
| isValid: false, | |
| error: errorMessage, | |
| fullSchema: generateSettingsJSONSchema(), | |
| } | |
| } catch (parseError) { | |
| return { | |
| isValid: false, | |
| error: `Invalid JSON: ${parseError instanceof Error ? parseError.message : 'Unknown parsing error'}`, | |
| fullSchema: generateSettingsJSONSchema(), | |
| } | |
| } | |
| } | |
| /** | |
| * Filters invalid permission rules from raw parsed JSON data before schema validation. | |
| * This prevents one bad rule from poisoning the entire settings file. | |
| * Returns warnings for each filtered rule. | |
| */ | |
| export function filterInvalidPermissionRules( | |
| data: unknown, | |
| filePath: string, | |
| ): ValidationError[] { | |
| if (!data || typeof data !== 'object') return [] | |
| const obj = data as Record<string, unknown> | |
| if (!obj.permissions || typeof obj.permissions !== 'object') return [] | |
| const perms = obj.permissions as Record<string, unknown> | |
| const warnings: ValidationError[] = [] | |
| for (const key of ['allow', 'deny', 'ask']) { | |
| const rules = perms[key] | |
| if (!Array.isArray(rules)) continue | |
| perms[key] = rules.filter(rule => { | |
| if (typeof rule !== 'string') { | |
| warnings.push({ | |
| file: filePath, | |
| path: `permissions.${key}`, | |
| message: `Non-string value in ${key} array was removed`, | |
| invalidValue: rule, | |
| }) | |
| return false | |
| } | |
| const result = validatePermissionRule(rule) | |
| if (!result.valid) { | |
| let message = `Invalid permission rule "${rule}" was skipped` | |
| if (result.error) message += `: ${result.error}` | |
| if (result.suggestion) message += `. ${result.suggestion}` | |
| warnings.push({ | |
| file: filePath, | |
| path: `permissions.${key}`, | |
| message, | |
| invalidValue: rule, | |
| }) | |
| return false | |
| } | |
| return true | |
| }) | |
| } | |
| return warnings | |
| } | |