Skip to content

Commit c713916

Browse files
authored
Move isToolAllowedForMode out of shared directory (#10089)
1 parent 3254109 commit c713916

File tree

10 files changed

+174
-168
lines changed

10 files changed

+174
-168
lines changed

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@ import { Anthropic } from "@anthropic-ai/sdk"
55
import type { ToolName, ClineAsk, ToolProgressStatus } from "@roo-code/types"
66
import { TelemetryService } from "@roo-code/telemetry"
77

8+
import { t } from "../../i18n"
9+
810
import { defaultModeSlug, getModeBySlug } from "../../shared/modes"
911
import type { ToolParamName, ToolResponse, ToolUse, McpToolUse } from "../../shared/tools"
10-
import { Package } from "../../shared/package"
11-
import { t } from "../../i18n"
12+
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
13+
1214
import { AskIgnoredError } from "../task/AskIgnoredError"
15+
import { Task } from "../task/Task"
1316

1417
import { fetchInstructionsTool } from "../tools/FetchInstructionsTool"
1518
import { listFilesTool } from "../tools/ListFilesTool"
@@ -30,17 +33,14 @@ import { askFollowupQuestionTool } from "../tools/AskFollowupQuestionTool"
3033
import { switchModeTool } from "../tools/SwitchModeTool"
3134
import { attemptCompletionTool, AttemptCompletionCallbacks } from "../tools/AttemptCompletionTool"
3235
import { newTaskTool } from "../tools/NewTaskTool"
33-
3436
import { updateTodoListTool } from "../tools/UpdateTodoListTool"
3537
import { runSlashCommandTool } from "../tools/RunSlashCommandTool"
3638
import { generateImageTool } from "../tools/GenerateImageTool"
37-
38-
import { formatResponse } from "../prompts/responses"
39+
import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
3940
import { validateToolUse } from "../tools/validateToolUse"
40-
import { Task } from "../task/Task"
4141
import { codebaseSearchTool } from "../tools/CodebaseSearchTool"
42-
import { experiments, EXPERIMENT_IDS } from "../../shared/experiments"
43-
import { applyDiffTool as applyDiffToolClass } from "../tools/ApplyDiffTool"
42+
43+
import { formatResponse } from "../prompts/responses"
4444

4545
/**
4646
* Processes and presents assistant message content to the user interface.
@@ -353,7 +353,7 @@ export async function presentAssistantMessage(cline: Task) {
353353
case "tool_use": {
354354
// Fetch state early so it's available for toolDescription and validation
355355
const state = await cline.providerRef.deref()?.getState()
356-
const { mode, customModes, experiments: stateExperiments, apiConfiguration } = state ?? {}
356+
const { mode, customModes, experiments: stateExperiments } = state ?? {}
357357

358358
const toolDescription = (): string => {
359359
switch (block.name) {
@@ -731,6 +731,7 @@ export async function presentAssistantMessage(cline: Task) {
731731
// This prevents the stream from being interrupted with "Response interrupted by tool use result"
732732
// which would cause the extension to appear to hang
733733
const errorContent = formatResponse.toolError(error.message, toolProtocol)
734+
734735
if (toolProtocol === TOOL_PROTOCOL.NATIVE && toolCallId) {
735736
// For native protocol, push tool_result directly without setting didAlreadyUseTool
736737
cline.userMessageContent.push({
@@ -743,6 +744,7 @@ export async function presentAssistantMessage(cline: Task) {
743744
// For XML protocol, use the standard pushToolResult
744745
pushToolResult(errorContent)
745746
}
747+
746748
break
747749
}
748750
}

src/core/environment/__tests__/getEnvironmentDetails.spec.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import type { Mock } from "vitest"
66

77
import { getEnvironmentDetails } from "../getEnvironmentDetails"
88
import { EXPERIMENT_IDS, experiments } from "../../../shared/experiments"
9-
import { defaultModeSlug, getFullModeDetails, getModeBySlug, isToolAllowedForMode } from "../../../shared/modes"
9+
import { getFullModeDetails } from "../../../shared/modes"
10+
import { isToolAllowedForMode } from "../../tools/validateToolUse"
1011
import { getApiMetrics } from "../../../shared/getApiMetrics"
1112
import { listFiles } from "../../../services/glob/list-files"
1213
import { TerminalRegistry } from "../../../integrations/terminal/TerminalRegistry"
@@ -51,6 +52,7 @@ vi.mock("../../../integrations/terminal/Terminal")
5152
vi.mock("../../../utils/path")
5253
vi.mock("../../../utils/git")
5354
vi.mock("../../prompts/responses")
55+
vi.mock("../../tools/validateToolUse")
5456

5557
describe("getEnvironmentDetails", () => {
5658
const mockCwd = "/test/path"

src/core/environment/getEnvironmentDetails.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { DEFAULT_TERMINAL_OUTPUT_CHARACTER_LIMIT } from "@roo-code/types"
1111
import { resolveToolProtocol } from "../../utils/resolveToolProtocol"
1212
import { EXPERIMENT_IDS, experiments as Experiments } from "../../shared/experiments"
1313
import { formatLanguage } from "../../shared/language"
14-
import { defaultModeSlug, getFullModeDetails, getModeBySlug, isToolAllowedForMode } from "../../shared/modes"
14+
import { defaultModeSlug, getFullModeDetails } from "../../shared/modes"
1515
import { getApiMetrics } from "../../shared/getApiMetrics"
1616
import { listFiles } from "../../services/glob/list-files"
1717
import { TerminalRegistry } from "../../integrations/terminal/TerminalRegistry"

src/core/prompts/tools/filter-tools-for-mode.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import type OpenAI from "openai"
22
import type { ModeConfig, ToolName, ToolGroup, ModelInfo } from "@roo-code/types"
3-
import { getModeBySlug, getToolsForMode, isToolAllowedForMode } from "../../../shared/modes"
3+
import { getModeBySlug, getToolsForMode } from "../../../shared/modes"
44
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, TOOL_ALIASES } from "../../../shared/tools"
55
import { defaultModeSlug } from "../../../shared/modes"
66
import type { CodeIndexManager } from "../../../services/code-index/manager"
77
import type { McpHub } from "../../../services/mcp/McpHub"
8+
import { isToolAllowedForMode } from "../../../core/tools/validateToolUse"
89

910
/**
1011
* Reverse lookup map - maps alias name to canonical tool name.

src/core/prompts/tools/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
import type { ToolName, ModeConfig } from "@roo-code/types"
2+
import { shouldUseSingleFileRead } from "@roo-code/types"
23

34
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, DiffStrategy } from "../../../shared/tools"
5+
import { Mode, getModeConfig, getGroupName } from "../../../shared/modes"
6+
7+
import { isToolAllowedForMode } from "../../tools/validateToolUse"
8+
49
import { McpHub } from "../../../services/mcp/McpHub"
5-
import { Mode, getModeConfig, isToolAllowedForMode, getGroupName } from "../../../shared/modes"
10+
import { CodeIndexManager } from "../../../services/code-index/manager"
611

712
import { ToolArgs } from "./types"
813
import { getExecuteCommandDescription } from "./execute-command"
914
import { getReadFileDescription } from "./read-file"
1015
import { getSimpleReadFileDescription } from "./simple-read-file"
1116
import { getFetchInstructionsDescription } from "./fetch-instructions"
12-
import { shouldUseSingleFileRead } from "@roo-code/types"
1317
import { getWriteToFileDescription } from "./write-to-file"
1418
import { getSearchFilesDescription } from "./search-files"
1519
import { getListFilesDescription } from "./list-files"
@@ -24,7 +28,6 @@ import { getCodebaseSearchDescription } from "./codebase-search"
2428
import { getUpdateTodoListDescription } from "./update-todo-list"
2529
import { getRunSlashCommandDescription } from "./run-slash-command"
2630
import { getGenerateImageDescription } from "./generate-image"
27-
import { CodeIndexManager } from "../../../services/code-index/manager"
2831

2932
// Map of tool names to their description functions
3033
const toolDescriptionMap: Record<string, (args: ToolArgs) => string | undefined> = {

src/core/task/__tests__/native-tools-filtering.spec.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { describe, it, expect, beforeEach, vi } from "vitest"
21
import type { ModeConfig } from "@roo-code/types"
32

43
describe("Native Tools Filtering by Mode", () => {
@@ -23,7 +22,7 @@ describe("Native Tools Filtering by Mode", () => {
2322
}
2423

2524
// Import the functions we need to test
26-
const { isToolAllowedForMode } = await import("../../../shared/modes")
25+
const { isToolAllowedForMode } = await import("../../tools/validateToolUse")
2726
const { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } = await import("../../../shared/tools")
2827

2928
// Test architect mode - should NOT have edit tools
@@ -95,7 +94,7 @@ describe("Native Tools Filtering by Mode", () => {
9594
groups: ["read"] as const,
9695
}
9796

98-
const { isToolAllowedForMode } = await import("../../../shared/modes")
97+
const { isToolAllowedForMode } = await import("../../tools/validateToolUse")
9998

10099
// Mode with MCP group should allow use_mcp_tool
101100
expect(isToolAllowedForMode("use_mcp_tool", "test-mode-with-mcp", [modeWithMcp])).toBe(true)
@@ -112,7 +111,7 @@ describe("Native Tools Filtering by Mode", () => {
112111
groups: [] as const, // No groups at all
113112
}
114113

115-
const { isToolAllowedForMode } = await import("../../../shared/modes")
114+
const { isToolAllowedForMode } = await import("../../tools/validateToolUse")
116115
const { ALWAYS_AVAILABLE_TOOLS } = await import("../../../shared/tools")
117116

118117
// Always-available tools should work even with no groups

src/core/tools/__tests__/validateToolUse.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import type { ModeConfig } from "@roo-code/types"
44

5-
import { isToolAllowedForMode, modes } from "../../../shared/modes"
5+
import { modes } from "../../../shared/modes"
66
import { TOOL_GROUPS } from "../../../shared/tools"
77

8-
import { validateToolUse } from "../validateToolUse"
8+
import { validateToolUse, isToolAllowedForMode } from "../validateToolUse"
99

1010
const codeMode = modes.find((m) => m.slug === "code")?.slug || "code"
1111
const architectMode = modes.find((m) => m.slug === "architect")?.slug || "architect"

src/core/tools/validateToolUse.ts

Lines changed: 144 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import type { ToolName, ModeConfig } from "@roo-code/types"
1+
import type { ToolName, ModeConfig, ExperimentId, GroupOptions, GroupEntry } from "@roo-code/types"
22
import { toolNames as validToolNames } from "@roo-code/types"
33

4-
import { Mode, isToolAllowedForMode } from "../../shared/modes"
4+
import { type Mode, FileRestrictionError, getModeBySlug, getGroupName } from "../../shared/modes"
5+
import { EXPERIMENT_IDS } from "../../shared/experiments"
6+
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "../../shared/tools"
57

68
/**
79
* Checks if a tool name is a valid, known tool.
@@ -14,7 +16,7 @@ export function isValidToolName(toolName: string): toolName is ToolName {
1416
return true
1517
}
1618

17-
// Check if it's a dynamic MCP tool (mcp_serverName_toolName format)
19+
// Check if it's a dynamic MCP tool (mcp_serverName_toolName format).
1820
if (toolName.startsWith("mcp_")) {
1921
return true
2022
}
@@ -54,3 +56,142 @@ export function validateToolUse(
5456
throw new Error(`Tool "${toolName}" is not allowed in ${mode} mode.`)
5557
}
5658
}
59+
60+
const EDIT_OPERATION_PARAMS = ["diff", "content", "operations", "search", "replace", "args", "line"] as const
61+
62+
function getGroupOptions(group: GroupEntry): GroupOptions | undefined {
63+
return Array.isArray(group) ? group[1] : undefined
64+
}
65+
66+
function doesFileMatchRegex(filePath: string, pattern: string): boolean {
67+
try {
68+
const regex = new RegExp(pattern)
69+
return regex.test(filePath)
70+
} catch (error) {
71+
console.error(`Invalid regex pattern: ${pattern}`, error)
72+
return false
73+
}
74+
}
75+
76+
export function isToolAllowedForMode(
77+
tool: string,
78+
modeSlug: string,
79+
customModes: ModeConfig[],
80+
toolRequirements?: Record<string, boolean>,
81+
toolParams?: Record<string, any>, // All tool parameters
82+
experiments?: Record<string, boolean>,
83+
includedTools?: string[], // Opt-in tools explicitly included (e.g., from modelInfo)
84+
): boolean {
85+
// Always allow these tools
86+
if (ALWAYS_AVAILABLE_TOOLS.includes(tool as any)) {
87+
return true
88+
}
89+
90+
// Check if this is a dynamic MCP tool (mcp_serverName_toolName)
91+
// These should be allowed if the mcp group is allowed for the mode
92+
const isDynamicMcpTool = tool.startsWith("mcp_")
93+
94+
if (experiments && Object.values(EXPERIMENT_IDS).includes(tool as ExperimentId)) {
95+
if (!experiments[tool]) {
96+
return false
97+
}
98+
}
99+
100+
// Check tool requirements if any exist
101+
if (toolRequirements && typeof toolRequirements === "object") {
102+
if (tool in toolRequirements && !toolRequirements[tool]) {
103+
return false
104+
}
105+
} else if (toolRequirements === false) {
106+
// If toolRequirements is a boolean false, all tools are disabled
107+
return false
108+
}
109+
110+
const mode = getModeBySlug(modeSlug, customModes)
111+
112+
if (!mode) {
113+
return false
114+
}
115+
116+
// Check if tool is in any of the mode's groups and respects any group options
117+
for (const group of mode.groups) {
118+
const groupName = getGroupName(group)
119+
const options = getGroupOptions(group)
120+
121+
const groupConfig = TOOL_GROUPS[groupName]
122+
123+
// Check if this is a dynamic MCP tool and the mcp group is allowed
124+
if (isDynamicMcpTool && groupName === "mcp") {
125+
// Dynamic MCP tools are allowed if the mcp group is in the mode's groups
126+
return true
127+
}
128+
129+
// Check if the tool is in the group's regular tools
130+
const isRegularTool = groupConfig.tools.includes(tool)
131+
132+
// Check if the tool is a custom tool that has been explicitly included
133+
const isCustomTool = groupConfig.customTools?.includes(tool) && includedTools?.includes(tool)
134+
135+
// If the tool isn't in regular tools and isn't an included custom tool, continue to next group
136+
if (!isRegularTool && !isCustomTool) {
137+
continue
138+
}
139+
140+
// If there are no options, allow the tool
141+
if (!options) {
142+
return true
143+
}
144+
145+
// For the edit group, check file regex if specified
146+
if (groupName === "edit" && options.fileRegex) {
147+
const filePath = toolParams?.path
148+
// Check if this is an actual edit operation (not just path-only for streaming)
149+
const isEditOperation = EDIT_OPERATION_PARAMS.some((param) => toolParams?.[param])
150+
151+
// Handle single file path validation
152+
if (filePath && isEditOperation && !doesFileMatchRegex(filePath, options.fileRegex)) {
153+
throw new FileRestrictionError(mode.name, options.fileRegex, options.description, filePath, tool)
154+
}
155+
156+
// Handle XML args parameter (used by MULTI_FILE_APPLY_DIFF experiment)
157+
if (toolParams?.args && typeof toolParams.args === "string") {
158+
// Extract file paths from XML args with improved validation
159+
try {
160+
const filePathMatches = toolParams.args.match(/<path>([^<]+)<\/path>/g)
161+
if (filePathMatches) {
162+
for (const match of filePathMatches) {
163+
// More robust path extraction with validation
164+
const pathMatch = match.match(/<path>([^<]+)<\/path>/)
165+
if (pathMatch && pathMatch[1]) {
166+
const extractedPath = pathMatch[1].trim()
167+
// Validate that the path is not empty and doesn't contain invalid characters
168+
if (extractedPath && !extractedPath.includes("<") && !extractedPath.includes(">")) {
169+
if (!doesFileMatchRegex(extractedPath, options.fileRegex)) {
170+
throw new FileRestrictionError(
171+
mode.name,
172+
options.fileRegex,
173+
options.description,
174+
extractedPath,
175+
tool,
176+
)
177+
}
178+
}
179+
}
180+
}
181+
}
182+
} catch (error) {
183+
// Re-throw FileRestrictionError as it's an expected validation error
184+
if (error instanceof FileRestrictionError) {
185+
throw error
186+
}
187+
// If XML parsing fails, log the error but don't block the operation
188+
console.warn(`Failed to parse XML args for file restriction validation: ${error}`)
189+
}
190+
}
191+
}
192+
193+
return true
194+
}
195+
196+
return false
197+
}

src/shared/__tests__/modes.spec.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ vi.mock("../../core/prompts/sections/custom-instructions", () => ({
99
addCustomInstructions: vi.fn().mockResolvedValue("Combined instructions"),
1010
}))
1111

12-
import { isToolAllowedForMode, FileRestrictionError, getFullModeDetails, modes, getModeSelection } from "../modes"
12+
import { FileRestrictionError, getFullModeDetails, modes, getModeSelection } from "../modes"
13+
import { isToolAllowedForMode } from "../../core/tools/validateToolUse"
1314
import { addCustomInstructions } from "../../core/prompts/sections/custom-instructions"
1415

1516
describe("isToolAllowedForMode", () => {

0 commit comments

Comments
 (0)