Skip to content

Commit dfff179

Browse files
authored
Merge branch 'dd/no-grep-or-bust' into copilot/fix-github-actions-integration-tests
2 parents 45728ff + 29da2f5 commit dfff179

15 files changed

+1019
-465
lines changed

extensions/vscode/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,11 @@
108108
"default": "latest",
109109
"description": "The npm version of codeql-development-mcp-server to install. Use 'latest' for the most recent release."
110110
},
111+
"codeql-mcp.scratchDir": {
112+
"type": "string",
113+
"default": ".codeql/ql-mcp",
114+
"markdownDescription": "Workspace-relative path for the ql-mcp scratch directory used for temporary files (query logs, external predicates, etc). The `.codeql/` parent is shared with other CodeQL CLI commands like `codeql pack bundle`. Set to an absolute path to override workspace-relative resolution."
115+
},
111116
"codeql-mcp.watchCodeqlExtension": {
112117
"type": "boolean",
113118
"default": true,

extensions/vscode/src/bridge/database-copier.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { existsSync } from 'fs';
2-
import { cp, mkdir, readdir, rm, stat, unlink } from 'fs/promises';
2+
import { cp, lstat, mkdir, readdir, rm, stat, unlink } from 'fs/promises';
33
import { join } from 'path';
44
import type { Logger } from '../common/logger';
55

@@ -34,10 +34,10 @@ export class DatabaseCopier {
3434
try {
3535
await mkdir(this.destinationBase, { recursive: true });
3636
} catch (err) {
37-
this.logger.error(
38-
`Failed to create managed database directory ${this.destinationBase}: ${err instanceof Error ? err.message : String(err)}`,
39-
);
40-
return [];
37+
const msg =
38+
`Failed to create managed database directory ${this.destinationBase}: ${err instanceof Error ? err.message : String(err)}`;
39+
this.logger.error(msg);
40+
throw new Error(msg, { cause: err });
4141
}
4242

4343
const copied: string[] = [];
@@ -137,8 +137,8 @@ async function isCodeQLDatabase(dirPath: string): Promise<boolean> {
137137

138138
/**
139139
* Recursively remove all `.lock` files under the given directory.
140-
* These are empty sentinel files created by the CodeQL query server in
141-
* `<dataset>/default/cache/.lock`.
140+
* Uses lstat to avoid following symlinks, keeping deletion scoped to
141+
* the copied database tree.
142142
*/
143143
async function removeLockFiles(dir: string): Promise<void> {
144144
let entries: string[];
@@ -151,7 +151,11 @@ async function removeLockFiles(dir: string): Promise<void> {
151151
for (const entry of entries) {
152152
const fullPath = join(dir, entry);
153153
try {
154-
const st = await stat(fullPath);
154+
const st = await lstat(fullPath);
155+
// Skip symlinks to avoid traversing outside the database directory
156+
if (st.isSymbolicLink()) {
157+
continue;
158+
}
155159
if (st.isDirectory()) {
156160
await removeLockFiles(fullPath);
157161
} else if (entry === '.lock') {

extensions/vscode/src/bridge/environment-builder.ts

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as vscode from 'vscode';
2-
import { delimiter, join } from 'path';
2+
import { delimiter, isAbsolute, join } from 'path';
33
import { DisposableObject } from '../common/disposable';
44
import type { Logger } from '../common/logger';
55
import type { CliResolver } from '../codeql/cli-resolver';
@@ -64,17 +64,34 @@ export class EnvironmentBuilder extends DisposableObject {
6464
env.CODEQL_PATH = cliPath;
6565
}
6666

67-
// Workspace root
67+
// Workspace root and all workspace folders
6868
const workspaceFolders = vscode.workspace.workspaceFolders;
6969
if (workspaceFolders && workspaceFolders.length > 0) {
7070
env.CODEQL_MCP_WORKSPACE = workspaceFolders[0].uri.fsPath;
71+
env.CODEQL_MCP_WORKSPACE_FOLDERS = workspaceFolders
72+
.map((f) => f.uri.fsPath)
73+
.join(delimiter);
7174
}
7275

73-
// Temp directory for MCP server scratch files
74-
env.CODEQL_MCP_TMP_DIR = join(
75-
this.context.globalStorageUri.fsPath,
76-
'tmp',
77-
);
76+
// Workspace-local scratch directory for tool output (query logs, etc.)
77+
// Defaults to `.codeql/ql-mcp` within the first workspace folder.
78+
// This is also used as CODEQL_MCP_TMP_DIR so that the server writes
79+
// all temporary output (query logs, external predicate CSVs) inside
80+
// the workspace, avoiding out-of-workspace file access prompts.
81+
const scratchRelative = config.get<string>('scratchDir', '.codeql/ql-mcp');
82+
if (workspaceFolders && workspaceFolders.length > 0) {
83+
const scratchDir = isAbsolute(scratchRelative)
84+
? scratchRelative
85+
: join(workspaceFolders[0].uri.fsPath, scratchRelative);
86+
env.CODEQL_MCP_SCRATCH_DIR = scratchDir;
87+
env.CODEQL_MCP_TMP_DIR = scratchDir;
88+
} else {
89+
// No workspace — fall back to extension globalStorage
90+
env.CODEQL_MCP_TMP_DIR = join(
91+
this.context.globalStorageUri.fsPath,
92+
'tmp',
93+
);
94+
}
7895

7996
// Additional packs path — include vscode-codeql's database storage
8097
// so the MCP server can discover databases registered there

extensions/vscode/test/bridge/environment-builder.test.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,42 @@ describe('EnvironmentBuilder', () => {
8787
expect(env.TRANSPORT_MODE).toBe('stdio');
8888
});
8989

90-
it('should include CODEQL_MCP_TMP_DIR under global storage', async () => {
90+
it('should include CODEQL_MCP_TMP_DIR under global storage when no workspace', async () => {
9191
const env = await builder.build();
9292
expect(env.CODEQL_MCP_TMP_DIR).toBe('/mock/global-storage/codeql-mcp/tmp');
9393
});
9494

95+
it('should set CODEQL_MCP_TMP_DIR to workspace scratch dir when workspace folders exist', async () => {
96+
const vscode = await import('vscode');
97+
const origFolders = vscode.workspace.workspaceFolders;
98+
(vscode.workspace.workspaceFolders as any) = [
99+
{ uri: { fsPath: '/mock/workspace' }, name: 'ws', index: 0 },
100+
];
101+
102+
builder.invalidate();
103+
const env = await builder.build();
104+
expect(env.CODEQL_MCP_TMP_DIR).toBe('/mock/workspace/.codeql/ql-mcp');
105+
expect(env.CODEQL_MCP_SCRATCH_DIR).toBe('/mock/workspace/.codeql/ql-mcp');
106+
107+
(vscode.workspace.workspaceFolders as any) = origFolders;
108+
});
109+
110+
it('should set CODEQL_MCP_WORKSPACE_FOLDERS with all workspace folder paths', async () => {
111+
const vscode = await import('vscode');
112+
const { delimiter } = await import('path');
113+
const origFolders = vscode.workspace.workspaceFolders;
114+
(vscode.workspace.workspaceFolders as any) = [
115+
{ uri: { fsPath: '/mock/ws-a' }, name: 'a', index: 0 },
116+
{ uri: { fsPath: '/mock/ws-b' }, name: 'b', index: 1 },
117+
];
118+
119+
builder.invalidate();
120+
const env = await builder.build();
121+
expect(env.CODEQL_MCP_WORKSPACE_FOLDERS).toBe(['/mock/ws-a', '/mock/ws-b'].join(delimiter));
122+
123+
(vscode.workspace.workspaceFolders as any) = origFolders;
124+
});
125+
95126
it('should include CODEQL_ADDITIONAL_PACKS with database storage path', async () => {
96127
const env = await builder.build();
97128
expect(env.CODEQL_ADDITIONAL_PACKS).toBeDefined();
@@ -206,6 +237,15 @@ describe('EnvironmentBuilder', () => {
206237
vscode.workspace.getConfiguration = originalGetConfig;
207238
});
208239

240+
it('should fall back to source dirs when syncAll throws', async () => {
241+
mockCopier.syncAll.mockRejectedValue(new Error('Failed to create managed database directory'));
242+
builder.invalidate();
243+
const env = await builder.build();
244+
expect(env.CODEQL_DATABASES_BASE_DIRS).toBe(
245+
['/mock/global-storage/GitHub.vscode-codeql', '/mock/workspace-storage/ws-123/GitHub.vscode-codeql'].join(delimiter),
246+
);
247+
});
248+
209249
it('should append user-configured dirs to CODEQL_QUERY_RUN_RESULTS_DIRS', async () => {
210250
const vscode = await import('vscode');
211251
const originalGetConfig = vscode.workspace.getConfiguration;

0 commit comments

Comments
 (0)