tools.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389
  1. // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
  2. import { toolMatchesName, type Tool, type Tools } from './Tool.js'
  3. import { AgentTool } from './tools/AgentTool/AgentTool.js'
  4. import { SkillTool } from './tools/SkillTool/SkillTool.js'
  5. import { BashTool } from './tools/BashTool/BashTool.js'
  6. import { FileEditTool } from './tools/FileEditTool/FileEditTool.js'
  7. import { FileReadTool } from './tools/FileReadTool/FileReadTool.js'
  8. import { FileWriteTool } from './tools/FileWriteTool/FileWriteTool.js'
  9. import { GlobTool } from './tools/GlobTool/GlobTool.js'
  10. import { NotebookEditTool } from './tools/NotebookEditTool/NotebookEditTool.js'
  11. import { WebFetchTool } from './tools/WebFetchTool/WebFetchTool.js'
  12. import { TaskStopTool } from './tools/TaskStopTool/TaskStopTool.js'
  13. import { BriefTool } from './tools/BriefTool/BriefTool.js'
  14. // Dead code elimination: conditional import for ant-only tools
  15. /* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
  16. const REPLTool =
  17. process.env.USER_TYPE === 'ant'
  18. ? require('./tools/REPLTool/REPLTool.js').REPLTool
  19. : null
  20. const SuggestBackgroundPRTool =
  21. process.env.USER_TYPE === 'ant'
  22. ? require('./tools/SuggestBackgroundPRTool/SuggestBackgroundPRTool.js')
  23. .SuggestBackgroundPRTool
  24. : null
  25. const SleepTool =
  26. feature('PROACTIVE') || feature('KAIROS')
  27. ? require('./tools/SleepTool/SleepTool.js').SleepTool
  28. : null
  29. const cronTools = feature('AGENT_TRIGGERS')
  30. ? [
  31. require('./tools/ScheduleCronTool/CronCreateTool.js').CronCreateTool,
  32. require('./tools/ScheduleCronTool/CronDeleteTool.js').CronDeleteTool,
  33. require('./tools/ScheduleCronTool/CronListTool.js').CronListTool,
  34. ]
  35. : []
  36. const RemoteTriggerTool = feature('AGENT_TRIGGERS_REMOTE')
  37. ? require('./tools/RemoteTriggerTool/RemoteTriggerTool.js').RemoteTriggerTool
  38. : null
  39. const MonitorTool = feature('MONITOR_TOOL')
  40. ? require('./tools/MonitorTool/MonitorTool.js').MonitorTool
  41. : null
  42. const SendUserFileTool = feature('KAIROS')
  43. ? require('./tools/SendUserFileTool/SendUserFileTool.js').SendUserFileTool
  44. : null
  45. const PushNotificationTool =
  46. feature('KAIROS') || feature('KAIROS_PUSH_NOTIFICATION')
  47. ? require('./tools/PushNotificationTool/PushNotificationTool.js')
  48. .PushNotificationTool
  49. : null
  50. const SubscribePRTool = feature('KAIROS_GITHUB_WEBHOOKS')
  51. ? require('./tools/SubscribePRTool/SubscribePRTool.js').SubscribePRTool
  52. : null
  53. /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
  54. import { TaskOutputTool } from './tools/TaskOutputTool/TaskOutputTool.js'
  55. import { WebSearchTool } from './tools/WebSearchTool/WebSearchTool.js'
  56. import { TodoWriteTool } from './tools/TodoWriteTool/TodoWriteTool.js'
  57. import { ExitPlanModeV2Tool } from './tools/ExitPlanModeTool/ExitPlanModeV2Tool.js'
  58. import { TestingPermissionTool } from './tools/testing/TestingPermissionTool.js'
  59. import { GrepTool } from './tools/GrepTool/GrepTool.js'
  60. import { TungstenTool } from './tools/TungstenTool/TungstenTool.js'
  61. // Lazy require to break circular dependency: tools.ts -> TeamCreateTool/TeamDeleteTool -> ... -> tools.ts
  62. /* eslint-disable @typescript-eslint/no-require-imports */
  63. const getTeamCreateTool = () =>
  64. require('./tools/TeamCreateTool/TeamCreateTool.js')
  65. .TeamCreateTool as typeof import('./tools/TeamCreateTool/TeamCreateTool.js').TeamCreateTool
  66. const getTeamDeleteTool = () =>
  67. require('./tools/TeamDeleteTool/TeamDeleteTool.js')
  68. .TeamDeleteTool as typeof import('./tools/TeamDeleteTool/TeamDeleteTool.js').TeamDeleteTool
  69. const getSendMessageTool = () =>
  70. require('./tools/SendMessageTool/SendMessageTool.js')
  71. .SendMessageTool as typeof import('./tools/SendMessageTool/SendMessageTool.js').SendMessageTool
  72. /* eslint-enable @typescript-eslint/no-require-imports */
  73. import { AskUserQuestionTool } from './tools/AskUserQuestionTool/AskUserQuestionTool.js'
  74. import { LSPTool } from './tools/LSPTool/LSPTool.js'
  75. import { ListMcpResourcesTool } from './tools/ListMcpResourcesTool/ListMcpResourcesTool.js'
  76. import { ReadMcpResourceTool } from './tools/ReadMcpResourceTool/ReadMcpResourceTool.js'
  77. import { ToolSearchTool } from './tools/ToolSearchTool/ToolSearchTool.js'
  78. import { EnterPlanModeTool } from './tools/EnterPlanModeTool/EnterPlanModeTool.js'
  79. import { EnterWorktreeTool } from './tools/EnterWorktreeTool/EnterWorktreeTool.js'
  80. import { ExitWorktreeTool } from './tools/ExitWorktreeTool/ExitWorktreeTool.js'
  81. import { ConfigTool } from './tools/ConfigTool/ConfigTool.js'
  82. import { TaskCreateTool } from './tools/TaskCreateTool/TaskCreateTool.js'
  83. import { TaskGetTool } from './tools/TaskGetTool/TaskGetTool.js'
  84. import { TaskUpdateTool } from './tools/TaskUpdateTool/TaskUpdateTool.js'
  85. import { TaskListTool } from './tools/TaskListTool/TaskListTool.js'
  86. import uniqBy from 'lodash-es/uniqBy.js'
  87. import { isToolSearchEnabledOptimistic } from './utils/toolSearch.js'
  88. import { isTodoV2Enabled } from './utils/tasks.js'
  89. // Dead code elimination: conditional import for CLAUDE_CODE_VERIFY_PLAN
  90. /* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
  91. const VerifyPlanExecutionTool =
  92. process.env.CLAUDE_CODE_VERIFY_PLAN === 'true'
  93. ? require('./tools/VerifyPlanExecutionTool/VerifyPlanExecutionTool.js')
  94. .VerifyPlanExecutionTool
  95. : null
  96. /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
  97. import { SYNTHETIC_OUTPUT_TOOL_NAME } from './tools/SyntheticOutputTool/SyntheticOutputTool.js'
  98. export {
  99. ALL_AGENT_DISALLOWED_TOOLS,
  100. CUSTOM_AGENT_DISALLOWED_TOOLS,
  101. ASYNC_AGENT_ALLOWED_TOOLS,
  102. COORDINATOR_MODE_ALLOWED_TOOLS,
  103. } from './constants/tools.js'
  104. import { feature } from 'bun:bundle'
  105. // Dead code elimination: conditional import for OVERFLOW_TEST_TOOL
  106. /* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
  107. const OverflowTestTool = feature('OVERFLOW_TEST_TOOL')
  108. ? require('./tools/OverflowTestTool/OverflowTestTool.js').OverflowTestTool
  109. : null
  110. const CtxInspectTool = feature('CONTEXT_COLLAPSE')
  111. ? require('./tools/CtxInspectTool/CtxInspectTool.js').CtxInspectTool
  112. : null
  113. const TerminalCaptureTool = feature('TERMINAL_PANEL')
  114. ? require('./tools/TerminalCaptureTool/TerminalCaptureTool.js')
  115. .TerminalCaptureTool
  116. : null
  117. const WebBrowserTool = feature('WEB_BROWSER_TOOL')
  118. ? require('./tools/WebBrowserTool/WebBrowserTool.js').WebBrowserTool
  119. : null
  120. const coordinatorModeModule = feature('COORDINATOR_MODE')
  121. ? (require('./coordinator/coordinatorMode.js') as typeof import('./coordinator/coordinatorMode.js'))
  122. : null
  123. const SnipTool = feature('HISTORY_SNIP')
  124. ? require('./tools/SnipTool/SnipTool.js').SnipTool
  125. : null
  126. const ListPeersTool = feature('UDS_INBOX')
  127. ? require('./tools/ListPeersTool/ListPeersTool.js').ListPeersTool
  128. : null
  129. const WorkflowTool = feature('WORKFLOW_SCRIPTS')
  130. ? (() => {
  131. require('./tools/WorkflowTool/bundled/index.js').initBundledWorkflows()
  132. return require('./tools/WorkflowTool/WorkflowTool.js').WorkflowTool
  133. })()
  134. : null
  135. /* eslint-enable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
  136. import type { ToolPermissionContext } from './Tool.js'
  137. import { getDenyRuleForTool } from './utils/permissions/permissions.js'
  138. import { hasEmbeddedSearchTools } from './utils/embeddedTools.js'
  139. import { isEnvTruthy } from './utils/envUtils.js'
  140. import { isPowerShellToolEnabled } from './utils/shell/shellToolUtils.js'
  141. import { isAgentSwarmsEnabled } from './utils/agentSwarmsEnabled.js'
  142. import { isWorktreeModeEnabled } from './utils/worktreeModeEnabled.js'
  143. import {
  144. REPL_TOOL_NAME,
  145. REPL_ONLY_TOOLS,
  146. isReplModeEnabled,
  147. } from './tools/REPLTool/constants.js'
  148. export { REPL_ONLY_TOOLS }
  149. /* eslint-disable @typescript-eslint/no-require-imports */
  150. const getPowerShellTool = () => {
  151. if (!isPowerShellToolEnabled()) return null
  152. return (
  153. require('./tools/PowerShellTool/PowerShellTool.js') as typeof import('./tools/PowerShellTool/PowerShellTool.js')
  154. ).PowerShellTool
  155. }
  156. /* eslint-enable @typescript-eslint/no-require-imports */
  157. /**
  158. * Predefined tool presets that can be used with --tools flag
  159. */
  160. export const TOOL_PRESETS = ['default'] as const
  161. export type ToolPreset = (typeof TOOL_PRESETS)[number]
  162. export function parseToolPreset(preset: string): ToolPreset | null {
  163. const presetString = preset.toLowerCase()
  164. if (!TOOL_PRESETS.includes(presetString as ToolPreset)) {
  165. return null
  166. }
  167. return presetString as ToolPreset
  168. }
  169. /**
  170. * Get the list of tool names for a given preset
  171. * Filters out tools that are disabled via isEnabled() check
  172. * @param preset The preset name
  173. * @returns Array of tool names
  174. */
  175. export function getToolsForDefaultPreset(): string[] {
  176. const tools = getAllBaseTools()
  177. const isEnabled = tools.map(tool => tool.isEnabled())
  178. return tools.filter((_, i) => isEnabled[i]).map(tool => tool.name)
  179. }
  180. /**
  181. * Get the complete exhaustive list of all tools that could be available
  182. * in the current environment (respecting process.env flags).
  183. * This is the source of truth for ALL tools.
  184. */
  185. /**
  186. * NOTE: This MUST stay in sync with https://console.statsig.com/4aF3Ewatb6xPVpCwxb5nA3/dynamic_configs/claude_code_global_system_caching, in order to cache the system prompt across users.
  187. */
  188. export function getAllBaseTools(): Tools {
  189. return [
  190. AgentTool,
  191. TaskOutputTool,
  192. BashTool,
  193. // Ant-native builds have bfs/ugrep embedded in the bun binary (same ARGV0
  194. // trick as ripgrep). When available, find/grep in Claude's shell are aliased
  195. // to these fast tools, so the dedicated Glob/Grep tools are unnecessary.
  196. ...(hasEmbeddedSearchTools() ? [] : [GlobTool, GrepTool]),
  197. ExitPlanModeV2Tool,
  198. FileReadTool,
  199. FileEditTool,
  200. FileWriteTool,
  201. NotebookEditTool,
  202. WebFetchTool,
  203. TodoWriteTool,
  204. WebSearchTool,
  205. TaskStopTool,
  206. AskUserQuestionTool,
  207. SkillTool,
  208. EnterPlanModeTool,
  209. ...(process.env.USER_TYPE === 'ant' ? [ConfigTool] : []),
  210. ...(process.env.USER_TYPE === 'ant' ? [TungstenTool] : []),
  211. ...(SuggestBackgroundPRTool ? [SuggestBackgroundPRTool] : []),
  212. ...(WebBrowserTool ? [WebBrowserTool] : []),
  213. ...(isTodoV2Enabled()
  214. ? [TaskCreateTool, TaskGetTool, TaskUpdateTool, TaskListTool]
  215. : []),
  216. ...(OverflowTestTool ? [OverflowTestTool] : []),
  217. ...(CtxInspectTool ? [CtxInspectTool] : []),
  218. ...(TerminalCaptureTool ? [TerminalCaptureTool] : []),
  219. ...(isEnvTruthy(process.env.ENABLE_LSP_TOOL) ? [LSPTool] : []),
  220. ...(isWorktreeModeEnabled() ? [EnterWorktreeTool, ExitWorktreeTool] : []),
  221. getSendMessageTool(),
  222. ...(ListPeersTool ? [ListPeersTool] : []),
  223. ...(isAgentSwarmsEnabled()
  224. ? [getTeamCreateTool(), getTeamDeleteTool()]
  225. : []),
  226. ...(VerifyPlanExecutionTool ? [VerifyPlanExecutionTool] : []),
  227. ...(process.env.USER_TYPE === 'ant' && REPLTool ? [REPLTool] : []),
  228. ...(WorkflowTool ? [WorkflowTool] : []),
  229. ...(SleepTool ? [SleepTool] : []),
  230. ...cronTools,
  231. ...(RemoteTriggerTool ? [RemoteTriggerTool] : []),
  232. ...(MonitorTool ? [MonitorTool] : []),
  233. BriefTool,
  234. ...(SendUserFileTool ? [SendUserFileTool] : []),
  235. ...(PushNotificationTool ? [PushNotificationTool] : []),
  236. ...(SubscribePRTool ? [SubscribePRTool] : []),
  237. ...(getPowerShellTool() ? [getPowerShellTool()] : []),
  238. ...(SnipTool ? [SnipTool] : []),
  239. ...(process.env.NODE_ENV === 'test' ? [TestingPermissionTool] : []),
  240. ListMcpResourcesTool,
  241. ReadMcpResourceTool,
  242. // Include ToolSearchTool when tool search might be enabled (optimistic check)
  243. // The actual decision to defer tools happens at request time in claude.ts
  244. ...(isToolSearchEnabledOptimistic() ? [ToolSearchTool] : []),
  245. ]
  246. }
  247. /**
  248. * Filters out tools that are blanket-denied by the permission context.
  249. * A tool is filtered out if there's a deny rule matching its name with no
  250. * ruleContent (i.e., a blanket deny for that tool).
  251. *
  252. * Uses the same matcher as the runtime permission check (step 1a), so MCP
  253. * server-prefix rules like `mcp__server` strip all tools from that server
  254. * before the model sees them — not just at call time.
  255. */
  256. export function filterToolsByDenyRules<
  257. T extends {
  258. name: string
  259. mcpInfo?: { serverName: string; toolName: string }
  260. },
  261. >(tools: readonly T[], permissionContext: ToolPermissionContext): T[] {
  262. return tools.filter(tool => !getDenyRuleForTool(permissionContext, tool))
  263. }
  264. export const getTools = (permissionContext: ToolPermissionContext): Tools => {
  265. // Simple mode: only Bash, Read, and Edit tools
  266. if (isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE)) {
  267. // --bare + REPL mode: REPL wraps Bash/Read/Edit/etc inside the VM, so
  268. // return REPL instead of the raw primitives. Matches the non-bare path
  269. // below which also hides REPL_ONLY_TOOLS when REPL is enabled.
  270. if (isReplModeEnabled() && REPLTool) {
  271. const replSimple: Tool[] = [REPLTool]
  272. if (
  273. feature('COORDINATOR_MODE') &&
  274. coordinatorModeModule?.isCoordinatorMode()
  275. ) {
  276. replSimple.push(TaskStopTool, getSendMessageTool())
  277. }
  278. return filterToolsByDenyRules(replSimple, permissionContext)
  279. }
  280. const simpleTools: Tool[] = [BashTool, FileReadTool, FileEditTool]
  281. // When coordinator mode is also active, include AgentTool and TaskStopTool
  282. // so the coordinator gets Task+TaskStop (via useMergedTools filtering) and
  283. // workers get Bash/Read/Edit (via filterToolsForAgent filtering).
  284. if (
  285. feature('COORDINATOR_MODE') &&
  286. coordinatorModeModule?.isCoordinatorMode()
  287. ) {
  288. simpleTools.push(AgentTool, TaskStopTool, getSendMessageTool())
  289. }
  290. return filterToolsByDenyRules(simpleTools, permissionContext)
  291. }
  292. // Get all base tools and filter out special tools that get added conditionally
  293. const specialTools = new Set([
  294. ListMcpResourcesTool.name,
  295. ReadMcpResourceTool.name,
  296. SYNTHETIC_OUTPUT_TOOL_NAME,
  297. ])
  298. const tools = getAllBaseTools().filter(tool => !specialTools.has(tool.name))
  299. // Filter out tools that are denied by the deny rules
  300. let allowedTools = filterToolsByDenyRules(tools, permissionContext)
  301. // When REPL mode is enabled, hide primitive tools from direct use.
  302. // They're still accessible inside REPL via the VM context.
  303. if (isReplModeEnabled()) {
  304. const replEnabled = allowedTools.some(tool =>
  305. toolMatchesName(tool, REPL_TOOL_NAME),
  306. )
  307. if (replEnabled) {
  308. allowedTools = allowedTools.filter(
  309. tool => !REPL_ONLY_TOOLS.has(tool.name),
  310. )
  311. }
  312. }
  313. const isEnabled = allowedTools.map(_ => _.isEnabled())
  314. return allowedTools.filter((_, i) => isEnabled[i])
  315. }
  316. /**
  317. * Assemble the full tool pool for a given permission context and MCP tools.
  318. *
  319. * This is the single source of truth for combining built-in tools with MCP tools.
  320. * Both REPL.tsx (via useMergedTools hook) and runAgent.ts (for coordinator workers)
  321. * use this function to ensure consistent tool pool assembly.
  322. *
  323. * The function:
  324. * 1. Gets built-in tools via getTools() (respects mode filtering)
  325. * 2. Filters MCP tools by deny rules
  326. * 3. Deduplicates by tool name (built-in tools take precedence)
  327. *
  328. * @param permissionContext - Permission context for filtering built-in tools
  329. * @param mcpTools - MCP tools from appState.mcp.tools
  330. * @returns Combined, deduplicated array of built-in and MCP tools
  331. */
  332. export function assembleToolPool(
  333. permissionContext: ToolPermissionContext,
  334. mcpTools: Tools,
  335. ): Tools {
  336. const builtInTools = getTools(permissionContext)
  337. // Filter out MCP tools that are in the deny list
  338. const allowedMcpTools = filterToolsByDenyRules(mcpTools, permissionContext)
  339. // Sort each partition for prompt-cache stability, keeping built-ins as a
  340. // contiguous prefix. The server's claude_code_system_cache_policy places a
  341. // global cache breakpoint after the last prefix-matched built-in tool; a flat
  342. // sort would interleave MCP tools into built-ins and invalidate all downstream
  343. // cache keys whenever an MCP tool sorts between existing built-ins. uniqBy
  344. // preserves insertion order, so built-ins win on name conflict.
  345. // Avoid Array.toSorted (Node 20+) — we support Node 18. builtInTools is
  346. // readonly so copy-then-sort; allowedMcpTools is a fresh .filter() result.
  347. const byName = (a: Tool, b: Tool) => a.name.localeCompare(b.name)
  348. return uniqBy(
  349. [...builtInTools].sort(byName).concat(allowedMcpTools.sort(byName)),
  350. 'name',
  351. )
  352. }
  353. /**
  354. * Get all tools including both built-in tools and MCP tools.
  355. *
  356. * This is the preferred function when you need the complete tools list for:
  357. * - Tool search threshold calculations (isToolSearchEnabled)
  358. * - Token counting that includes MCP tools
  359. * - Any context where MCP tools should be considered
  360. *
  361. * Use getTools() only when you specifically need just built-in tools.
  362. *
  363. * @param permissionContext - Permission context for filtering built-in tools
  364. * @param mcpTools - MCP tools from appState.mcp.tools
  365. * @returns Combined array of built-in and MCP tools
  366. */
  367. export function getMergedTools(
  368. permissionContext: ToolPermissionContext,
  369. mcpTools: Tools,
  370. ): Tools {
  371. const builtInTools = getTools(permissionContext)
  372. return [...builtInTools, ...mcpTools]
  373. }