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 { runWhenGlobalIdle } from '../../../../../base/common/async.js';
7
>
import { CancellationToken } from '../../../../../base/common/cancellation.js';
8
>
import { Event } from '../../../../../base/common/event.js';
9
>
import { parse as parseJSONC } from '../../../../../base/common/json.js';
10
>
import { Lazy } from '../../../../../base/common/lazy.js';
11
>
import { Disposable } from '../../../../../base/common/lifecycle.js';
12
>
import { revive } from '../../../../../base/common/marshalling.js';
13
>
import { autorun, derived, IObservable, observableFromEvent, observableValue } from '../../../../../base/common/observable.js';
14
>
import { isEqual, isEqualOrParent, joinPath, normalizePath, relativePath } from '../../../../../base/common/resources.js';
15
>
import { URI } from '../../../../../base/common/uri.js';
16
>
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
17
>
import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js';
18
>
import { IFileService } from '../../../../../platform/files/common/files.js';
19
>
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
20
>
import { ILogService } from '../../../../../platform/log/common/log.js';
21
>
import { ObservableMemento, observableMemento } from '../../../../../platform/observable/common/observableMemento.js';
22
>
import { asJson, IRequestService } from '../../../../../platform/request/common/request.js';
23
>
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
24
>
import type { Dto } from '../../../../services/extensions/common/proxyIdentifier.js';
25
>
import { AutoUpdateConfigurationKey, IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';
26
>
import { ChatConfiguration } from '../constants.js';
27
>
import { IAgentPluginRepositoryService } from './agentPluginRepositoryService.js';
28
>
import { FileBackedInstalledPluginsStore, IStoredInstalledPlugin } from './fileBackedInstalledPluginsStore.js';
29
>
import { IWorkspacePluginSettingsService } from './workspacePluginSettingsService.js';
30
>
import { IWorkspaceTrustManagementService } from '../../../../../platform/workspace/common/workspaceTrust.js';
31
>
import { readAgentPluginManifest } from '../../../../../platform/agentPlugins/common/agentPluginParser.js';
32
>
import { type IMarketplaceReference, deduplicateMarketplaceReferences, MarketplaceReferenceKind, parseMarketplaceObjectEntry, parseMarketplaceReference, parseMarketplaceReferences, readConfiguredMarketplaces } from './marketplaceReference.js';
33
>
import { getStrictKnownMarketplaces, isMarketplaceReferenceAllowed } from './strictKnownMarketplaces.js';
34
>
35
>
// Re-export marketplace reference types for downstream consumers.
36
>
export { deduplicateMarketplaceReferences, extraKnownMarketplacesToConfigDict, MarketplaceReferenceKind, parseMarketplaceReference, parseMarketplaceReferences, readConfiguredMarketplaces } from './marketplaceReference.js';
37
>
export type { IConfiguredMarketplaces, IMarketplaceReference } from './marketplaceReference.js';
38
>
39
>
export const enum MarketplaceType {
40
>
Copilot = 'copilot',
41
>
Claude = 'claude',
42
>
OpenPlugin = 'openPlugin',
43
>
}
44
>
45
>
export const enum PluginSourceKind {
46
>
RelativePath = 'relativePath',
47
>
GitHub = 'github',
48
>
GitUrl = 'url',
49
>
Npm = 'npm',
50
>
Pip = 'pip',
51
>
}
52
>
53
>
export interface IRelativePathPluginSource {
54
>
readonly kind: PluginSourceKind.RelativePath;
55
>
/** Resolved relative path within the marketplace repository. */
56
>
readonly path: string;
57
>
}
58
>
59
>
export interface IGitHubPluginSource {
60
>
readonly kind: PluginSourceKind.GitHub;
61
>
readonly repo: string;
62
>
readonly ref?: string;
63
>
readonly sha?: string;
64
>
readonly path?: string;
65
>
}
66
>
67
>
export interface IGitUrlPluginSource {
68
>
readonly kind: PluginSourceKind.GitUrl;
69
>
/** Full git repository URL (must end with .git). */
70
>
readonly url: string;
71
>
readonly ref?: string;
72
>
readonly sha?: string;
73
>
/** Subdirectory within the repository where the plugin lives (for `git-subdir` sources). */
74
>
readonly path?: string;
75
>
}
76
>
77
>
export interface INpmPluginSource {
78
>
readonly kind: PluginSourceKind.Npm;
79
>
readonly package: string;
80
>
readonly version?: string;
81
>
readonly registry?: string;
82
>
}
83
>
84
>
export interface IPipPluginSource {
85
>
readonly kind: PluginSourceKind.Pip;
86
>
readonly package: string;
87
>
readonly version?: string;
88
>
readonly registry?: string;
89
>
}
90
>
91
>
export type IPluginSourceDescriptor =
92
>
| IRelativePathPluginSource
93
>
| IGitHubPluginSource
94
>
| IGitUrlPluginSource
95
>
| INpmPluginSource
96
>
| IPipPluginSource;
97
>
98
>
export interface IMarketplacePlugin {
99
>
readonly name: string;
100
>
readonly description: string;
101
>
readonly version: string;
102
>
/** Subdirectory within the repository where the plugin lives (for relative-path sources). */
103
>
readonly source: string;
104
>
/** Structured source descriptor indicating how the plugin should be fetched/installed. */
105
>
readonly sourceDescriptor: IPluginSourceDescriptor;
106
>
/** Marketplace label shown in UI and plugin provenance. */
107
>
readonly marketplace: string;
108
>
/** Canonical reference for clone/update/install location resolution. */
109
>
readonly marketplaceReference: IMarketplaceReference;
110
>
/** The type of marketplace this plugin comes from. */
111
>
readonly marketplaceType: MarketplaceType;
112
>
readonly readmeUri?: URI;
113
>
}
114
>
115
>
/** Raw JSON shape of a remote plugin source object in marketplace.json. */
116
>
interface IJsonPluginSource {
117
>
readonly source: string;
118
>
readonly repo?: string;
119
>
readonly url?: string;
120
>
readonly package?: string;
121
>
readonly ref?: string;
122
>
readonly sha?: string;
123
>
readonly path?: string;
124
>
readonly version?: string;
125
>
readonly registry?: string;
126
>
}
127
>
128
>
interface IMarketplaceJson {
129
>
readonly metadata?: {
130
>
readonly pluginRoot?: string;
131
>
};
132
>
readonly plugins?: readonly {
133
>
readonly name?: string;
134
>
readonly description?: string;
135
>
readonly version?: string;
136
>
readonly source?: string | IJsonPluginSource;
137
>
}[];
138
>
}
139
>
140
>
export interface IMarketplaceInstalledPlugin {
141
>
readonly pluginUri: URI;
142
>
readonly plugin: IMarketplacePlugin;
143
>
}
144
>
145
>
export const IPluginMarketplaceService = createDecorator<IPluginMarketplaceService>('pluginMarketplaceService');
146
>
147
>
export interface IPluginMarketplaceService {
148
>
readonly _serviceBrand: undefined;
149
>
readonly onDidChangeMarketplaces: Event<void>;
150
>
/** Installed marketplace plugins, backed by storage. */
151
>
readonly installedPlugins: IObservable<readonly IMarketplaceInstalledPlugin[]>;
152
>
/**
153
>
* Observable that is `true` when at least one cloned marketplace
154
>
* repository has upstream changes available. Checked periodically
155
>
* (approximately once per day) when `extensions.autoUpdate` is enabled.
156
>
*/
157
>
readonly hasUpdatesAvailable: IObservable<boolean>;
158
>
/**
159
>
* Observable snapshot of the last {@link fetchMarketplacePlugins} result.
160
>
* Empty until the first fetch completes. Views should use this for
161
>
* synchronous outdated-detection instead of calling fetchMarketplacePlugins.
162
>
*/
163
>
readonly lastFetchedPlugins: IObservable<readonly IMarketplacePlugin[]>;
164
>
/**
165
>
* Set of recommended plugin keys (`"pluginName@marketplaceName"`) aggregated
166
>
* from workspace-defined settings (e.g. `.claude/settings.json`). Providers
167
>
* may be added over time; consumers should not assume a specific source.
168
>
*/
169
>
readonly recommendedPlugins: IObservable<ReadonlySet<string>>;
170
>
/** Resets {@link hasUpdatesAvailable} to `false`. */
171
>
clearUpdatesAvailable(): void;
172
>
fetchMarketplacePlugins(token: CancellationToken): Promise<IMarketplacePlugin[]>;
173
>
getMarketplacePluginMetadata(pluginUri: URI): IMarketplacePlugin | undefined;
174
>
addInstalledPlugin(pluginUri: URI, plugin: IMarketplacePlugin): void;
175
>
removeInstalledPlugin(pluginUri: URI): void;
176
>
/** Returns whether the given marketplace is trusted — either explicitly trusted by the user, or allowed by the enterprise allowlist when strict mode is active. */
177
>
isMarketplaceTrusted(ref: IMarketplaceReference): boolean;
178
>
/**
179
>
* Returns whether the strict-marketplace enterprise policy
180
>
* (`chat.plugins.strictMarketplaces`) is active — i.e. an allowlist is
181
>
* configured. When active, blocked marketplaces cannot be trusted by the user.
182
>
*/
183
>
isStrictMarketplacePolicyActive(): boolean;
184
>
/** Records that the user trusts the given marketplace, persisted permanently. */
185
>
trustMarketplace(ref: IMarketplaceReference): void;
186
>
/**
187
>
* Reads marketplace definition files from an already-cloned repository
188
>
* directory and returns the declared plugins. Used by direct-install flows
189
>
* that clone a repo first, then need to discover its plugins.
190
>
*/
191
>
readPluginsFromDirectory(repoDir: URI, reference: IMarketplaceReference): Promise<IMarketplacePlugin[]>;
192
>
/**
193
>
* Reads a single-plugin manifest (e.g. `.claude-plugin/plugin.json`) at the
194
>
* root of an already-cloned repository directory and returns a synthesised
195
>
* {@link IMarketplacePlugin} describing the repository as a single plugin.
196
>
* Used by direct-install flows when {@link readPluginsFromDirectory} finds
197
>
* no marketplace index.
198
>
*
199
>
* Returns `undefined` when no recognised manifest is present at the repo
200
>
* root.
201
>
*/
202
>
readSinglePluginManifest(repoDir: URI, reference: IMarketplaceReference): Promise<IMarketplacePlugin | undefined>;
203
>
/**
204
>
* Returns whether the given directory is a standalone plugin — i.e. it
205
>
* contains a single-plugin manifest (e.g. `.plugin/plugin.json`,
206
>
* `.claude-plugin/plugin.json`, or `plugin.json`) at its root but is not a
207
>
* marketplace. Used by direct-install flows to route a local folder to the
208
>
* appropriate configuration.
209
>
*/
210
>
isPluginDirectory(repoDir: URI): Promise<boolean>;
211
>
}
212
>
213
>
/**
214
>
* Marketplace definition files by type, checked in order per repository.
215
>
* The first match determines the marketplace type.
216
>
*/
217
>
const MARKETPLACE_DEFINITIONS: { type: MarketplaceType; path: string }[] = [
218
>
{ type: MarketplaceType.OpenPlugin, path: 'marketplace.json' },
219
>
{ type: MarketplaceType.OpenPlugin, path: '.plugin/marketplace.json' },
220
>
{ type: MarketplaceType.Copilot, path: '.github/plugin/marketplace.json' },
221
>
{ type: MarketplaceType.Claude, path: '.claude-plugin/marketplace.json' },
222
>
];
223
>
224
>
/**
225
>
* Single-plugin manifest files by type, checked in order. Used when a cloned
226
>
* source repository has no marketplace index — the repository itself is the
227
>
* plugin. Order matches {@link detectPluginFormat} so that runtime format
228
>
* detection later agrees with the marketplace type chosen here.
229
>
*/
230
>
const SINGLE_PLUGIN_MANIFEST_DEFINITIONS: { type: MarketplaceType; path: string }[] = [
231
>
{ type: MarketplaceType.OpenPlugin, path: '.plugin/plugin.json' },
232
>
{ type: MarketplaceType.Claude, path: '.claude-plugin/plugin.json' },
233
>
{ type: MarketplaceType.Copilot, path: 'plugin.json' },
234
>
];
235
>
236
>
const GITHUB_MARKETPLACE_CACHE_TTL_MS = 8 * 60 * 60 * 1000;
237
>
const GITHUB_MARKETPLACE_CACHE_STORAGE_KEY = 'chat.plugins.marketplaces.githubCache.v1';
238
>
239
>
/** Interval between periodic plugin update checks (24 hours). */
240
>
const PLUGIN_UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000;
241
>
242
>
const PLUGIN_UPDATE_LAST_CHECK_STORAGE_KEY = 'chat.plugins.lastUpdateCheck.v1';
243
>
244
>
interface IGitHubMarketplaceCacheEntry {
245
>
readonly plugins: readonly IMarketplacePlugin[];
246
>
readonly expiresAt: number;
247
>
readonly referenceRawValue: string;
248
>
}
249
>
250
>
type IStoredGitHubMarketplaceCache = Dto<Record<string, IGitHubMarketplaceCacheEntry>>;
251
>
252
>
/**
253
>
* Ensures that an {@link IMarketplacePlugin} loaded from storage has a
254
>
* {@link IMarketplacePlugin.sourceDescriptor sourceDescriptor}. Plugins
255
>
* persisted before the sourceDescriptor field was introduced will only
256
>
* have the legacy `source` string — this function synthesises a
257
>
* {@link PluginSourceKind.RelativePath} descriptor from it.
258
>
*/
259
function ensureSourceDescriptor(plugin: IMarketplacePlugin): IMarketplacePlugin {
260
if (plugin.sourceDescriptor) {