feat(plan): enforce read-only constraints in Plan Mode (#19433)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Jerop Kipruto <jerop@google.com>
This commit is contained in:
matt korwel
2026-02-20 13:33:04 -06:00
committed by GitHub
parent f97b04cc9a
commit 6cfd29ef9b
3 changed files with 52 additions and 2 deletions

View File

@@ -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."

View File

@@ -460,7 +460,7 @@ ${options.planModeToolsList}
</available_tools>
## 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(

View File

@@ -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<string, unknown>;
@@ -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<string>(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;
}