mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-17 22:05:50 +00:00
* feat: 第一版大重构 * fix: 修复类型问题 * chore: 更新版本到 1.3.2 * Add brave as alternative WebSearchTool * fix: 修正顺序 * fix: 修复对穷鬼模式的 auto dream 和 session memory 越过 * feat: 穷鬼模式去除 session-summary * feat: 创建 builtin-tools 包,搬运所有工具实现 将 src/tools/ 下的全部 60 个工具目录迁移至 packages/builtin-tools/src/tools/, 内部导入路径已更新为 src/ alias 模式。 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: 更新 src/ 中所有工具引用至 builtin-tools 包,删除 src/tools/ - src/tools.ts 及 178 个 src/ 文件的 import 路径从 ./tools/ 改为 builtin-tools/tools/ - 删除 src/tools/ 整个目录(已迁移至 packages/builtin-tools/) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: 添加 builtin-tools 路径别名至 tsconfig,更新 bun.lock - tsconfig.json 新增 builtin-tools/* 和 builtin-tools 路径映射 - 新增 packages/builtin-tools/src 至 include Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: 为 builtin-tools、mcp-client、agent-tools 添加 @claude-code-best 作用域前缀 所有包名及 import 路径统一添加 @claude-code-best/ 前缀: - builtin-tools → @claude-code-best/builtin-tools - mcp-client → @claude-code-best/mcp-client - agent-tools → @claude-code-best/agent-tools Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: 修复 node 环境没有 bun 的问题 --------- Co-authored-by: Eric-Guo <eric.guocz@gmail.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
355 lines
14 KiB
TypeScript
355 lines
14 KiB
TypeScript
import { feature } from 'bun:bundle'
|
|
import { APIUserAbortError } from '@anthropic-ai/sdk'
|
|
import * as React from 'react'
|
|
import { useCallback } from 'react'
|
|
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 { ToolUseConfirm } from '../components/permissions/PermissionRequest.js'
|
|
import { Text } from '@anthropic/ink'
|
|
import type {
|
|
ToolPermissionContext,
|
|
Tool as ToolType,
|
|
ToolUseContext,
|
|
} from '../Tool.js'
|
|
import {
|
|
consumeSpeculativeClassifierCheck,
|
|
peekSpeculativeClassifierCheck,
|
|
} from '@claude-code-best/builtin-tools/tools/BashTool/bashPermissions.js'
|
|
import { BASH_TOOL_NAME } from '@claude-code-best/builtin-tools/tools/BashTool/toolName.js'
|
|
import type { AssistantMessage } from '../types/message.js'
|
|
import { recordAutoModeDenial } from '../utils/autoModeDenials.js'
|
|
import {
|
|
clearClassifierChecking,
|
|
setClassifierApproval,
|
|
setYoloClassifierApproval,
|
|
} from '../utils/classifierApprovals.js'
|
|
import { logForDebugging } from '../utils/debug.js'
|
|
import { AbortError } from '../utils/errors.js'
|
|
import { logError } from '../utils/log.js'
|
|
import type { PermissionDecision } from '../utils/permissions/PermissionResult.js'
|
|
import { hasPermissionsToUseTool } from '../utils/permissions/permissions.js'
|
|
import { jsonStringify } from '../utils/slowOperations.js'
|
|
import { handleCoordinatorPermission } from './toolPermission/handlers/coordinatorHandler.js'
|
|
import { handleInteractivePermission } from './toolPermission/handlers/interactiveHandler.js'
|
|
import { handleSwarmWorkerPermission } from './toolPermission/handlers/swarmWorkerHandler.js'
|
|
import {
|
|
createPermissionContext,
|
|
createPermissionQueueOps,
|
|
} from './toolPermission/PermissionContext.js'
|
|
import { logPermissionDecision } from './toolPermission/permissionLogging.js'
|
|
|
|
export type CanUseToolFn<
|
|
Input extends Record<string, unknown> = Record<string, unknown>,
|
|
> = (
|
|
tool: ToolType,
|
|
input: Input,
|
|
toolUseContext: ToolUseContext,
|
|
assistantMessage: AssistantMessage,
|
|
toolUseID: string,
|
|
forceDecision?: PermissionDecision<Input>,
|
|
) => Promise<PermissionDecision<Input>>
|
|
|
|
function useCanUseTool(
|
|
setToolUseConfirmQueue: React.Dispatch<
|
|
React.SetStateAction<ToolUseConfirm[]>
|
|
>,
|
|
setToolPermissionContext: (context: ToolPermissionContext) => void,
|
|
): CanUseToolFn {
|
|
return useCallback<CanUseToolFn>(
|
|
async (
|
|
tool,
|
|
input,
|
|
toolUseContext,
|
|
assistantMessage,
|
|
toolUseID,
|
|
forceDecision,
|
|
) => {
|
|
return new Promise(resolve => {
|
|
const ctx = createPermissionContext(
|
|
tool,
|
|
input,
|
|
toolUseContext,
|
|
assistantMessage,
|
|
toolUseID,
|
|
setToolPermissionContext,
|
|
createPermissionQueueOps(setToolUseConfirmQueue),
|
|
)
|
|
|
|
if (ctx.resolveIfAborted(resolve)) return
|
|
|
|
const decisionPromise =
|
|
forceDecision !== undefined
|
|
? Promise.resolve(forceDecision)
|
|
: hasPermissionsToUseTool(
|
|
tool,
|
|
input,
|
|
toolUseContext,
|
|
assistantMessage,
|
|
toolUseID,
|
|
)
|
|
|
|
return decisionPromise
|
|
.then(async result => {
|
|
// [ANT-ONLY] Log all tool permission decisions with tool name and args
|
|
if (process.env.USER_TYPE === 'ant') {
|
|
logEvent('tengu_internal_tool_permission_decision', {
|
|
toolName: sanitizeToolNameForAnalytics(tool.name),
|
|
behavior:
|
|
result.behavior as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
// Note: input contains code/filepaths, only log for ants
|
|
input: jsonStringify(
|
|
input,
|
|
) as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
messageID:
|
|
ctx.messageId as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
|
|
isMcp: tool.isMcp ?? false,
|
|
})
|
|
}
|
|
|
|
// Has permissions to use tool, granted in config
|
|
if (result.behavior === 'allow') {
|
|
if (ctx.resolveIfAborted(resolve)) return
|
|
// Track auto mode classifier approvals for UI display
|
|
if (
|
|
feature('TRANSCRIPT_CLASSIFIER') &&
|
|
result.decisionReason?.type === 'classifier' &&
|
|
result.decisionReason.classifier === 'auto-mode'
|
|
) {
|
|
setYoloClassifierApproval(
|
|
toolUseID,
|
|
result.decisionReason.reason,
|
|
)
|
|
}
|
|
|
|
ctx.logDecision({ decision: 'accept', source: 'config' })
|
|
|
|
resolve(
|
|
ctx.buildAllow(result.updatedInput ?? input, {
|
|
decisionReason: result.decisionReason,
|
|
}),
|
|
)
|
|
return
|
|
}
|
|
|
|
const appState = toolUseContext.getAppState()
|
|
const description = await tool.description(input as never, {
|
|
isNonInteractiveSession:
|
|
toolUseContext.options.isNonInteractiveSession,
|
|
toolPermissionContext: appState.toolPermissionContext,
|
|
tools: toolUseContext.options.tools,
|
|
})
|
|
|
|
if (ctx.resolveIfAborted(resolve)) return
|
|
|
|
// Does not have permissions to use tool, check the behavior
|
|
switch (result.behavior) {
|
|
case 'deny': {
|
|
logPermissionDecision(
|
|
{
|
|
tool,
|
|
input,
|
|
toolUseContext,
|
|
messageId: ctx.messageId!,
|
|
toolUseID,
|
|
},
|
|
{ decision: 'reject', source: 'config' },
|
|
)
|
|
if (
|
|
feature('TRANSCRIPT_CLASSIFIER') &&
|
|
result.decisionReason?.type === 'classifier' &&
|
|
result.decisionReason.classifier === 'auto-mode'
|
|
) {
|
|
recordAutoModeDenial({
|
|
toolName: tool.name,
|
|
display: description,
|
|
reason: result.decisionReason.reason ?? '',
|
|
timestamp: Date.now(),
|
|
})
|
|
toolUseContext.addNotification?.({
|
|
key: 'auto-mode-denied',
|
|
priority: 'immediate',
|
|
jsx: (
|
|
<>
|
|
<Text color="error">
|
|
{tool.userFacingName(input).toLowerCase()} denied by
|
|
auto mode
|
|
</Text>
|
|
<Text dimColor> · /permissions</Text>
|
|
</>
|
|
),
|
|
})
|
|
}
|
|
resolve(result)
|
|
return
|
|
}
|
|
|
|
case 'ask': {
|
|
// For coordinator workers, await automated checks before showing dialog.
|
|
// Background workers should only interrupt the user when automated checks can't decide.
|
|
if (
|
|
appState.toolPermissionContext
|
|
.awaitAutomatedChecksBeforeDialog
|
|
) {
|
|
const coordinatorDecision = await handleCoordinatorPermission(
|
|
{
|
|
ctx,
|
|
...(feature('BASH_CLASSIFIER')
|
|
? {
|
|
pendingClassifierCheck:
|
|
result.pendingClassifierCheck,
|
|
}
|
|
: {}),
|
|
updatedInput: result.updatedInput,
|
|
suggestions: result.suggestions,
|
|
permissionMode: appState.toolPermissionContext.mode,
|
|
},
|
|
)
|
|
if (coordinatorDecision) {
|
|
resolve(coordinatorDecision)
|
|
return
|
|
}
|
|
// null means neither automated check resolved -- fall through to dialog below.
|
|
// Hooks already ran, classifier already consumed.
|
|
}
|
|
|
|
// After awaiting automated checks, verify the request wasn't aborted
|
|
// while we were waiting. Without this check, a stale dialog could appear.
|
|
if (ctx.resolveIfAborted(resolve)) return
|
|
|
|
// For swarm workers, try classifier auto-approval then
|
|
// forward permission requests to the leader via mailbox.
|
|
const swarmDecision = await handleSwarmWorkerPermission({
|
|
ctx,
|
|
description,
|
|
...(feature('BASH_CLASSIFIER')
|
|
? {
|
|
pendingClassifierCheck: result.pendingClassifierCheck,
|
|
}
|
|
: {}),
|
|
updatedInput: result.updatedInput,
|
|
suggestions: result.suggestions,
|
|
})
|
|
if (swarmDecision) {
|
|
resolve(swarmDecision)
|
|
return
|
|
}
|
|
|
|
// Grace period: wait up to 2s for speculative classifier
|
|
// to resolve before showing the dialog (main agent only)
|
|
if (
|
|
feature('BASH_CLASSIFIER') &&
|
|
result.pendingClassifierCheck &&
|
|
tool.name === BASH_TOOL_NAME &&
|
|
!appState.toolPermissionContext
|
|
.awaitAutomatedChecksBeforeDialog
|
|
) {
|
|
const speculativePromise = peekSpeculativeClassifierCheck(
|
|
(input as { command: string }).command,
|
|
)
|
|
if (speculativePromise) {
|
|
const raceResult = await Promise.race([
|
|
speculativePromise.then(r => ({
|
|
type: 'result' as const,
|
|
result: r,
|
|
})),
|
|
new Promise<{ type: 'timeout' }>(res =>
|
|
// eslint-disable-next-line no-restricted-syntax -- resolves with a value, not void
|
|
setTimeout(res, 2000, { type: 'timeout' as const }),
|
|
),
|
|
])
|
|
|
|
if (ctx.resolveIfAborted(resolve)) return
|
|
|
|
if (
|
|
raceResult.type === 'result' &&
|
|
raceResult.result.matches &&
|
|
raceResult.result.confidence === 'high' &&
|
|
feature('BASH_CLASSIFIER')
|
|
) {
|
|
// Classifier approved within grace period — skip dialog
|
|
void consumeSpeculativeClassifierCheck(
|
|
(input as { command: string }).command,
|
|
)
|
|
|
|
const matchedRule =
|
|
raceResult.result.matchedDescription ?? undefined
|
|
if (matchedRule) {
|
|
setClassifierApproval(toolUseID, matchedRule)
|
|
}
|
|
|
|
ctx.logDecision({
|
|
decision: 'accept',
|
|
source: { type: 'classifier' },
|
|
})
|
|
resolve(
|
|
ctx.buildAllow(
|
|
result.updatedInput ??
|
|
(input as Record<string, unknown>),
|
|
{
|
|
decisionReason: {
|
|
type: 'classifier' as const,
|
|
classifier: 'bash_allow' as const,
|
|
reason: `Allowed by prompt rule: "${raceResult.result.matchedDescription}"`,
|
|
},
|
|
},
|
|
),
|
|
)
|
|
return
|
|
}
|
|
// Timeout or no match — fall through to show dialog
|
|
}
|
|
}
|
|
|
|
// Show dialog and start hooks/classifier in background
|
|
handleInteractivePermission(
|
|
{
|
|
ctx,
|
|
description,
|
|
result,
|
|
awaitAutomatedChecksBeforeDialog:
|
|
appState.toolPermissionContext
|
|
.awaitAutomatedChecksBeforeDialog,
|
|
bridgeCallbacks: feature('BRIDGE_MODE')
|
|
? appState.replBridgePermissionCallbacks
|
|
: undefined,
|
|
channelCallbacks:
|
|
feature('KAIROS') || feature('KAIROS_CHANNELS')
|
|
? appState.channelPermissionCallbacks
|
|
: undefined,
|
|
},
|
|
resolve,
|
|
)
|
|
|
|
return
|
|
}
|
|
}
|
|
})
|
|
.catch(error => {
|
|
if (
|
|
error instanceof AbortError ||
|
|
error instanceof APIUserAbortError
|
|
) {
|
|
logForDebugging(
|
|
`Permission check threw ${error.constructor.name} for tool=${tool.name}: ${error.message}`,
|
|
)
|
|
ctx.logCancelled()
|
|
resolve(ctx.cancelAndAbort(undefined, true))
|
|
} else {
|
|
logError(error)
|
|
resolve(ctx.cancelAndAbort(undefined, true))
|
|
}
|
|
})
|
|
.finally(() => {
|
|
clearClassifierChecking(toolUseID)
|
|
})
|
|
})
|
|
},
|
|
[setToolUseConfirmQueue, setToolPermissionContext],
|
|
)
|
|
}
|
|
|
|
export default useCanUseTool
|