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