fix(plan): ensure sessionId is always present for plans storage

The Plan Mode security policy expects a consistent two-level subdirectory structure after tmp/ (~/.gemini/tmp/<project>/<session-id>/plans/). Previously, Storage allowed sessionId to be optional, which resulted in a one-level structure (~/.gemini/tmp/<project>/plans/) when missing, causing the Policy Engine to deny write operations.

This change enforces sessionId in the Storage constructor (defaulting to the process-wide sessionId) and ensures the Storage instance is updated when a session is resumed via Config.setSessionId(). This guarantees that generated paths always match the policy's regular expression, even across session restarts.

Fixes https://github.com/google-gemini/gemini-cli/issues/21359
This commit is contained in:
Jerop Kipruto
2026-03-05 22:08:53 -05:00
parent 4d310dda68
commit bdc78795ad
3 changed files with 13 additions and 12 deletions

View File

@@ -1349,6 +1349,7 @@ export class Config implements McpContext {
setSessionId(sessionId: string): void {
this.sessionId = sessionId;
this.storage.setSessionId(sessionId);
}
setTerminalBackground(terminalBackground: string | undefined): void {

View File

@@ -13,6 +13,7 @@ vi.unmock('./storageMigration.js');
import * as os from 'node:os';
import * as path from 'node:path';
import * as fs from 'node:fs';
import { sessionId as defaultSessionId } from '../utils/session.js';
vi.mock('fs', async (importOriginal) => {
const actual = await importOriginal<typeof import('fs')>();
@@ -161,10 +162,10 @@ describe('Storage additional helpers', () => {
expect(Storage.getGlobalBinDir()).toBe(expected);
});
it('getProjectTempPlansDir returns ~/.gemini/tmp/<identifier>/plans when no sessionId is provided', async () => {
it('getProjectTempPlansDir returns ~/.gemini/tmp/<identifier>/<defaultSessionId>/plans when no sessionId is provided', async () => {
await storage.initialize();
const tempDir = storage.getProjectTempDir();
const expected = path.join(tempDir, 'plans');
const expected = path.join(tempDir, defaultSessionId, 'plans');
expect(storage.getProjectTempPlansDir()).toBe(expected);
});

View File

@@ -16,6 +16,7 @@ import {
resolveToRealPath,
normalizePath,
} from '../utils/paths.js';
import { sessionId as defaultSessionId } from '../utils/session.js';
import { ProjectRegistry } from './projectRegistry.js';
import { StorageMigration } from './storageMigration.js';
@@ -28,16 +29,20 @@ export const AUTO_SAVED_POLICY_FILENAME = 'auto-saved.toml';
export class Storage {
private readonly targetDir: string;
private readonly sessionId: string | undefined;
private sessionId: string;
private projectIdentifier: string | undefined;
private initPromise: Promise<void> | undefined;
private customPlansDir: string | undefined;
constructor(targetDir: string, sessionId?: string) {
constructor(targetDir: string, sessionId: string = defaultSessionId) {
this.targetDir = targetDir;
this.sessionId = sessionId;
}
setSessionId(sessionId: string): void {
this.sessionId = sessionId;
}
setCustomPlansDir(dir: string | undefined): void {
this.customPlansDir = dir;
}
@@ -280,10 +285,7 @@ export class Storage {
}
getProjectTempPlansDir(): string {
if (this.sessionId) {
return path.join(this.getProjectTempDir(), this.sessionId, 'plans');
}
return path.join(this.getProjectTempDir(), 'plans');
return path.join(this.getProjectTempDir(), this.sessionId, 'plans');
}
getProjectTempTrackerDir(): string {
@@ -311,10 +313,7 @@ export class Storage {
}
getProjectTempTasksDir(): string {
if (this.sessionId) {
return path.join(this.getProjectTempDir(), this.sessionId, 'tasks');
}
return path.join(this.getProjectTempDir(), 'tasks');
return path.join(this.getProjectTempDir(), this.sessionId, 'tasks');
}
async listProjectChatFiles(): Promise<