mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
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:
@@ -55,3 +55,11 @@ decision = "allow"
|
|||||||
priority = 70
|
priority = 70
|
||||||
modes = ["plan"]
|
modes = ["plan"]
|
||||||
argsPattern = "\"file_path\":\"[^\"]+/\\.gemini/tmp/[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+/plans/[a-zA-Z0-9_-]+\\.md\""
|
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."
|
||||||
|
|||||||
@@ -460,7 +460,7 @@ ${options.planModeToolsList}
|
|||||||
</available_tools>
|
</available_tools>
|
||||||
|
|
||||||
## Rules
|
## 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.
|
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.
|
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(
|
- **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(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import type {
|
|||||||
} from './tools.js';
|
} from './tools.js';
|
||||||
import { Kind, BaseDeclarativeTool, BaseToolInvocation } from './tools.js';
|
import { Kind, BaseDeclarativeTool, BaseToolInvocation } from './tools.js';
|
||||||
import type { Config } from '../config/config.js';
|
import type { Config } from '../config/config.js';
|
||||||
|
import { ApprovalMode } from '../policy/types.js';
|
||||||
import { spawn } from 'node:child_process';
|
import { spawn } from 'node:child_process';
|
||||||
import { StringDecoder } from 'node:string_decoder';
|
import { StringDecoder } from 'node:string_decoder';
|
||||||
import { DiscoveredMCPTool } from './mcp-tool.js';
|
import { DiscoveredMCPTool } from './mcp-tool.js';
|
||||||
@@ -25,6 +26,9 @@ import {
|
|||||||
DISCOVERED_TOOL_PREFIX,
|
DISCOVERED_TOOL_PREFIX,
|
||||||
TOOL_LEGACY_ALIASES,
|
TOOL_LEGACY_ALIASES,
|
||||||
getToolAliases,
|
getToolAliases,
|
||||||
|
PLAN_MODE_TOOLS,
|
||||||
|
WRITE_FILE_TOOL_NAME,
|
||||||
|
EDIT_TOOL_NAME,
|
||||||
} from './tool-names.js';
|
} from './tool-names.js';
|
||||||
|
|
||||||
type ToolParams = Record<string, unknown>;
|
type ToolParams = Record<string, unknown>;
|
||||||
@@ -484,6 +488,31 @@ export class ToolRegistry {
|
|||||||
excludeTools ??=
|
excludeTools ??=
|
||||||
this.expandExcludeToolsWithAliases(this.config.getExcludeTools()) ??
|
this.expandExcludeToolsWithAliases(this.config.getExcludeTools()) ??
|
||||||
new Set([]);
|
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 normalizedClassName = tool.constructor.name.replace(/^_+/, '');
|
||||||
const possibleNames = [tool.name, normalizedClassName];
|
const possibleNames = [tool.name, normalizedClassName];
|
||||||
if (tool instanceof DiscoveredMCPTool) {
|
if (tool instanceof DiscoveredMCPTool) {
|
||||||
@@ -507,9 +536,22 @@ export class ToolRegistry {
|
|||||||
* @returns An array of FunctionDeclarations.
|
* @returns An array of FunctionDeclarations.
|
||||||
*/
|
*/
|
||||||
getFunctionDeclarations(modelId?: string): FunctionDeclaration[] {
|
getFunctionDeclarations(modelId?: string): FunctionDeclaration[] {
|
||||||
|
const isPlanMode = this.config.getApprovalMode() === ApprovalMode.PLAN;
|
||||||
|
const plansDir = this.config.storage.getPlansDir();
|
||||||
|
|
||||||
const declarations: FunctionDeclaration[] = [];
|
const declarations: FunctionDeclaration[] = [];
|
||||||
this.getActiveTools().forEach((tool) => {
|
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;
|
return declarations;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user