| |
| |
| |
| import { feature } from 'bun:bundle' |
| import { |
| type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| logEvent, |
| } from 'src/services/analytics/index.js' |
| import { sanitizeToolNameForAnalytics } from 'src/services/analytics/metadata.js' |
| import { getCodeEditToolDecisionCounter } from '../../bootstrap/state.js' |
| import type { Tool as ToolType, ToolUseContext } from '../../Tool.js' |
| import { getLanguageName } from '../../utils/cliHighlight.js' |
| import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js' |
| import { logOTelEvent } from '../../utils/telemetry/events.js' |
| import type { |
| PermissionApprovalSource, |
| PermissionRejectionSource, |
| } from './PermissionContext.js' |
|
|
| type PermissionLogContext = { |
| tool: ToolType |
| input: unknown |
| toolUseContext: ToolUseContext |
| messageId: string |
| toolUseID: string |
| } |
|
|
| |
| type PermissionDecisionArgs = |
| | { decision: 'accept'; source: PermissionApprovalSource | 'config' } |
| | { decision: 'reject'; source: PermissionRejectionSource | 'config' } |
|
|
| const CODE_EDITING_TOOLS = ['Edit', 'Write', 'NotebookEdit'] |
|
|
| function isCodeEditingTool(toolName: string): boolean { |
| return CODE_EDITING_TOOLS.includes(toolName) |
| } |
|
|
| |
| |
| async function buildCodeEditToolAttributes( |
| tool: ToolType, |
| input: unknown, |
| decision: 'accept' | 'reject', |
| source: string, |
| ): Promise<Record<string, string>> { |
| |
| let language: string | undefined |
| if (tool.getPath && input) { |
| const parseResult = tool.inputSchema.safeParse(input) |
| if (parseResult.success) { |
| const filePath = tool.getPath(parseResult.data) |
| if (filePath) { |
| language = await getLanguageName(filePath) |
| } |
| } |
| } |
|
|
| return { |
| decision, |
| source, |
| tool_name: tool.name, |
| ...(language && { language }), |
| } |
| } |
|
|
| |
| function sourceToString( |
| source: PermissionApprovalSource | PermissionRejectionSource, |
| ): string { |
| if ( |
| (feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) && |
| source.type === 'classifier' |
| ) { |
| return 'classifier' |
| } |
| switch (source.type) { |
| case 'hook': |
| return 'hook' |
| case 'user': |
| return source.permanent ? 'user_permanent' : 'user_temporary' |
| case 'user_abort': |
| return 'user_abort' |
| case 'user_reject': |
| return 'user_reject' |
| default: |
| return 'unknown' |
| } |
| } |
|
|
| function baseMetadata( |
| messageId: string, |
| toolName: string, |
| waitMs: number | undefined, |
| ): { [key: string]: boolean | number | undefined } { |
| return { |
| messageID: |
| messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS, |
| toolName: sanitizeToolNameForAnalytics(toolName), |
| sandboxEnabled: SandboxManager.isSandboxingEnabled(), |
| |
| ...(waitMs !== undefined && { waiting_for_user_permission_ms: waitMs }), |
| } |
| } |
|
|
| |
| function logApprovalEvent( |
| tool: ToolType, |
| messageId: string, |
| source: PermissionApprovalSource | 'config', |
| waitMs: number | undefined, |
| ): void { |
| if (source === 'config') { |
| |
| logEvent( |
| 'tengu_tool_use_granted_in_config', |
| baseMetadata(messageId, tool.name, undefined), |
| ) |
| return |
| } |
| if ( |
| (feature('BASH_CLASSIFIER') || feature('TRANSCRIPT_CLASSIFIER')) && |
| source.type === 'classifier' |
| ) { |
| logEvent( |
| 'tengu_tool_use_granted_by_classifier', |
| baseMetadata(messageId, tool.name, waitMs), |
| ) |
| return |
| } |
| switch (source.type) { |
| case 'user': |
| logEvent( |
| source.permanent |
| ? 'tengu_tool_use_granted_in_prompt_permanent' |
| : 'tengu_tool_use_granted_in_prompt_temporary', |
| baseMetadata(messageId, tool.name, waitMs), |
| ) |
| break |
| case 'hook': |
| logEvent('tengu_tool_use_granted_by_permission_hook', { |
| ...baseMetadata(messageId, tool.name, waitMs), |
| permanent: source.permanent ?? false, |
| }) |
| break |
| default: |
| break |
| } |
| } |
|
|
| |
| function logRejectionEvent( |
| tool: ToolType, |
| messageId: string, |
| source: PermissionRejectionSource | 'config', |
| waitMs: number | undefined, |
| ): void { |
| if (source === 'config') { |
| |
| logEvent( |
| 'tengu_tool_use_denied_in_config', |
| baseMetadata(messageId, tool.name, undefined), |
| ) |
| return |
| } |
| logEvent('tengu_tool_use_rejected_in_prompt', { |
| ...baseMetadata(messageId, tool.name, waitMs), |
| |
| ...(source.type === 'hook' |
| ? { isHook: true } |
| : { |
| hasFeedback: |
| source.type === 'user_reject' ? source.hasFeedback : false, |
| }), |
| }) |
| } |
|
|
| |
| |
| |
| function logPermissionDecision( |
| ctx: PermissionLogContext, |
| args: PermissionDecisionArgs, |
| permissionPromptStartTimeMs?: number, |
| ): void { |
| const { tool, input, toolUseContext, messageId, toolUseID } = ctx |
| const { decision, source } = args |
|
|
| const waiting_for_user_permission_ms = |
| permissionPromptStartTimeMs !== undefined |
| ? Date.now() - permissionPromptStartTimeMs |
| : undefined |
|
|
| |
| if (args.decision === 'accept') { |
| logApprovalEvent( |
| tool, |
| messageId, |
| args.source, |
| waiting_for_user_permission_ms, |
| ) |
| } else { |
| logRejectionEvent( |
| tool, |
| messageId, |
| args.source, |
| waiting_for_user_permission_ms, |
| ) |
| } |
|
|
| const sourceString = source === 'config' ? 'config' : sourceToString(source) |
|
|
| |
| if (isCodeEditingTool(tool.name)) { |
| void buildCodeEditToolAttributes(tool, input, decision, sourceString).then( |
| attributes => getCodeEditToolDecisionCounter()?.add(1, attributes), |
| ) |
| } |
|
|
| |
| if (!toolUseContext.toolDecisions) { |
| toolUseContext.toolDecisions = new Map() |
| } |
| toolUseContext.toolDecisions.set(toolUseID, { |
| source: sourceString, |
| decision, |
| timestamp: Date.now(), |
| }) |
|
|
| void logOTelEvent('tool_decision', { |
| decision, |
| source: sourceString, |
| tool_name: sanitizeToolNameForAnalytics(tool.name), |
| }) |
| } |
|
|
| export { isCodeEditingTool, buildCodeEditToolAttributes, logPermissionDecision } |
| export type { PermissionLogContext, PermissionDecisionArgs } |
|
|