source dump of claude code
at main 209 lines 8.5 kB view raw
1import { feature } from 'bun:bundle' 2import { useEffect, useRef } from 'react' 3import { 4 type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 5 logEvent, 6} from 'src/services/analytics/index.js' 7import { sanitizeToolNameForAnalytics } from 'src/services/analytics/metadata.js' 8import { BashTool } from 'src/tools/BashTool/BashTool.js' 9import { splitCommand_DEPRECATED } from 'src/utils/bash/commands.js' 10import type { 11 PermissionDecisionReason, 12 PermissionResult, 13} from 'src/utils/permissions/PermissionResult.js' 14import { 15 extractRules, 16 hasRules, 17} from 'src/utils/permissions/PermissionUpdate.js' 18import { permissionRuleValueToString } from 'src/utils/permissions/permissionRuleParser.js' 19import { SandboxManager } from 'src/utils/sandbox/sandbox-adapter.js' 20import type { ToolUseConfirm } from '../../components/permissions/PermissionRequest.js' 21import { useSetAppState } from '../../state/AppState.js' 22import { env } from '../../utils/env.js' 23import { jsonStringify } from '../../utils/slowOperations.js' 24import { type CompletionType, logUnaryEvent } from '../../utils/unaryLogging.js' 25 26export type UnaryEvent = { 27 completion_type: CompletionType 28 language_name: string | Promise<string> 29} 30 31function permissionResultToLog(permissionResult: PermissionResult): string { 32 switch (permissionResult.behavior) { 33 case 'allow': 34 return 'allow' 35 case 'ask': { 36 const rules = extractRules(permissionResult.suggestions) 37 const suggestions = 38 rules.length > 0 39 ? rules.map(r => permissionRuleValueToString(r)).join(', ') 40 : 'none' 41 return `ask: ${permissionResult.message}, 42suggestions: ${suggestions} 43reason: ${decisionReasonToString(permissionResult.decisionReason)}` 44 } 45 case 'deny': 46 return `deny: ${permissionResult.message}, 47reason: ${decisionReasonToString(permissionResult.decisionReason)}` 48 case 'passthrough': { 49 const rules = extractRules(permissionResult.suggestions) 50 const suggestions = 51 rules.length > 0 52 ? rules.map(r => permissionRuleValueToString(r)).join(', ') 53 : 'none' 54 return `passthrough: ${permissionResult.message}, 55suggestions: ${suggestions} 56reason: ${decisionReasonToString(permissionResult.decisionReason)}` 57 } 58 } 59} 60 61function decisionReasonToString( 62 decisionReason: PermissionDecisionReason | undefined, 63): string { 64 if (!decisionReason) { 65 return 'No decision reason' 66 } 67 if ( 68 (feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) && 69 decisionReason.type === 'classifier' 70 ) { 71 return `Classifier: ${decisionReason.classifier}, Reason: ${decisionReason.reason}` 72 } 73 switch (decisionReason.type) { 74 case 'rule': 75 return `Rule: ${permissionRuleValueToString(decisionReason.rule.ruleValue)}` 76 case 'mode': 77 return `Mode: ${decisionReason.mode}` 78 case 'subcommandResults': 79 return `Subcommand Results: ${Array.from(decisionReason.reasons.entries()) 80 .map(([key, value]) => `${key}: ${permissionResultToLog(value)}`) 81 .join(', \n')}` 82 case 'permissionPromptTool': 83 return `Permission Tool: ${decisionReason.permissionPromptToolName}, Result: ${jsonStringify(decisionReason.toolResult)}` 84 case 'hook': 85 return `Hook: ${decisionReason.hookName}${decisionReason.reason ? `, Reason: ${decisionReason.reason}` : ''}` 86 case 'workingDir': 87 return `Working Directory: ${decisionReason.reason}` 88 case 'safetyCheck': 89 return `Safety check: ${decisionReason.reason}` 90 case 'other': 91 return `Other: ${decisionReason.reason}` 92 default: 93 return jsonStringify(decisionReason, null, 2) 94 } 95} 96 97/** 98 * Logs permission request events using analytics and unary logging. 99 * Handles both the analytics event and the unary event logging. 100 */ 101export function usePermissionRequestLogging( 102 toolUseConfirm: ToolUseConfirm, 103 unaryEvent: UnaryEvent, 104): void { 105 const setAppState = useSetAppState() 106 // Guard against effect re-firing if toolUseConfirm's object reference 107 // changes during a single dialog's lifetime (e.g., parent re-renders with a 108 // fresh object). Without this, the unconditional setAppState below can 109 // cascade into an infinite microtask loop — each re-fire does another 110 // setAppState spread + (ant builds) splitCommand → shell-quote regex, 111 // pegging CPU at 100% and leaking ~500MB/min in JSRopeString/RegExp allocs. 112 // The component is keyed by toolUseID, so this ref resets on remount — 113 // we only need to dedupe re-fires WITHIN one dialog instance. 114 const loggedToolUseID = useRef<string | null>(null) 115 116 useEffect(() => { 117 if (loggedToolUseID.current === toolUseConfirm.toolUseID) { 118 return 119 } 120 loggedToolUseID.current = toolUseConfirm.toolUseID 121 122 // Increment permission prompt count for attribution tracking 123 setAppState(prev => ({ 124 ...prev, 125 attribution: { 126 ...prev.attribution, 127 permissionPromptCount: prev.attribution.permissionPromptCount + 1, 128 }, 129 })) 130 131 // Log analytics event 132 logEvent('tengu_tool_use_show_permission_request', { 133 messageID: toolUseConfirm.assistantMessage.message 134 .id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 135 toolName: sanitizeToolNameForAnalytics(toolUseConfirm.tool.name), 136 isMcp: toolUseConfirm.tool.isMcp ?? false, 137 decisionReasonType: toolUseConfirm.permissionResult.decisionReason 138 ?.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 139 sandboxEnabled: SandboxManager.isSandboxingEnabled(), 140 }) 141 142 if (process.env.USER_TYPE === 'ant') { 143 const permissionResult = toolUseConfirm.permissionResult 144 if ( 145 toolUseConfirm.tool.name === BashTool.name && 146 permissionResult.behavior === 'ask' && 147 !hasRules(permissionResult.suggestions) 148 ) { 149 // Log if no rule suggestions ("always allow") are provided 150 logEvent('tengu_internal_tool_use_permission_request_no_always_allow', { 151 messageID: toolUseConfirm.assistantMessage.message 152 .id as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 153 toolName: sanitizeToolNameForAnalytics(toolUseConfirm.tool.name), 154 isMcp: toolUseConfirm.tool.isMcp ?? false, 155 decisionReasonType: (permissionResult.decisionReason?.type ?? 156 'unknown') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 157 sandboxEnabled: SandboxManager.isSandboxingEnabled(), 158 159 // This DOES contain code/filepaths and should not be logged in the public build! 160 decisionReasonDetails: decisionReasonToString( 161 permissionResult.decisionReason, 162 ) as never, 163 }) 164 } 165 } 166 167 // [ANT-ONLY] Log bash tool calls, so we can categorize 168 // & burn down calls that should have been allowed 169 if (process.env.USER_TYPE === 'ant') { 170 const parsedInput = BashTool.inputSchema.safeParse(toolUseConfirm.input) 171 if ( 172 toolUseConfirm.tool.name === BashTool.name && 173 toolUseConfirm.permissionResult.behavior === 'ask' && 174 parsedInput.success 175 ) { 176 // Note: All metadata fields in this event contain code/filepaths 177 let split = [parsedInput.data.command] 178 try { 179 split = splitCommand_DEPRECATED(parsedInput.data.command) 180 } catch { 181 // Ignore parse errors here - just log the full command 182 } 183 logEvent('tengu_internal_bash_tool_use_permission_request', { 184 parts: jsonStringify( 185 split, 186 ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 187 input: jsonStringify( 188 toolUseConfirm.input, 189 ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 190 decisionReasonType: toolUseConfirm.permissionResult.decisionReason 191 ?.type as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 192 decisionReason: decisionReasonToString( 193 toolUseConfirm.permissionResult.decisionReason, 194 ) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, 195 }) 196 } 197 } 198 199 void logUnaryEvent({ 200 completion_type: unaryEvent.completion_type, 201 event: 'response', 202 metadata: { 203 language_name: unaryEvent.language_name, 204 message_id: toolUseConfirm.assistantMessage.message.id, 205 platform: env.platform, 206 }, 207 }) 208 }, [toolUseConfirm, unaryEvent, setAppState]) 209}