Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/agentHost/node/agentPluginManager.ts
13394 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { VSBuffer } from '../../../base/common/buffer.js';
7
import { SequencerByKey } from '../../../base/common/async.js';
8
import { URI } from '../../../base/common/uri.js';
9
import { IFileService } from '../../files/common/files.js';
10
import { ILogService } from '../../log/common/log.js';
11
import { IAgentPluginManager, type ISyncedCustomization } from '../common/agentPluginManager.js';
12
import { CustomizationStatus, type CustomizationRef, type SessionCustomization } from '../common/state/sessionState.js';
13
import { toAgentClientUri } from '../common/agentClientUri.js';
14
15
const DEFAULT_MAX_PLUGINS = 20;
16
17
/** On-disk cache entry format. */
18
interface ICacheEntry {
19
readonly uri: string;
20
readonly nonce: string;
21
}
22
23
/**
24
* Implementation of {@link IAgentPluginManager}.
25
*
26
* Syncs plugin directories to local storage under
27
* `{userDataPath}/agentPlugins/{key}/`. Uses a {@link SequencerByKey}
28
* per plugin URI so that concurrent syncs of the same plugin are
29
* serialized and cannot clobber each other.
30
*
31
* The nonce cache and LRU order are persisted to a JSON file in the
32
* base path so they survive process restarts.
33
*/
34
export class AgentPluginManager implements IAgentPluginManager {
35
declare readonly _serviceBrand: undefined;
36
37
private readonly _basePath: URI;
38
private readonly _cachePath: URI;
39
private readonly _maxPlugins: number;
40
41
/** Serializes concurrent sync operations per plugin URI. */
42
private readonly _sequencer = new SequencerByKey<string>();
43
44
/** Nonces for plugins on disk, keyed by original customization URI string. */
45
private readonly _cachedNonces = new Map<string, string>();
46
47
/** LRU order: most recently used original customization URI strings at the end. */
48
private readonly _lruOrder: string[] = [];
49
50
/** Whether the on-disk cache has been loaded. */
51
private _cacheLoaded = false;
52
53
constructor(
54
userDataPath: URI,
55
@IFileService private readonly _fileService: IFileService,
56
@ILogService private readonly _logService: ILogService,
57
maxPlugins: number = DEFAULT_MAX_PLUGINS,
58
) {
59
this._basePath = URI.joinPath(userDataPath, 'agentPlugins');
60
this._cachePath = URI.joinPath(this._basePath, 'cache.json');
61
this._maxPlugins = maxPlugins;
62
}
63
64
async syncCustomizations(
65
clientId: string,
66
customizations: CustomizationRef[],
67
progress?: (status: SessionCustomization[]) => void,
68
): Promise<ISyncedCustomization[]> {
69
await this._ensureCacheLoaded();
70
71
// Build initial loading status and fire it immediately via progress
72
const statuses: SessionCustomization[] = customizations.map(c => ({
73
customization: c,
74
enabled: true,
75
status: CustomizationStatus.Loading,
76
}));
77
progress?.([...statuses]);
78
79
// Sync each customization in parallel, serialized per URI
80
const results = await Promise.all(customizations.map((ref, i) =>
81
this._sequencer.queue(ref.uri, async (): Promise<ISyncedCustomization> => {
82
try {
83
const pluginDir = await this._syncPlugin(clientId, ref);
84
statuses[i] = { customization: ref, enabled: true, status: CustomizationStatus.Loaded };
85
progress?.([...statuses]);
86
return { customization: statuses[i], pluginDir };
87
} catch (err) {
88
const message = err instanceof Error ? err.message : String(err);
89
this._logService.error(`[AgentPluginManager] Failed to sync plugin ${ref.uri}: ${message}`);
90
statuses[i] = { customization: ref, enabled: true, status: CustomizationStatus.Error, statusMessage: message };
91
progress?.([...statuses]);
92
return { customization: statuses[i] };
93
}
94
})
95
));
96
97
return results;
98
}
99
100
// ---- plugin storage logic -----------------------------------------------
101
102
/**
103
* Syncs a single plugin to local storage. Skips the copy when the
104
* nonce matches the cached value. Returns the local directory URI.
105
*/
106
private async _syncPlugin(clientId: string, ref: CustomizationRef): Promise<URI> {
107
const pluginUri = toAgentClientUri(URI.parse(ref.uri), clientId);
108
const key = this._keyForUri(ref.uri);
109
const destDir = URI.joinPath(this._basePath, key);
110
111
// Nonce cache hit — skip copy
112
if (ref.nonce && this._cachedNonces.get(ref.uri) === ref.nonce) {
113
this._touchLru(ref.uri);
114
this._logService.trace(`[AgentPluginManager] Nonce match for ${ref.uri}, skipping copy`);
115
return destDir;
116
}
117
118
this._logService.info(`[AgentPluginManager] Syncing plugin: ${ref.uri} → ${destDir.toString()}`);
119
120
await this._fileService.copy(pluginUri, destDir, true);
121
122
if (ref.nonce) {
123
this._cachedNonces.set(ref.uri, ref.nonce);
124
}
125
this._touchLru(ref.uri);
126
await this._evictIfNeeded();
127
await this._persistCache();
128
129
return destDir;
130
}
131
132
private _keyForUri(uri: string): string {
133
return uri.replace(/[^a-zA-Z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').substring(0, 128);
134
}
135
136
private _touchLru(uri: string): void {
137
const idx = this._lruOrder.indexOf(uri);
138
if (idx !== -1) {
139
this._lruOrder.splice(idx, 1);
140
}
141
this._lruOrder.push(uri);
142
}
143
144
private async _evictIfNeeded(): Promise<void> {
145
while (this._lruOrder.length > this._maxPlugins) {
146
const evictUri = this._lruOrder.shift();
147
if (!evictUri) {
148
break;
149
}
150
this._cachedNonces.delete(evictUri);
151
const evictKey = this._keyForUri(evictUri);
152
const evictDir = URI.joinPath(this._basePath, evictKey);
153
this._logService.info(`[AgentPluginManager] Evicting plugin: ${evictUri}`);
154
try {
155
await this._fileService.del(evictDir, { recursive: true });
156
} catch (err) {
157
this._logService.warn(`[AgentPluginManager] Failed to evict plugin: ${evictUri}`, err);
158
}
159
}
160
}
161
162
// ---- cache persistence --------------------------------------------------
163
164
private async _ensureCacheLoaded(): Promise<void> {
165
if (this._cacheLoaded) {
166
return;
167
}
168
this._cacheLoaded = true;
169
170
try {
171
if (!await this._fileService.exists(this._cachePath)) {
172
return;
173
}
174
const content = await this._fileService.readFile(this._cachePath);
175
const entries: ICacheEntry[] = JSON.parse(content.value.toString());
176
if (!Array.isArray(entries)) {
177
return;
178
}
179
180
// Entries are stored in LRU order (oldest first)
181
for (const entry of entries) {
182
if (typeof entry.uri === 'string' && typeof entry.nonce === 'string') {
183
this._cachedNonces.set(entry.uri, entry.nonce);
184
this._lruOrder.push(entry.uri);
185
}
186
}
187
this._logService.trace(`[AgentPluginManager] Loaded ${entries.length} cache entries from disk`);
188
} catch (err) {
189
this._logService.warn('[AgentPluginManager] Failed to load cache from disk', err);
190
}
191
}
192
193
private async _persistCache(): Promise<void> {
194
try {
195
// Write entries in LRU order (oldest first)
196
const entries: ICacheEntry[] = [];
197
for (const uri of this._lruOrder) {
198
const nonce = this._cachedNonces.get(uri);
199
if (nonce) {
200
entries.push({ uri, nonce });
201
}
202
}
203
await this._fileService.createFolder(this._basePath);
204
await this._fileService.writeFile(this._cachePath, VSBuffer.fromString(JSON.stringify(entries)));
205
} catch (err) {
206
this._logService.warn('[AgentPluginManager] Failed to persist cache to disk', err);
207
}
208
}
209
}
210
211