@@ -19,9 +19,13 @@ import type { MessageBus } from '../confirmation-bus/message-bus.js';
1919import path from 'node:path' ;
2020import type { Config } from '../config/config.js' ;
2121import { 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' ;
2327import { ApprovalMode } from '../policy/types.js' ;
24- import { resolveToRealPath , isSubpath } from '../utils/paths.js' ;
28+ // Remove unused imports
2529import { logPlanExecution } from '../telemetry/loggers.js' ;
2630import { PlanExecutionEvent } from '../telemetry/types.js' ;
2731import { 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