usePermissionHandler.ts
1 import { 2 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 3 logEvent, 4 } from '../../../services/analytics/index.js' 5 import { sanitizeToolNameForAnalytics } from '../../../services/analytics/metadata.js' 6 import type { ToolPermissionContext } from '../../../Tool.js' 7 import { 8 CLAUDE_FOLDER_PERMISSION_PATTERN, 9 FILE_EDIT_TOOL_NAME, 10 GLOBAL_CLAUDE_FOLDER_PERMISSION_PATTERN, 11 } from '../../../tools/FileEditTool/constants.js' 12 import { env } from '../../../utils/env.js' 13 import { generateSuggestions } from '../../../utils/permissions/filesystem.js' 14 import type { PermissionUpdate } from '../../../utils/permissions/PermissionUpdateSchema.js' 15 import { 16 type CompletionType, 17 logUnaryEvent, 18 } from '../../../utils/unaryLogging.js' 19 import type { ToolUseConfirm } from '../PermissionRequest.js' 20 import type { 21 FileOperationType, 22 PermissionOption, 23 } from './permissionOptions.js' 24 25 function logPermissionEvent( 26 event: 'accept' | 'reject', 27 completionType: CompletionType, 28 languageName: string | Promise<string>, 29 messageId: string, 30 hasFeedback?: boolean, 31 ): void { 32 void logUnaryEvent({ 33 completion_type: completionType, 34 event, 35 metadata: { 36 language_name: languageName, 37 message_id: messageId, 38 platform: env.platform, 39 hasFeedback: hasFeedback ?? false, 40 }, 41 }) 42 } 43 44 export type PermissionHandlerParams = { 45 messageId: string 46 path: string | null 47 toolUseConfirm: ToolUseConfirm 48 toolPermissionContext: ToolPermissionContext 49 onDone: () => void 50 onReject: () => void 51 completionType: CompletionType 52 languageName: string | Promise<string> 53 operationType: FileOperationType 54 } 55 56 export type PermissionHandlerOptions = { 57 hasFeedback?: boolean 58 feedback?: string 59 enteredFeedbackMode?: boolean 60 scope?: 'claude-folder' | 'global-claude-folder' 61 } 62 63 function handleAcceptOnce( 64 params: PermissionHandlerParams, 65 options?: PermissionHandlerOptions, 66 ): void { 67 const { messageId, toolUseConfirm, onDone, completionType, languageName } = 68 params 69 70 logPermissionEvent('accept', completionType, languageName, messageId) 71 72 // Log accept submission with feedback context 73 logEvent('tengu_accept_submitted', { 74 toolName: sanitizeToolNameForAnalytics( 75 toolUseConfirm.tool.name, 76 ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 77 isMcp: toolUseConfirm.tool.isMcp ?? false, 78 has_instructions: !!options?.feedback, 79 instructions_length: options?.feedback?.length ?? 0, 80 entered_feedback_mode: options?.enteredFeedbackMode ?? false, 81 }) 82 83 onDone() 84 toolUseConfirm.onAllow(toolUseConfirm.input, [], options?.feedback) 85 } 86 87 function handleAcceptSession( 88 params: PermissionHandlerParams, 89 options?: PermissionHandlerOptions, 90 ): void { 91 const { 92 messageId, 93 path, 94 toolUseConfirm, 95 toolPermissionContext, 96 onDone, 97 completionType, 98 languageName, 99 operationType, 100 } = params 101 102 logPermissionEvent('accept', completionType, languageName, messageId) 103 104 // For claude-folder scope, grant session-level access to all .claude/ files 105 if ( 106 options?.scope === 'claude-folder' || 107 options?.scope === 'global-claude-folder' 108 ) { 109 const pattern = 110 options.scope === 'global-claude-folder' 111 ? GLOBAL_CLAUDE_FOLDER_PERMISSION_PATTERN 112 : CLAUDE_FOLDER_PERMISSION_PATTERN 113 const suggestions: PermissionUpdate[] = [ 114 { 115 type: 'addRules', 116 rules: [ 117 { 118 toolName: FILE_EDIT_TOOL_NAME, 119 ruleContent: pattern, 120 }, 121 ], 122 behavior: 'allow', 123 destination: 'session', 124 }, 125 ] 126 onDone() 127 toolUseConfirm.onAllow(toolUseConfirm.input, suggestions) 128 return 129 } 130 131 // Generate permission updates if path is provided 132 const suggestions = path 133 ? generateSuggestions(path, operationType, toolPermissionContext) 134 : [] 135 136 onDone() 137 // Pass permission updates directly to onAllow 138 toolUseConfirm.onAllow(toolUseConfirm.input, suggestions) 139 } 140 141 function handleReject( 142 params: PermissionHandlerParams, 143 options?: PermissionHandlerOptions, 144 ): void { 145 const { 146 messageId, 147 toolUseConfirm, 148 onDone, 149 onReject, 150 completionType, 151 languageName, 152 } = params 153 154 logPermissionEvent( 155 'reject', 156 completionType, 157 languageName, 158 messageId, 159 options?.hasFeedback, 160 ) 161 162 // Log reject submission with feedback context 163 logEvent('tengu_reject_submitted', { 164 toolName: sanitizeToolNameForAnalytics( 165 toolUseConfirm.tool.name, 166 ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 167 isMcp: toolUseConfirm.tool.isMcp ?? false, 168 has_instructions: !!options?.feedback, 169 instructions_length: options?.feedback?.length ?? 0, 170 entered_feedback_mode: options?.enteredFeedbackMode ?? false, 171 }) 172 173 onDone() 174 onReject() 175 toolUseConfirm.onReject(options?.feedback) 176 } 177 178 export const PERMISSION_HANDLERS: Record< 179 PermissionOption['type'], 180 (params: PermissionHandlerParams, options?: PermissionHandlerOptions) => void 181 > = { 182 'accept-once': handleAcceptOnce, 183 'accept-session': handleAcceptSession, 184 reject: handleReject, 185 }