mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-28 15:01:14 -07:00
Implementation of sandbox "Write-Protected" Governance Files (#23139)
Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com>
This commit is contained in:
@@ -76,6 +76,16 @@ export interface SandboxManager {
|
||||
prepareCommand(req: SandboxRequest): Promise<SandboxedCommand>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Files that represent the governance or "constitution" of the repository
|
||||
* and should be write-protected in any sandbox.
|
||||
*/
|
||||
export const GOVERNANCE_FILES = [
|
||||
{ path: '.gitignore', isDirectory: false },
|
||||
{ path: '.geminiignore', isDirectory: false },
|
||||
{ path: '.git', isDirectory: true },
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* A no-op implementation of SandboxManager that silently passes commands
|
||||
* through while applying environment sanitization.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { WindowsSandboxManager } from './windowsSandboxManager.js';
|
||||
@@ -17,21 +18,24 @@ vi.mock('../utils/shell-utils.js', () => ({
|
||||
|
||||
describe('WindowsSandboxManager', () => {
|
||||
let manager: WindowsSandboxManager;
|
||||
let testCwd: string;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.spyOn(os, 'platform').mockReturnValue('win32');
|
||||
manager = new WindowsSandboxManager({ workspace: '/test/workspace' });
|
||||
testCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'gemini-cli-test-'));
|
||||
manager = new WindowsSandboxManager({ workspace: testCwd });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
fs.rmSync(testCwd, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('should prepare a GeminiSandbox.exe command', async () => {
|
||||
const req: SandboxRequest = {
|
||||
command: 'whoami',
|
||||
args: ['/groups'],
|
||||
cwd: '/test/cwd',
|
||||
cwd: testCwd,
|
||||
env: { TEST_VAR: 'test_value' },
|
||||
policy: {
|
||||
networkAccess: false,
|
||||
@@ -41,14 +45,14 @@ describe('WindowsSandboxManager', () => {
|
||||
const result = await manager.prepareCommand(req);
|
||||
|
||||
expect(result.program).toContain('GeminiSandbox.exe');
|
||||
expect(result.args).toEqual(['0', '/test/cwd', 'whoami', '/groups']);
|
||||
expect(result.args).toEqual(['0', testCwd, 'whoami', '/groups']);
|
||||
});
|
||||
|
||||
it('should handle networkAccess from config', async () => {
|
||||
const req: SandboxRequest = {
|
||||
command: 'whoami',
|
||||
args: [],
|
||||
cwd: '/test/cwd',
|
||||
cwd: testCwd,
|
||||
env: {},
|
||||
policy: {
|
||||
networkAccess: true,
|
||||
@@ -63,7 +67,7 @@ describe('WindowsSandboxManager', () => {
|
||||
const req: SandboxRequest = {
|
||||
command: 'test',
|
||||
args: [],
|
||||
cwd: '/test/cwd',
|
||||
cwd: testCwd,
|
||||
env: {
|
||||
API_KEY: 'secret',
|
||||
PATH: '/usr/bin',
|
||||
@@ -82,29 +86,53 @@ describe('WindowsSandboxManager', () => {
|
||||
expect(result.env['API_KEY']).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should grant Low Integrity access to the workspace and allowed paths', async () => {
|
||||
it('should ensure governance files exist', async () => {
|
||||
const req: SandboxRequest = {
|
||||
command: 'test',
|
||||
args: [],
|
||||
cwd: '/test/cwd',
|
||||
cwd: testCwd,
|
||||
env: {},
|
||||
policy: {
|
||||
allowedPaths: ['/test/allowed1'],
|
||||
},
|
||||
};
|
||||
|
||||
await manager.prepareCommand(req);
|
||||
|
||||
expect(spawnAsync).toHaveBeenCalledWith('icacls', [
|
||||
path.resolve('/test/workspace'),
|
||||
'/setintegritylevel',
|
||||
'Low',
|
||||
]);
|
||||
expect(fs.existsSync(path.join(testCwd, '.gitignore'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(testCwd, '.geminiignore'))).toBe(true);
|
||||
expect(fs.existsSync(path.join(testCwd, '.git'))).toBe(true);
|
||||
expect(fs.lstatSync(path.join(testCwd, '.git')).isDirectory()).toBe(true);
|
||||
});
|
||||
|
||||
expect(spawnAsync).toHaveBeenCalledWith('icacls', [
|
||||
path.resolve('/test/allowed1'),
|
||||
'/setintegritylevel',
|
||||
'Low',
|
||||
]);
|
||||
it('should grant Low Integrity access to the workspace and allowed paths', async () => {
|
||||
const allowedPath = path.join(os.tmpdir(), 'gemini-cli-test-allowed');
|
||||
if (!fs.existsSync(allowedPath)) {
|
||||
fs.mkdirSync(allowedPath);
|
||||
}
|
||||
try {
|
||||
const req: SandboxRequest = {
|
||||
command: 'test',
|
||||
args: [],
|
||||
cwd: testCwd,
|
||||
env: {},
|
||||
policy: {
|
||||
allowedPaths: [allowedPath],
|
||||
},
|
||||
};
|
||||
|
||||
await manager.prepareCommand(req);
|
||||
|
||||
expect(spawnAsync).toHaveBeenCalledWith('icacls', [
|
||||
path.resolve(testCwd),
|
||||
'/setintegritylevel',
|
||||
'Low',
|
||||
]);
|
||||
|
||||
expect(spawnAsync).toHaveBeenCalledWith('icacls', [
|
||||
path.resolve(allowedPath),
|
||||
'/setintegritylevel',
|
||||
'Low',
|
||||
]);
|
||||
} finally {
|
||||
fs.rmSync(allowedPath, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
type SandboxManager,
|
||||
type SandboxRequest,
|
||||
type SandboxedCommand,
|
||||
GOVERNANCE_FILES,
|
||||
type GlobalSandboxOptions,
|
||||
sanitizePaths,
|
||||
} from './sandboxManager.js';
|
||||
@@ -39,6 +40,28 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
this.helperPath = path.resolve(__dirname, 'scripts', 'GeminiSandbox.exe');
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures a file or directory exists.
|
||||
*/
|
||||
private touch(filePath: string, isDirectory: boolean): void {
|
||||
try {
|
||||
// If it exists (even as a broken symlink), do nothing
|
||||
if (fs.lstatSync(filePath)) return;
|
||||
} catch {
|
||||
// Ignore ENOENT
|
||||
}
|
||||
|
||||
if (isDirectory) {
|
||||
fs.mkdirSync(filePath, { recursive: true });
|
||||
} else {
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
fs.closeSync(fs.openSync(filePath, 'a'));
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureInitialized(): Promise<void> {
|
||||
if (this.initialized) return;
|
||||
if (os.platform() !== 'win32') {
|
||||
@@ -164,7 +187,28 @@ export class WindowsSandboxManager implements SandboxManager {
|
||||
|
||||
// TODO: handle forbidden paths
|
||||
|
||||
// 2. Construct the helper command
|
||||
// 2. Protected governance files
|
||||
// These must exist on the host before running the sandbox to prevent
|
||||
// the sandboxed process from creating them with Low integrity.
|
||||
// By being created as Medium integrity, they are write-protected from Low processes.
|
||||
for (const file of GOVERNANCE_FILES) {
|
||||
const filePath = path.join(this.options.workspace, file.path);
|
||||
this.touch(filePath, file.isDirectory);
|
||||
|
||||
// We resolve real paths to ensure protection for both the symlink and its target.
|
||||
try {
|
||||
const realPath = fs.realpathSync(filePath);
|
||||
if (realPath !== filePath) {
|
||||
// If it's a symlink, the target is already implicitly protected
|
||||
// if it's outside the Low integrity workspace (likely Medium).
|
||||
// If it's inside, we ensure it's not accidentally Low.
|
||||
}
|
||||
} catch {
|
||||
// Ignore realpath errors
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Construct the helper command
|
||||
// GeminiSandbox.exe <network:0|1> <cwd> <command> [args...]
|
||||
const program = this.helperPath;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user