Skip to content
This repository was archived by the owner on May 20, 2026. It is now read-only.

Commit c82e562

Browse files
BestraCopilot
andcommitted
Fix chat response resource reads
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 9e668cb commit c82e562

3 files changed

Lines changed: 107 additions & 34 deletions

File tree

src/extension/tools/node/test/readFile.spec.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,38 @@ suite('ReadFile', () => {
343343
testAccessor.dispose();
344344
});
345345

346+
test('reads chat response resources without requiring an open tab', async () => {
347+
const resourceUri = URI.parse('vscode-chat-response-resource://session/tool/call-1/0/file.md');
348+
const resourceDoc = createTextDocumentData(resourceUri, 'linked resource line 1\nlinked resource line 2', 'markdown').document;
349+
350+
const services = createExtensionUnitTestingServices();
351+
services.define(IWorkspaceService, new SyncDescriptor(
352+
TestWorkspaceService,
353+
[
354+
[],
355+
[resourceDoc],
356+
]
357+
));
358+
359+
const testAccessor = services.createTestingAccessor();
360+
const readFileTool = testAccessor.get(IInstantiationService).createInstance(ReadFileTool);
361+
362+
const input: IReadFileParamsV2 = {
363+
filePath: resourceUri.toString()
364+
};
365+
366+
const prepareResult = await readFileTool.prepareInvocation(
367+
{ input },
368+
CancellationToken.None
369+
);
370+
371+
expect(prepareResult).toBeDefined();
372+
expect((prepareResult!.invocationMessage as MarkdownString).value).toBe(`Reading [](${resourceUri.toString()})`);
373+
expect((prepareResult!.pastTenseMessage as MarkdownString).value).toBe(`Read [](${resourceUri.toString()})`);
374+
375+
testAccessor.dispose();
376+
});
377+
346378
test('should return "Reading skill/Read skill" message for skill files with line range', async () => {
347379
const testDoc = createTextDocumentData(URI.file('/workspace/test.skill.md'), 'line 1\nline 2\nline 3\nline 4\nline 5', 'markdown').document;
348380

src/extension/tools/node/test/toolUtils.spec.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,17 @@ suite('toolUtils - additionalReadAccessPaths', () => {
154154
await expect(invokeAssertFileOkForTool(URI.file('/external/file.ts'), true))
155155
.rejects.toThrow(/outside of the workspace/);
156156
});
157+
158+
test('chat response resources are allowed for read-only access', async () => {
159+
const resourceUri = URI.parse('vscode-chat-response-resource://session/tool/call-1/0/file.md');
160+
await expect(invokeAssertFileOkForTool(resourceUri, true)).resolves.toBeUndefined();
161+
});
162+
163+
test('chat response resources are not allowlisted outside read-only access', async () => {
164+
const resourceUri = URI.parse('vscode-chat-response-resource://session/tool/call-1/0/file.md');
165+
await expect(invokeAssertFileOkForTool(resourceUri))
166+
.rejects.toThrow(/outside of the workspace/);
167+
});
157168
});
158169

159170
describe('isFileExternalAndNeedsConfirmation', () => {
@@ -197,6 +208,11 @@ suite('toolUtils - additionalReadAccessPaths', () => {
197208
await expect(invokeIsFileExternalAndNeedsConfirmation(URI.file('/disallowed/file.ts'), true))
198209
.rejects.toThrow(/does not exist/);
199210
});
211+
212+
test('chat response resources do not need confirmation for read-only access', async () => {
213+
const resourceUri = URI.parse('vscode-chat-response-resource://session/tool/call-1/0/file.md');
214+
expect(await invokeIsFileExternalAndNeedsConfirmation(resourceUri, true)).toBe(false);
215+
});
200216
});
201217

