From 8ae1218627d1637a4e77a58572daa13f6366d55b Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Thu, 5 Feb 2026 19:00:19 -0500 Subject: [PATCH] feat: enforce plan approval for multi-file modifications The plan approval system now has enforcement at the tool execution level: - After modifying more than 2 files, the agent MUST use plan_approval - File-modifying tools (write, edit, delete, etc.) are blocked until an approved plan exists - Clear error message guides agent to create and submit a plan This ensures agents can't bypass plan approval instructions. --- src/interfaces/AgentOptions.ts | 2 ++ src/services/agent-stream.ts | 53 ++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/interfaces/AgentOptions.ts b/src/interfaces/AgentOptions.ts index e16280c..32b8701 100644 --- a/src/interfaces/AgentOptions.ts +++ b/src/interfaces/AgentOptions.ts @@ -19,6 +19,8 @@ export interface AgentOptions { autoApprove?: boolean; /** Chat mode - only read-only tools, no file modifications */ chatMode?: boolean; + /** Enforce plan approval for multi-file modifications (default: true) */ + enforcePlanApproval?: boolean; /** Model-specific parameters from tier detection */ modelParams?: { temperature?: number; diff --git a/src/services/agent-stream.ts b/src/services/agent-stream.ts index b01ea48..e24ee5f 100644 --- a/src/services/agent-stream.ts +++ b/src/services/agent-stream.ts @@ -31,6 +31,22 @@ import { createExecutionControl, captureFileState, } from "@services/execution-control"; +import { getActivePlans } from "@services/plan-mode/plan-service"; + +// ============================================================================= +// Constants +// ============================================================================= + +/** + * Number of file modifications allowed before plan approval is required + * After this threshold, agents must use plan_approval tool + */ +const PLAN_APPROVAL_FILE_THRESHOLD = 2; + +/** + * Tools that modify files and require plan approval tracking + */ +const FILE_MODIFYING_TOOLS = new Set(["write", "edit", "delete", "multi_edit", "apply_patch"]); // ============================================================================= // Types @@ -43,6 +59,10 @@ interface StreamAgentState { options: AgentOptions; callbacks: Partial; executionControl: ReturnType; + /** Track files modified in this session for plan approval enforcement */ + modifiedFiles: Set; + /** Whether plan approval enforcement is enabled */ + enforcePlanApproval: boolean; } /** @@ -92,6 +112,8 @@ const createStreamAgentState = ( options, callbacks, executionControl: createExecutionControl(executionControlEvents), + modifiedFiles: new Set(), + enforcePlanApproval: options.enforcePlanApproval ?? true, }; }; @@ -332,6 +354,37 @@ const executeTool = async ( }; } + // Check for plan approval enforcement on file-modifying tools + if (state.enforcePlanApproval && FILE_MODIFYING_TOOLS.has(toolCall.name)) { + const toolFilePath = (toolCall.arguments.filePath ?? toolCall.arguments.file_path) as string | undefined; + + // Track this file modification + if (toolFilePath) { + state.modifiedFiles.add(toolFilePath); + } + + // Check if we've exceeded the threshold and need plan approval + if (state.modifiedFiles.size > PLAN_APPROVAL_FILE_THRESHOLD) { + // Check if there's an approved plan + const activePlans = getActivePlans(); + const hasApprovedPlan = activePlans.some( + p => p.status === "approved" || p.status === "executing" + ); + + if (!hasApprovedPlan) { + return { + success: false, + title: "Plan approval required", + output: "", + error: `You have modified ${state.modifiedFiles.size} files which exceeds the threshold of ${PLAN_APPROVAL_FILE_THRESHOLD}. ` + + `Before continuing, you MUST use the plan_approval tool to create and submit an implementation plan for user approval. ` + + `Use plan_approval action="create" to start, then add steps, context, and risks, finally submit with action="submit". ` + + `Wait for the user to approve the plan before proceeding with more file modifications.`, + }; + } + } + } + // Check for debug error markers from truncated/malformed JSON const debugError = toolCall.arguments.__debug_error as string | undefined; if (debugError) {