fix(plan): isolate plan files per session (#18757)

This commit is contained in:
Adib234
2026-02-12 14:02:59 -05:00
committed by GitHub
parent d243dfce14
commit 0b3130cec7
6 changed files with 32 additions and 9 deletions
+1 -1
View File
@@ -99,7 +99,7 @@ These are the only allowed tools:
- **MCP Tools (Read):** Read-only [MCP tools] (e.g., `github_read_issue`, - **MCP Tools (Read):** Read-only [MCP tools] (e.g., `github_read_issue`,
`postgres_read_schema`) are allowed. `postgres_read_schema`) are allowed.
- **Planning (Write):** [`write_file`] and [`replace`] ONLY allowed for `.md` - **Planning (Write):** [`write_file`] and [`replace`] ONLY allowed for `.md`
files in the `~/.gemini/tmp/<project>/plans/` directory. files in the `~/.gemini/tmp/<project>/<session-id>/plans/` directory.
- **Skills:** [`activate_skill`] (allows loading specialized instructions and - **Skills:** [`activate_skill`] (allows loading specialized instructions and
resources in a read-only manner) resources in a read-only manner)
@@ -336,9 +336,9 @@ describe('Policy Engine Integration Tests', () => {
// Valid plan file paths // Valid plan file paths
const validPaths = [ const validPaths = [
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/my-plan.md', '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/my-plan.md',
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/feature_auth.md', '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/feature_auth.md',
'/home/user/.gemini/tmp/new-temp_dir_123/plans/plan.md', // new style of temp directory '/home/user/.gemini/tmp/new-temp_dir_123/session-1/plans/plan.md', // new style of temp directory
]; ];
for (const file_path of validPaths) { for (const file_path of validPaths) {
@@ -365,7 +365,6 @@ describe('Policy Engine Integration Tests', () => {
'/project/src/file.ts', // Workspace '/project/src/file.ts', // Workspace
'/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/script.js', // Wrong extension '/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/../../../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 '/home/user/.gemini/non-tmp/new-temp_dir_123/plans/plan.md', // outside of temp dir
]; ];
+1 -1
View File
@@ -823,7 +823,7 @@ export class Config {
(params.shellToolInactivityTimeout ?? 300) * 1000; // 5 minutes (params.shellToolInactivityTimeout ?? 300) * 1000; // 5 minutes
this.extensionManagement = params.extensionManagement ?? true; this.extensionManagement = params.extensionManagement ?? true;
this.enableExtensionReloading = params.enableExtensionReloading ?? false; this.enableExtensionReloading = params.enableExtensionReloading ?? false;
this.storage = new Storage(this.targetDir); this.storage = new Storage(this.targetDir, this.sessionId);
this.fakeResponses = params.fakeResponses; this.fakeResponses = params.fakeResponses;
this.recordResponses = params.recordResponses; this.recordResponses = params.recordResponses;
+13 -1
View File
@@ -154,12 +154,24 @@ describe('Storage additional helpers', () => {
expect(Storage.getGlobalBinDir()).toBe(expected); expect(Storage.getGlobalBinDir()).toBe(expected);
}); });
it('getProjectTempPlansDir returns ~/.gemini/tmp/<identifier>/plans', async () => { it('getProjectTempPlansDir returns ~/.gemini/tmp/<identifier>/plans when no sessionId is provided', async () => {
await storage.initialize(); await storage.initialize();
const tempDir = storage.getProjectTempDir(); const tempDir = storage.getProjectTempDir();
const expected = path.join(tempDir, 'plans'); const expected = path.join(tempDir, 'plans');
expect(storage.getProjectTempPlansDir()).toBe(expected); expect(storage.getProjectTempPlansDir()).toBe(expected);
}); });
it('getProjectTempPlansDir returns ~/.gemini/tmp/<identifier>/<sessionId>/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', () => { describe('Storage - System Paths', () => {
+13 -1
View File
@@ -20,11 +20,13 @@ const AGENTS_DIR_NAME = '.agents';
export class Storage { export class Storage {
private readonly targetDir: string; private readonly targetDir: string;
private readonly sessionId: string | undefined;
private projectIdentifier: string | undefined; private projectIdentifier: string | undefined;
private initPromise: Promise<void> | undefined; private initPromise: Promise<void> | undefined;
constructor(targetDir: string) { constructor(targetDir: string, sessionId?: string) {
this.targetDir = targetDir; this.targetDir = targetDir;
this.sessionId = sessionId;
} }
static getGlobalGeminiDir(): string { static getGlobalGeminiDir(): string {
@@ -242,9 +244,19 @@ export class Storage {
} }
getProjectTempPlansDir(): string { getProjectTempPlansDir(): string {
if (this.sessionId) {
return path.join(this.getProjectTempDir(), this.sessionId, 'plans');
}
return path.join(this.getProjectTempDir(), '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 { getExtensionsDir(): string {
return path.join(this.getGeminiDir(), 'extensions'); return path.join(this.getGeminiDir(), 'extensions');
} }
+1 -1
View File
@@ -53,4 +53,4 @@ toolName = ["write_file", "replace"]
decision = "allow" decision = "allow"
priority = 70 priority = 70
modes = ["plan"] 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\""