Skip to content

Commit 2010e15

Browse files
authored
Extension-contributed prompts/instructions/modes should not edit files in the extensions folder (#270360)
1 parent f83c2a6 commit 2010e15

File tree

2 files changed

+47
-8
lines changed

2 files changed

+47
-8
lines changed

src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts

Lines changed: 24 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import { IChatModeInstructions, IVariableReference } from '../../chatModes.js';
3131
import { dirname, isEqual } from '../../../../../../base/common/resources.js';
3232
import { IExtensionDescription } from '../../../../../../platform/extensions/common/extensions.js';
3333
import { Delayer } from '../../../../../../base/common/async.js';
34+
import { IFilesConfigurationService } from '../../../../../services/filesConfiguration/common/filesConfigurationService.js';
3435

3536
/**
3637
* Provides prompt services.
@@ -55,9 +56,9 @@ export class PromptsService extends Disposable implements IPromptsService {
5556
* Contributed files from extensions keyed by prompt type then name.
5657
*/
5758
private readonly contributedFiles = {
58-
[PromptsType.prompt]: new ResourceMap<IExtensionPromptPath>(),
59-
[PromptsType.instructions]: new ResourceMap<IExtensionPromptPath>(),
60-
[PromptsType.mode]: new ResourceMap<IExtensionPromptPath>(),
59+
[PromptsType.prompt]: new ResourceMap<Promise<IExtensionPromptPath>>(),
60+
[PromptsType.instructions]: new ResourceMap<Promise<IExtensionPromptPath>>(),
61+
[PromptsType.mode]: new ResourceMap<Promise<IExtensionPromptPath>>(),
6162
};
6263

6364
/**
@@ -73,7 +74,8 @@ export class PromptsService extends Disposable implements IPromptsService {
7374
@IUserDataProfileService private readonly userDataService: IUserDataProfileService,
7475
@ILanguageService private readonly languageService: ILanguageService,
7576
@IConfigurationService private readonly configurationService: IConfigurationService,
76-
@IFileService private readonly fileService: IFileService
77+
@IFileService private readonly fileService: IFileService,
78+
@IFilesConfigurationService private readonly filesConfigService: IFilesConfigurationService
7779
) {
7880
super();
7981

@@ -125,10 +127,11 @@ export class PromptsService extends Disposable implements IPromptsService {
125127

126128
const prompts = await Promise.all([
127129
this.fileLocator.listFiles(type, PromptsStorage.user, token).then(uris => uris.map(uri => ({ uri, storage: PromptsStorage.user, type } satisfies IUserPromptPath))),
128-
this.fileLocator.listFiles(type, PromptsStorage.local, token).then(uris => uris.map(uri => ({ uri, storage: PromptsStorage.local, type } satisfies ILocalPromptPath)))
130+
this.fileLocator.listFiles(type, PromptsStorage.local, token).then(uris => uris.map(uri => ({ uri, storage: PromptsStorage.local, type } satisfies ILocalPromptPath))),
131+
this.getExtensionContributions(type)
129132
]);
130133

131-
return [...prompts.flat(), ...this.contributedFiles[type].values()];
134+
return [...prompts.flat()];
132135
}
133136

134137
public async listPromptFilesForStorage(type: PromptsType, storage: PromptsStorage, token: CancellationToken): Promise<readonly IPromptPath[]> {
@@ -138,7 +141,7 @@ export class PromptsService extends Disposable implements IPromptsService {
138141

139142
switch (storage) {
140143
case PromptsStorage.extension:
141-
return Promise.resolve(Array.from(this.contributedFiles[type].values()));
144+
return this.getExtensionContributions(type);
142145
case PromptsStorage.local:
143146
return this.fileLocator.listFiles(type, PromptsStorage.local, token).then(uris => uris.map(uri => ({ uri, storage: PromptsStorage.local, type } satisfies ILocalPromptPath)));
144147
case PromptsStorage.user:
@@ -148,6 +151,10 @@ export class PromptsService extends Disposable implements IPromptsService {
148151
}
149152
}
150153

154+
private async getExtensionContributions(type: PromptsType): Promise<IPromptPath[]> {
155+
return Promise.all(this.contributedFiles[type].values());
156+
}
157+
151158
public getSourceFolders(type: PromptsType): readonly IPromptPath[] {
152159
if (!PromptsConfig.enabled(this.configurationService)) {
153160
return [];
@@ -301,7 +308,16 @@ export class PromptsService extends Disposable implements IPromptsService {
301308
// keep first registration per extension (handler filters duplicates per extension already)
302309
return Disposable.None;
303310
}
304-
bucket.set(uri, { uri, name, description, storage: PromptsStorage.extension, type, extension } satisfies IExtensionPromptPath);
311+
const entryPromise = (async () => {
312+
try {
313+
await this.filesConfigService.updateReadonly(uri, true);
314+
} catch (e) {
315+
const msg = e instanceof Error ? e.message : String(e);
316+
this.logger.error(`[registerContributedFile] Failed to make prompt file readonly: ${uri}`, msg);
317+
}
318+
return { uri, name, description, storage: PromptsStorage.extension, type, extension } satisfies IExtensionPromptPath;
319+
})();
320+
bucket.set(uri, entryPromise);
305321

306322
const updateModesIfRequired = () => {
307323
if (type === PromptsType.mode) {

src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ import { IUserDataProfileService } from '../../../../../../services/userDataProf
3737
import { ITelemetryService } from '../../../../../../../platform/telemetry/common/telemetry.js';
3838
import { NullTelemetryService } from '../../../../../../../platform/telemetry/common/telemetryUtils.js';
3939
import { Event } from '../../../../../../../base/common/event.js';
40+
import { IFilesConfigurationService } from '../../../../../../services/filesConfiguration/common/filesConfigurationService.js';
41+
import { IExtensionDescription } from '../../../../../../../platform/extensions/common/extensions.js';
4042

4143
suite('PromptsService', () => {
4244
const disposables = ensureNoDisposablesAreLeakedInTestSuite();
@@ -86,6 +88,8 @@ suite('PromptsService', () => {
8688
const fileSystemProvider = disposables.add(new InMemoryFileSystemProvider());
8789
disposables.add(fileService.registerProvider(Schemas.file, fileSystemProvider));
8890

91+
instaService.stub(IFilesConfigurationService, { updateReadonly: () => Promise.resolve() });
92+
8993
service = disposables.add(instaService.createInstance(PromptsService));
9094
instaService.stub(IPromptsService, service);
9195
});
@@ -812,5 +816,24 @@ suite('PromptsService', () => {
812816
'Must get custom chat modes.',
813817
);
814818
});
819+
820+
test('Contributed prompt file', async () => {
821+
const uri = URI.parse('file://extensions/my-extension/textMate.instructions.md');
822+
const extension = {} as IExtensionDescription;
823+
const registered = service.registerContributedFile(PromptsType.instructions,
824+
"TextMate Instructions",
825+
"Instructions to follow when authoring TextMate grammars",
826+
uri,
827+
extension
828+
);
829+
830+
const actual = await service.listPromptFiles(PromptsType.instructions, CancellationToken.None);
831+
assert.strictEqual(actual.length, 1);
832+
assert.strictEqual(actual[0].uri.toString(), uri.toString());
833+
assert.strictEqual(actual[0].name, "TextMate Instructions");
834+
assert.strictEqual(actual[0].storage, PromptsStorage.extension);
835+
assert.strictEqual(actual[0].type, PromptsType.instructions);
836+
registered.dispose();
837+
});
815838
});
816839
});

0 commit comments

Comments
 (0)