|
|
@@ -1,11 +1,9 @@
|
|
|
import * as vscode from "vscode"
|
|
|
|
|
|
import {
|
|
|
- type GroupOptions,
|
|
|
type GroupEntry,
|
|
|
type ModeConfig,
|
|
|
type CustomModePrompts,
|
|
|
- type ExperimentId,
|
|
|
type ToolGroup,
|
|
|
type PromptComponent,
|
|
|
DEFAULT_MODES,
|
|
|
@@ -13,7 +11,6 @@ import {
|
|
|
|
|
|
import { addCustomInstructions } from "../core/prompts/sections/custom-instructions"
|
|
|
|
|
|
-import { EXPERIMENT_IDS } from "./experiments"
|
|
|
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "./tools"
|
|
|
|
|
|
export type Mode = string
|
|
|
@@ -27,22 +24,6 @@ export function getGroupName(group: GroupEntry): ToolGroup {
|
|
|
return group[0]
|
|
|
}
|
|
|
|
|
|
-// Helper to get group options if they exist
|
|
|
-function getGroupOptions(group: GroupEntry): GroupOptions | undefined {
|
|
|
- return Array.isArray(group) ? group[1] : undefined
|
|
|
-}
|
|
|
-
|
|
|
-// Helper to check if a file path matches a regex pattern
|
|
|
-export function doesFileMatchRegex(filePath: string, pattern: string): boolean {
|
|
|
- try {
|
|
|
- const regex = new RegExp(pattern)
|
|
|
- return regex.test(filePath)
|
|
|
- } catch (error) {
|
|
|
- console.error(`Invalid regex pattern: ${pattern}`, error)
|
|
|
- return false
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
// Helper to get all tools for a mode
|
|
|
export function getToolsForMode(groups: readonly GroupEntry[]): string[] {
|
|
|
const tools = new Set<string>()
|
|
|
@@ -150,9 +131,6 @@ export function getModeSelection(mode: string, promptComponent?: PromptComponent
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// Edit operation parameters that indicate an actual edit operation
|
|
|
-const EDIT_OPERATION_PARAMS = ["diff", "content", "operations", "search", "replace", "args", "line"] as const
|
|
|
-
|
|
|
// Custom error class for file restrictions
|
|
|
export class FileRestrictionError extends Error {
|
|
|
constructor(mode: string, pattern: string, description: string | undefined, filePath: string, tool?: string) {
|
|
|
@@ -164,127 +142,6 @@ export class FileRestrictionError extends Error {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-export function isToolAllowedForMode(
|
|
|
- tool: string,
|
|
|
- modeSlug: string,
|
|
|
- customModes: ModeConfig[],
|
|
|
- toolRequirements?: Record<string, boolean>,
|
|
|
- toolParams?: Record<string, any>, // All tool parameters
|
|
|
- experiments?: Record<string, boolean>,
|
|
|
- includedTools?: string[], // Opt-in tools explicitly included (e.g., from modelInfo)
|
|
|
-): boolean {
|
|
|
- // Always allow these tools
|
|
|
- if (ALWAYS_AVAILABLE_TOOLS.includes(tool as any)) {
|
|
|
- return true
|
|
|
- }
|
|
|
-
|
|
|
- // Check if this is a dynamic MCP tool (mcp_serverName_toolName)
|
|
|
- // These should be allowed if the mcp group is allowed for the mode
|
|
|
- const isDynamicMcpTool = tool.startsWith("mcp_")
|
|
|
- if (experiments && Object.values(EXPERIMENT_IDS).includes(tool as ExperimentId)) {
|
|
|
- if (!experiments[tool]) {
|
|
|
- return false
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // Check tool requirements if any exist
|
|
|
- if (toolRequirements && typeof toolRequirements === "object") {
|
|
|
- if (tool in toolRequirements && !toolRequirements[tool]) {
|
|
|
- return false
|
|
|
- }
|
|
|
- } else if (toolRequirements === false) {
|
|
|
- // If toolRequirements is a boolean false, all tools are disabled
|
|
|
- return false
|
|
|
- }
|
|
|
-
|
|
|
- const mode = getModeBySlug(modeSlug, customModes)
|
|
|
- if (!mode) {
|
|
|
- return false
|
|
|
- }
|
|
|
-
|
|
|
- // Check if tool is in any of the mode's groups and respects any group options
|
|
|
- for (const group of mode.groups) {
|
|
|
- const groupName = getGroupName(group)
|
|
|
- const options = getGroupOptions(group)
|
|
|
-
|
|
|
- const groupConfig = TOOL_GROUPS[groupName]
|
|
|
-
|
|
|
- // Check if this is a dynamic MCP tool and the mcp group is allowed
|
|
|
- if (isDynamicMcpTool && groupName === "mcp") {
|
|
|
- // Dynamic MCP tools are allowed if the mcp group is in the mode's groups
|
|
|
- return true
|
|
|
- }
|
|
|
-
|
|
|
- // Check if the tool is in the group's regular tools
|
|
|
- const isRegularTool = groupConfig.tools.includes(tool)
|
|
|
-
|
|
|
- // Check if the tool is a custom tool that has been explicitly included
|
|
|
- const isCustomTool = groupConfig.customTools?.includes(tool) && includedTools?.includes(tool)
|
|
|
-
|
|
|
- // If the tool isn't in regular tools and isn't an included custom tool, continue to next group
|
|
|
- if (!isRegularTool && !isCustomTool) {
|
|
|
- continue
|
|
|
- }
|
|
|
-
|
|
|
- // If there are no options, allow the tool
|
|
|
- if (!options) {
|
|
|
- return true
|
|
|
- }
|
|
|
-
|
|
|
- // For the edit group, check file regex if specified
|
|
|
- if (groupName === "edit" && options.fileRegex) {
|
|
|
- const filePath = toolParams?.path
|
|
|
- // Check if this is an actual edit operation (not just path-only for streaming)
|
|
|
- const isEditOperation = EDIT_OPERATION_PARAMS.some((param) => toolParams?.[param])
|
|
|
-
|
|
|
- // Handle single file path validation
|
|
|
- if (filePath && isEditOperation && !doesFileMatchRegex(filePath, options.fileRegex)) {
|
|
|
- throw new FileRestrictionError(mode.name, options.fileRegex, options.description, filePath, tool)
|
|
|
- }
|
|
|
-
|
|
|
- // Handle XML args parameter (used by MULTI_FILE_APPLY_DIFF experiment)
|
|
|
- if (toolParams?.args && typeof toolParams.args === "string") {
|
|
|
- // Extract file paths from XML args with improved validation
|
|
|
- try {
|
|
|
- const filePathMatches = toolParams.args.match(/<path>([^<]+)<\/path>/g)
|
|
|
- if (filePathMatches) {
|
|
|
- for (const match of filePathMatches) {
|
|
|
- // More robust path extraction with validation
|
|
|
- const pathMatch = match.match(/<path>([^<]+)<\/path>/)
|
|
|
- if (pathMatch && pathMatch[1]) {
|
|
|
- const extractedPath = pathMatch[1].trim()
|
|
|
- // Validate that the path is not empty and doesn't contain invalid characters
|
|
|
- if (extractedPath && !extractedPath.includes("<") && !extractedPath.includes(">")) {
|
|
|
- if (!doesFileMatchRegex(extractedPath, options.fileRegex)) {
|
|
|
- throw new FileRestrictionError(
|
|
|
- mode.name,
|
|
|
- options.fileRegex,
|
|
|
- options.description,
|
|
|
- extractedPath,
|
|
|
- tool,
|
|
|
- )
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- } catch (error) {
|
|
|
- // Re-throw FileRestrictionError as it's an expected validation error
|
|
|
- if (error instanceof FileRestrictionError) {
|
|
|
- throw error
|
|
|
- }
|
|
|
- // If XML parsing fails, log the error but don't block the operation
|
|
|
- console.warn(`Failed to parse XML args for file restriction validation: ${error}`)
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- return true
|
|
|
- }
|
|
|
-
|
|
|
- return false
|
|
|
-}
|
|
|
-
|
|
|
// Create the mode-specific default prompts
|
|
|
export const defaultPrompts: Readonly<CustomModePrompts> = Object.freeze(
|
|
|
Object.fromEntries(
|