| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389 |
- 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,
- }
- }
|