diff --git a/packages/core/src/policy/policies/plan.toml b/packages/core/src/policy/policies/plan.toml index e7129208c8..6b963f72d2 100644 --- a/packages/core/src/policy/policies/plan.toml +++ b/packages/core/src/policy/policies/plan.toml @@ -55,3 +55,11 @@ decision = "allow" priority = 70 modes = ["plan"] argsPattern = "\"file_path\":\"[^\"]+/\\.gemini/tmp/[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+/plans/[a-zA-Z0-9_-]+\\.md\"" + +# Explicitly Deny other write operations in Plan mode with a clear message. +[[rule]] +toolName = ["write_file", "edit"] +decision = "deny" +priority = 65 +modes = ["plan"] +deny_message = "You are in Plan Mode and cannot modify source code. You may ONLY use write_file or replace to save plans to the designated plans directory as .md files." diff --git a/packages/core/src/prompts/snippets.ts b/packages/core/src/prompts/snippets.ts index 3791a856bf..c5f4c13360 100644 --- a/packages/core/src/prompts/snippets.ts +++ b/packages/core/src/prompts/snippets.ts @@ -460,7 +460,7 @@ ${options.planModeToolsList} ## Rules -1. **Read-Only:** You cannot modify source code. You may ONLY use read-only tools to explore, and you can only write to \`${options.plansDir}/\`. +1. **Read-Only:** You cannot modify source code. You may ONLY use read-only tools to explore, and you can only write to \`${options.plansDir}/\`. If the user asks you to modify source code directly, you MUST explain that you are in Plan Mode and must first create a detailed plan in the plans directory and get approval before any source code changes can be made. 2. **Efficiency:** Autonomously combine discovery and drafting phases to minimize conversational turns. If the request is ambiguous, use ${formatToolName(ASK_USER_TOOL_NAME)} to clarify. Otherwise, explore the codebase and write the draft in one fluid motion. 3. **Inquiries and Directives:** Distinguish between Inquiries and Directives to minimize unnecessary planning. - **Inquiries:** If the request is an **Inquiry** (e.g., "How does X work?"), use read-only tools to explore and answer directly in your chat response. DO NOT create a plan or call ${formatToolName( diff --git a/packages/core/src/tools/tool-registry.ts b/packages/core/src/tools/tool-registry.ts index 60b1451838..abcf34e1f8 100644 --- a/packages/core/src/tools/tool-registry.ts +++ b/packages/core/src/tools/tool-registry.ts @@ -12,6 +12,7 @@ import type { } from './tools.js'; import { Kind, BaseDeclarativeTool, BaseToolInvocation } from './tools.js'; import type { Config } from '../config/config.js'; +import { ApprovalMode } from '../policy/types.js'; import { spawn } from 'node:child_process'; import { StringDecoder } from 'node:string_decoder'; import { DiscoveredMCPTool } from './mcp-tool.js'; @@ -25,6 +26,9 @@ import { DISCOVERED_TOOL_PREFIX, TOOL_LEGACY_ALIASES, getToolAliases, + PLAN_MODE_TOOLS, + WRITE_FILE_TOOL_NAME, + EDIT_TOOL_NAME, } from './tool-names.js'; type ToolParams = Record; @@ -484,6 +488,31 @@ export class ToolRegistry { excludeTools ??= this.expandExcludeToolsWithAliases(this.config.getExcludeTools()) ?? new Set([]); + + // Filter tools in Plan Mode to only allow approved read-only tools. + const isPlanMode = + typeof this.config.getApprovalMode === 'function' && + this.config.getApprovalMode() === ApprovalMode.PLAN; + if (isPlanMode) { + const allowedToolNames = new Set(PLAN_MODE_TOOLS); + // We allow write_file and replace for writing plans specifically. + allowedToolNames.add(WRITE_FILE_TOOL_NAME); + allowedToolNames.add(EDIT_TOOL_NAME); + + // Discovered MCP tools are allowed if they are read-only. + if ( + tool instanceof DiscoveredMCPTool && + tool.isReadOnly && + !allowedToolNames.has(tool.name) + ) { + allowedToolNames.add(tool.name); + } + + if (!allowedToolNames.has(tool.name)) { + return false; + } + } + const normalizedClassName = tool.constructor.name.replace(/^_+/, ''); const possibleNames = [tool.name, normalizedClassName]; if (tool instanceof DiscoveredMCPTool) { @@ -507,9 +536,22 @@ export class ToolRegistry { * @returns An array of FunctionDeclarations. */ getFunctionDeclarations(modelId?: string): FunctionDeclaration[] { + const isPlanMode = this.config.getApprovalMode() === ApprovalMode.PLAN; + const plansDir = this.config.storage.getPlansDir(); + const declarations: FunctionDeclaration[] = []; this.getActiveTools().forEach((tool) => { - declarations.push(tool.getSchema(modelId)); + let schema = tool.getSchema(modelId); + if ( + isPlanMode && + (tool.name === WRITE_FILE_TOOL_NAME || tool.name === EDIT_TOOL_NAME) + ) { + schema = { + ...schema, + description: `ONLY FOR PLANS: ${schema.description}. You are currently in Plan Mode and may ONLY use this tool to write or update plans (.md files) in the plans directory: ${plansDir}/. You cannot use this tool to modify source code directly.`, + }; + } + declarations.push(schema); }); return declarations; }