202218
describe('isDirExternalAndNeedsConfirmation', () => {

src/extension/tools/node/toolUtils.ts

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,42 @@ export interface AssertFileOkForToolOptions {
164164
readOnly?: boolean;
165165
}
166166

167+
async function isUriAllowedWithoutWorkspaceMembership(
168+
uri: URI,
169+
normalizedUri: URI,
170+
tabsAndEditorsService: ITabsAndEditorsService,
171+
customInstructionsService: ICustomInstructionsService,
172+
diskSessionResources: IChatDiskSessionResources,
173+
chatDebugFileLogger: IChatDebugFileLoggerService,
174+
sessionTranscriptService: ISessionTranscriptService,
175+
buildPromptContext?: IBuildPromptContext,
176+
options?: AssertFileOkForToolOptions,
177+
): Promise<boolean> {
178+
if (uri.scheme === Schemas.untitled) {
179+
return true;
180+
}
181+
if (options?.readOnly && uri.scheme === 'vscode-chat-response-resource') {
182+
return true;
183+
}
184+
if (await isExternalInstructionsFile(normalizedUri, customInstructionsService, buildPromptContext)) {
185+
return true;
186+
}
187+
if (diskSessionResources.isSessionResourceUri(normalizedUri)) {
188+
return true;
189+
}
190+
if (chatDebugFileLogger.isDebugLogUri(normalizedUri)) {
191+
return true;
192+
}
193+
if (sessionTranscriptService.isTranscriptUri(normalizedUri)) {
194+
return true;
195+
}
196+
if (tabsAndEditorsService.tabs.some(tab => isEqual(tab.uri, uri))) {
197+
return true;
198+
}
199+
200+
return false;
201+
}
202+
167203
export async function assertFileOkForTool(accessor: ServicesAccessor, uri: URI, buildPromptContext?: IBuildPromptContext, options?: AssertFileOkForToolOptions): Promise<void> {
168204
const workspaceService = accessor.get(IWorkspaceService);
169205
const tabsAndEditorsService = accessor.get(ITabsAndEditorsService);
@@ -183,23 +219,17 @@ export async function assertFileOkForTool(accessor: ServicesAccessor, uri: URI,
183219
if (options?.readOnly && isUriUnderAdditionalReadAccessPaths(normalizedUri, configurationService)) {
184220
return;
185221
}
186-
if (uri.scheme === Schemas.untitled) {
187-
return;
188-
}
189-
const fileOpenInSomeTab = tabsAndEditorsService.tabs.some(tab => isEqual(tab.uri, uri));
190-
if (fileOpenInSomeTab) {
191-
return;
192-
}
193-
if (diskSessionResources.isSessionResourceUri(normalizedUri)) {
194-
return;
195-
}
196-
if (chatDebugFileLogger.isDebugLogUri(normalizedUri)) {
197-
return;
198-
}
199-
if (sessionTranscriptService.isTranscriptUri(normalizedUri)) {
200-
return;
201-
}
202-
if (await isExternalInstructionsFile(normalizedUri, customInstructionsService, buildPromptContext)) {
222+
if (await isUriAllowedWithoutWorkspaceMembership(
223+
uri,
224+
normalizedUri,
225+
tabsAndEditorsService,
226+
customInstructionsService,
227+
diskSessionResources,
228+
chatDebugFileLogger,
229+
sessionTranscriptService,
230+
buildPromptContext,
231+
options,
232+
)) {
203233
return;
204234
}
205235
throw new Error(`File ${promptPathRepresentationService.getFilePath(normalizedUri)} is outside of the workspace, and not open in an editor, and can't be read`);
@@ -281,29 +311,24 @@ export async function isFileExternalAndNeedsConfirmation(accessor: ServicesAcces
281311

282312
const normalizedUri = normalizePath(uri);
283313

284-
// Not external if: in workspace, untitled, instructions file, session resource, or open in editor
314+
// Not external if: in workspace, under configured read-only paths, or otherwise allowlisted for read-only access.
285315
if (workspaceService.getWorkspaceFolder(normalizedUri)) {
286316
return false;
287317
}
288318
if (options?.readOnly && isUriUnderAdditionalReadAccessPaths(normalizedUri, configurationService)) {
289319
return false;
290320
}
291-
if (uri.scheme === Schemas.untitled || uri.scheme === 'vscode-chat-response-resource') {
292-
return false;
293-
}
294-
if (await isExternalInstructionsFile(normalizedUri, customInstructionsService, buildPromptContext)) {
295-
return false;
296-
}
297-
if (diskSessionResources.isSessionResourceUri(normalizedUri)) {
298-
return false;
299-
}
300-
if (chatDebugFileLogger.isDebugLogUri(normalizedUri)) {
301-
return false;
302-
}
303-
if (sessionTranscriptService.isTranscriptUri(normalizedUri)) {
304-
return false;
305-
}
306-
if (tabsAndEditorsService.tabs.some(tab => isEqual(tab.uri, uri))) {
321+
if (await isUriAllowedWithoutWorkspaceMembership(
322+
uri,
323+
normalizedUri,
324+
tabsAndEditorsService,
325+
customInstructionsService,
326+
diskSessionResources,
327+
chatDebugFileLogger,
328+
sessionTranscriptService,
329+
buildPromptContext,
330+
options,
331+
)) {
307332
return false;
308333
}
309334

0 commit comments

Comments
 (0)