Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions docs/cli/sandbox.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,14 @@ To set up runsc:
2. Configure the Docker daemon to use the runsc runtime.
3. Verify the installation.

**Limitations**:

- Linux only (gVisor is not available on macOS or Windows).
- [IDE integration](../ide-integration/index.md) is not supported when using
`runsc` because gVisor's isolated network stack blocks communication with the
IDE companion server on the host loopback interface. Use `docker` sandboxing
if you need IDE companion integration inside a container.

### 5. LXC/LXD (Linux only, experimental)

Full-system container sandboxing using LXC/LXD. Unlike Docker/Podman, LXC
Expand Down
15 changes: 15 additions & 0 deletions docs/ide-integration/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,11 @@ If you are using Gemini CLI within a sandbox, be aware of the following:
IDE server on `host.docker.internal`. No special configuration is usually
required, but you may need to ensure your Docker networking setup allows
connections from the container to the host.
- **In a gVisor (`runsc`) sandbox:** The IDE companion integration is not
supported when running with `GEMINI_SANDBOX=runsc` because gVisor's isolated
user-space network stack blocks host loopback communication. Use
`GEMINI_SANDBOX=docker` if you require IDE companion integration with
container sandboxing.

## Troubleshooting

Expand All @@ -240,6 +245,16 @@ If you are using Gemini CLI within a sandbox, be aware of the following:
2. Open a new terminal window in your IDE to ensure it picks up the correct
environment.

- **Message:**
`🔴 Disconnected: Failed to connect to IDE companion extension in [IDE Name]: gVisor (runsc) sandboxing enforces strict network isolation which prevents host loopback communication.`

- **Cause:** You are running Gemini CLI with gVisor (`runsc`) sandboxing
enabled, which isolates container network traffic from the host loopback
interface used by the IDE companion server.
- **Solution:** Switch to standard Docker sandboxing (`GEMINI_SANDBOX=docker`)
or run Gemini CLI outside the `runsc` container when using IDE companion
features.

- **Message:**
`🔴 Disconnected: IDE connection error. The connection was lost unexpectedly. Please try reconnecting by running /ide enable`
- **Cause:** The connection to the IDE companion was lost.
Expand Down
87 changes: 80 additions & 7 deletions packages/cli/src/utils/sandbox.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1713,6 +1713,56 @@ describe('sandbox', () => {
);
});

it('should pass through IDE mode environment variables to lxc exec', async () => {
vi.stubEnv('TEST_LXC_LIST_OUTPUT', LXC_RUNNING);
vi.stubEnv('GEMINI_CLI_IDE_SERVER_PORT', '12345');
vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', '/workspace');
vi.stubEnv('GEMINI_CLI_IDE_SERVER_STDIO_COMMAND', 'node');
vi.stubEnv('GEMINI_CLI_IDE_SERVER_STDIO_ARGS', '["server.js"]');
vi.stubEnv('TERM_PROGRAM', 'vscode');

const config: SandboxConfig = createMockSandboxConfig({
command: 'lxc',
image: 'gemini-sandbox',
});

const mockSpawnProcess = new EventEmitter() as unknown as ReturnType<
typeof spawn
>;
mockSpawnProcess.on = vi.fn().mockImplementation((event, cb) => {
if (event === 'close') {
setTimeout(() => cb(0), 10);
}
return mockSpawnProcess;
});

vi.mocked(spawn).mockImplementation((cmd) => {
if (cmd === 'lxc') {
return mockSpawnProcess;
}
return new EventEmitter() as unknown as ReturnType<typeof spawn>;
});

await expect(start_sandbox(config)).resolves.toBe(0);

expect(spawn).toHaveBeenCalledWith(
'lxc',
expect.arrayContaining([
'--env',
'GEMINI_CLI_IDE_SERVER_PORT=12345',
'--env',
'GEMINI_CLI_IDE_WORKSPACE_PATH=/workspace',
'--env',
'GEMINI_CLI_IDE_SERVER_STDIO_COMMAND=node',
'--env',
'GEMINI_CLI_IDE_SERVER_STDIO_ARGS=["server.js"]',
'--env',
'TERM_PROGRAM=vscode',
]),
expect.objectContaining({ stdio: 'inherit' }),
);
});

