mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-08 04:10:35 -07:00
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
177 lines
5.0 KiB
TypeScript
177 lines
5.0 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2026 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import { spawn } from 'node:child_process';
|
|
import { type FileSystemService } from './fileSystemService.js';
|
|
import { type SandboxManager } from './sandboxManager.js';
|
|
import { debugLogger } from '../utils/debugLogger.js';
|
|
import { isNodeError } from '../utils/errors.js';
|
|
import { resolveToRealPath, isSubpath } from '../utils/paths.js';
|
|
|
|
/**
|
|
* A FileSystemService implementation that performs operations through a sandbox.
|
|
*/
|
|
export class SandboxedFileSystemService implements FileSystemService {
|
|
constructor(
|
|
private sandboxManager: SandboxManager,
|
|
private cwd: string,
|
|
) {}
|
|
|
|
private sanitizeAndValidatePath(filePath: string): string {
|
|
const resolvedPath = resolveToRealPath(filePath);
|
|
const workspace = resolveToRealPath(this.sandboxManager.getWorkspace());
|
|
|
|
if (isSubpath(workspace, resolvedPath) || workspace === resolvedPath) {
|
|
return resolvedPath;
|
|
}
|
|
|
|
// Check if the path is explicitly allowed by the sandbox manager
|
|
const options = this.sandboxManager.getOptions();
|
|
const allowedPaths = options?.includeDirectories ?? [];
|
|
|
|
for (const allowed of allowedPaths) {
|
|
const resolvedAllowed = resolveToRealPath(allowed);
|
|
if (
|
|
isSubpath(resolvedAllowed, resolvedPath) ||
|
|
resolvedAllowed === resolvedPath
|
|
) {
|
|
return resolvedPath;
|
|
}
|
|
}
|
|
|
|
throw new Error(
|
|
`Access denied: Path '${filePath}' is outside the workspace and not in allowed paths.`,
|
|
);
|
|
}
|
|
|
|
async readTextFile(filePath: string): Promise<string> {
|
|
const safePath = this.sanitizeAndValidatePath(filePath);
|
|
const prepared = await this.sandboxManager.prepareCommand({
|
|
command: '__read',
|
|
args: [safePath],
|
|
cwd: this.cwd,
|
|
env: process.env,
|
|
policy: {
|
|
allowedPaths: [safePath],
|
|
},
|
|
});
|
|
|
|
return new Promise((resolve, reject) => {
|
|
// Direct spawn is necessary here for streaming large file contents.
|
|
|
|
const child = spawn(prepared.program, prepared.args, {
|
|
cwd: this.cwd,
|
|
env: prepared.env,
|
|
});
|
|
|
|
let output = '';
|
|
let error = '';
|
|
|
|
child.stdout?.on('data', (data) => {
|
|
output += data.toString();
|
|
});
|
|
|
|
child.stderr?.on('data', (data) => {
|
|
error += data.toString();
|
|
});
|
|
|
|
child.on('close', (code) => {
|
|
if (code === 0) {
|
|
resolve(output);
|
|
} else {
|
|
const isEnoent =
|
|
error.toLowerCase().includes('no such file or directory') ||
|
|
error.toLowerCase().includes('enoent') ||
|
|
error.toLowerCase().includes('could not find file') ||
|
|
error.toLowerCase().includes('could not find a part of the path');
|
|
const err = new Error(
|
|
`Sandbox Error: read_file failed for '${filePath}'. Exit code ${code}. ${error ? 'Details: ' + error : ''}`,
|
|
);
|
|
if (isEnoent) {
|
|
Object.assign(err, { code: 'ENOENT' });
|
|
}
|
|
reject(err);
|
|
}
|
|
});
|
|
|
|
child.on('error', (err) => {
|
|
reject(
|
|
new Error(
|
|
`Sandbox Error: Failed to spawn read_file for '${filePath}': ${err.message}`,
|
|
),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
|
|
async writeTextFile(filePath: string, content: string): Promise<void> {
|
|
const safePath = this.sanitizeAndValidatePath(filePath);
|
|
const prepared = await this.sandboxManager.prepareCommand({
|
|
command: '__write',
|
|
args: [safePath],
|
|
cwd: this.cwd,
|
|
env: process.env,
|
|
policy: {
|
|
allowedPaths: [safePath],
|
|
additionalPermissions: {
|
|
fileSystem: {
|
|
write: [safePath],
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
return new Promise((resolve, reject) => {
|
|
// Direct spawn is necessary here for streaming large file contents.
|
|
|
|
const child = spawn(prepared.program, prepared.args, {
|
|
cwd: this.cwd,
|
|
env: prepared.env,
|
|
});
|
|
|
|
child.stdin?.on('error', (err) => {
|
|
// Silently ignore EPIPE errors on stdin, they will be caught by the process error/close listeners
|
|
if (isNodeError(err) && err.code === 'EPIPE') {
|
|
return;
|
|
}
|
|
debugLogger.error(
|
|
`Sandbox Error: stdin error for '${filePath}': ${
|
|
err instanceof Error ? err.message : String(err)
|
|
}`,
|
|
);
|
|
});
|
|
|
|
child.stdin?.write(content);
|
|
child.stdin?.end();
|
|
|
|
let error = '';
|
|
child.stderr?.on('data', (data) => {
|
|
error += data.toString();
|
|
});
|
|
|
|
child.on('close', (code) => {
|
|
if (code === 0) {
|
|
resolve();
|
|
} else {
|
|
reject(
|
|
new Error(
|
|
`Sandbox Error: write_file failed for '${filePath}'. Exit code ${code}. ${error ? 'Details: ' + error : ''}`,
|
|
),
|
|
);
|
|
}
|
|
});
|
|
|
|
child.on('error', (err) => {
|
|
reject(
|
|
new Error(
|
|
`Sandbox Error: Failed to spawn write_file for '${filePath}': ${err.message}`,
|
|
),
|
|
);
|
|
});
|
|
});
|
|
}
|
|
}
|