From f94dbbeb4650f749a2875d0f578d8b93bfbbc017 Mon Sep 17 00:00:00 2001 From: Mahima Shanware Date: Tue, 14 Apr 2026 07:21:09 +0000 Subject: [PATCH] feat(core): pass resolved plan directory context to tools and system prompt via activeExtensionName --- .../core/src/config/agent-loop-context.ts | 3 ++ packages/core/src/config/config.ts | 11 +++++ packages/core/src/prompts/promptProvider.ts | 28 +++++++++--- packages/core/src/tools/edit.ts | 20 +++++++-- packages/core/src/tools/exit-plan-mode.ts | 43 +++++++++++++++---- packages/core/src/tools/tool-registry.ts | 18 +++++++- packages/core/src/tools/tools.ts | 1 + packages/core/src/tools/write-file.ts | 20 +++++++-- 8 files changed, 121 insertions(+), 23 deletions(-) diff --git a/packages/core/src/config/agent-loop-context.ts b/packages/core/src/config/agent-loop-context.ts index 7325fc0b73..e3c681e166 100644 --- a/packages/core/src/config/agent-loop-context.ts +++ b/packages/core/src/config/agent-loop-context.ts @@ -26,6 +26,9 @@ export interface AgentLoopContext { /** The unique ID for the parent session if this is a subagent. */ readonly parentSessionId?: string; + /** The name of the active extension driving this context, if any. */ + readonly activeExtensionName?: string; + /** The registry of tools available to the agent in this context. */ readonly toolRegistry: ToolRegistry; diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index bf5bdf04e9..f7d97e1c8f 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -737,6 +737,17 @@ export class Config implements McpContext, AgentLoopContext { private blockedEnvironmentVariables: string[]; private readonly enableEnvironmentVariableRedaction: boolean; private _promptRegistry!: PromptRegistry; + private _activeExtensionName?: string; + + get activeExtensionName(): string | undefined { + return ( + this._activeExtensionName || process.env['GEMINI_CLI_ACTIVE_EXTENSION'] + ); + } + + setActiveExtensionName(name: string | undefined): void { + this._activeExtensionName = name; + } private _resourceRegistry!: ResourceRegistry; private agentRegistry!: AgentRegistry; private readonly acknowledgedAgentsService: AcknowledgedAgentsService; diff --git a/packages/core/src/prompts/promptProvider.ts b/packages/core/src/prompts/promptProvider.ts index 36d08c7e74..89367b8091 100644 --- a/packages/core/src/prompts/promptProvider.ts +++ b/packages/core/src/prompts/promptProvider.ts @@ -192,12 +192,28 @@ export class PromptProvider { ), planningWorkflow: this.withSection( 'planningWorkflow', - () => ({ - interactive: interactiveMode, - planModeToolsList, - plansDir: context.config.storage.getPlansDir(), - approvedPlanPath: context.config.getApprovedPlanPath(), - }), + () => { + let plansDir = ''; + const activeExt = context.config.activeExtensionName; + let customDir: string | undefined; + if (activeExt) { + customDir = context.config.getExtensionSetting( + activeExt, + 'plan.directory', + ); + } + try { + plansDir = context.config.storage.getPlansDir(customDir); + } catch { + // ignore + } + return { + interactive: interactiveMode, + planModeToolsList, + plansDir, + approvedPlanPath: context.config.getApprovedPlanPath(), + }; + }, isPlanMode, ), operationalGuidelines: this.withSection( diff --git a/packages/core/src/tools/edit.ts b/packages/core/src/tools/edit.ts index f0b9b448a3..69519e17d5 100644 --- a/packages/core/src/tools/edit.ts +++ b/packages/core/src/tools/edit.ts @@ -466,10 +466,22 @@ class EditToolInvocation ); if (this.config.isPlanMode()) { const safeFilename = path.basename(this.params.file_path); - this.resolvedPath = path.join( - this.config.storage.getPlansDir(), - safeFilename, - ); + let customDir: string | undefined; + const activeExt = this.config.activeExtensionName; + if (activeExt) { + customDir = this.config.getExtensionSetting( + activeExt, + 'plan.directory', + ); + } + try { + this.resolvedPath = path.join( + this.config.storage.getPlansDir(customDir), + safeFilename, + ); + } catch { + this.resolvedPath = ''; // Handled safely downstream + } } else if (!path.isAbsolute(this.params.file_path)) { const result = correctPath(this.params.file_path, this.config); if (result.success) { diff --git a/packages/core/src/tools/exit-plan-mode.ts b/packages/core/src/tools/exit-plan-mode.ts index 476aa88b7d..4b705306de 100644 --- a/packages/core/src/tools/exit-plan-mode.ts +++ b/packages/core/src/tools/exit-plan-mode.ts @@ -53,6 +53,18 @@ export class ExitPlanModeTool extends BaseDeclarativeTool< ); } + private getResolvedPlansDir(): string { + let customDir: string | undefined; + const activeExt = this.config.activeExtensionName; + if (activeExt) { + customDir = this.config.getExtensionSetting( + activeExt, + 'plan.directory', + ); + } + return this.config.storage.getPlansDir(customDir); + } + protected override validateToolParamValues( params: ExitPlanModeParams, ): string | null { @@ -61,11 +73,14 @@ export class ExitPlanModeTool extends BaseDeclarativeTool< } const safeFilename = path.basename(params.plan_filename); - const plansDir = resolveToRealPath(this.config.storage.getPlansDir()); - const resolvedPath = path.join( - this.config.storage.getPlansDir(), - safeFilename, - ); + let plansDir: string; + let resolvedPath: string; + try { + plansDir = resolveToRealPath(this.getResolvedPlansDir()); + resolvedPath = path.join(this.getResolvedPlansDir(), safeFilename); + } catch { + return 'Failed to read plan directory: Path traversal attempt detected.'; + } const realPath = resolveToRealPath(resolvedPath); @@ -114,6 +129,18 @@ export class ExitPlanModeInvocation extends BaseToolInvocation< super(params, messageBus, toolName, toolDisplayName); } + private getResolvedPlansDir(): string { + let customDir: string | undefined; + const activeExt = this.config.activeExtensionName; + if (activeExt) { + customDir = this.config.getExtensionSetting( + activeExt, + 'plan.directory', + ); + } + return this.config.storage.getPlansDir(customDir); + } + override async shouldConfirmExecute( abortSignal: AbortSignal, ): Promise { @@ -121,7 +148,7 @@ export class ExitPlanModeInvocation extends BaseToolInvocation< const pathError = await validatePlanPath( this.params.plan_filename, - this.config.storage.getPlansDir(), + this.getResolvedPlansDir(), ); if (pathError) { this.planValidationError = pathError; @@ -171,7 +198,7 @@ export class ExitPlanModeInvocation extends BaseToolInvocation< } getDescription(): string { - return `Requesting plan approval for: ${path.join(this.config.storage.getPlansDir(), this.params.plan_filename)}`; + return `Requesting plan approval for: ${path.join(this.getResolvedPlansDir(), this.params.plan_filename)}`; } /** @@ -180,7 +207,7 @@ export class ExitPlanModeInvocation extends BaseToolInvocation< */ private getResolvedPlanPath(): string { const safeFilename = path.basename(this.params.plan_filename); - return path.join(this.config.storage.getPlansDir(), safeFilename); + return path.join(this.getResolvedPlansDir(), safeFilename); } async execute({ abortSignal: _signal }: ExecuteOptions): Promise { diff --git a/packages/core/src/tools/tool-registry.ts b/packages/core/src/tools/tool-registry.ts index c4c194620f..3775612bdc 100644 --- a/packages/core/src/tools/tool-registry.ts +++ b/packages/core/src/tools/tool-registry.ts @@ -634,7 +634,23 @@ export class ToolRegistry { */ getFunctionDeclarations(modelId?: string): FunctionDeclaration[] { const isPlanMode = this.config.getApprovalMode() === ApprovalMode.PLAN; - const plansDir = this.config.storage.getPlansDir(); + + let plansDir: string | undefined; + if (isPlanMode) { + let customDir: string | undefined; + const activeExt = this.config.activeExtensionName; + if (activeExt) { + customDir = this.config.getExtensionSetting( + activeExt, + 'plan.directory', + ); + } + try { + plansDir = this.config.storage.getPlansDir(customDir); + } catch { + // ignore + } + } const declarations: FunctionDeclaration[] = []; const seenNames = new Set(); diff --git a/packages/core/src/tools/tools.ts b/packages/core/src/tools/tools.ts index cd6209079c..432addc729 100644 --- a/packages/core/src/tools/tools.ts +++ b/packages/core/src/tools/tools.ts @@ -38,6 +38,7 @@ export interface ExecuteOptions { updateOutput?: (output: ToolLiveOutput) => void; shellExecutionConfig?: ShellExecutionConfig; setExecutionIdCallback?: (executionId: number) => void; + activeExtensionName?: string; } /** diff --git a/packages/core/src/tools/write-file.ts b/packages/core/src/tools/write-file.ts index 5766789f0c..4bf60bdbe2 100644 --- a/packages/core/src/tools/write-file.ts +++ b/packages/core/src/tools/write-file.ts @@ -169,10 +169,22 @@ class WriteFileToolInvocation extends BaseToolInvocation< if (this.config.isPlanMode()) { const safeFilename = path.basename(this.params.file_path); - this.resolvedPath = path.join( - this.config.storage.getPlansDir(), - safeFilename, - ); + let customDir: string | undefined; + const activeExt = this.config.activeExtensionName; + if (activeExt) { + customDir = this.config.getExtensionSetting( + activeExt, + 'plan.directory', + ); + } + try { + this.resolvedPath = path.join( + this.config.storage.getPlansDir(customDir), + safeFilename, + ); + } catch { + this.resolvedPath = ''; // handled safely downstream + } } else { this.resolvedPath = path.resolve( this.config.getTargetDir(),