Skip to content

Commit 0b21597

Browse files
fix(core): refactor exit-plan-mode to use resolveAndValidatePlanPath consistently
This fixes unit test failures by centralizing path resolution logic.
1 parent 10154d9 commit 0b21597

2 files changed

Lines changed: 28 additions & 19 deletions

File tree

packages/core/src/tools/exit-plan-mode.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,8 +72,10 @@ describe('ExitPlanModeTool', () => {
7272

7373
const createPlanFile = (name: string, content: string) => {
7474
const filePath = path.join(mockPlansDir, name);
75+
// Ensure parent directory exists for nested tests
76+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
7577
fs.writeFileSync(filePath, content);
76-
return path.join('plans', name);
78+
return name;
7779
};
7880

7981
describe('shouldConfirmExecute', () => {

packages/core/src/tools/exit-plan-mode.ts

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,13 @@ import type { MessageBus } from '../confirmation-bus/message-bus.js';
1919
import path from 'node:path';
2020
import type { Config } from '../config/config.js';
2121
import { EXIT_PLAN_MODE_TOOL_NAME } from './tool-names.js';
22-
import { validatePlanPath, validatePlanContent } from '../utils/planUtils.js';
22+
import {
23+
validatePlanPath,
24+
validatePlanContent,
25+
resolveAndValidatePlanPath,
26+
} from '../utils/planUtils.js';
2327
import { ApprovalMode } from '../policy/types.js';
24-
import { resolveToRealPath, isSubpath } from '../utils/paths.js';
28+
// Remove unused imports
2529
import { logPlanExecution } from '../telemetry/loggers.js';
2630
import { PlanExecutionEvent } from '../telemetry/types.js';
2731
import { getExitPlanModeDefinition } from './definitions/coreTools.js';
@@ -59,18 +63,19 @@ export class ExitPlanModeTool extends BaseDeclarativeTool<
5963
if (!params.plan_filename || params.plan_filename.trim() === '') {
6064
return 'plan_filename is required.';
6165
}
62-
63-
const safeFilename = path.basename(params.plan_filename);
64-
const plansDir = resolveToRealPath(this.config.storage.getPlansDir());
65-
const resolvedPath = path.join(
66-
this.config.storage.getPlansDir(),
67-
safeFilename,
68-
);
69-
70-
const realPath = resolveToRealPath(resolvedPath);
71-
72-
if (!isSubpath(plansDir, realPath)) {
73-
return `Access denied: plan path (${resolvedPath}) must be within the designated plans directory (${plansDir}).`;
66+
try {
67+
resolveAndValidatePlanPath(
68+
params.plan_filename,
69+
this.config.getPlansDir(),
70+
);
71+
} catch (e) {
72+
if (e instanceof Error && e.message.startsWith('Security violation')) {
73+
return `Access denied: plan path (${path.join(
74+
this.config.getPlansDir(),
75+
params.plan_filename,
76+
)}) must be within the designated plans directory (${this.config.getPlansDir()}).`;
77+
}
78+
return e instanceof Error ? e.message : String(e);
7479
}
7580

7681
return null;
@@ -121,7 +126,7 @@ export class ExitPlanModeInvocation extends BaseToolInvocation<
121126

122127
const pathError = await validatePlanPath(
123128
this.params.plan_filename,
124-
this.config.storage.getPlansDir(),
129+
this.config.getPlansDir(),
125130
);
126131
if (pathError) {
127132
this.planValidationError = pathError;
@@ -171,16 +176,18 @@ export class ExitPlanModeInvocation extends BaseToolInvocation<
171176
}
172177

173178
getDescription(): string {
174-
return `Requesting plan approval for: ${path.join(this.config.storage.getPlansDir(), this.params.plan_filename)}`;
179+
return `Requesting plan approval for: ${path.join(this.config.getPlansDir(), this.params.plan_filename)}`;
175180
}
176181

177182
/**
178183
* Returns the resolved plan path.
179184
* Note: Validation is done in validateToolParamValues, so this assumes the path is valid.
180185
*/
181186
private getResolvedPlanPath(): string {
182-
const safeFilename = path.basename(this.params.plan_filename);
183-
return path.join(this.config.storage.getPlansDir(), safeFilename);
187+
return resolveAndValidatePlanPath(
188+
this.params.plan_filename,
189+
this.config.getPlansDir(),
190+
);
184191
}
185192

186193
async execute({ abortSignal: _signal }: ExecuteOptions): Promise<ToolResult> {

0 commit comments

Comments
 (0)