mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-26 13:04:49 -07:00
fix(plan): sandbox path resolution in Plan Mode to prevent hallucinations (#22737)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -28,7 +28,7 @@ import { resolveToolDeclaration } from './definitions/resolver.js';
|
||||
import { getPlanModeExitMessage } from '../utils/approvalModeUtils.js';
|
||||
|
||||
export interface ExitPlanModeParams {
|
||||
plan_path: string;
|
||||
plan_filename: string;
|
||||
}
|
||||
|
||||
export class ExitPlanModeTool extends BaseDeclarativeTool<
|
||||
@@ -41,8 +41,7 @@ export class ExitPlanModeTool extends BaseDeclarativeTool<
|
||||
private config: Config,
|
||||
messageBus: MessageBus,
|
||||
) {
|
||||
const plansDir = config.storage.getPlansDir();
|
||||
const definition = getExitPlanModeDefinition(plansDir);
|
||||
const definition = getExitPlanModeDefinition();
|
||||
super(
|
||||
ExitPlanModeTool.Name,
|
||||
'Exit Plan Mode',
|
||||
@@ -56,22 +55,21 @@ export class ExitPlanModeTool extends BaseDeclarativeTool<
|
||||
protected override validateToolParamValues(
|
||||
params: ExitPlanModeParams,
|
||||
): string | null {
|
||||
if (!params.plan_path || params.plan_path.trim() === '') {
|
||||
return 'plan_path is required.';
|
||||
if (!params.plan_filename || params.plan_filename.trim() === '') {
|
||||
return 'plan_filename is required.';
|
||||
}
|
||||
|
||||
// Since validateToolParamValues is synchronous, we use a basic synchronous check
|
||||
// for path traversal safety. High-level async validation is deferred to shouldConfirmExecute.
|
||||
const safeFilename = path.basename(params.plan_filename);
|
||||
const plansDir = resolveToRealPath(this.config.storage.getPlansDir());
|
||||
const resolvedPath = path.resolve(
|
||||
this.config.getTargetDir(),
|
||||
params.plan_path,
|
||||
const resolvedPath = path.join(
|
||||
this.config.storage.getPlansDir(),
|
||||
safeFilename,
|
||||
);
|
||||
|
||||
const realPath = resolveToRealPath(resolvedPath);
|
||||
|
||||
if (!isSubpath(plansDir, realPath)) {
|
||||
return `Access denied: plan path must be within the designated plans directory.`;
|
||||
return `Access denied: plan path (${resolvedPath}) must be within the designated plans directory (${plansDir}).`;
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -93,8 +91,7 @@ export class ExitPlanModeTool extends BaseDeclarativeTool<
|
||||
}
|
||||
|
||||
override getSchema(modelId?: string) {
|
||||
const plansDir = this.config.storage.getPlansDir();
|
||||
return resolveToolDeclaration(getExitPlanModeDefinition(plansDir), modelId);
|
||||
return resolveToolDeclaration(getExitPlanModeDefinition(), modelId);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,9 +119,8 @@ export class ExitPlanModeInvocation extends BaseToolInvocation<
|
||||
const resolvedPlanPath = this.getResolvedPlanPath();
|
||||
|
||||
const pathError = await validatePlanPath(
|
||||
this.params.plan_path,
|
||||
this.params.plan_filename,
|
||||
this.config.storage.getPlansDir(),
|
||||
this.config.getTargetDir(),
|
||||
);
|
||||
if (pathError) {
|
||||
this.planValidationError = pathError;
|
||||
@@ -174,7 +170,7 @@ export class ExitPlanModeInvocation extends BaseToolInvocation<
|
||||
}
|
||||
|
||||
getDescription(): string {
|
||||
return `Requesting plan approval for: ${this.params.plan_path}`;
|
||||
return `Requesting plan approval for: ${path.join(this.config.storage.getPlansDir(), this.params.plan_filename)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,7 +178,8 @@ export class ExitPlanModeInvocation extends BaseToolInvocation<
|
||||
* Note: Validation is done in validateToolParamValues, so this assumes the path is valid.
|
||||
*/
|
||||
private getResolvedPlanPath(): string {
|
||||
return path.resolve(this.config.getTargetDir(), this.params.plan_path);
|
||||
const safeFilename = path.basename(this.params.plan_filename);
|
||||
return path.join(this.config.storage.getPlansDir(), safeFilename);
|
||||
}
|
||||
|
||||
async execute(_signal: AbortSignal): Promise<ToolResult> {
|
||||
|
||||
Reference in New Issue
Block a user