mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
bug: fix ide-client connection to ide-companion when inside docker via ssh/devcontainer (#15049)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -23,6 +23,7 @@ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'
|
||||
import { detectIde, IDE_DEFINITIONS } from './detect-ide.js';
|
||||
import * as os from 'node:os';
|
||||
import * as path from 'node:path';
|
||||
import { getIdeServerHost } from './ide-client.js';
|
||||
|
||||
vi.mock('node:fs', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof fs>();
|
||||
@@ -34,7 +35,7 @@ vi.mock('node:fs', async (importOriginal) => {
|
||||
readdir: vi.fn(),
|
||||
},
|
||||
realpathSync: (p: string) => p,
|
||||
existsSync: () => false,
|
||||
existsSync: vi.fn(() => false),
|
||||
};
|
||||
});
|
||||
vi.mock('./process-utils.js');
|
||||
@@ -110,7 +111,7 @@ describe('IdeClient', () => {
|
||||
await ideClient.connect();
|
||||
|
||||
expect(fs.promises.readFile).toHaveBeenCalledWith(
|
||||
path.join('/tmp/', 'gemini-ide-server-12345.json'),
|
||||
path.join('/tmp', 'gemini', 'ide', 'gemini-ide-server-12345.json'),
|
||||
'utf8',
|
||||
);
|
||||
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(
|
||||
@@ -277,7 +278,7 @@ describe('IdeClient', () => {
|
||||
|
||||
expect(result).toEqual(config);
|
||||
expect(fs.promises.readFile).toHaveBeenCalledWith(
|
||||
path.join('/tmp', 'gemini-ide-server-12345.json'),
|
||||
path.join('/tmp', 'gemini', 'ide', 'gemini-ide-server-12345.json'),
|
||||
'utf8',
|
||||
);
|
||||
});
|
||||
@@ -324,7 +325,7 @@ describe('IdeClient', () => {
|
||||
|
||||
expect(result).toEqual(config);
|
||||
expect(fs.promises.readFile).toHaveBeenCalledWith(
|
||||
path.join('/tmp/gemini/ide', 'gemini-ide-server-12345-123.json'),
|
||||
path.join('/tmp', 'gemini', 'ide', 'gemini-ide-server-12345-123.json'),
|
||||
'utf8',
|
||||
);
|
||||
});
|
||||
@@ -517,11 +518,11 @@ describe('IdeClient', () => {
|
||||
|
||||
expect(result).toEqual(validConfig);
|
||||
expect(fs.promises.readFile).toHaveBeenCalledWith(
|
||||
path.join('/tmp/gemini/ide', 'gemini-ide-server-12345-111.json'),
|
||||
path.join('/tmp', 'gemini', 'ide', 'gemini-ide-server-12345-111.json'),
|
||||
'utf8',
|
||||
);
|
||||
expect(fs.promises.readFile).not.toHaveBeenCalledWith(
|
||||
path.join('/tmp/gemini/ide', 'not-a-config-file.txt'),
|
||||
path.join('/tmp', 'gemini', 'ide', 'not-a-config-file.txt'),
|
||||
'utf8',
|
||||
);
|
||||
});
|
||||
@@ -956,3 +957,178 @@ describe('IdeClient', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIdeServerHost', () => {
|
||||
let existsSyncMock: Mock;
|
||||
let originalSshConnection: string | undefined;
|
||||
let originalVscodeRemoteSession: string | undefined;
|
||||
let originalRemoteContainers: string | undefined;
|
||||
|
||||
beforeEach(() => {
|
||||
existsSyncMock = vi.mocked(fs.existsSync);
|
||||
existsSyncMock.mockClear();
|
||||
originalSshConnection = process.env['SSH_CONNECTION'];
|
||||
originalVscodeRemoteSession =
|
||||
process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
|
||||
originalRemoteContainers = process.env['REMOTE_CONTAINERS'];
|
||||
|
||||
delete process.env['SSH_CONNECTION'];
|
||||
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
|
||||
delete process.env['REMOTE_CONTAINERS'];
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
if (originalSshConnection !== undefined) {
|
||||
process.env['SSH_CONNECTION'] = originalSshConnection;
|
||||
} else {
|
||||
delete process.env['SSH_CONNECTION'];
|
||||
}
|
||||
if (originalVscodeRemoteSession !== undefined) {
|
||||
process.env['VSCODE_REMOTE_CONTAINERS_SESSION'] =
|
||||
originalVscodeRemoteSession;
|
||||
} else {
|
||||
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
|
||||
}
|
||||
if (originalRemoteContainers !== undefined) {
|
||||
process.env['REMOTE_CONTAINERS'] = originalRemoteContainers;
|
||||
} else {
|
||||
delete process.env['REMOTE_CONTAINERS'];
|
||||
}
|
||||
});
|
||||
|
||||
// Helper to set existsSync mock behavior
|
||||
const setupFsMocks = (
|
||||
dockerenvExists: boolean,
|
||||
containerenvExists: boolean,
|
||||
) => {
|
||||
existsSyncMock.mockImplementation((path: string) => {
|
||||
if (path === '/.dockerenv') {
|
||||
return dockerenvExists;
|
||||
}
|
||||
if (path === '/run/.containerenv') {
|
||||
return containerenvExists;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
};
|
||||
|
||||
it('should return 127.0.0.1 when not in container and no SSH_CONNECTION or Dev Container env vars', () => {
|
||||
setupFsMocks(false, false);
|
||||
delete process.env['SSH_CONNECTION'];
|
||||
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
|
||||
delete process.env['REMOTE_CONTAINERS'];
|
||||
expect(getIdeServerHost()).toBe('127.0.0.1');
|
||||
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
|
||||
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/run/.containerenv');
|
||||
});
|
||||
|
||||
it('should return 127.0.0.1 when not in container but SSH_CONNECTION is set', () => {
|
||||
setupFsMocks(false, false);
|
||||
process.env['SSH_CONNECTION'] = 'some_ssh_value';
|
||||
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
|
||||
delete process.env['REMOTE_CONTAINERS'];
|
||||
expect(getIdeServerHost()).toBe('127.0.0.1');
|
||||
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
|
||||
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/run/.containerenv');
|
||||
});
|
||||
|
||||
it('should return host.docker.internal when in .dockerenv container and no SSH_CONNECTION or Dev Container env vars', () => {
|
||||
setupFsMocks(true, false);
|
||||
delete process.env['SSH_CONNECTION'];
|
||||
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
|
||||
delete process.env['REMOTE_CONTAINERS'];
|
||||
expect(getIdeServerHost()).toBe('host.docker.internal');
|
||||
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
|
||||
expect(vi.mocked(fs.existsSync)).not.toHaveBeenCalledWith(
|
||||
'/run/.containerenv',
|
||||
); // Short-circuiting
|
||||
});
|
||||
|
||||
it('should return 127.0.0.1 when in .dockerenv container and SSH_CONNECTION is set', () => {
|
||||
setupFsMocks(true, false);
|
||||
process.env['SSH_CONNECTION'] = 'some_ssh_value';
|
||||
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
|
||||
delete process.env['REMOTE_CONTAINERS'];
|
||||
expect(getIdeServerHost()).toBe('127.0.0.1');
|
||||
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
|
||||
expect(vi.mocked(fs.existsSync)).not.toHaveBeenCalledWith(
|
||||
'/run/.containerenv',
|
||||
); // Short-circuiting
|
||||
});
|
||||
|
||||
it('should return 127.0.0.1 when in .dockerenv container and VSCODE_REMOTE_CONTAINERS_SESSION is set', () => {
|
||||
setupFsMocks(true, false);
|
||||
delete process.env['SSH_CONNECTION'];
|
||||
process.env['VSCODE_REMOTE_CONTAINERS_SESSION'] = 'some_session_id';
|
||||
expect(getIdeServerHost()).toBe('127.0.0.1');
|
||||
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
|
||||
expect(vi.mocked(fs.existsSync)).not.toHaveBeenCalledWith(
|
||||
'/run/.containerenv',
|
||||
); // Short-circuiting
|
||||
});
|
||||
|
||||
it('should return host.docker.internal when in .containerenv container and no SSH_CONNECTION or Dev Container env vars', () => {
|
||||
setupFsMocks(false, true);
|
||||
delete process.env['SSH_CONNECTION'];
|
||||
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
|
||||
delete process.env['REMOTE_CONTAINERS'];
|
||||
expect(getIdeServerHost()).toBe('host.docker.internal');
|
||||
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
|
||||
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/run/.containerenv');
|
||||
});
|
||||
|
||||
it('should return 127.0.0.1 when in .containerenv container and SSH_CONNECTION is set', () => {
|
||||
setupFsMocks(false, true);
|
||||
process.env['SSH_CONNECTION'] = 'some_ssh_value';
|
||||
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
|
||||
delete process.env['REMOTE_CONTAINERS'];
|
||||
expect(getIdeServerHost()).toBe('127.0.0.1');
|
||||
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
|
||||
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/run/.containerenv');
|
||||
});
|
||||
|
||||
it('should return 127.0.0.1 when in .containerenv container and REMOTE_CONTAINERS is set', () => {
|
||||
setupFsMocks(false, true);
|
||||
delete process.env['SSH_CONNECTION'];
|
||||
process.env['REMOTE_CONTAINERS'] = 'true';
|
||||
expect(getIdeServerHost()).toBe('127.0.0.1');
|
||||
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
|
||||
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/run/.containerenv');
|
||||
});
|
||||
|
||||
it('should return host.docker.internal when in both containers and no SSH_CONNECTION or Dev Container env vars', () => {
|
||||
setupFsMocks(true, true);
|
||||
delete process.env['SSH_CONNECTION'];
|
||||
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
|
||||
delete process.env['REMOTE_CONTAINERS'];
|
||||
expect(getIdeServerHost()).toBe('host.docker.internal');
|
||||
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
|
||||
expect(vi.mocked(fs.existsSync)).not.toHaveBeenCalledWith(
|
||||
'/run/.containerenv',
|
||||
); // Short-circuiting
|
||||
});
|
||||
|
||||
it('should return 127.0.0.1 when in both containers and SSH_CONNECTION is set', () => {
|
||||
setupFsMocks(true, true);
|
||||
process.env['SSH_CONNECTION'] = 'some_ssh_value';
|
||||
delete process.env['VSCODE_REMOTE_CONTAINERS_SESSION'];
|
||||
delete process.env['REMOTE_CONTAINERS'];
|
||||
expect(getIdeServerHost()).toBe('127.0.0.1');
|
||||
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
|
||||
expect(vi.mocked(fs.existsSync)).not.toHaveBeenCalledWith(
|
||||
'/run/.containerenv',
|
||||
); // Short-circuiting
|
||||
});
|
||||
|
||||
it('should return 127.0.0.1 when in both containers and VSCODE_REMOTE_CONTAINERS_SESSION is set', () => {
|
||||
setupFsMocks(true, true);
|
||||
delete process.env['SSH_CONNECTION'];
|
||||
process.env['VSCODE_REMOTE_CONTAINERS_SESSION'] = 'some_session_id';
|
||||
expect(getIdeServerHost()).toBe('127.0.0.1');
|
||||
expect(vi.mocked(fs.existsSync)).toHaveBeenCalledWith('/.dockerenv');
|
||||
expect(vi.mocked(fs.existsSync)).not.toHaveBeenCalledWith(
|
||||
'/run/.containerenv',
|
||||
); // Short-circuiting
|
||||
});
|
||||
});
|
||||
|
||||
@@ -585,6 +585,8 @@ export class IdeClient {
|
||||
try {
|
||||
const portFile = path.join(
|
||||
os.tmpdir(),
|
||||
'gemini',
|
||||
'ide',
|
||||
`gemini-ide-server-${this.ideProcessInfo.pid}.json`,
|
||||
);
|
||||
const portFileContents = await fs.promises.readFile(portFile, 'utf8');
|
||||
@@ -671,11 +673,11 @@ export class IdeClient {
|
||||
return validWorkspaces[0];
|
||||
}
|
||||
|
||||
private createProxyAwareFetch() {
|
||||
// ignore proxy for '127.0.0.1' by default to allow connecting to the ide mcp server
|
||||
private async createProxyAwareFetch(ideServerHost: string) {
|
||||
// ignore proxy for the IDE server host to allow connecting to the ide mcp server
|
||||
const existingNoProxy = process.env['NO_PROXY'] || '';
|
||||
const agent = new EnvHttpProxyAgent({
|
||||
noProxy: [existingNoProxy, '127.0.0.1'].filter(Boolean).join(','),
|
||||
noProxy: [existingNoProxy, ideServerHost].filter(Boolean).join(','),
|
||||
});
|
||||
const undiciPromise = import('undici');
|
||||
// Suppress unhandled rejection if the promise is not awaited immediately.
|
||||
@@ -777,23 +779,28 @@ export class IdeClient {
|
||||
private async establishHttpConnection(port: string): Promise<boolean> {
|
||||
let transport: StreamableHTTPClientTransport | undefined;
|
||||
try {
|
||||
const ideServerHost = getIdeServerHost();
|
||||
const portNumber = parseInt(port, 10);
|
||||
// validate port to prevent Server-Side Request Forgery (SSRF) vulnerability
|
||||
if (isNaN(portNumber) || portNumber <= 0 || portNumber > 65535) {
|
||||
return false;
|
||||
}
|
||||
const serverUrl = `http://${ideServerHost}:${portNumber}/mcp`;
|
||||
logger.debug('Attempting to connect to IDE via HTTP SSE');
|
||||
logger.debug(`Server URL: ${serverUrl}`);
|
||||
this.client = new Client({
|
||||
name: 'streamable-http-client',
|
||||
// TODO(#3487): use the CLI version here.
|
||||
version: '1.0.0',
|
||||
});
|
||||
transport = new StreamableHTTPClientTransport(
|
||||
new URL(`http://${getIdeServerHost()}:${port}/mcp`),
|
||||
{
|
||||
fetch: this.createProxyAwareFetch(),
|
||||
requestInit: {
|
||||
headers: this.authToken
|
||||
? { Authorization: `Bearer ${this.authToken}` }
|
||||
: {},
|
||||
},
|
||||
transport = new StreamableHTTPClientTransport(new URL(serverUrl), {
|
||||
fetch: await this.createProxyAwareFetch(ideServerHost),
|
||||
requestInit: {
|
||||
headers: this.authToken
|
||||
? { Authorization: `Bearer ${this.authToken}` }
|
||||
: {},
|
||||
},
|
||||
);
|
||||
});
|
||||
await this.client.connect(transport);
|
||||
this.registerClientHandlers();
|
||||
await this.discoverTools();
|
||||
@@ -846,8 +853,31 @@ export class IdeClient {
|
||||
}
|
||||
}
|
||||
|
||||
function getIdeServerHost() {
|
||||
const isInContainer =
|
||||
fs.existsSync('/.dockerenv') || fs.existsSync('/run/.containerenv');
|
||||
return isInContainer ? 'host.docker.internal' : '127.0.0.1';
|
||||
export function getIdeServerHost() {
|
||||
let host: string;
|
||||
host = '127.0.0.1';
|
||||
if (isInContainer()) {
|
||||
// when ssh-connection (e.g. remote-ssh) or devcontainer setup:
|
||||
// --> host must be '127.0.0.1' to have cli companion working
|
||||
if (!isSshConnected() && !isDevContainer()) {
|
||||
host = 'host.docker.internal';
|
||||
}
|
||||
}
|
||||
logger.debug(`[getIdeServerHost] Mapping IdeServerHost to '${host}'`);
|
||||
return host;
|
||||
}
|
||||
|
||||
function isInContainer() {
|
||||
return fs.existsSync('/.dockerenv') || fs.existsSync('/run/.containerenv');
|
||||
}
|
||||
|
||||
function isSshConnected() {
|
||||
return !!process.env['SSH_CONNECTION'];
|
||||
}
|
||||
|
||||
function isDevContainer() {
|
||||
return !!(
|
||||
process.env['VSCODE_REMOTE_CONTAINERS_SESSION'] ||
|
||||
process.env['REMOTE_CONTAINERS']
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user