651 lines
22 KiB
TypeScript
651 lines
22 KiB
TypeScript
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 type z from 'zod/v4'
|
|
import type { CanUseToolFn } from '../../hooks/useCanUseTool.js'
|
|
import type { AnyObject, Tool, ToolUseContext } from '../../Tool.js'
|
|
import type { HookProgress } from '../../types/hooks.js'
|
|
import type {
|
|
AssistantMessage,
|
|
AttachmentMessage,
|
|
ProgressMessage,
|
|
} from '../../types/message.js'
|
|
import type { PermissionDecision } from '../../types/permissions.js'
|
|
import { createAttachmentMessage } from '../../utils/attachments.js'
|
|
import { logForDebugging } from '../../utils/debug.js'
|
|
import {
|
|
executePostToolHooks,
|
|
executePostToolUseFailureHooks,
|
|
executePreToolHooks,
|
|
getPreToolHookBlockingMessage,
|
|
} from '../../utils/hooks.js'
|
|
import { logError } from '../../utils/log.js'
|
|
import {
|
|
getRuleBehaviorDescription,
|
|
type PermissionDecisionReason,
|
|
type PermissionResult,
|
|
} from '../../utils/permissions/PermissionResult.js'
|
|
import { checkRuleBasedPermissions } from '../../utils/permissions/permissions.js'
|
|
import { formatError } from '../../utils/toolErrors.js'
|
|
import { isMcpTool } from '../mcp/utils.js'
|
|
import type { McpServerType, MessageUpdateLazy } from './toolExecution.js'
|
|
|
|
export type PostToolUseHooksResult<Output> =
|
|
| MessageUpdateLazy<AttachmentMessage | ProgressMessage<HookProgress>>
|
|
| { updatedMCPToolOutput: Output }
|
|
|
|
export async function* runPostToolUseHooks<Input extends AnyObject, Output>(
|
|
toolUseContext: ToolUseContext,
|
|
tool: Tool<Input, Output>,
|
|
toolUseID: string,
|
|
messageId: string,
|
|
toolInput: Record<string, unknown>,
|
|
toolResponse: Output,
|
|
requestId: string | undefined,
|
|
mcpServerType: McpServerType,
|
|
mcpServerBaseUrl: string | undefined,
|
|
): AsyncGenerator<PostToolUseHooksResult<Output>> {
|
|
const postToolStartTime = Date.now()
|
|
try {
|
|
const appState = toolUseContext.getAppState()
|
|
const permissionMode = appState.toolPermissionContext.mode
|
|
|
|
let toolOutput = toolResponse
|
|
for await (const result of executePostToolHooks(
|
|
tool.name,
|
|
toolUseID,
|
|
toolInput,
|
|
toolOutput,
|
|
toolUseContext,
|
|
permissionMode,
|
|
toolUseContext.abortController.signal,
|
|
)) {
|
|
try {
|
|
// Check if we were aborted during hook execution
|
|
// IMPORTANT: We emit a cancelled event per hook
|
|
if (
|
|
result.message?.type === 'attachment' &&
|
|
result.message.attachment.type === 'hook_cancelled'
|
|
) {
|
|
logEvent('tengu_post_tool_hooks_cancelled', {
|
|
toolName: sanitizeToolNameForAnalytics(tool.name),
|
|
|
|
queryChainId: toolUseContext.queryTracking
|
|
?.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
queryDepth: toolUseContext.queryTracking?.depth,
|
|
})
|
|
yield {
|
|
message: createAttachmentMessage({
|
|
type: 'hook_cancelled',
|
|
hookName: `PostToolUse:${tool.name}`,
|
|
toolUseID,
|
|
hookEvent: 'PostToolUse',
|
|
}),
|
|
}
|
|
continue
|
|
}
|
|
|
|
// For JSON {decision:"block"} hooks, executeHooks yields two results:
|
|
// {blockingError} and {message: hook_blocking_error attachment}. The
|
|
// blockingError path below creates that same attachment, so skip it
|
|
// here to avoid displaying the block reason twice (#31301). The
|
|
// exit-code-2 path only yields {blockingError}, so it's unaffected.
|
|
if (
|
|
result.message &&
|
|
!(
|
|
result.message.type === 'attachment' &&
|
|
result.message.attachment.type === 'hook_blocking_error'
|
|
)
|
|
) {
|
|
yield { message: result.message }
|
|
}
|
|
|
|
if (result.blockingError) {
|
|
yield {
|
|
message: createAttachmentMessage({
|
|
type: 'hook_blocking_error',
|
|
hookName: `PostToolUse:${tool.name}`,
|
|
toolUseID: toolUseID,
|
|
hookEvent: 'PostToolUse',
|
|
blockingError: result.blockingError,
|
|
}),
|
|
}
|
|
}
|
|
|
|
// If hook indicated to prevent continuation, yield a stop reason message
|
|
if (result.preventContinuation) {
|
|
yield {
|
|
message: createAttachmentMessage({
|
|
type: 'hook_stopped_continuation',
|
|
message:
|
|
result.stopReason || 'Execution stopped by PostToolUse hook',
|
|
hookName: `PostToolUse:${tool.name}`,
|
|
toolUseID: toolUseID,
|
|
hookEvent: 'PostToolUse',
|
|
}),
|
|
}
|
|
return
|
|
}
|
|
|
|
// If hooks provided additional context, add it as a message
|
|
if (result.additionalContexts && result.additionalContexts.length > 0) {
|
|
yield {
|
|
message: createAttachmentMessage({
|
|
type: 'hook_additional_context',
|
|
content: result.additionalContexts,
|
|
hookName: `PostToolUse:${tool.name}`,
|
|
toolUseID: toolUseID,
|
|
hookEvent: 'PostToolUse',
|
|
}),
|
|
}
|
|
}
|
|
|
|
// If hooks provided updatedMCPToolOutput, yield it if this is an MCP tool
|
|
if (result.updatedMCPToolOutput && isMcpTool(tool)) {
|
|
toolOutput = result.updatedMCPToolOutput as Output
|
|
yield {
|
|
updatedMCPToolOutput: toolOutput,
|
|
}
|
|
}
|
|
} catch (error) {
|
|
const postToolDurationMs = Date.now() - postToolStartTime
|
|
logEvent('tengu_post_tool_hook_error', {
|
|
messageID:
|
|
messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
toolName: sanitizeToolNameForAnalytics(tool.name),
|
|
isMcp: tool.isMcp ?? false,
|
|
duration: postToolDurationMs,
|
|
|
|
queryChainId: toolUseContext.queryTracking
|
|
?.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
queryDepth: toolUseContext.queryTracking?.depth,
|
|
...(mcpServerType
|
|
? {
|
|
mcpServerType:
|
|
mcpServerType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
}
|
|
: {}),
|
|
...(requestId
|
|
? {
|
|
requestId:
|
|
requestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
}
|
|
: {}),
|
|
})
|
|
yield {
|
|
message: createAttachmentMessage({
|
|
type: 'hook_error_during_execution',
|
|
content: formatError(error),
|
|
hookName: `PostToolUse:${tool.name}`,
|
|
toolUseID: toolUseID,
|
|
hookEvent: 'PostToolUse',
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logError(error)
|
|
}
|
|
}
|
|
|
|
export async function* runPostToolUseFailureHooks<Input extends AnyObject>(
|
|
toolUseContext: ToolUseContext,
|
|
tool: Tool<Input, unknown>,
|
|
toolUseID: string,
|
|
messageId: string,
|
|
processedInput: z.infer<Input>,
|
|
error: string,
|
|
isInterrupt: boolean | undefined,
|
|
requestId: string | undefined,
|
|
mcpServerType: McpServerType,
|
|
mcpServerBaseUrl: string | undefined,
|
|
): AsyncGenerator<
|
|
MessageUpdateLazy<AttachmentMessage | ProgressMessage<HookProgress>>
|
|
> {
|
|
const postToolStartTime = Date.now()
|
|
try {
|
|
const appState = toolUseContext.getAppState()
|
|
const permissionMode = appState.toolPermissionContext.mode
|
|
|
|
for await (const result of executePostToolUseFailureHooks(
|
|
tool.name,
|
|
toolUseID,
|
|
processedInput,
|
|
error,
|
|
toolUseContext,
|
|
isInterrupt,
|
|
permissionMode,
|
|
toolUseContext.abortController.signal,
|
|
)) {
|
|
try {
|
|
// Check if we were aborted during hook execution
|
|
if (
|
|
result.message?.type === 'attachment' &&
|
|
result.message.attachment.type === 'hook_cancelled'
|
|
) {
|
|
logEvent('tengu_post_tool_failure_hooks_cancelled', {
|
|
toolName: sanitizeToolNameForAnalytics(tool.name),
|
|
queryChainId: toolUseContext.queryTracking
|
|
?.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
queryDepth: toolUseContext.queryTracking?.depth,
|
|
})
|
|
yield {
|
|
message: createAttachmentMessage({
|
|
type: 'hook_cancelled',
|
|
hookName: `PostToolUseFailure:${tool.name}`,
|
|
toolUseID,
|
|
hookEvent: 'PostToolUseFailure',
|
|
}),
|
|
}
|
|
continue
|
|
}
|
|
|
|
// Skip hook_blocking_error in result.message — blockingError path
|
|
// below creates the same attachment (see #31301 / PostToolUse above).
|
|
if (
|
|
result.message &&
|
|
!(
|
|
result.message.type === 'attachment' &&
|
|
result.message.attachment.type === 'hook_blocking_error'
|
|
)
|
|
) {
|
|
yield { message: result.message }
|
|
}
|
|
|
|
if (result.blockingError) {
|
|
yield {
|
|
message: createAttachmentMessage({
|
|
type: 'hook_blocking_error',
|
|
hookName: `PostToolUseFailure:${tool.name}`,
|
|
toolUseID: toolUseID,
|
|
hookEvent: 'PostToolUseFailure',
|
|
blockingError: result.blockingError,
|
|
}),
|
|
}
|
|
}
|
|
|
|
// If hooks provided additional context, add it as a message
|
|
if (result.additionalContexts && result.additionalContexts.length > 0) {
|
|
yield {
|
|
message: createAttachmentMessage({
|
|
type: 'hook_additional_context',
|
|
content: result.additionalContexts,
|
|
hookName: `PostToolUseFailure:${tool.name}`,
|
|
toolUseID: toolUseID,
|
|
hookEvent: 'PostToolUseFailure',
|
|
}),
|
|
}
|
|
}
|
|
} catch (hookError) {
|
|
const postToolDurationMs = Date.now() - postToolStartTime
|
|
logEvent('tengu_post_tool_failure_hook_error', {
|
|
messageID:
|
|
messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
toolName: sanitizeToolNameForAnalytics(tool.name),
|
|
isMcp: tool.isMcp ?? false,
|
|
duration: postToolDurationMs,
|
|
queryChainId: toolUseContext.queryTracking
|
|
?.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
queryDepth: toolUseContext.queryTracking?.depth,
|
|
...(mcpServerType
|
|
? {
|
|
mcpServerType:
|
|
mcpServerType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
}
|
|
: {}),
|
|
...(requestId
|
|
? {
|
|
requestId:
|
|
requestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
}
|
|
: {}),
|
|
})
|
|
yield {
|
|
message: createAttachmentMessage({
|
|
type: 'hook_error_during_execution',
|
|
content: formatError(hookError),
|
|
hookName: `PostToolUseFailure:${tool.name}`,
|
|
toolUseID: toolUseID,
|
|
hookEvent: 'PostToolUseFailure',
|
|
}),
|
|
}
|
|
}
|
|
}
|
|
} catch (outerError) {
|
|
logError(outerError)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Resolve a PreToolUse hook's permission result into a final PermissionDecision.
|
|
*
|
|
* Encapsulates the invariant that hook 'allow' does NOT bypass settings.json
|
|
* deny/ask rules — checkRuleBasedPermissions still applies (inc-4788 analog).
|
|
* Also handles the requiresUserInteraction/requireCanUseTool guards and the
|
|
* 'ask' forceDecision passthrough.
|
|
*
|
|
* Shared by toolExecution.ts (main query loop) and REPLTool/toolWrappers.ts
|
|
* (REPL inner calls) so the permission semantics stay in lockstep.
|
|
*/
|
|
export async function resolveHookPermissionDecision(
|
|
hookPermissionResult: PermissionResult | undefined,
|
|
tool: Tool,
|
|
input: Record<string, unknown>,
|
|
toolUseContext: ToolUseContext,
|
|
canUseTool: CanUseToolFn,
|
|
assistantMessage: AssistantMessage,
|
|
toolUseID: string,
|
|
): Promise<{
|
|
decision: PermissionDecision
|
|
input: Record<string, unknown>
|
|
}> {
|
|
const requiresInteraction = tool.requiresUserInteraction?.()
|
|
const requireCanUseTool = toolUseContext.requireCanUseTool
|
|
|
|
if (hookPermissionResult?.behavior === 'allow') {
|
|
const hookInput = hookPermissionResult.updatedInput ?? input
|
|
|
|
// Hook provided updatedInput for an interactive tool — the hook IS the
|
|
// user interaction (e.g. headless wrapper that collected AskUserQuestion
|
|
// answers). Treat as non-interactive for the rule-check path.
|
|
const interactionSatisfied =
|
|
requiresInteraction && hookPermissionResult.updatedInput !== undefined
|
|
|
|
if ((requiresInteraction && !interactionSatisfied) || requireCanUseTool) {
|
|
logForDebugging(
|
|
`Hook approved tool use for ${tool.name}, but canUseTool is required`,
|
|
)
|
|
return {
|
|
decision: await canUseTool(
|
|
tool,
|
|
hookInput,
|
|
toolUseContext,
|
|
assistantMessage,
|
|
toolUseID,
|
|
),
|
|
input: hookInput,
|
|
}
|
|
}
|
|
|
|
// Hook allow skips the interactive prompt, but deny/ask rules still apply.
|
|
const ruleCheck = await checkRuleBasedPermissions(
|
|
tool,
|
|
hookInput,
|
|
toolUseContext,
|
|
)
|
|
if (ruleCheck === null) {
|
|
logForDebugging(
|
|
interactionSatisfied
|
|
? `Hook satisfied user interaction for ${tool.name} via updatedInput`
|
|
: `Hook approved tool use for ${tool.name}, bypassing permission prompt`,
|
|
)
|
|
return { decision: hookPermissionResult, input: hookInput }
|
|
}
|
|
if (ruleCheck.behavior === 'deny') {
|
|
logForDebugging(
|
|
`Hook approved tool use for ${tool.name}, but deny rule overrides: ${ruleCheck.message}`,
|
|
)
|
|
return { decision: ruleCheck, input: hookInput }
|
|
}
|
|
// ask rule — dialog required despite hook approval
|
|
logForDebugging(
|
|
`Hook approved tool use for ${tool.name}, but ask rule requires prompt`,
|
|
)
|
|
return {
|
|
decision: await canUseTool(
|
|
tool,
|
|
hookInput,
|
|
toolUseContext,
|
|
assistantMessage,
|
|
toolUseID,
|
|
),
|
|
input: hookInput,
|
|
}
|
|
}
|
|
|
|
if (hookPermissionResult?.behavior === 'deny') {
|
|
logForDebugging(`Hook denied tool use for ${tool.name}`)
|
|
return { decision: hookPermissionResult, input }
|
|
}
|
|
|
|
// No hook decision or 'ask' — normal permission flow, possibly with
|
|
// forceDecision so the dialog shows the hook's ask message.
|
|
const forceDecision =
|
|
hookPermissionResult?.behavior === 'ask' ? hookPermissionResult : undefined
|
|
const askInput =
|
|
hookPermissionResult?.behavior === 'ask' &&
|
|
hookPermissionResult.updatedInput
|
|
? hookPermissionResult.updatedInput
|
|
: input
|
|
return {
|
|
decision: await canUseTool(
|
|
tool,
|
|
askInput,
|
|
toolUseContext,
|
|
assistantMessage,
|
|
toolUseID,
|
|
forceDecision,
|
|
),
|
|
input: askInput,
|
|
}
|
|
}
|
|
|
|
export async function* runPreToolUseHooks(
|
|
toolUseContext: ToolUseContext,
|
|
tool: Tool,
|
|
processedInput: Record<string, unknown>,
|
|
toolUseID: string,
|
|
messageId: string,
|
|
requestId: string | undefined,
|
|
mcpServerType: McpServerType,
|
|
mcpServerBaseUrl: string | undefined,
|
|
): AsyncGenerator<
|
|
| {
|
|
type: 'message'
|
|
message: MessageUpdateLazy<
|
|
AttachmentMessage | ProgressMessage<HookProgress>
|
|
>
|
|
}
|
|
| { type: 'hookPermissionResult'; hookPermissionResult: PermissionResult }
|
|
| { type: 'hookUpdatedInput'; updatedInput: Record<string, unknown> }
|
|
| { type: 'preventContinuation'; shouldPreventContinuation: boolean }
|
|
| { type: 'stopReason'; stopReason: string }
|
|
| {
|
|
type: 'additionalContext'
|
|
message: MessageUpdateLazy<AttachmentMessage>
|
|
}
|
|
// stop execution
|
|
| { type: 'stop' }
|
|
> {
|
|
const hookStartTime = Date.now()
|
|
try {
|
|
const appState = toolUseContext.getAppState()
|
|
|
|
for await (const result of executePreToolHooks(
|
|
tool.name,
|
|
toolUseID,
|
|
processedInput,
|
|
toolUseContext,
|
|
appState.toolPermissionContext.mode,
|
|
toolUseContext.abortController.signal,
|
|
undefined, // timeoutMs - use default
|
|
toolUseContext.requestPrompt,
|
|
tool.getToolUseSummary?.(processedInput),
|
|
)) {
|
|
try {
|
|
if (result.message) {
|
|
yield { type: 'message', message: { message: result.message } }
|
|
}
|
|
if (result.blockingError) {
|
|
const denialMessage = getPreToolHookBlockingMessage(
|
|
`PreToolUse:${tool.name}`,
|
|
result.blockingError,
|
|
)
|
|
yield {
|
|
type: 'hookPermissionResult',
|
|
hookPermissionResult: {
|
|
behavior: 'deny',
|
|
message: denialMessage,
|
|
decisionReason: {
|
|
type: 'hook',
|
|
hookName: `PreToolUse:${tool.name}`,
|
|
reason: denialMessage,
|
|
},
|
|
},
|
|
}
|
|
}
|
|
// Check if hook wants to prevent continuation
|
|
if (result.preventContinuation) {
|
|
yield {
|
|
type: 'preventContinuation',
|
|
shouldPreventContinuation: true,
|
|
}
|
|
if (result.stopReason) {
|
|
yield { type: 'stopReason', stopReason: result.stopReason }
|
|
}
|
|
}
|
|
// Check for hook-defined permission behavior
|
|
if (result.permissionBehavior !== undefined) {
|
|
logForDebugging(
|
|
`Hook result has permissionBehavior=${result.permissionBehavior}`,
|
|
)
|
|
const decisionReason: PermissionDecisionReason = {
|
|
type: 'hook',
|
|
hookName: `PreToolUse:${tool.name}`,
|
|
hookSource: result.hookSource,
|
|
reason: result.hookPermissionDecisionReason,
|
|
}
|
|
if (result.permissionBehavior === 'allow') {
|
|
yield {
|
|
type: 'hookPermissionResult',
|
|
hookPermissionResult: {
|
|
behavior: 'allow',
|
|
updatedInput: result.updatedInput,
|
|
decisionReason,
|
|
},
|
|
}
|
|
} else if (result.permissionBehavior === 'ask') {
|
|
yield {
|
|
type: 'hookPermissionResult',
|
|
hookPermissionResult: {
|
|
behavior: 'ask',
|
|
updatedInput: result.updatedInput,
|
|
message:
|
|
result.hookPermissionDecisionReason ||
|
|
`Hook PreToolUse:${tool.name} ${getRuleBehaviorDescription(result.permissionBehavior)} this tool`,
|
|
decisionReason,
|
|
},
|
|
}
|
|
} else {
|
|
// deny - updatedInput is irrelevant since tool won't run
|
|
yield {
|
|
type: 'hookPermissionResult',
|
|
hookPermissionResult: {
|
|
behavior: result.permissionBehavior,
|
|
message:
|
|
result.hookPermissionDecisionReason ||
|
|
`Hook PreToolUse:${tool.name} ${getRuleBehaviorDescription(result.permissionBehavior)} this tool`,
|
|
decisionReason,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
// Yield updatedInput for passthrough case (no permission decision)
|
|
// This allows hooks to modify input while letting normal permission flow continue
|
|
if (result.updatedInput && result.permissionBehavior === undefined) {
|
|
yield {
|
|
type: 'hookUpdatedInput',
|
|
updatedInput: result.updatedInput,
|
|
}
|
|
}
|
|
|
|
// If hooks provided additional context, add it as a message
|
|
if (result.additionalContexts && result.additionalContexts.length > 0) {
|
|
yield {
|
|
type: 'additionalContext',
|
|
message: {
|
|
message: createAttachmentMessage({
|
|
type: 'hook_additional_context',
|
|
content: result.additionalContexts,
|
|
hookName: `PreToolUse:${tool.name}`,
|
|
toolUseID,
|
|
hookEvent: 'PreToolUse',
|
|
}),
|
|
},
|
|
}
|
|
}
|
|
|
|
// Check if we were aborted during hook execution
|
|
if (toolUseContext.abortController.signal.aborted) {
|
|
logEvent('tengu_pre_tool_hooks_cancelled', {
|
|
toolName: sanitizeToolNameForAnalytics(tool.name),
|
|
|
|
queryChainId: toolUseContext.queryTracking
|
|
?.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
queryDepth: toolUseContext.queryTracking?.depth,
|
|
})
|
|
yield {
|
|
type: 'message',
|
|
message: {
|
|
message: createAttachmentMessage({
|
|
type: 'hook_cancelled',
|
|
hookName: `PreToolUse:${tool.name}`,
|
|
toolUseID,
|
|
hookEvent: 'PreToolUse',
|
|
}),
|
|
},
|
|
}
|
|
yield { type: 'stop' }
|
|
return
|
|
}
|
|
} catch (error) {
|
|
logError(error)
|
|
const durationMs = Date.now() - hookStartTime
|
|
logEvent('tengu_pre_tool_hook_error', {
|
|
messageID:
|
|
messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
toolName: sanitizeToolNameForAnalytics(tool.name),
|
|
isMcp: tool.isMcp ?? false,
|
|
duration: durationMs,
|
|
|
|
queryChainId: toolUseContext.queryTracking
|
|
?.chainId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
queryDepth: toolUseContext.queryTracking?.depth,
|
|
...(mcpServerType
|
|
? {
|
|
mcpServerType:
|
|
mcpServerType as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
}
|
|
: {}),
|
|
...(requestId
|
|
? {
|
|
requestId:
|
|
requestId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
}
|
|
: {}),
|
|
})
|
|
yield {
|
|
type: 'message',
|
|
message: {
|
|
message: createAttachmentMessage({
|
|
type: 'hook_error_during_execution',
|
|
content: formatError(error),
|
|
hookName: `PreToolUse:${tool.name}`,
|
|
toolUseID: toolUseID,
|
|
hookEvent: 'PreToolUse',
|
|
}),
|
|
},
|
|
}
|
|
yield { type: 'stop' }
|
|
}
|
|
}
|
|
} catch (error) {
|
|
logError(error)
|
|
yield { type: 'stop' }
|
|
return
|
|
}
|
|
}
|