source dump of claude code
0
fork

Configure Feed

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

at main 389 lines 12 kB view raw
1import { posix } from 'path' 2import type { ToolPermissionContext } from '../../Tool.js' 3// Types extracted to src/types/permissions.ts to break import cycles 4import type { 5 AdditionalWorkingDirectory, 6 WorkingDirectorySource, 7} from '../../types/permissions.js' 8import { logForDebugging } from '../debug.js' 9import type { EditableSettingSource } from '../settings/constants.js' 10import { 11 getSettingsForSource, 12 updateSettingsForSource, 13} from '../settings/settings.js' 14import { jsonStringify } from '../slowOperations.js' 15import { toPosixPath } from './filesystem.js' 16import type { PermissionRuleValue } from './PermissionRule.js' 17import type { 18 PermissionUpdate, 19 PermissionUpdateDestination, 20} from './PermissionUpdateSchema.js' 21import { 22 permissionRuleValueFromString, 23 permissionRuleValueToString, 24} from './permissionRuleParser.js' 25import { addPermissionRulesToSettings } from './permissionsLoader.js' 26 27// Re-export for backwards compatibility 28export type { AdditionalWorkingDirectory, WorkingDirectorySource } 29 30export function extractRules( 31 updates: PermissionUpdate[] | undefined, 32): PermissionRuleValue[] { 33 if (!updates) return [] 34 35 return updates.flatMap(update => { 36 switch (update.type) { 37 case 'addRules': 38 return update.rules 39 default: 40 return [] 41 } 42 }) 43} 44 45export function hasRules(updates: PermissionUpdate[] | undefined): boolean { 46 return extractRules(updates).length > 0 47} 48 49/** 50 * Applies a single permission update to the context and returns the updated context 51 * @param context The current permission context 52 * @param update The permission update to apply 53 * @returns The updated permission context 54 */ 55export function applyPermissionUpdate( 56 context: ToolPermissionContext, 57 update: PermissionUpdate, 58): ToolPermissionContext { 59 switch (update.type) { 60 case 'setMode': 61 logForDebugging( 62 `Applying permission update: Setting mode to '${update.mode}'`, 63 ) 64 return { 65 ...context, 66 mode: update.mode, 67 } 68 69 case 'addRules': { 70 const ruleStrings = update.rules.map(rule => 71 permissionRuleValueToString(rule), 72 ) 73 logForDebugging( 74 `Applying permission update: Adding ${update.rules.length} ${update.behavior} rule(s) to destination '${update.destination}': ${jsonStringify(ruleStrings)}`, 75 ) 76 77 // Determine which collection to update based on behavior 78 const ruleKind = 79 update.behavior === 'allow' 80 ? 'alwaysAllowRules' 81 : update.behavior === 'deny' 82 ? 'alwaysDenyRules' 83 : 'alwaysAskRules' 84 85 return { 86 ...context, 87 [ruleKind]: { 88 ...context[ruleKind], 89 [update.destination]: [ 90 ...(context[ruleKind][update.destination] || []), 91 ...ruleStrings, 92 ], 93 }, 94 } 95 } 96 97 case 'replaceRules': { 98 const ruleStrings = update.rules.map(rule => 99 permissionRuleValueToString(rule), 100 ) 101 logForDebugging( 102 `Replacing all ${update.behavior} rules for destination '${update.destination}' with ${update.rules.length} rule(s): ${jsonStringify(ruleStrings)}`, 103 ) 104 105 // Determine which collection to update based on behavior 106 const ruleKind = 107 update.behavior === 'allow' 108 ? 'alwaysAllowRules' 109 : update.behavior === 'deny' 110 ? 'alwaysDenyRules' 111 : 'alwaysAskRules' 112 113 return { 114 ...context, 115 [ruleKind]: { 116 ...context[ruleKind], 117 [update.destination]: ruleStrings, // Replace all rules for this source 118 }, 119 } 120 } 121 122 case 'addDirectories': { 123 logForDebugging( 124 `Applying permission update: Adding ${update.directories.length} director${update.directories.length === 1 ? 'y' : 'ies'} with destination '${update.destination}': ${jsonStringify(update.directories)}`, 125 ) 126 const newAdditionalDirs = new Map(context.additionalWorkingDirectories) 127 for (const directory of update.directories) { 128 newAdditionalDirs.set(directory, { 129 path: directory, 130 source: update.destination, 131 }) 132 } 133 return { 134 ...context, 135 additionalWorkingDirectories: newAdditionalDirs, 136 } 137 } 138 139 case 'removeRules': { 140 const ruleStrings = update.rules.map(rule => 141 permissionRuleValueToString(rule), 142 ) 143 logForDebugging( 144 `Applying permission update: Removing ${update.rules.length} ${update.behavior} rule(s) from source '${update.destination}': ${jsonStringify(ruleStrings)}`, 145 ) 146 147 // Determine which collection to update based on behavior 148 const ruleKind = 149 update.behavior === 'allow' 150 ? 'alwaysAllowRules' 151 : update.behavior === 'deny' 152 ? 'alwaysDenyRules' 153 : 'alwaysAskRules' 154 155 // Filter out the rules to be removed 156 const existingRules = context[ruleKind][update.destination] || [] 157 const rulesToRemove = new Set(ruleStrings) 158 const filteredRules = existingRules.filter( 159 rule => !rulesToRemove.has(rule), 160 ) 161 162 return { 163 ...context, 164 [ruleKind]: { 165 ...context[ruleKind], 166 [update.destination]: filteredRules, 167 }, 168 } 169 } 170 171 case 'removeDirectories': { 172 logForDebugging( 173 `Applying permission update: Removing ${update.directories.length} director${update.directories.length === 1 ? 'y' : 'ies'}: ${jsonStringify(update.directories)}`, 174 ) 175 const newAdditionalDirs = new Map(context.additionalWorkingDirectories) 176 for (const directory of update.directories) { 177 newAdditionalDirs.delete(directory) 178 } 179 return { 180 ...context, 181 additionalWorkingDirectories: newAdditionalDirs, 182 } 183 } 184 185 default: 186 return context 187 } 188} 189 190/** 191 * Applies multiple permission updates to the context and returns the updated context 192 * @param context The current permission context 193 * @param updates The permission updates to apply 194 * @returns The updated permission context 195 */ 196export function applyPermissionUpdates( 197 context: ToolPermissionContext, 198 updates: PermissionUpdate[], 199): ToolPermissionContext { 200 let updatedContext = context 201 for (const update of updates) { 202 updatedContext = applyPermissionUpdate(updatedContext, update) 203 } 204 205 return updatedContext 206} 207 208export function supportsPersistence( 209 destination: PermissionUpdateDestination, 210): destination is EditableSettingSource { 211 return ( 212 destination === 'localSettings' || 213 destination === 'userSettings' || 214 destination === 'projectSettings' 215 ) 216} 217 218/** 219 * Persists a permission update to the appropriate settings source 220 * @param update The permission update to persist 221 */ 222export function persistPermissionUpdate(update: PermissionUpdate): void { 223 if (!supportsPersistence(update.destination)) return 224 225 logForDebugging( 226 `Persisting permission update: ${update.type} to source '${update.destination}'`, 227 ) 228 229 switch (update.type) { 230 case 'addRules': { 231 logForDebugging( 232 `Persisting ${update.rules.length} ${update.behavior} rule(s) to ${update.destination}`, 233 ) 234 addPermissionRulesToSettings( 235 { 236 ruleValues: update.rules, 237 ruleBehavior: update.behavior, 238 }, 239 update.destination, 240 ) 241 break 242 } 243 244 case 'addDirectories': { 245 logForDebugging( 246 `Persisting ${update.directories.length} director${update.directories.length === 1 ? 'y' : 'ies'} to ${update.destination}`, 247 ) 248 const existingSettings = getSettingsForSource(update.destination) 249 const existingDirs = 250 existingSettings?.permissions?.additionalDirectories || [] 251 252 // Add new directories, avoiding duplicates 253 const dirsToAdd = update.directories.filter( 254 dir => !existingDirs.includes(dir), 255 ) 256 257 if (dirsToAdd.length > 0) { 258 const updatedDirs = [...existingDirs, ...dirsToAdd] 259 updateSettingsForSource(update.destination, { 260 permissions: { 261 additionalDirectories: updatedDirs, 262 }, 263 }) 264 } 265 break 266 } 267 268 case 'removeRules': { 269 // Handle rule removal 270 logForDebugging( 271 `Removing ${update.rules.length} ${update.behavior} rule(s) from ${update.destination}`, 272 ) 273 const existingSettings = getSettingsForSource(update.destination) 274 const existingPermissions = existingSettings?.permissions || {} 275 const existingRules = existingPermissions[update.behavior] || [] 276 277 // Convert rules to normalized strings for comparison 278 // Normalize via parse→serialize roundtrip so "Bash(*)" and "Bash" match 279 const rulesToRemove = new Set( 280 update.rules.map(permissionRuleValueToString), 281 ) 282 const filteredRules = existingRules.filter(rule => { 283 const normalized = permissionRuleValueToString( 284 permissionRuleValueFromString(rule), 285 ) 286 return !rulesToRemove.has(normalized) 287 }) 288 289 updateSettingsForSource(update.destination, { 290 permissions: { 291 [update.behavior]: filteredRules, 292 }, 293 }) 294 break 295 } 296 297 case 'removeDirectories': { 298 logForDebugging( 299 `Removing ${update.directories.length} director${update.directories.length === 1 ? 'y' : 'ies'} from ${update.destination}`, 300 ) 301 const existingSettings = getSettingsForSource(update.destination) 302 const existingDirs = 303 existingSettings?.permissions?.additionalDirectories || [] 304 305 // Remove specified directories 306 const dirsToRemove = new Set(update.directories) 307 const filteredDirs = existingDirs.filter(dir => !dirsToRemove.has(dir)) 308 309 updateSettingsForSource(update.destination, { 310 permissions: { 311 additionalDirectories: filteredDirs, 312 }, 313 }) 314 break 315 } 316 317 case 'setMode': { 318 logForDebugging( 319 `Persisting mode '${update.mode}' to ${update.destination}`, 320 ) 321 updateSettingsForSource(update.destination, { 322 permissions: { 323 defaultMode: update.mode, 324 }, 325 }) 326 break 327 } 328 329 case 'replaceRules': { 330 logForDebugging( 331 `Replacing all ${update.behavior} rules in ${update.destination} with ${update.rules.length} rule(s)`, 332 ) 333 const ruleStrings = update.rules.map(permissionRuleValueToString) 334 updateSettingsForSource(update.destination, { 335 permissions: { 336 [update.behavior]: ruleStrings, 337 }, 338 }) 339 break 340 } 341 } 342} 343 344/** 345 * Persists multiple permission updates to the appropriate settings sources 346 * Only persists updates with persistable sources 347 * @param updates The permission updates to persist 348 */ 349export function persistPermissionUpdates(updates: PermissionUpdate[]): void { 350 for (const update of updates) { 351 persistPermissionUpdate(update) 352 } 353} 354 355/** 356 * Creates a Read rule suggestion for a directory. 357 * @param dirPath The directory path to create a rule for 358 * @param destination The destination for the permission rule (defaults to 'session') 359 * @returns A PermissionUpdate for a Read rule, or undefined for the root directory 360 */ 361export function createReadRuleSuggestion( 362 dirPath: string, 363 destination: PermissionUpdateDestination = 'session', 364): PermissionUpdate | undefined { 365 // Convert to POSIX format for pattern matching (handles Windows internally) 366 const pathForPattern = toPosixPath(dirPath) 367 368 // Root directory is too broad to be a reasonable permission target 369 if (pathForPattern === '/') { 370 return undefined 371 } 372 373 // For absolute paths, prepend an extra / to create //path/** pattern 374 const ruleContent = posix.isAbsolute(pathForPattern) 375 ? `/${pathForPattern}/**` 376 : `${pathForPattern}/**` 377 378 return { 379 type: 'addRules', 380 rules: [ 381 { 382 toolName: 'Read', 383 ruleContent, 384 }, 385 ], 386 behavior: 'allow', 387 destination, 388 } 389}