mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-24 20:14:44 -07:00
fix(plan): isolate plan files per session (#18757)
This commit is contained in:
@@ -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
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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', () => {
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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\""
|
||||||
|
|||||||
Reference in New Issue
Block a user