Skip to content

Commit ebc61f7

Browse files
author
Evyatar Mitrani
committed
temp
1 parent b77fe87 commit ebc61f7

File tree

2 files changed

+186
-28
lines changed

2 files changed

+186
-28
lines changed

src/shared/modes.ts

Lines changed: 85 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import type {
1212
} from "@roo-code/types"
1313

1414
import { addCustomInstructions } from "../core/prompts/sections/custom-instructions"
15-
import { matchesGlobPattern, matchesAnyPattern } from "./pattern-matching"
15+
import { matchesGlobPattern, matchesAnyPattern, findMostSpecificMatchingPattern } from "./pattern-matching"
1616

1717
import { EXPERIMENT_IDS } from "./experiments"
1818
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "./tools"
@@ -229,44 +229,101 @@ export function isServerAllowedForMode(
229229
return true
230230
}
231231

232+
/**
233+
* Resolve tool access using pattern specificity logic
234+
* Exact patterns (no wildcards) always win over wildcard patterns
235+
* Among wildcard patterns, more specific patterns win
236+
* @param serverName - Name of the server
237+
* @param toolName - Name of the tool
238+
* @param allowPatterns - Array of allow patterns
239+
* @param blockPatterns - Array of block patterns
240+
* @returns boolean indicating if tool should be allowed
241+
*/
242+
function resolveToolAccess(
243+
serverName: string,
244+
toolName: string,
245+
allowPatterns: { serverName: string; toolName: string }[],
246+
blockPatterns: { serverName: string; toolName: string }[],
247+
): boolean {
248+
// Collect all matching tool patterns from both allow and block lists
249+
const matchingAllowPatterns: string[] = []
250+
const matchingBlockPatterns: string[] = []
251+
252+
// Check allow patterns
253+
for (const pattern of allowPatterns) {
254+
const serverMatches = matchesGlobPattern(serverName, pattern.serverName)
255+
const toolMatches = matchesGlobPattern(toolName, pattern.toolName)
256+
if (serverMatches && toolMatches) {
257+
// Store the tool pattern (not combined) for specificity comparison
258+
matchingAllowPatterns.push(pattern.toolName)
259+
}
260+
}
261+
262+
// Check block patterns
263+
for (const pattern of blockPatterns) {
264+
const serverMatches = matchesGlobPattern(serverName, pattern.serverName)
265+
const toolMatches = matchesGlobPattern(toolName, pattern.toolName)
266+
if (serverMatches && toolMatches) {
267+
// Store the tool pattern (not combined) for specificity comparison
268+
matchingBlockPatterns.push(pattern.toolName)
269+
}
270+
}
271+
272+
// If no patterns match, allow by default
273+
if (matchingAllowPatterns.length === 0 && matchingBlockPatterns.length === 0) {
274+
return true
275+
}
276+
277+
// Find the most specific pattern from all matching patterns
278+
const allMatchingPatterns = [...matchingAllowPatterns, ...matchingBlockPatterns]
279+
const mostSpecificPattern = findMostSpecificMatchingPattern(toolName, allMatchingPatterns)
280+
281+
if (!mostSpecificPattern) {
282+
return true // Default to allow if no pattern found (shouldn't happen)
283+
}
284+
285+
// Determine if the most specific pattern is from allow or block list
286+
return matchingAllowPatterns.includes(mostSpecificPattern)
287+
}
288+
232289
export function isToolAllowedForModeAndServer(
233290
serverName: string,
234291
toolName: string,
235292
restrictions: McpRestrictions,
236293
): boolean {
237-
// If allowedTools is defined, tool must match at least one entry
238-
if (restrictions.allowedTools) {
239-
// Filter out empty entries before checking
240-
const validAllowedTools = restrictions.allowedTools.filter((t) => t.serverName?.trim() && t.toolName?.trim())
241-
if (validAllowedTools.length > 0) {
242-
const isAllowed = validAllowedTools.some((t) => {
243-
// Check if server name matches (with pattern support)
244-
const serverMatches = matchesAnyPattern(serverName, [t.serverName])
245-
// Check if tool name matches (with pattern support)
246-
const toolMatches = matchesAnyPattern(toolName, [t.toolName])
247-
return serverMatches && toolMatches
248-
})
249-
if (!isAllowed) return false
250-
}
294+
// Get valid allow and block patterns
295+
const validAllowedTools = (restrictions.allowedTools || []).filter(
296+
(t) => t.serverName?.trim() && t.toolName?.trim(),
297+
)
298+
const validDisallowedTools = (restrictions.disallowedTools || []).filter(
299+
(t) => t.serverName?.trim() && t.toolName?.trim(),
300+
)
301+
302+
// If there are no restrictions at all, allow the tool
303+
if (validAllowedTools.length === 0 && validDisallowedTools.length === 0) {
304+
return true
251305
}
252306

253-
// If disallowedTools is defined, tool must not match any entry
254-
if (restrictions.disallowedTools) {
255-
// Filter out empty entries before checking
256-
const validDisallowedTools = restrictions.disallowedTools.filter(
257-
(t) => t.serverName?.trim() && t.toolName?.trim(),
258-
)
259-
const isDisallowed = validDisallowedTools.some((t) => {
260-
// Check if server name matches (with pattern support)
261-
const serverMatches = matchesAnyPattern(serverName, [t.serverName])
262-
// Check if tool name matches (with pattern support)
263-
const toolMatches = matchesAnyPattern(toolName, [t.toolName])
307+
// If only allowedTools is defined, tool must match at least one entry
308+
if (validAllowedTools.length > 0 && validDisallowedTools.length === 0) {
309+
return validAllowedTools.some((t) => {
310+
const serverMatches = matchesGlobPattern(serverName, t.serverName)
311+
const toolMatches = matchesGlobPattern(toolName, t.toolName)
264312
return serverMatches && toolMatches
265313
})
266-
if (isDisallowed) return false
267314
}
268315

269-
return true
316+
// If only disallowedTools is defined, tool must not match any entry
317+
if (validAllowedTools.length === 0 && validDisallowedTools.length > 0) {
318+
return !validDisallowedTools.some((t) => {
319+
const serverMatches = matchesGlobPattern(serverName, t.serverName)
320+
const toolMatches = matchesGlobPattern(toolName, t.toolName)
321+
return serverMatches && toolMatches
322+
})
323+
}
324+
325+
// If both allowedTools and disallowedTools are defined, use pattern specificity resolution
326+
return resolveToolAccess(serverName, toolName, validAllowedTools, validDisallowedTools)
270327
}
271328

272329
// Custom error class for file restrictions

src/shared/pattern-matching.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,104 @@ export function filterByPattern<T>(items: T[], searchTerm: string, fields: (keyo
104104
}),
105105
)
106106
}
107+
108+
/**
109+
* Pattern specificity information for ranking patterns
110+
*/
111+
export interface PatternSpecificity {
112+
/** Whether the pattern is an exact match (no wildcards) */
113+
isExact: boolean
114+
/** Specificity score - higher is more specific */
115+
specificity: number
116+
}
117+
118+
/**
119+
* Calculate pattern specificity for ranking purposes
120+
* Exact patterns (no wildcards) always rank higher than wildcard patterns
121+
* Among wildcard patterns, longer patterns with more literal characters rank higher
122+
* @param pattern - Pattern to analyze
123+
* @returns Pattern specificity information
124+
*/
125+
export function getPatternSpecificity(pattern: string): PatternSpecificity {
126+
// Safety check for undefined pattern
127+
if (!pattern || typeof pattern !== "string") {
128+
return {
129+
isExact: false,
130+
specificity: 0,
131+
}
132+
}
133+
134+
const hasWildcards = isWildcardPattern(pattern)
135+
136+
if (!hasWildcards) {
137+
// Exact patterns always have highest priority
138+
return {
139+
isExact: true,
140+
specificity: pattern.length,
141+
}
142+
}
143+
144+
// For wildcard patterns, count non-wildcard characters
145+
const literalCharCount = pattern.replace(/[*?]/g, "").length
146+
return {
147+
isExact: false,
148+
specificity: literalCharCount,
149+
}
150+
}
151+
152+
/**
153+
* Find the most specific pattern that matches the given text
154+
* Prioritizes exact matches over wildcard patterns
155+
* Among wildcard patterns, prioritizes those with more literal characters
156+
* @param text - Text to match against
157+
* @param patterns - Array of patterns to check
158+
* @returns Most specific matching pattern, or null if no match
159+
*/
160+
export function findMostSpecificMatchingPattern(text: string, patterns: string[]): string | null {
161+
// Safety check for undefined patterns
162+
if (!patterns || !Array.isArray(patterns)) {
163+
return null
164+
}
165+
166+
const matchingPatterns = patterns.filter((pattern) => matchesGlobPattern(text, pattern))
167+
168+
if (matchingPatterns.length === 0) {
169+
return null
170+
}
171+
172+
if (matchingPatterns.length === 1) {
173+
return matchingPatterns[0]
174+
}
175+
176+
// Find the most specific pattern
177+
let mostSpecific = matchingPatterns[0]
178+
let bestSpecificity = getPatternSpecificity(mostSpecific)
179+
180+
for (let i = 1; i < matchingPatterns.length; i++) {
181+
const currentPattern = matchingPatterns[i]
182+
const currentSpecificity = getPatternSpecificity(currentPattern)
183+
184+
// Exact patterns always win over wildcard patterns
185+
if (currentSpecificity.isExact && !bestSpecificity.isExact) {
186+
mostSpecific = currentPattern
187+
bestSpecificity = currentSpecificity
188+
}
189+
// Among exact patterns, prefer longer ones
190+
else if (currentSpecificity.isExact && bestSpecificity.isExact) {
191+
if (currentSpecificity.specificity > bestSpecificity.specificity) {
192+
mostSpecific = currentPattern
193+
bestSpecificity = currentSpecificity
194+
}
195+
}
196+
// Among wildcard patterns, prefer more specific ones (when current is not exact and best is not exact)
197+
else if (!currentSpecificity.isExact && !bestSpecificity.isExact) {
198+
if (currentSpecificity.specificity > bestSpecificity.specificity) {
199+
mostSpecific = currentPattern
200+
bestSpecificity = currentSpecificity
201+
}
202+
}
203+
// If best is exact and current is wildcard, keep best (exact wins)
204+
}
205+
206+
return mostSpecific
207+
}

0 commit comments

Comments
 (0)