diff --git a/docs/cli/plan-mode.md b/docs/cli/plan-mode.md index 751794996b..1e88560f7a 100644 --- a/docs/cli/plan-mode.md +++ b/docs/cli/plan-mode.md @@ -99,7 +99,7 @@ These are the only allowed tools: - **MCP Tools (Read):** Read-only [MCP tools] (e.g., `github_read_issue`, `postgres_read_schema`) are allowed. - **Planning (Write):** [`write_file`] and [`replace`] ONLY allowed for `.md` - files in the `~/.gemini/tmp//plans/` directory. + files in the `~/.gemini/tmp///plans/` directory. - **Skills:** [`activate_skill`] (allows loading specialized instructions and resources in a read-only manner) diff --git a/packages/cli/src/config/policy-engine.integration.test.ts b/packages/cli/src/config/policy-engine.integration.test.ts index 0568aa62bc..2c7ce599da 100644 --- a/packages/cli/src/config/policy-engine.integration.test.ts +++ b/packages/cli/src/config/policy-engine.integration.test.ts @@ -336,9 +336,9 @@ describe('Policy Engine Integration Tests', () => { // Valid plan file paths const validPaths = [ - '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/my-plan.md', - '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/feature_auth.md', - '/home/user/.gemini/tmp/new-temp_dir_123/plans/plan.md', // new style of temp directory + '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/my-plan.md', + '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/feature_auth.md', + '/home/user/.gemini/tmp/new-temp_dir_123/session-1/plans/plan.md', // new style of temp directory ]; for (const file_path of validPaths) { @@ -365,7 +365,6 @@ describe('Policy Engine Integration Tests', () => { '/project/src/file.ts', // Workspace '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/script.js', // Wrong extension '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/../../../etc/passwd.md', // Path traversal - '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/subdir/plan.md', // Subdirectory '/home/user/.gemini/non-tmp/new-temp_dir_123/plans/plan.md', // outside of temp dir ]; diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 8aab58da08..6dfc62f322 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -823,7 +823,7 @@ export class Config { (params.shellToolInactivityTimeout ?? 300) * 1000; // 5 minutes this.extensionManagement = params.extensionManagement ?? true; this.enableExtensionReloading = params.enableExtensionReloading ?? false; - this.storage = new Storage(this.targetDir); + this.storage = new Storage(this.targetDir, this.sessionId); this.fakeResponses = params.fakeResponses; this.recordResponses = params.recordResponses; diff --git a/packages/core/src/config/storage.test.ts b/packages/core/src/config/storage.test.ts index 8232033c07..8d91ca1a3e 100644 --- a/packages/core/src/config/storage.test.ts +++ b/packages/core/src/config/storage.test.ts @@ -154,12 +154,24 @@ describe('Storage – additional helpers', () => { expect(Storage.getGlobalBinDir()).toBe(expected); }); - it('getProjectTempPlansDir returns ~/.gemini/tmp//plans', async () => { + it('getProjectTempPlansDir returns ~/.gemini/tmp//plans when no sessionId is provided', async () => { await storage.initialize(); const tempDir = storage.getProjectTempDir(); const expected = path.join(tempDir, 'plans'); expect(storage.getProjectTempPlansDir()).toBe(expected); }); + + it('getProjectTempPlansDir returns ~/.gemini/tmp///plans when sessionId is provided', async () => { + const sessionId = 'test-session-id'; + const storageWithSession = new Storage(projectRoot, sessionId); + ProjectRegistry.prototype.getShortId = vi + .fn() + .mockReturnValue(PROJECT_SLUG); + await storageWithSession.initialize(); + const tempDir = storageWithSession.getProjectTempDir(); + const expected = path.join(tempDir, sessionId, 'plans'); + expect(storageWithSession.getProjectTempPlansDir()).toBe(expected); + }); }); describe('Storage - System Paths', () => { diff --git a/packages/core/src/config/storage.ts b/packages/core/src/config/storage.ts index f407c29539..bd0fec1c8e 100644 --- a/packages/core/src/config/storage.ts +++ b/packages/core/src/config/storage.ts @@ -20,11 +20,13 @@ const AGENTS_DIR_NAME = '.agents'; export class Storage { private readonly targetDir: string; + private readonly sessionId: string | undefined; private projectIdentifier: string | undefined; private initPromise: Promise | undefined; - constructor(targetDir: string) { + constructor(targetDir: string, sessionId?: string) { this.targetDir = targetDir; + this.sessionId = sessionId; } static getGlobalGeminiDir(): string { @@ -242,9 +244,19 @@ export class Storage { } getProjectTempPlansDir(): string { + if (this.sessionId) { + return path.join(this.getProjectTempDir(), this.sessionId, 'plans'); + } return path.join(this.getProjectTempDir(), 'plans'); } + getProjectTempTasksDir(): string { + if (this.sessionId) { + return path.join(this.getProjectTempDir(), this.sessionId, 'tasks'); + } + return path.join(this.getProjectTempDir(), 'tasks'); + } + getExtensionsDir(): string { return path.join(this.getGeminiDir(), 'extensions'); } diff --git a/packages/core/src/policy/policies/plan.toml b/packages/core/src/policy/policies/plan.toml index 656c100845..12648fec5f 100644 --- a/packages/core/src/policy/policies/plan.toml +++ b/packages/core/src/policy/policies/plan.toml @@ -53,4 +53,4 @@ toolName = ["write_file", "replace"] decision = "allow" priority = 70 modes = ["plan"] -argsPattern = "\"file_path\":\"[^\"]+/\\.gemini/tmp/[a-zA-Z0-9_-]+/plans/[a-zA-Z0-9_-]+\\.md\"" +argsPattern = "\"file_path\":\"[^\"]+/\\.gemini/tmp/[a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+/plans/[a-zA-Z0-9_-]+\\.md\""