Compare commits

..

1 Commits

Author SHA1 Message Date
amelidev 5024443c72 fix(core): resolve swallowed directory mismatch in IDE connections (#28729)
Co-authored-by: David Pierce <davidapierce@google.com>
2026-08-11 22:01:47 +00:00
11 changed files with 229 additions and 32 deletions
+9 -9
View File
@@ -1,12 +1,12 @@
{
"name": "@google/gemini-cli",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@google/gemini-cli",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"workspaces": [
"packages/*"
],
@@ -17782,7 +17782,7 @@
},
"packages/a2a-server": {
"name": "@google/gemini-cli-a2a-server",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"dependencies": {
"@a2a-js/sdk": "0.3.11",
"@google-cloud/storage": "7.19.0",
@@ -18242,7 +18242,7 @@
},
"packages/cli": {
"name": "@google/gemini-cli",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"license": "Apache-2.0",
"dependencies": {
"@agentclientprotocol/sdk": "0.16.1",
@@ -18458,7 +18458,7 @@
},
"packages/core": {
"name": "@google/gemini-cli-core",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"license": "Apache-2.0",
"dependencies": {
"@a2a-js/sdk": "0.3.11",
@@ -19131,7 +19131,7 @@
},
"packages/devtools": {
"name": "@google/gemini-cli-devtools",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"license": "Apache-2.0",
"dependencies": {
"ws": "8.16.0"
@@ -19167,7 +19167,7 @@
},
"packages/sdk": {
"name": "@google/gemini-cli-sdk",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -19506,7 +19506,7 @@
},
"packages/test-utils": {
"name": "@google/gemini-cli-test-utils",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"license": "Apache-2.0",
"dependencies": {
"@google/gemini-cli-core": "file:../core",
@@ -19524,7 +19524,7 @@
},
"packages/vscode-ide-companion": {
"name": "gemini-cli-vscode-ide-companion",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "1.23.0",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"engines": {
"node": ">=20.0.0"
},
@@ -14,7 +14,7 @@
"url": "git+https://github.com/google-gemini/gemini-cli.git"
},
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.55.1"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.56.0-nightly.20260806.g761f604c1"
},
"scripts": {
"start": "cross-env NODE_ENV=development node scripts/start.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-a2a-server",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"description": "Gemini CLI A2A Server",
"repository": {
"type": "git",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"description": "Gemini CLI",
"license": "Apache-2.0",
"repository": {
@@ -27,7 +27,7 @@
"dist"
],
"config": {
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.55.1"
"sandboxImageUri": "us-docker.pkg.dev/gemini-code-dev/gemini-cli/sandbox:0.56.0-nightly.20260806.g761f604c1"
},
"dependencies": {
"@agentclientprotocol/sdk": "0.16.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-core",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"description": "Gemini CLI Core",
"license": "Apache-2.0",
"repository": {
@@ -31,6 +31,21 @@ vi.mock('node:fs', async (importOriginal) => {
...actual.promises,
readFile: vi.fn(),
readdir: vi.fn(),
realpath: vi.fn((p) => Promise.resolve(p)),
stat: vi.fn(() =>
Promise.resolve({ uid: process.getuid ? process.getuid() : 1000 }),
),
open: vi.fn((filePath: string) =>
Promise.resolve({
stat: () => fs.promises.stat(filePath),
readFile: (options?: string | { encoding?: string }) =>
fs.promises.readFile(
filePath,
options as unknown as BufferEncoding | undefined,
),
close: () => Promise.resolve(),
} as unknown as fs.promises.FileHandle),
),
},
realpathSync: (p: string) => p,
existsSync: vi.fn(() => false),
@@ -430,6 +445,141 @@ describe('ide-connection-utils', () => {
expect(result).toEqual(config2);
});
it('should NOT filter out config if all found config files are mismatched/invalid workspaces, returning the best sorted match so that the correct Directory Mismatch error is raised downstream', async () => {
const invalidConfig1 = {
port: '1111',
workspacePath: '/invalid/workspace1',
};
const invalidConfig2 = {
port: '2222',
workspacePath: '/invalid/workspace2',
};
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
new Error('not found'),
);
(
vi.mocked(fs.promises.readdir) as Mock<
(path: fs.PathLike) => Promise<string[]>
>
).mockResolvedValue([
'gemini-ide-server-12345-111.json',
'gemini-ide-server-12345-222.json',
]);
vi.mocked(fs.promises.readFile)
.mockResolvedValueOnce(JSON.stringify(invalidConfig1))
.mockResolvedValueOnce(JSON.stringify(invalidConfig2));
const result = await getConnectionConfigFromFile(12345);
expect(result).toEqual(invalidConfig1);
});
it('should prioritize the config matching the port from the environment variable when all found config files are mismatched/invalid workspaces', async () => {
vi.stubEnv('GEMINI_CLI_IDE_SERVER_PORT', '2222');
const invalidConfig1 = {
port: '1111',
workspacePath: '/invalid/workspace1',
};
const invalidConfig2 = {
port: '2222',
workspacePath: '/invalid/workspace2',
};
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
new Error('not found'),
);
(
vi.mocked(fs.promises.readdir) as Mock<
(path: fs.PathLike) => Promise<string[]>
>
).mockResolvedValue([
'gemini-ide-server-12345-111.json',
'gemini-ide-server-12345-222.json',
]);
vi.mocked(fs.promises.readFile)
.mockResolvedValueOnce(JSON.stringify(invalidConfig1))
.mockResolvedValueOnce(JSON.stringify(invalidConfig2));
const result = await getConnectionConfigFromFile(12345);
expect(result).toEqual(invalidConfig2);
});
it.runIf(process.getuid !== undefined)(
'should reject and ignore config files owned by a different user UID to prevent hijacking/information disclosure',
async () => {
const config1 = {
port: '1111',
workspacePath: '/test/workspace',
};
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
new Error('not found'),
);
(
vi.mocked(fs.promises.readdir) as Mock<
(path: fs.PathLike) => Promise<string[]>
>
).mockResolvedValue(['gemini-ide-server-12345-111.json']);
vi.mocked(fs.promises.readFile).mockResolvedValueOnce(
JSON.stringify(config1),
);
const otherUid = (process.getuid ? process.getuid() : 1000) + 1;
vi.mocked(fs.promises.stat).mockResolvedValueOnce({
uid: otherUid,
} as unknown as fs.Stats);
const result = await getConnectionConfigFromFile(12345);
expect(result).toBeUndefined();
},
);
it('should accept and parse config files owned by the current user UID', async () => {
const config1 = {
port: '1111',
workspacePath: '/test/workspace',
};
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
new Error('not found'),
);
(
vi.mocked(fs.promises.readdir) as Mock<
(path: fs.PathLike) => Promise<string[]>
>
).mockResolvedValue(['gemini-ide-server-12345-111.json']);
vi.mocked(fs.promises.readFile).mockResolvedValueOnce(
JSON.stringify(config1),
);
const currentUid = process.getuid ? process.getuid() : 1000;
vi.mocked(fs.promises.stat).mockResolvedValueOnce({
uid: currentUid,
} as unknown as fs.Stats);
const result = await getConnectionConfigFromFile(12345);
expect(result).toEqual(config1);
});
it('should reject and ignore config files if fs.promises.open throws an error', async () => {
vi.mocked(fs.promises.readFile).mockRejectedValueOnce(
new Error('not found'),
);
(
vi.mocked(fs.promises.readdir) as Mock<
(path: fs.PathLike) => Promise<string[]>
>
).mockResolvedValue(['gemini-ide-server-12345-111.json']);
vi.mocked(fs.promises.open).mockRejectedValueOnce(
new Error('symlink loop / permission denied'),
);
const result = await getConnectionConfigFromFile(12345);
expect(result).toBeUndefined();
});
});
describe('validateWorkspacePath', () => {
+60 -13
View File
@@ -109,6 +109,26 @@ export function getStdioConfigFromEnv(): StdioConfig | undefined {
const IDE_SERVER_FILE_REGEX = /^gemini-ide-server-(\d+)-\d+\.json$/;
async function verifyAndReadFile(
filePath: string,
): Promise<string | undefined> {
let handle: fs.promises.FileHandle | undefined;
try {
handle = await fs.promises.open(filePath, 'r');
const stat = await handle.stat();
if (process.getuid && stat.uid !== process.getuid()) {
return undefined;
}
return await handle.readFile('utf8');
} catch {
return undefined;
} finally {
if (handle) {
await handle.close();
}
}
}
export async function getConnectionConfigFromFile(
pid: number,
): Promise<
@@ -122,7 +142,10 @@ export async function getConnectionConfigFromFile(
'ide',
`gemini-ide-server-${pid}.json`,
);
const portFileContents = await fs.promises.readFile(portFile, 'utf8');
const portFileContents = await verifyAndReadFile(portFile);
if (!portFileContents) {
throw new Error('Verification failed or file not found');
}
const parsed: unknown = JSON.parse(portFileContents);
type ConfigType = ConnectionConfig & {
workspacePath?: string;
@@ -164,23 +187,21 @@ export async function getConnectionConfigFromFile(
sortConnectionFiles(matchingFiles, pid);
let fileContents: string[];
try {
fileContents = await Promise.all(
matchingFiles.map((file) =>
fs.promises.readFile(path.join(portFileDir, file), 'utf8'),
),
);
} catch (e) {
logger.debug('Failed to read IDE connection config file(s):', e);
return undefined;
}
const fileContents = await Promise.all(
matchingFiles.map((file) =>
verifyAndReadFile(path.join(portFileDir, file)),
),
);
const parsedContents = fileContents.map(
(
content,
):
| (ConnectionConfig & { workspacePath?: string; ideInfo?: IdeInfo })
| undefined => {
if (!content) {
return undefined;
}
try {
const parsed: unknown = JSON.parse(content);
type ConfigType = ConnectionConfig & {
@@ -219,6 +240,31 @@ export async function getConnectionConfigFromFile(
);
if (validWorkspaces.length === 0) {
// If no workspace matches the current CWD, but we found and parsed
// valid connection config file(s), return the best-sorted config.
// This lets downstream connection logic raise a helpful, detailed
// "Directory mismatch" warning instead of a generic connection error.
let fileIndex = -1;
const portFromEnv = getPortFromEnv();
if (portFromEnv) {
fileIndex = parsedContents.findIndex(
(content) =>
!!content &&
content.port !== undefined &&
String(content.port) === portFromEnv,
);
}
if (fileIndex === -1) {
fileIndex = parsedContents.findIndex((content) => !!content);
}
if (fileIndex !== -1) {
const selected = parsedContents[fileIndex]!;
logger.debug(
`Selected best mismatched IDE connection file: ${matchingFiles[fileIndex]}`,
);
return selected;
}
return undefined;
}
@@ -234,7 +280,8 @@ export async function getConnectionConfigFromFile(
const portFromEnv = getPortFromEnv();
if (portFromEnv) {
const matchingPortIndex = validWorkspaces.findIndex(
(content) => String(content.port) === portFromEnv,
(content) =>
content.port !== undefined && String(content.port) === portFromEnv,
);
if (matchingPortIndex !== -1) {
const selected = validWorkspaces[matchingPortIndex];
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-devtools",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"license": "Apache-2.0",
"type": "module",
"main": "dist/src/index.js",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-sdk",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"description": "Gemini CLI SDK",
"license": "Apache-2.0",
"repository": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@google/gemini-cli-test-utils",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "gemini-cli-vscode-ide-companion",
"displayName": "Gemini CLI Companion",
"description": "Enable Gemini CLI with direct access to your IDE workspace.",
"version": "0.55.1",
"version": "0.56.0-nightly.20260806.g761f604c1",
"publisher": "google",
"icon": "assets/icon.png",
"repository": {