init claude-code
This commit is contained in:
@@ -0,0 +1,389 @@
|
||||
import { posix } from 'path'
|
||||
import type { ToolPermissionContext } from '../../Tool.js'
|
||||
// Types extracted to src/types/permissions.ts to break import cycles
|
||||
import type {
|
||||
AdditionalWorkingDirectory,
|
||||
WorkingDirectorySource,
|
||||
} from '../../types/permissions.js'
|
||||
import { logForDebugging } from '../debug.js'
|
||||
import type { EditableSettingSource } from '../settings/constants.js'
|
||||
import {
|
||||
getSettingsForSource,
|
||||
updateSettingsForSource,
|
||||
} from '../settings/settings.js'
|
||||
import { jsonStringify } from '../slowOperations.js'
|
||||
import { toPosixPath } from './filesystem.js'
|
||||
import type { PermissionRuleValue } from './PermissionRule.js'
|
||||
import type {
|
||||
PermissionUpdate,
|
||||
PermissionUpdateDestination,
|
||||
} from './PermissionUpdateSchema.js'
|
||||
import {
|
||||
permissionRuleValueFromString,
|
||||
permissionRuleValueToString,
|
||||
} from './permissionRuleParser.js'
|
||||
import { addPermissionRulesToSettings } from './permissionsLoader.js'
|
||||
|
||||
// Re-export for backwards compatibility
|
||||
export type { AdditionalWorkingDirectory, WorkingDirectorySource }
|
||||
|
||||
export function extractRules(
|
||||
updates: PermissionUpdate[] | undefined,
|
||||
): PermissionRuleValue[] {
|
||||
if (!updates) return []
|
||||
|
||||
return updates.flatMap(update => {
|
||||
switch (update.type) {
|
||||
case 'addRules':
|
||||
return update.rules
|
||||
default:
|
||||
return []
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export function hasRules(updates: PermissionUpdate[] | undefined): boolean {
|
||||
return extractRules(updates).length > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a single permission update to the context and returns the updated context
|
||||
* @param context The current permission context
|
||||
* @param update The permission update to apply
|
||||
* @returns The updated permission context
|
||||
*/
|
||||
export function applyPermissionUpdate(
|
||||
context: ToolPermissionContext,
|
||||
update: PermissionUpdate,
|
||||
): ToolPermissionContext {
|
||||
switch (update.type) {
|
||||
case 'setMode':
|
||||
logForDebugging(
|
||||
`Applying permission update: Setting mode to '${update.mode}'`,
|
||||
)
|
||||
return {
|
||||
...context,
|
||||
mode: update.mode,
|
||||
}
|
||||
|
||||
case 'addRules': {
|
||||
const ruleStrings = update.rules.map(rule =>
|
||||
permissionRuleValueToString(rule),
|
||||
)
|
||||
logForDebugging(
|
||||
`Applying permission update: Adding ${update.rules.length} ${update.behavior} rule(s) to destination '${update.destination}': ${jsonStringify(ruleStrings)}`,
|
||||
)
|
||||
|
||||
// Determine which collection to update based on behavior
|
||||
const ruleKind =
|
||||
update.behavior === 'allow'
|
||||
? 'alwaysAllowRules'
|
||||
: update.behavior === 'deny'
|
||||
? 'alwaysDenyRules'
|
||||
: 'alwaysAskRules'
|
||||
|
||||
return {
|
||||
...context,
|
||||
[ruleKind]: {
|
||||
...context[ruleKind],
|
||||
[update.destination]: [
|
||||
...(context[ruleKind][update.destination] || []),
|
||||
...ruleStrings,
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
case 'replaceRules': {
|
||||
const ruleStrings = update.rules.map(rule =>
|
||||
permissionRuleValueToString(rule),
|
||||
)
|
||||
logForDebugging(
|
||||
`Replacing all ${update.behavior} rules for destination '${update.destination}' with ${update.rules.length} rule(s): ${jsonStringify(ruleStrings)}`,
|
||||
)
|
||||
|
||||
// Determine which collection to update based on behavior
|
||||
const ruleKind =
|
||||
update.behavior === 'allow'
|
||||
? 'alwaysAllowRules'
|
||||
: update.behavior === 'deny'
|
||||
? 'alwaysDenyRules'
|
||||
: 'alwaysAskRules'
|
||||
|
||||
return {
|
||||
...context,
|
||||
[ruleKind]: {
|
||||
...context[ruleKind],
|
||||
[update.destination]: ruleStrings, // Replace all rules for this source
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
case 'addDirectories': {
|
||||
logForDebugging(
|
||||
`Applying permission update: Adding ${update.directories.length} director${update.directories.length === 1 ? 'y' : 'ies'} with destination '${update.destination}': ${jsonStringify(update.directories)}`,
|
||||
)
|
||||
const newAdditionalDirs = new Map(context.additionalWorkingDirectories)
|
||||
for (const directory of update.directories) {
|
||||
newAdditionalDirs.set(directory, {
|
||||
path: directory,
|
||||
source: update.destination,
|
||||
})
|
||||
}
|
||||
return {
|
||||
...context,
|
||||
additionalWorkingDirectories: newAdditionalDirs,
|
||||
}
|
||||
}
|
||||
|
||||
case 'removeRules': {
|
||||
const ruleStrings = update.rules.map(rule =>
|
||||
permissionRuleValueToString(rule),
|
||||
)
|
||||
logForDebugging(
|
||||
`Applying permission update: Removing ${update.rules.length} ${update.behavior} rule(s) from source '${update.destination}': ${jsonStringify(ruleStrings)}`,
|
||||
)
|
||||
|
||||
// Determine which collection to update based on behavior
|
||||
const ruleKind =
|
||||
update.behavior === 'allow'
|
||||
? 'alwaysAllowRules'
|
||||
: update.behavior === 'deny'
|
||||
? 'alwaysDenyRules'
|
||||
: 'alwaysAskRules'
|
||||
|
||||
// Filter out the rules to be removed
|
||||
const existingRules = context[ruleKind][update.destination] || []
|
||||
const rulesToRemove = new Set(ruleStrings)
|
||||
const filteredRules = existingRules.filter(
|
||||
rule => !rulesToRemove.has(rule),
|
||||
)
|
||||
|
||||
return {
|
||||
...context,
|
||||
[ruleKind]: {
|
||||
...context[ruleKind],
|
||||
[update.destination]: filteredRules,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
case 'removeDirectories': {
|
||||
logForDebugging(
|
||||
`Applying permission update: Removing ${update.directories.length} director${update.directories.length === 1 ? 'y' : 'ies'}: ${jsonStringify(update.directories)}`,
|
||||
)
|
||||
const newAdditionalDirs = new Map(context.additionalWorkingDirectories)
|
||||
for (const directory of update.directories) {
|
||||
newAdditionalDirs.delete(directory)
|
||||
}
|
||||
return {
|
||||
...context,
|
||||
additionalWorkingDirectories: newAdditionalDirs,
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
return context
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies multiple permission updates to the context and returns the updated context
|
||||
* @param context The current permission context
|
||||
* @param updates The permission updates to apply
|
||||
* @returns The updated permission context
|
||||
*/
|
||||
export function applyPermissionUpdates(
|
||||
context: ToolPermissionContext,
|
||||
updates: PermissionUpdate[],
|
||||
): ToolPermissionContext {
|
||||
let updatedContext = context
|
||||
for (const update of updates) {
|
||||
updatedContext = applyPermissionUpdate(updatedContext, update)
|
||||
}
|
||||
|
||||
return updatedContext
|
||||
}
|
||||
|
||||
export function supportsPersistence(
|
||||
destination: PermissionUpdateDestination,
|
||||
): destination is EditableSettingSource {
|
||||
return (
|
||||
destination === 'localSettings' ||
|
||||
destination === 'userSettings' ||
|
||||
destination === 'projectSettings'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists a permission update to the appropriate settings source
|
||||
* @param update The permission update to persist
|
||||
*/
|
||||
export function persistPermissionUpdate(update: PermissionUpdate): void {
|
||||
if (!supportsPersistence(update.destination)) return
|
||||
|
||||
logForDebugging(
|
||||
`Persisting permission update: ${update.type} to source '${update.destination}'`,
|
||||
)
|
||||
|
||||
switch (update.type) {
|
||||
case 'addRules': {
|
||||
logForDebugging(
|
||||
`Persisting ${update.rules.length} ${update.behavior} rule(s) to ${update.destination}`,
|
||||
)
|
||||
addPermissionRulesToSettings(
|
||||
{
|
||||
ruleValues: update.rules,
|
||||
ruleBehavior: update.behavior,
|
||||
},
|
||||
update.destination,
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
case 'addDirectories': {
|
||||
logForDebugging(
|
||||
`Persisting ${update.directories.length} director${update.directories.length === 1 ? 'y' : 'ies'} to ${update.destination}`,
|
||||
)
|
||||
const existingSettings = getSettingsForSource(update.destination)
|
||||
const existingDirs =
|
||||
existingSettings?.permissions?.additionalDirectories || []
|
||||
|
||||
// Add new directories, avoiding duplicates
|
||||
const dirsToAdd = update.directories.filter(
|
||||
dir => !existingDirs.includes(dir),
|
||||
)
|
||||
|
||||
if (dirsToAdd.length > 0) {
|
||||
const updatedDirs = [...existingDirs, ...dirsToAdd]
|
||||
updateSettingsForSource(update.destination, {
|
||||
permissions: {
|
||||
additionalDirectories: updatedDirs,
|
||||
},
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
case 'removeRules': {
|
||||
// Handle rule removal
|
||||
logForDebugging(
|
||||
`Removing ${update.rules.length} ${update.behavior} rule(s) from ${update.destination}`,
|
||||
)
|
||||
const existingSettings = getSettingsForSource(update.destination)
|
||||
const existingPermissions = existingSettings?.permissions || {}
|
||||
const existingRules = existingPermissions[update.behavior] || []
|
||||
|
||||
// Convert rules to normalized strings for comparison
|
||||
// Normalize via parse→serialize roundtrip so "Bash(*)" and "Bash" match
|
||||
const rulesToRemove = new Set(
|
||||
update.rules.map(permissionRuleValueToString),
|
||||
)
|
||||
const filteredRules = existingRules.filter(rule => {
|
||||
const normalized = permissionRuleValueToString(
|
||||
permissionRuleValueFromString(rule),
|
||||
)
|
||||
return !rulesToRemove.has(normalized)
|
||||
})
|
||||
|
||||
updateSettingsForSource(update.destination, {
|
||||
permissions: {
|
||||
[update.behavior]: filteredRules,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'removeDirectories': {
|
||||
logForDebugging(
|
||||
`Removing ${update.directories.length} director${update.directories.length === 1 ? 'y' : 'ies'} from ${update.destination}`,
|
||||
)
|
||||
const existingSettings = getSettingsForSource(update.destination)
|
||||
const existingDirs =
|
||||
existingSettings?.permissions?.additionalDirectories || []
|
||||
|
||||
// Remove specified directories
|
||||
const dirsToRemove = new Set(update.directories)
|
||||
const filteredDirs = existingDirs.filter(dir => !dirsToRemove.has(dir))
|
||||
|
||||
updateSettingsForSource(update.destination, {
|
||||
permissions: {
|
||||
additionalDirectories: filteredDirs,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'setMode': {
|
||||
logForDebugging(
|
||||
`Persisting mode '${update.mode}' to ${update.destination}`,
|
||||
)
|
||||
updateSettingsForSource(update.destination, {
|
||||
permissions: {
|
||||
defaultMode: update.mode,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
case 'replaceRules': {
|
||||
logForDebugging(
|
||||
`Replacing all ${update.behavior} rules in ${update.destination} with ${update.rules.length} rule(s)`,
|
||||
)
|
||||
const ruleStrings = update.rules.map(permissionRuleValueToString)
|
||||
updateSettingsForSource(update.destination, {
|
||||
permissions: {
|
||||
[update.behavior]: ruleStrings,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Persists multiple permission updates to the appropriate settings sources
|
||||
* Only persists updates with persistable sources
|
||||
* @param updates The permission updates to persist
|
||||
*/
|
||||
export function persistPermissionUpdates(updates: PermissionUpdate[]): void {
|
||||
for (const update of updates) {
|
||||
persistPermissionUpdate(update)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Read rule suggestion for a directory.
|
||||
* @param dirPath The directory path to create a rule for
|
||||
* @param destination The destination for the permission rule (defaults to 'session')
|
||||
* @returns A PermissionUpdate for a Read rule, or undefined for the root directory
|
||||
*/
|
||||
export function createReadRuleSuggestion(
|
||||
dirPath: string,
|
||||
destination: PermissionUpdateDestination = 'session',
|
||||
): PermissionUpdate | undefined {
|
||||
// Convert to POSIX format for pattern matching (handles Windows internally)
|
||||
const pathForPattern = toPosixPath(dirPath)
|
||||
|
||||
// Root directory is too broad to be a reasonable permission target
|
||||
if (pathForPattern === '/') {
|
||||
return undefined
|
||||
}
|
||||
|
||||
// For absolute paths, prepend an extra / to create //path/** pattern
|
||||
const ruleContent = posix.isAbsolute(pathForPattern)
|
||||
? `/${pathForPattern}/**`
|
||||
: `${pathForPattern}/**`
|
||||
|
||||
return {
|
||||
type: 'addRules',
|
||||
rules: [
|
||||
{
|
||||
toolName: 'Read',
|
||||
ruleContent,
|
||||
},
|
||||
],
|
||||
behavior: 'allow',
|
||||
destination,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user