1- import type { ToolName , ModeConfig } from "@roo-code/types"
1+ import type { ToolName , ModeConfig , ExperimentId , GroupOptions , GroupEntry } from "@roo-code/types"
22import { 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 ( / < p a t h > ( [ ^ < ] + ) < \/ p a t h > / g)
161+ if ( filePathMatches ) {
162+ for ( const match of filePathMatches ) {
163+ // More robust path extraction with validation
164+ const pathMatch = match . match ( / < p a t h > ( [ ^ < ] + ) < \/ p a t h > / )
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+ }
0 commit comments