source dump of claude code
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 198 lines 7.3 kB view raw
1import { feature } from 'bun:bundle' 2import { AGENT_TOOL_NAME } from '../../tools/AgentTool/constants.js' 3import { TASK_OUTPUT_TOOL_NAME } from '../../tools/TaskOutputTool/constants.js' 4import { TASK_STOP_TOOL_NAME } from '../../tools/TaskStopTool/prompt.js' 5import type { PermissionRuleValue } from './PermissionRule.js' 6 7// Dead code elimination: ant-only tool names are conditionally required so 8// their strings don't leak into external builds. Static imports always bundle. 9/* eslint-disable @typescript-eslint/no-require-imports */ 10const BRIEF_TOOL_NAME: string | null = 11 feature('KAIROS') || feature('KAIROS_BRIEF') 12 ? ( 13 require('../../tools/BriefTool/prompt.js') as typeof import('../../tools/BriefTool/prompt.js') 14 ).BRIEF_TOOL_NAME 15 : null 16/* eslint-enable @typescript-eslint/no-require-imports */ 17 18// Maps legacy tool names to their current canonical names. 19// When a tool is renamed, add old → new here so permission rules, 20// hooks, and persisted wire names resolve to the canonical name. 21const LEGACY_TOOL_NAME_ALIASES: Record<string, string> = { 22 Task: AGENT_TOOL_NAME, 23 KillShell: TASK_STOP_TOOL_NAME, 24 AgentOutputTool: TASK_OUTPUT_TOOL_NAME, 25 BashOutputTool: TASK_OUTPUT_TOOL_NAME, 26 ...((feature('KAIROS') || feature('KAIROS_BRIEF')) && BRIEF_TOOL_NAME 27 ? { Brief: BRIEF_TOOL_NAME } 28 : {}), 29} 30 31export function normalizeLegacyToolName(name: string): string { 32 return LEGACY_TOOL_NAME_ALIASES[name] ?? name 33} 34 35export function getLegacyToolNames(canonicalName: string): string[] { 36 const result: string[] = [] 37 for (const [legacy, canonical] of Object.entries(LEGACY_TOOL_NAME_ALIASES)) { 38 if (canonical === canonicalName) result.push(legacy) 39 } 40 return result 41} 42 43/** 44 * Escapes special characters in rule content for safe storage in permission rules. 45 * Permission rules use the format "Tool(content)", so parentheses in content must be escaped. 46 * 47 * Escaping order matters: 48 * 1. Escape existing backslashes first (\ -> \\) 49 * 2. Then escape parentheses (( -> \(, ) -> \)) 50 * 51 * @example 52 * escapeRuleContent('psycopg2.connect()') // => 'psycopg2.connect\\(\\)' 53 * escapeRuleContent('echo "test\\nvalue"') // => 'echo "test\\\\nvalue"' 54 */ 55export function escapeRuleContent(content: string): string { 56 return content 57 .replace(/\\/g, '\\\\') // Escape backslashes first 58 .replace(/\(/g, '\\(') // Escape opening parentheses 59 .replace(/\)/g, '\\)') // Escape closing parentheses 60} 61 62/** 63 * Unescapes special characters in rule content after parsing from permission rules. 64 * This reverses the escaping done by escapeRuleContent. 65 * 66 * Unescaping order matters (reverse of escaping): 67 * 1. Unescape parentheses first (\( -> (, \) -> )) 68 * 2. Then unescape backslashes (\\ -> \) 69 * 70 * @example 71 * unescapeRuleContent('psycopg2.connect\\(\\)') // => 'psycopg2.connect()' 72 * unescapeRuleContent('echo "test\\\\nvalue"') // => 'echo "test\\nvalue"' 73 */ 74export function unescapeRuleContent(content: string): string { 75 return content 76 .replace(/\\\(/g, '(') // Unescape opening parentheses 77 .replace(/\\\)/g, ')') // Unescape closing parentheses 78 .replace(/\\\\/g, '\\') // Unescape backslashes last 79} 80 81/** 82 * Parses a permission rule string into its components. 83 * Handles escaped parentheses in the content portion. 84 * 85 * Format: "ToolName" or "ToolName(content)" 86 * Content may contain escaped parentheses: \( and \) 87 * 88 * @example 89 * permissionRuleValueFromString('Bash') // => { toolName: 'Bash' } 90 * permissionRuleValueFromString('Bash(npm install)') // => { toolName: 'Bash', ruleContent: 'npm install' } 91 * permissionRuleValueFromString('Bash(python -c "print\\(1\\)")') // => { toolName: 'Bash', ruleContent: 'python -c "print(1)"' } 92 */ 93export function permissionRuleValueFromString( 94 ruleString: string, 95): PermissionRuleValue { 96 // Find the first unescaped opening parenthesis 97 const openParenIndex = findFirstUnescapedChar(ruleString, '(') 98 if (openParenIndex === -1) { 99 // No parenthesis found - this is just a tool name 100 return { toolName: normalizeLegacyToolName(ruleString) } 101 } 102 103 // Find the last unescaped closing parenthesis 104 const closeParenIndex = findLastUnescapedChar(ruleString, ')') 105 if (closeParenIndex === -1 || closeParenIndex <= openParenIndex) { 106 // No matching closing paren or malformed - treat as tool name 107 return { toolName: normalizeLegacyToolName(ruleString) } 108 } 109 110 // Ensure the closing paren is at the end 111 if (closeParenIndex !== ruleString.length - 1) { 112 // Content after closing paren - treat as tool name 113 return { toolName: normalizeLegacyToolName(ruleString) } 114 } 115 116 const toolName = ruleString.substring(0, openParenIndex) 117 const rawContent = ruleString.substring(openParenIndex + 1, closeParenIndex) 118 119 // Missing toolName (e.g., "(foo)") is malformed - treat whole string as tool name 120 if (!toolName) { 121 return { toolName: normalizeLegacyToolName(ruleString) } 122 } 123 124 // Empty content (e.g., "Bash()") or standalone wildcard (e.g., "Bash(*)") 125 // should be treated as just the tool name (tool-wide rule) 126 if (rawContent === '' || rawContent === '*') { 127 return { toolName: normalizeLegacyToolName(toolName) } 128 } 129 130 // Unescape the content 131 const ruleContent = unescapeRuleContent(rawContent) 132 return { toolName: normalizeLegacyToolName(toolName), ruleContent } 133} 134 135/** 136 * Converts a permission rule value to its string representation. 137 * Escapes parentheses in the content to prevent parsing issues. 138 * 139 * @example 140 * permissionRuleValueToString({ toolName: 'Bash' }) // => 'Bash' 141 * permissionRuleValueToString({ toolName: 'Bash', ruleContent: 'npm install' }) // => 'Bash(npm install)' 142 * permissionRuleValueToString({ toolName: 'Bash', ruleContent: 'python -c "print(1)"' }) // => 'Bash(python -c "print\\(1\\)")' 143 */ 144export function permissionRuleValueToString( 145 ruleValue: PermissionRuleValue, 146): string { 147 if (!ruleValue.ruleContent) { 148 return ruleValue.toolName 149 } 150 const escapedContent = escapeRuleContent(ruleValue.ruleContent) 151 return `${ruleValue.toolName}(${escapedContent})` 152} 153 154/** 155 * Find the index of the first unescaped occurrence of a character. 156 * A character is escaped if preceded by an odd number of backslashes. 157 */ 158function findFirstUnescapedChar(str: string, char: string): number { 159 for (let i = 0; i < str.length; i++) { 160 if (str[i] === char) { 161 // Count preceding backslashes 162 let backslashCount = 0 163 let j = i - 1 164 while (j >= 0 && str[j] === '\\') { 165 backslashCount++ 166 j-- 167 } 168 // If even number of backslashes, the char is unescaped 169 if (backslashCount % 2 === 0) { 170 return i 171 } 172 } 173 } 174 return -1 175} 176 177/** 178 * Find the index of the last unescaped occurrence of a character. 179 * A character is escaped if preceded by an odd number of backslashes. 180 */ 181function findLastUnescapedChar(str: string, char: string): number { 182 for (let i = str.length - 1; i >= 0; i--) { 183 if (str[i] === char) { 184 // Count preceding backslashes 185 let backslashCount = 0 186 let j = i - 1 187 while (j >= 0 && str[j] === '\\') { 188 backslashCount++ 189 j-- 190 } 191 // If even number of backslashes, the char is unescaped 192 if (backslashCount % 2 === 0) { 193 return i 194 } 195 } 196 } 197 return -1 198}