fix(core): resolve Plan Mode deadlock during plan file creation due to sandbox restrictions (#24047)

This commit is contained in:
David Pierce
2026-03-31 22:06:50 +00:00
committed by GitHub
parent 9364dd8a49
commit 94f9480a3a
13 changed files with 555 additions and 97 deletions

View File

@@ -11,6 +11,22 @@ import type { Config } from '../config/config.js';
import type { MessageBus } from '../confirmation-bus/message-bus.js';
import { ToolConfirmationOutcome } from './tools.js';
import { ApprovalMode } from '../policy/types.js';
import fs from 'node:fs';
vi.mock('node:fs', async () => {
const actual = await vi.importActual<typeof import('node:fs')>('node:fs');
return {
...actual,
default: {
// @ts-expect-error - Property 'default' does not exist on type 'typeof import("node:fs")'
...actual.default,
existsSync: vi.fn(),
mkdirSync: vi.fn(),
},
existsSync: vi.fn(),
mkdirSync: vi.fn(),
};
});
describe('EnterPlanModeTool', () => {
let tool: EnterPlanModeTool;
@@ -103,6 +119,7 @@ describe('EnterPlanModeTool', () => {
describe('execute', () => {
it('should set approval mode to PLAN and return message', async () => {
const invocation = tool.build({});
vi.mocked(fs.existsSync).mockReturnValue(true);
const result = await invocation.execute(new AbortController().signal);
@@ -113,9 +130,21 @@ describe('EnterPlanModeTool', () => {
expect(result.returnDisplay).toBe('Switching to Plan mode');
});
it('should create plans directory if it does not exist', async () => {
const invocation = tool.build({});
vi.mocked(fs.existsSync).mockReturnValue(false);
await invocation.execute(new AbortController().signal);
expect(fs.mkdirSync).toHaveBeenCalledWith('/mock/plans/dir', {
recursive: true,
});
});
it('should include optional reason in output display but not in llmContent', async () => {
const reason = 'Design new database schema';
const invocation = tool.build({ reason });
vi.mocked(fs.existsSync).mockReturnValue(true);
const result = await invocation.execute(new AbortController().signal);

View File

@@ -4,6 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import fs from 'node:fs';
import {
BaseDeclarativeTool,
BaseToolInvocation,
@@ -18,6 +19,7 @@ import { ENTER_PLAN_MODE_TOOL_NAME } from './tool-names.js';
import { ApprovalMode } from '../policy/types.js';
import { ENTER_PLAN_MODE_DEFINITION } from './definitions/coreTools.js';
import { resolveToolDeclaration } from './definitions/resolver.js';
import { debugLogger } from '../utils/debugLogger.js';
export interface EnterPlanModeParams {
reason?: string;
@@ -122,6 +124,19 @@ export class EnterPlanModeInvocation extends BaseToolInvocation<
this.config.setApprovalMode(ApprovalMode.PLAN);
// Ensure plans directory exists so that the agent can write the plan file.
// In sandboxed environments, the plans directory must exist on the host
// before it can be bound/allowed in the sandbox.
const plansDir = this.config.storage.getPlansDir();
if (!fs.existsSync(plansDir)) {
try {
fs.mkdirSync(plansDir, { recursive: true });
} catch (e) {
// Log error but don't fail; write_file will try again later
debugLogger.error(`Failed to create plans directory: ${plansDir}`, e);
}
}
return {
llmContent: 'Switching to Plan mode.',
returnDisplay: this.params.reason