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.
This commit is contained in:
2026-02-05 19:00:19 -05:00
parent 80fd7b2de3
commit 8ae1218627
2 changed files with 55 additions and 0 deletions

View File

@@ -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;

View File

@@ -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<StreamCallbacks>;
executionControl: ReturnType<typeof createExecutionControl>;
/** Track files modified in this session for plan approval enforcement */
modifiedFiles: Set<string>;
/** Whether plan approval enforcement is enabled */
enforcePlanApproval: boolean;
}
/**
@@ -92,6 +112,8 @@ const createStreamAgentState = (
options,
callbacks,
executionControl: createExecutionControl(executionControlEvents),
modifiedFiles: new Set<string>(),
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) {