it('should throw FatalSandboxError if lxc list fails', async () => {
process.env['TEST_LXC_LIST_OUTPUT'] = 'throw';
const config: SandboxConfig = createMockSandboxConfig({
Expand Down Expand Up @@ -1748,8 +1798,15 @@ describe('sandbox', () => {
});

describe('gVisor (runsc)', () => {
it('should use docker with --runtime=runsc on Linux', async () => {
it('should use docker with --runtime=runsc on Linux and forward GEMINI_SANDBOX=runsc and IDE env vars', async () => {
vi.mocked(os.platform).mockReturnValue('linux');
vi.stubEnv('GEMINI_CLI_IDE_SERVER_PORT', '54321');
vi.stubEnv('GEMINI_CLI_IDE_WORKSPACE_PATH', '/workspace/project');
vi.stubEnv('GEMINI_CLI_IDE_AUTH_TOKEN', 'ide-auth-token-123');
vi.stubEnv('GEMINI_CLI_IDE_SERVER_STDIO_COMMAND', 'ide-mcp-cmd');
vi.stubEnv('GEMINI_CLI_IDE_SERVER_STDIO_ARGS', '["--stdio"]');
vi.stubEnv('TERM_PROGRAM', 'vscode');

const config: SandboxConfig = createMockSandboxConfig({
command: 'runsc',
image: 'gemini-cli-sandbox',
Expand Down Expand Up @@ -1790,12 +1847,28 @@ describe('sandbox', () => {
expect.arrayContaining(['images', '-q', 'gemini-cli-sandbox']),
);

// Verify docker run includes --runtime=runsc
expect(spawn).toHaveBeenNthCalledWith(
2,
'docker',
expect.arrayContaining(['run', '--runtime=runsc']),
expect.objectContaining({ stdio: 'inherit' }),
// Verify docker run includes --runtime=runsc, GEMINI_SANDBOX=runsc, and safe IDE env vars (excluding GEMINI_CLI_IDE_AUTH_TOKEN)
const dockerRunArgs = vi.mocked(spawn).mock.calls[1][1] as string[];
expect(dockerRunArgs).toEqual(
expect.arrayContaining([
'run',
'--runtime=runsc',
'--env',
'GEMINI_SANDBOX=runsc',
'--env',
'GEMINI_CLI_IDE_SERVER_PORT=54321',
'--env',
'GEMINI_CLI_IDE_WORKSPACE_PATH=/workspace/project',
'--env',
'GEMINI_CLI_IDE_SERVER_STDIO_COMMAND=ide-mcp-cmd',
'--env',
'GEMINI_CLI_IDE_SERVER_STDIO_ARGS=["--stdio"]',
'--env',
'TERM_PROGRAM=vscode',
]),
);
expect(dockerRunArgs).not.toContain(
'GEMINI_CLI_IDE_AUTH_TOKEN=ide-auth-token-123',
);
});
});
Expand Down
12 changes: 12 additions & 0 deletions packages/cli/src/utils/sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -794,13 +794,21 @@ export async function start_sandbox(
for (const envVar of [
'GEMINI_CLI_IDE_SERVER_PORT',
'GEMINI_CLI_IDE_WORKSPACE_PATH',
'GEMINI_CLI_IDE_SERVER_STDIO_COMMAND',
'GEMINI_CLI_IDE_SERVER_STDIO_ARGS',
'TERM_PROGRAM',
]) {
Comment thread
elberthc-byte marked this conversation as resolved.
if (process.env[envVar]) {
args.push('--env', `${envVar}=${process.env[envVar]}`);
}
}

const geminiSandboxEnv =
config.command === 'runsc' ? 'runsc' : process.env['GEMINI_SANDBOX'];
if (geminiSandboxEnv) {
args.push('--env', `GEMINI_SANDBOX=${geminiSandboxEnv}`);
}

// copy VIRTUAL_ENV if under working directory
// also mount-replace VIRTUAL_ENV directory with <project_settings>/sandbox.venv
// sandbox can then set up this new VIRTUAL_ENV directory using sandbox.bashrc (see below)
Expand Down Expand Up @@ -1209,6 +1217,10 @@ async function start_lxc_sandbox(
GEMINI_CLI_IDE_SERVER_PORT: process.env['GEMINI_CLI_IDE_SERVER_PORT'],
GEMINI_CLI_IDE_WORKSPACE_PATH:
process.env['GEMINI_CLI_IDE_WORKSPACE_PATH'],
GEMINI_CLI_IDE_SERVER_STDIO_COMMAND:
process.env['GEMINI_CLI_IDE_SERVER_STDIO_COMMAND'],
GEMINI_CLI_IDE_SERVER_STDIO_ARGS:
process.env['GEMINI_CLI_IDE_SERVER_STDIO_ARGS'],
TERM_PROGRAM: process.env['TERM_PROGRAM'],
};
for (const [key, value] of Object.entries(envVarsToForward)) {
Expand Down
21 changes: 15 additions & 6 deletions packages/core/src/ide/ide-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
getIdeServerHost,
getPortFromEnv,
getStdioConfigFromEnv,
isGvisorSandbox,
validateWorkspacePath,
createProxyAwareFetch,
type StdioConfig,
Expand Down Expand Up @@ -145,13 +146,21 @@ export class IdeClient {
connectionConfig?.workspacePath ??
process.env['GEMINI_CLI_IDE_WORKSPACE_PATH'];

const isGvisor = isGvisorSandbox();
const ideName = this.currentIde.displayName;
const gvisorFailureDetails = `Failed to connect to IDE companion extension in ${ideName}: gVisor (runsc) sandboxing enforces strict network isolation which prevents host loopback communication.`;

const { isValid, error } = validateWorkspacePath(
workspacePath,
process.cwd(),
);

if (!isValid) {
this.setState(IDEConnectionStatus.Disconnected, error, logError);
this.setState(
IDEConnectionStatus.Disconnected,
workspacePath === undefined && isGvisor ? gvisorFailureDetails : error,
logError,
);
return;
}

Expand Down Expand Up @@ -194,11 +203,11 @@ export class IdeClient {
}
}

this.setState(
IDEConnectionStatus.Disconnected,
`Failed to connect to IDE companion extension in ${this.currentIde.displayName}. Please ensure the extension is running. To install the extension, run /ide install.`,
logError,
);
const failureDetails = isGvisor
? gvisorFailureDetails
: `Failed to connect to IDE companion extension in ${ideName}. Please ensure the extension is running. To install the extension, run /ide install.`;

this.setState(IDEConnectionStatus.Disconnected, failureDetails, logError);
}

/**
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/ide/ide-connection-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,13 @@ export function getIdeServerHost() {
return host;
}

export function isGvisorSandbox(): boolean {
return (
Boolean(process.env['SANDBOX']?.toLowerCase().includes('runsc')) ||
process.env['GEMINI_SANDBOX']?.toLowerCase().trim() === 'runsc'
);
}

function isInContainer() {
return fs.existsSync('/.dockerenv') || fs.existsSync('/run/.containerenv');
}
Expand Down
Loading
Loading