Download src/utils/settings/permissionValidation.ts from chenbhao/codev: direct link, hf CLI and curl.
- Browser
- Download file 8.66 kB
-
https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/settings/permissionValidation.ts
- Command line
-
hf download hf://chenbhao/codev@afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/settings/permissionValidation.ts
-
curl -L -o permissionValidation.ts https://huggingface.co/chenbhao/codev/resolve/afbc7edaae4e6bb0102ba024b49ce8be89d2264c/src/utils/settings/permissionValidation.ts
8.66 kB
| import { z } from 'zod/v4' | |
| import { mcpInfoFromString } from '../../services/mcp/mcpStringUtils.js' | |
| import { lazySchema } from '../lazySchema.js' | |
| import { permissionRuleValueFromString } from '../permissions/permissionRuleParser.js' | |
| import { capitalize } from '../stringUtils.js' | |
| import { | |
| getCustomValidation, | |
| isBashPrefixTool, | |
| isFilePatternTool, | |
| } from './toolValidationConfig.js' | |
| /** | |
| * Checks if a character at a given index is escaped (preceded by odd number of backslashes). | |
| */ | |
| function isEscaped(str: string, index: number): boolean { | |
| let backslashCount = 0 | |
| let j = index - 1 | |
| while (j >= 0 && str[j] === '\\') { | |
| backslashCount++ | |
| j-- | |
| } | |
| return backslashCount % 2 !== 0 | |
| } | |
| /** | |
| * Counts unescaped occurrences of a character in a string. | |
| * A character is considered escaped if preceded by an odd number of backslashes. | |
| */ | |
| function countUnescapedChar(str: string, char: string): number { | |
| let count = 0 | |
| for (let i = 0; i < str.length; i++) { | |
| if (str[i] === char && !isEscaped(str, i)) { | |
| count++ | |
| } | |
| } | |
| return count | |
| } | |
| /** | |
| * Checks if a string contains unescaped empty parentheses "()". | |
| * Returns true only if both the "(" and ")" are unescaped and adjacent. | |
| */ | |
| function hasUnescapedEmptyParens(str: string): boolean { | |
| for (let i = 0; i < str.length - 1; i++) { | |
| if (str[i] === '(' && str[i + 1] === ')') { | |
| // Check if the opening paren is unescaped | |
| if (!isEscaped(str, i)) { | |
| return true | |
| } | |
| } | |
| } | |
| return false | |
| } | |
| /** | |
| * Validates permission rule format and content | |
| */ | |
| export function validatePermissionRule(rule: string): { | |
| valid: boolean | |
| error?: string | |
| suggestion?: string | |
| examples?: string[] | |
| } { | |
| // Empty rule check | |
| if (!rule || rule.trim() === '') { | |
| return { valid: false, error: 'Permission rule cannot be empty' } | |
| } | |
| // Check parentheses matching first (only count unescaped parens) | |
| const openCount = countUnescapedChar(rule, '(') | |
| const closeCount = countUnescapedChar(rule, ')') | |
| if (openCount !== closeCount) { | |
| return { | |
| valid: false, | |
| error: 'Mismatched parentheses', | |
| suggestion: | |
| 'Ensure all opening parentheses have matching closing parentheses', | |
| } | |
| } | |
| // Check for empty parentheses (escape-aware) | |
| if (hasUnescapedEmptyParens(rule)) { | |
| const toolName = rule.substring(0, rule.indexOf('(')) | |
| if (!toolName) { | |
| return { | |
| valid: false, | |
| error: 'Empty parentheses with no tool name', | |
| suggestion: 'Specify a tool name before the parentheses', | |
| } | |
| } | |
| return { | |
| valid: false, | |
| error: 'Empty parentheses', | |
| suggestion: `Either specify a pattern or use just "${toolName}" without parentheses`, | |
| examples: [`${toolName}`, `${toolName}(some-pattern)`], | |
| } | |
| } | |
| // Parse the rule | |
| const parsed = permissionRuleValueFromString(rule) | |
| // MCP validation - must be done before general tool validation | |
| const mcpInfo = mcpInfoFromString(parsed.toolName) | |
| if (mcpInfo) { | |
| // MCP rules support server-level, tool-level, and wildcard permissions | |
| // Valid formats: | |
| // - mcp__server (server-level, all tools) | |
| // - mcp__server__* (wildcard, all tools - equivalent to server-level) | |
| // - mcp__server__tool (specific tool) | |
| // MCP rules cannot have any pattern/content (parentheses) | |
| // Check both parsed content and raw string since the parser normalizes | |
| // standalone wildcards (e.g., "mcp__server(*)") to undefined ruleContent | |
| if (parsed.ruleContent !== undefined || countUnescapedChar(rule, '(') > 0) { | |
| return { | |
| valid: false, | |
| error: 'MCP rules do not support patterns in parentheses', | |
| suggestion: `Use "${parsed.toolName}" without parentheses, or use "mcp__${mcpInfo.serverName}__*" for all tools`, | |
| examples: [ | |
| `mcp__${mcpInfo.serverName}`, | |
| `mcp__${mcpInfo.serverName}__*`, | |
| mcpInfo.toolName && mcpInfo.toolName !== '*' | |
| ? `mcp__${mcpInfo.serverName}__${mcpInfo.toolName}` | |
| : undefined, | |
| ].filter(Boolean) as string[], | |
| } | |
| } | |
| return { valid: true } // Valid MCP rule | |
| } | |
| // Tool name validation (for non-MCP tools) | |
| if (!parsed.toolName || parsed.toolName.length === 0) { | |
| return { valid: false, error: 'Tool name cannot be empty' } | |
| } | |
| // Check tool name starts with uppercase (standard tools) | |
| if (parsed.toolName[0] !== parsed.toolName[0]?.toUpperCase()) { | |
| return { | |
| valid: false, | |
| error: 'Tool names must start with uppercase', | |
| suggestion: `Use "${capitalize(String(parsed.toolName))}"`, | |
| } | |
| } | |
| // Check for custom validation rules first | |
| const customValidation = getCustomValidation(parsed.toolName) | |
| if (customValidation && parsed.ruleContent !== undefined) { | |
| const customResult = customValidation(parsed.ruleContent) | |
| if (!customResult.valid) { | |
| return customResult | |
| } | |
| } | |
| // Bash-specific validation | |
| if (isBashPrefixTool(parsed.toolName) && parsed.ruleContent !== undefined) { | |
| const content = parsed.ruleContent | |
| // Check for common :* mistakes - :* must be at the end (legacy prefix syntax) | |
| if (content.includes(':*') && !content.endsWith(':*')) { | |
| return { | |
| valid: false, | |
| error: 'The :* pattern must be at the end', | |
| suggestion: | |
| 'Move :* to the end for prefix matching, or use * for wildcard matching', | |
| examples: [ | |
| 'Bash(npm run:*) - prefix matching (legacy)', | |
| 'Bash(npm run *) - wildcard matching', | |
| ], | |
| } | |
| } | |
| // Check for :* without a prefix | |
| if (content === ':*') { | |
| return { | |
| valid: false, | |
| error: 'Prefix cannot be empty before :*', | |
| suggestion: 'Specify a command prefix before :*', | |
| examples: ['Bash(npm:*)', 'Bash(git:*)'], | |
| } | |
| } | |
| // Note: We don't validate quote balancing because bash quoting rules are complex. | |
| // A command like `grep '"'` has valid unbalanced double quotes. | |
| // Users who create patterns with unintended quote mismatches will discover | |
| // the issue when matching doesn't work as expected. | |
| // Wildcards are now allowed at any position for flexible pattern matching | |
| // Examples of valid wildcard patterns: | |
| // - "npm *" matches "npm install", "npm run test", etc. | |
| // - "* install" matches "npm install", "yarn install", etc. | |
| // - "git * main" matches "git checkout main", "git push main", etc. | |
| // - "npm * --save" matches "npm install foo --save", etc. | |
| // | |
| // Legacy :* syntax continues to work for backwards compatibility: | |
| // - "npm:*" matches "npm" or "npm <anything>" (prefix matching with word boundary) | |
| } | |
| // File tool validation | |
| if (isFilePatternTool(parsed.toolName) && parsed.ruleContent !== undefined) { | |
| const content = parsed.ruleContent | |
| // Check for :* in file patterns (common mistake from Bash patterns) | |
| if (content.includes(':*')) { | |
| return { | |
| valid: false, | |
| error: 'The ":*" syntax is only for Bash prefix rules', | |
| suggestion: 'Use glob patterns like "*" or "**" for file matching', | |
| examples: [ | |
| `${parsed.toolName}(*.ts) - matches .ts files`, | |
| `${parsed.toolName}(src/**) - matches all files in src`, | |
| `${parsed.toolName}(**/*.test.ts) - matches test files`, | |
| ], | |
| } | |
| } | |
| // Warn about wildcards not at boundaries | |
| if ( | |
| content.includes('*') && | |
| !content.match(/^\*|\*$|\*\*|\/\*|\*\.|\*\)/) && | |
| !content.includes('**') | |
| ) { | |
| // This is a loose check - wildcards in the middle might be valid in some cases | |
| // but often indicate confusion | |
| return { | |
| valid: false, | |
| error: 'Wildcard placement might be incorrect', | |
| suggestion: 'Wildcards are typically used at path boundaries', | |
| examples: [ | |
| `${parsed.toolName}(*.js) - all .js files`, | |
| `${parsed.toolName}(src/*) - all files directly in src`, | |
| `${parsed.toolName}(src/**) - all files recursively in src`, | |
| ], | |
| } | |
| } | |
| } | |
| return { valid: true } | |
| } | |
| /** | |
| * Custom Zod schema for permission rule arrays | |
| */ | |
| export const PermissionRuleSchema = lazySchema(() => | |
| z.string().superRefine((val, ctx) => { | |
| const result = validatePermissionRule(val) | |
| if (!result.valid) { | |
| let message = result.error! | |
| if (result.suggestion) { | |
| message += `. ${result.suggestion}` | |
| } | |
| if (result.examples && result.examples.length > 0) { | |
| message += `. Examples: ${result.examples.join(', ')}` | |
| } | |
| ctx.addIssue({ | |
| code: z.ZodIssueCode.custom, | |
| message, | |
| params: { received: val }, | |
| }) | |
| } | |
| }), | |
| ) | |