mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
Harden modifiable tool temp workspace (#12837)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -85,11 +85,19 @@ describe('modifyWithEditor', () => {
|
|||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
await fsp.rm(testProjectDir, { recursive: true, force: true });
|
await fsp.rm(testProjectDir, { recursive: true, force: true });
|
||||||
const diffDir = path.join(os.tmpdir(), 'gemini-cli-tool-modify-diffs');
|
|
||||||
await fsp.rm(diffDir, { recursive: true, force: true });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('successful modification', () => {
|
describe('successful modification', () => {
|
||||||
|
const assertMode = (mode: number, expected: number): void => {
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
// Windows reports POSIX modes as 0o666 regardless of requested bits.
|
||||||
|
// At minimum confirm the owner read/write bits are present.
|
||||||
|
expect(mode & 0o600).toBe(0o600);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
expect(mode & 0o777).toBe(expected);
|
||||||
|
};
|
||||||
|
|
||||||
it('should successfully modify content with VSCode editor', async () => {
|
it('should successfully modify content with VSCode editor', async () => {
|
||||||
const result = await modifyWithEditor(
|
const result = await modifyWithEditor(
|
||||||
mockParams,
|
mockParams,
|
||||||
@@ -142,9 +150,21 @@ describe('modifyWithEditor', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create temp directory if it does not exist', async () => {
|
it('should create temp directory and files with restrictive permissions', async () => {
|
||||||
const diffDir = path.join(os.tmpdir(), 'gemini-cli-tool-modify-diffs');
|
mockOpenDiff.mockImplementation(async (oldPath, newPath) => {
|
||||||
await fsp.rm(diffDir, { recursive: true, force: true }).catch(() => {});
|
const diffDir = path.dirname(oldPath);
|
||||||
|
expect(diffDir).toBe(path.dirname(newPath));
|
||||||
|
|
||||||
|
const dirStats = await fsp.stat(diffDir);
|
||||||
|
const oldStats = await fsp.stat(oldPath);
|
||||||
|
const newStats = await fsp.stat(newPath);
|
||||||
|
|
||||||
|
assertMode(dirStats.mode, 0o700);
|
||||||
|
assertMode(oldStats.mode, 0o600);
|
||||||
|
assertMode(newStats.mode, 0o600);
|
||||||
|
|
||||||
|
await fsp.writeFile(newPath, modifiedContent, 'utf8');
|
||||||
|
});
|
||||||
|
|
||||||
await modifyWithEditor(
|
await modifyWithEditor(
|
||||||
mockParams,
|
mockParams,
|
||||||
@@ -153,27 +173,6 @@ describe('modifyWithEditor', () => {
|
|||||||
abortSignal,
|
abortSignal,
|
||||||
vi.fn(),
|
vi.fn(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const stats = await fsp.stat(diffDir);
|
|
||||||
expect(stats.isDirectory()).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not create temp directory if it already exists', async () => {
|
|
||||||
const diffDir = path.join(os.tmpdir(), 'gemini-cli-tool-modify-diffs');
|
|
||||||
await fsp.mkdir(diffDir, { recursive: true });
|
|
||||||
|
|
||||||
const mkdirSpy = vi.spyOn(fs, 'mkdirSync');
|
|
||||||
|
|
||||||
await modifyWithEditor(
|
|
||||||
mockParams,
|
|
||||||
mockModifyContext,
|
|
||||||
'vscode' as EditorType,
|
|
||||||
abortSignal,
|
|
||||||
vi.fn(),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(mkdirSpy).not.toHaveBeenCalled();
|
|
||||||
mkdirSpy.mockRestore();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -269,6 +268,9 @@ describe('modifyWithEditor', () => {
|
|||||||
vi.spyOn(fs, 'unlinkSync').mockImplementation(() => {
|
vi.spyOn(fs, 'unlinkSync').mockImplementation(() => {
|
||||||
throw new Error('Failed to delete file');
|
throw new Error('Failed to delete file');
|
||||||
});
|
});
|
||||||
|
vi.spyOn(fs, 'rmdirSync').mockImplementation(() => {
|
||||||
|
throw new Error('Failed to delete directory');
|
||||||
|
});
|
||||||
|
|
||||||
await modifyWithEditor(
|
await modifyWithEditor(
|
||||||
mockParams,
|
mockParams,
|
||||||
@@ -278,10 +280,13 @@ describe('modifyWithEditor', () => {
|
|||||||
vi.fn(),
|
vi.fn(),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledTimes(2);
|
expect(consoleErrorSpy).toHaveBeenCalledTimes(3);
|
||||||
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
expect.stringContaining('Error deleting temp diff file:'),
|
expect.stringContaining('Error deleting temp diff file:'),
|
||||||
);
|
);
|
||||||
|
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
||||||
|
expect.stringContaining('Error deleting temp diff directory:'),
|
||||||
|
);
|
||||||
|
|
||||||
consoleErrorSpy.mockRestore();
|
consoleErrorSpy.mockRestore();
|
||||||
});
|
});
|
||||||
@@ -307,9 +312,9 @@ describe('modifyWithEditor', () => {
|
|||||||
expect(oldFilePath).toMatch(/gemini-cli-modify-test-file-old-\d+\.txt$/);
|
expect(oldFilePath).toMatch(/gemini-cli-modify-test-file-old-\d+\.txt$/);
|
||||||
expect(newFilePath).toMatch(/gemini-cli-modify-test-file-new-\d+\.txt$/);
|
expect(newFilePath).toMatch(/gemini-cli-modify-test-file-new-\d+\.txt$/);
|
||||||
|
|
||||||
const diffDir = path.join(os.tmpdir(), 'gemini-cli-tool-modify-diffs');
|
const diffDirPrefix = path.join(os.tmpdir(), 'gemini-cli-tool-modify-');
|
||||||
expect(path.dirname(oldFilePath)).toBe(diffDir);
|
expect(path.dirname(oldFilePath).startsWith(diffDirPrefix)).toBe(true);
|
||||||
expect(path.dirname(newFilePath)).toBe(diffDir);
|
expect(path.dirname(newFilePath).startsWith(diffDirPrefix)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create temp files with correct naming without extension', async () => {
|
it('should create temp files with correct naming without extension', async () => {
|
||||||
@@ -329,9 +334,9 @@ describe('modifyWithEditor', () => {
|
|||||||
expect(oldFilePath).toMatch(/gemini-cli-modify-test-file-old-\d+$/);
|
expect(oldFilePath).toMatch(/gemini-cli-modify-test-file-old-\d+$/);
|
||||||
expect(newFilePath).toMatch(/gemini-cli-modify-test-file-new-\d+$/);
|
expect(newFilePath).toMatch(/gemini-cli-modify-test-file-new-\d+$/);
|
||||||
|
|
||||||
const diffDir = path.join(os.tmpdir(), 'gemini-cli-tool-modify-diffs');
|
const diffDirPrefix = path.join(os.tmpdir(), 'gemini-cli-tool-modify-');
|
||||||
expect(path.dirname(oldFilePath)).toBe(diffDir);
|
expect(path.dirname(oldFilePath).startsWith(diffDirPrefix)).toBe(true);
|
||||||
expect(path.dirname(newFilePath)).toBe(diffDir);
|
expect(path.dirname(newFilePath).startsWith(diffDirPrefix)).toBe(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -59,12 +59,19 @@ function createTempFilesForModify(
|
|||||||
currentContent: string,
|
currentContent: string,
|
||||||
proposedContent: string,
|
proposedContent: string,
|
||||||
file_path: string,
|
file_path: string,
|
||||||
): { oldPath: string; newPath: string } {
|
): { oldPath: string; newPath: string; dirPath: string } {
|
||||||
const tempDir = os.tmpdir();
|
const diffDir = fs.mkdtempSync(
|
||||||
const diffDir = path.join(tempDir, 'gemini-cli-tool-modify-diffs');
|
path.join(os.tmpdir(), 'gemini-cli-tool-modify-'),
|
||||||
|
);
|
||||||
|
|
||||||
if (!fs.existsSync(diffDir)) {
|
try {
|
||||||
fs.mkdirSync(diffDir, { recursive: true });
|
fs.chmodSync(diffDir, 0o700);
|
||||||
|
} catch (e) {
|
||||||
|
debugLogger.error(
|
||||||
|
`Error setting permissions on temp diff directory: ${diffDir}`,
|
||||||
|
e,
|
||||||
|
);
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ext = path.extname(file_path);
|
const ext = path.extname(file_path);
|
||||||
@@ -79,10 +86,16 @@ function createTempFilesForModify(
|
|||||||
`gemini-cli-modify-${fileName}-new-${timestamp}${ext}`,
|
`gemini-cli-modify-${fileName}-new-${timestamp}${ext}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
fs.writeFileSync(tempOldPath, currentContent, 'utf8');
|
fs.writeFileSync(tempOldPath, currentContent, {
|
||||||
fs.writeFileSync(tempNewPath, proposedContent, 'utf8');
|
encoding: 'utf8',
|
||||||
|
mode: 0o600,
|
||||||
|
});
|
||||||
|
fs.writeFileSync(tempNewPath, proposedContent, {
|
||||||
|
encoding: 'utf8',
|
||||||
|
mode: 0o600,
|
||||||
|
});
|
||||||
|
|
||||||
return { oldPath: tempOldPath, newPath: tempNewPath };
|
return { oldPath: tempOldPath, newPath: tempNewPath, dirPath: diffDir };
|
||||||
}
|
}
|
||||||
|
|
||||||
function getUpdatedParams<ToolParams>(
|
function getUpdatedParams<ToolParams>(
|
||||||
@@ -125,7 +138,11 @@ function getUpdatedParams<ToolParams>(
|
|||||||
return { updatedParams, updatedDiff };
|
return { updatedParams, updatedDiff };
|
||||||
}
|
}
|
||||||
|
|
||||||
function deleteTempFiles(oldPath: string, newPath: string): void {
|
function deleteTempFiles(
|
||||||
|
oldPath: string,
|
||||||
|
newPath: string,
|
||||||
|
dirPath: string,
|
||||||
|
): void {
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(oldPath);
|
fs.unlinkSync(oldPath);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -137,6 +154,12 @@ function deleteTempFiles(oldPath: string, newPath: string): void {
|
|||||||
} catch {
|
} catch {
|
||||||
debugLogger.error(`Error deleting temp diff file: ${newPath}`);
|
debugLogger.error(`Error deleting temp diff file: ${newPath}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.rmdirSync(dirPath);
|
||||||
|
} catch {
|
||||||
|
debugLogger.error(`Error deleting temp diff directory: ${dirPath}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -154,7 +177,7 @@ export async function modifyWithEditor<ToolParams>(
|
|||||||
const proposedContent =
|
const proposedContent =
|
||||||
await modifyContext.getProposedContent(originalParams);
|
await modifyContext.getProposedContent(originalParams);
|
||||||
|
|
||||||
const { oldPath, newPath } = createTempFilesForModify(
|
const { oldPath, newPath, dirPath } = createTempFilesForModify(
|
||||||
currentContent,
|
currentContent,
|
||||||
proposedContent,
|
proposedContent,
|
||||||
modifyContext.getFilePath(originalParams),
|
modifyContext.getFilePath(originalParams),
|
||||||
@@ -171,6 +194,6 @@ export async function modifyWithEditor<ToolParams>(
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
deleteTempFiles(oldPath, newPath);
|
deleteTempFiles(oldPath, newPath, dirPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user