@theia/plugin-ext 1.70.0-next.34 → 1.70.0-next.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/lib/common/plugin-api-rpc-model.d.ts +6 -1
  2. package/lib/common/plugin-api-rpc-model.d.ts.map +1 -1
  3. package/lib/common/plugin-api-rpc-model.js +2 -1
  4. package/lib/common/plugin-api-rpc-model.js.map +1 -1
  5. package/lib/common/plugin-protocol.d.ts +8 -0
  6. package/lib/common/plugin-protocol.d.ts.map +1 -1
  7. package/lib/common/plugin-protocol.js.map +1 -1
  8. package/lib/hosted/browser/hosted-plugin.d.ts +2 -0
  9. package/lib/hosted/browser/hosted-plugin.d.ts.map +1 -1
  10. package/lib/hosted/browser/hosted-plugin.js +7 -0
  11. package/lib/hosted/browser/hosted-plugin.js.map +1 -1
  12. package/lib/hosted/common/hosted-plugin.d.ts +3 -0
  13. package/lib/hosted/common/hosted-plugin.d.ts.map +1 -1
  14. package/lib/hosted/common/hosted-plugin.js +13 -0
  15. package/lib/hosted/common/hosted-plugin.js.map +1 -1
  16. package/lib/hosted/common/hosted-plugin.spec.d.ts +2 -0
  17. package/lib/hosted/common/hosted-plugin.spec.d.ts.map +1 -0
  18. package/lib/hosted/common/hosted-plugin.spec.js +308 -0
  19. package/lib/hosted/common/hosted-plugin.spec.js.map +1 -0
  20. package/lib/hosted/node/hosted-plugin-process.d.ts +2 -0
  21. package/lib/hosted/node/hosted-plugin-process.d.ts.map +1 -1
  22. package/lib/hosted/node/hosted-plugin-process.js +8 -0
  23. package/lib/hosted/node/hosted-plugin-process.js.map +1 -1
  24. package/lib/hosted/node/plugin-host-navigator-override.d.ts +12 -0
  25. package/lib/hosted/node/plugin-host-navigator-override.d.ts.map +1 -0
  26. package/lib/hosted/node/plugin-host-navigator-override.js +37 -0
  27. package/lib/hosted/node/plugin-host-navigator-override.js.map +1 -0
  28. package/lib/hosted/node/plugin-host-navigator-override.spec.d.ts +2 -0
  29. package/lib/hosted/node/plugin-host-navigator-override.spec.d.ts.map +1 -0
  30. package/lib/hosted/node/plugin-host-navigator-override.spec.js +49 -0
  31. package/lib/hosted/node/plugin-host-navigator-override.spec.js.map +1 -0
  32. package/lib/hosted/node/plugin-host.js +3 -0
  33. package/lib/hosted/node/plugin-host.js.map +1 -1
  34. package/lib/hosted/node/scanners/scanner-theia.d.ts +1 -0
  35. package/lib/hosted/node/scanners/scanner-theia.d.ts.map +1 -1
  36. package/lib/hosted/node/scanners/scanner-theia.js +7 -0
  37. package/lib/hosted/node/scanners/scanner-theia.js.map +1 -1
  38. package/lib/hosted/node/scanners/scanner-theia.spec.d.ts +2 -0
  39. package/lib/hosted/node/scanners/scanner-theia.spec.d.ts.map +1 -0
  40. package/lib/hosted/node/scanners/scanner-theia.spec.js +93 -0
  41. package/lib/hosted/node/scanners/scanner-theia.spec.js.map +1 -0
  42. package/lib/main/browser/languages-main.d.ts +2 -2
  43. package/lib/main/browser/languages-main.d.ts.map +1 -1
  44. package/lib/main/browser/languages-main.js +8 -9
  45. package/lib/main/browser/languages-main.js.map +1 -1
  46. package/lib/main/browser/plugin-ext-frontend-module.d.ts.map +1 -1
  47. package/lib/main/browser/plugin-ext-frontend-module.js +26 -0
  48. package/lib/main/browser/plugin-ext-frontend-module.js.map +1 -1
  49. package/lib/main/browser/plugin-ext-widget.d.ts +1 -1
  50. package/lib/main/browser/plugin-ext-widget.d.ts.map +1 -1
  51. package/lib/main/browser/plugin-ext-widget.js +10 -3
  52. package/lib/main/browser/plugin-ext-widget.js.map +1 -1
  53. package/lib/main/browser/text-editor-main.js +2 -2
  54. package/lib/main/common/plugin-host-environment-preferences.d.ts +14 -0
  55. package/lib/main/common/plugin-host-environment-preferences.d.ts.map +1 -0
  56. package/lib/main/common/plugin-host-environment-preferences.js +43 -0
  57. package/lib/main/common/plugin-host-environment-preferences.js.map +1 -0
  58. package/lib/main/node/plugin-ext-backend-module.d.ts.map +1 -1
  59. package/lib/main/node/plugin-ext-backend-module.js +5 -0
  60. package/lib/main/node/plugin-ext-backend-module.js.map +1 -1
  61. package/lib/main/node/plugin-host-navigator-state-initializer.d.ts +17 -0
  62. package/lib/main/node/plugin-host-navigator-state-initializer.d.ts.map +1 -0
  63. package/lib/main/node/plugin-host-navigator-state-initializer.js +53 -0
  64. package/lib/main/node/plugin-host-navigator-state-initializer.js.map +1 -0
  65. package/lib/plugin/file-system-event-service-ext-impl.d.ts +1 -1
  66. package/lib/plugin/file-system-event-service-ext-impl.d.ts.map +1 -1
  67. package/lib/plugin/file-system-ext-impl.d.ts +1 -1
  68. package/lib/plugin/file-system-ext-impl.d.ts.map +1 -1
  69. package/lib/plugin/file-system-ext-impl.js +5 -1
  70. package/lib/plugin/file-system-ext-impl.js.map +1 -1
  71. package/lib/plugin/types-impl.d.ts +2 -1
  72. package/lib/plugin/types-impl.d.ts.map +1 -1
  73. package/lib/plugin/types-impl.js.map +1 -1
  74. package/package.json +30 -30
  75. package/src/common/plugin-api-rpc-model.ts +10 -1
  76. package/src/common/plugin-protocol.ts +9 -1
  77. package/src/hosted/browser/hosted-plugin.ts +6 -0
  78. package/src/hosted/common/hosted-plugin.spec.ts +377 -0
  79. package/src/hosted/common/hosted-plugin.ts +18 -1
  80. package/src/hosted/node/hosted-plugin-process.ts +7 -0
  81. package/src/hosted/node/plugin-host-navigator-override.spec.ts +53 -0
  82. package/src/hosted/node/plugin-host-navigator-override.ts +34 -0
  83. package/src/hosted/node/plugin-host.ts +4 -0
  84. package/src/hosted/node/scanners/scanner-theia.spec.ts +112 -0
  85. package/src/hosted/node/scanners/scanner-theia.ts +8 -0
  86. package/src/main/browser/languages-main.ts +32 -51
  87. package/src/main/browser/plugin-ext-frontend-module.ts +31 -1
  88. package/src/main/browser/plugin-ext-widget.tsx +13 -3
  89. package/src/main/browser/style/plugin-sidebar.css +13 -0
  90. package/src/main/common/plugin-host-environment-preferences.ts +48 -0
  91. package/src/main/node/plugin-ext-backend-module.ts +5 -1
  92. package/src/main/node/plugin-host-navigator-state-initializer.ts +47 -0
  93. package/src/plugin/file-system-event-service-ext-impl.ts +1 -1
  94. package/src/plugin/file-system-ext-impl.ts +1 -2
  95. package/src/plugin/types-impl.ts +2 -1
@@ -0,0 +1,377 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
19
+ import { DeployedPlugin, PluginIdentifiers } from '../../common/plugin-protocol';
20
+ import { AbstractHostedPluginSupport, PluginContributions } from './hosted-plugin';
21
+ import { Measurement } from '@theia/core/lib/common/performance/measurement';
22
+
23
+ /* eslint-disable @typescript-eslint/no-explicit-any */
24
+
25
+ function createMockDeployedPlugin(id: string, untrustedWorkspacesSupport?: boolean | 'limited'): DeployedPlugin {
26
+ return {
27
+ metadata: {
28
+ host: 'main',
29
+ model: {
30
+ id,
31
+ name: id.split('.')[1] || id,
32
+ publisher: id.split('.')[0] || 'test',
33
+ version: '1.0.0',
34
+ displayName: id,
35
+ description: '',
36
+ engine: { type: 'theiaPlugin' as any, version: '1.0.0' },
37
+ entryPoint: { backend: 'main.js' },
38
+ packageUri: '',
39
+ packagePath: '',
40
+ untrustedWorkspacesSupport
41
+ },
42
+ lifecycle: { startMethod: 'activate', stopMethod: 'deactivate' } as any,
43
+ outOfSync: false
44
+ }
45
+ };
46
+ }
47
+
48
+ function createNoopMeasurement(): Measurement {
49
+ return {
50
+ stop: () => 0,
51
+ name: 'test',
52
+ log: () => { },
53
+ debug: () => { },
54
+ info: () => { },
55
+ warn: () => { },
56
+ error: () => { }
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Minimal concrete subclass of AbstractHostedPluginSupport for testing.
62
+ * Overrides all abstract methods with no-ops/stubs.
63
+ */
64
+ class TestHostedPluginSupport extends AbstractHostedPluginSupport<any, any> {
65
+ constructor() {
66
+ super('test-client');
67
+ }
68
+
69
+ protected createTheiaReadyPromise(): Promise<unknown> {
70
+ return Promise.resolve();
71
+ }
72
+
73
+ protected acceptPlugin(_plugin: DeployedPlugin): boolean | DeployedPlugin {
74
+ return true;
75
+ }
76
+
77
+ protected handleContributions(_plugin: DeployedPlugin): Disposable {
78
+ return Disposable.NULL;
79
+ }
80
+
81
+ protected async obtainManager(
82
+ _host: string, _hostContributions: PluginContributions[],
83
+ _toDisconnect: DisposableCollection
84
+ ): Promise<undefined> {
85
+ return undefined;
86
+ }
87
+
88
+ protected async getStoragePath(): Promise<string | undefined> {
89
+ return undefined;
90
+ }
91
+
92
+ protected async getHostGlobalStoragePath(): Promise<string> {
93
+ return '';
94
+ }
95
+
96
+ protected override measure(_name: string): Measurement {
97
+ return createNoopMeasurement();
98
+ }
99
+
100
+ /**
101
+ * Populate the contributions map with mock deployed plugins in the given state.
102
+ */
103
+ addPlugin(plugin: DeployedPlugin, state = PluginContributions.State.INITIALIZING): void {
104
+ const pluginId = PluginIdentifiers.componentsToUnversionedId(plugin.metadata.model);
105
+ const contributions = new PluginContributions(plugin);
106
+ contributions.state = state;
107
+ this.contributions.set(pluginId, contributions);
108
+ }
109
+
110
+ /**
111
+ * Expose the protected loadContributions for testing.
112
+ */
113
+ testLoadContributions(): Map<string, PluginContributions[]> {
114
+ const toDisconnect = new DisposableCollection(Disposable.create(() => { }));
115
+ return this.loadContributions(toDisconnect);
116
+ }
117
+
118
+ setWorkspaceTrusted(trusted: boolean): void {
119
+ this.workspaceTrusted = trusted;
120
+ }
121
+
122
+ clearDisabledByTrust(): void {
123
+ this._disabledByTrust.clear();
124
+ }
125
+
126
+ simulateLoadCycle(): Map<string, PluginContributions[]> {
127
+ this._disabledByTrust.clear();
128
+ const result = this.testLoadContributions();
129
+ if (this._disabledByTrust.size > 0) {
130
+ this.onDidChangePluginsEmitter.fire(undefined);
131
+ }
132
+ return result;
133
+ }
134
+ }
135
+
136
+ describe('AbstractHostedPluginSupport - workspace trust filtering', () => {
137
+ let support: TestHostedPluginSupport;
138
+
139
+ beforeEach(() => {
140
+ support = new TestHostedPluginSupport();
141
+ });
142
+
143
+ it('should skip plugin with untrustedWorkspacesSupport: false when workspace is untrusted', () => {
144
+ support.setWorkspaceTrusted(false);
145
+ const plugin = createMockDeployedPlugin('test.untrusted-false', false);
146
+ support.addPlugin(plugin);
147
+
148
+ const result = support.testLoadContributions();
149
+
150
+ // Plugin should not be in the returned host contributions
151
+ let pluginFound = false;
152
+ for (const contributions of result.values()) {
153
+ for (const c of contributions) {
154
+ if (c.plugin.metadata.model.id === 'test.untrusted-false') {
155
+ pluginFound = true;
156
+ }
157
+ }
158
+ }
159
+ expect(pluginFound).to.equal(false);
160
+ expect(support.disabledByTrust.has('test.untrusted-false')).to.equal(true);
161
+ });
162
+
163
+ it('should load plugin with untrustedWorkspacesSupport: true when workspace is untrusted', () => {
164
+ support.setWorkspaceTrusted(false);
165
+ const plugin = createMockDeployedPlugin('test.untrusted-true', true);
166
+ support.addPlugin(plugin);
167
+
168
+ const result = support.testLoadContributions();
169
+
170
+ let pluginFound = false;
171
+ for (const contributions of result.values()) {
172
+ for (const c of contributions) {
173
+ if (c.plugin.metadata.model.id === 'test.untrusted-true') {
174
+ pluginFound = true;
175
+ }
176
+ }
177
+ }
178
+ expect(pluginFound).to.equal(true);
179
+ expect(support.disabledByTrust.has('test.untrusted-true')).to.equal(false);
180
+ });
181
+
182
+ it('should load plugin with untrustedWorkspacesSupport: "limited" when workspace is untrusted', () => {
183
+ support.setWorkspaceTrusted(false);
184
+ const plugin = createMockDeployedPlugin('test.untrusted-limited', 'limited');
185
+ support.addPlugin(plugin);
186
+
187
+ const result = support.testLoadContributions();
188
+
189
+ let pluginFound = false;
190
+ for (const contributions of result.values()) {
191
+ for (const c of contributions) {
192
+ if (c.plugin.metadata.model.id === 'test.untrusted-limited') {
193
+ pluginFound = true;
194
+ }
195
+ }
196
+ }
197
+ expect(pluginFound).to.equal(true);
198
+ expect(support.disabledByTrust.has('test.untrusted-limited')).to.equal(false);
199
+ });
200
+
201
+ it('should load plugin with untrustedWorkspacesSupport: undefined when workspace is untrusted', () => {
202
+ support.setWorkspaceTrusted(false);
203
+ const plugin = createMockDeployedPlugin('test.untrusted-undefined', undefined);
204
+ support.addPlugin(plugin);
205
+
206
+ const result = support.testLoadContributions();
207
+
208
+ let pluginFound = false;
209
+ for (const contributions of result.values()) {
210
+ for (const c of contributions) {
211
+ if (c.plugin.metadata.model.id === 'test.untrusted-undefined') {
212
+ pluginFound = true;
213
+ }
214
+ }
215
+ }
216
+ expect(pluginFound).to.equal(true);
217
+ expect(support.disabledByTrust.has('test.untrusted-undefined')).to.equal(false);
218
+ });
219
+
220
+ it('should load plugin with untrustedWorkspacesSupport: false when workspace is trusted', () => {
221
+ support.setWorkspaceTrusted(true);
222
+ const plugin = createMockDeployedPlugin('test.trusted-false', false);
223
+ support.addPlugin(plugin);
224
+
225
+ const result = support.testLoadContributions();
226
+
227
+ let pluginFound = false;
228
+ for (const contributions of result.values()) {
229
+ for (const c of contributions) {
230
+ if (c.plugin.metadata.model.id === 'test.trusted-false') {
231
+ pluginFound = true;
232
+ }
233
+ }
234
+ }
235
+ expect(pluginFound).to.equal(true);
236
+ expect(support.disabledByTrust.has('test.trusted-false')).to.equal(false);
237
+ });
238
+
239
+ it('should clear disabledByTrust when workspace becomes trusted on re-load', () => {
240
+ // First load: untrusted workspace, plugin should be disabled
241
+ support.setWorkspaceTrusted(false);
242
+ const plugin = createMockDeployedPlugin('test.reload-trust', false);
243
+ support.addPlugin(plugin);
244
+
245
+ support.testLoadContributions();
246
+ expect(support.disabledByTrust.has('test.reload-trust')).to.equal(true);
247
+ expect(support.disabledByTrust.size).to.equal(1);
248
+
249
+ // Second load: simulate doLoad() which clears disabledByTrust before loadContributions
250
+ support.setWorkspaceTrusted(true);
251
+ support.clearDisabledByTrust();
252
+ support.testLoadContributions();
253
+ expect(support.disabledByTrust.size).to.equal(0);
254
+ });
255
+
256
+ it('should correctly populate disabledByTrust with only filtered plugins', () => {
257
+ support.setWorkspaceTrusted(false);
258
+ const blockedPlugin = createMockDeployedPlugin('test.blocked', false);
259
+ const allowedPlugin = createMockDeployedPlugin('test.allowed', true);
260
+ support.addPlugin(blockedPlugin);
261
+ support.addPlugin(allowedPlugin);
262
+
263
+ support.testLoadContributions();
264
+
265
+ expect(support.disabledByTrust.size).to.equal(1);
266
+ expect(support.disabledByTrust.has('test.blocked')).to.equal(true);
267
+ expect(support.disabledByTrust.has('test.allowed')).to.equal(false);
268
+ });
269
+
270
+ describe('plugins deployed after initial load (re-load cycle)', () => {
271
+ it('should disable a newly deployed plugin with untrustedWorkspacesSupport: false on re-load', () => {
272
+ support.setWorkspaceTrusted(false);
273
+
274
+ const initialPlugin = createMockDeployedPlugin('test.initial', false);
275
+ support.addPlugin(initialPlugin);
276
+ support.simulateLoadCycle();
277
+ expect(support.disabledByTrust.has('test.initial')).to.equal(true);
278
+
279
+ const newPlugin = createMockDeployedPlugin('test.new-deploy', false);
280
+ support.addPlugin(newPlugin);
281
+ support.simulateLoadCycle();
282
+
283
+ expect(support.disabledByTrust.has('test.initial')).to.equal(true);
284
+ expect(support.disabledByTrust.has('test.new-deploy')).to.equal(true);
285
+ expect(support.disabledByTrust.size).to.equal(2);
286
+ });
287
+
288
+ it('should re-add previously disabled plugins to disabledByTrust on each re-load cycle', () => {
289
+ support.setWorkspaceTrusted(false);
290
+ const plugin = createMockDeployedPlugin('test.persistent', false);
291
+ support.addPlugin(plugin);
292
+
293
+ support.simulateLoadCycle();
294
+ expect(support.disabledByTrust.has('test.persistent')).to.equal(true);
295
+
296
+ support.simulateLoadCycle();
297
+ expect(support.disabledByTrust.has('test.persistent')).to.equal(true);
298
+
299
+ support.simulateLoadCycle();
300
+ expect(support.disabledByTrust.has('test.persistent')).to.equal(true);
301
+ });
302
+
303
+ it('should not add already-started allowed plugins to disabledByTrust on re-load', () => {
304
+ support.setWorkspaceTrusted(false);
305
+ const startedAllowed = createMockDeployedPlugin('test.started-allowed', true);
306
+ support.addPlugin(startedAllowed, PluginContributions.State.STARTED);
307
+ const startedUndefined = createMockDeployedPlugin('test.started-undefined', undefined);
308
+ support.addPlugin(startedUndefined, PluginContributions.State.STARTED);
309
+
310
+ support.simulateLoadCycle();
311
+
312
+ expect(support.disabledByTrust.has('test.started-allowed')).to.equal(false);
313
+ expect(support.disabledByTrust.has('test.started-undefined')).to.equal(false);
314
+ expect(support.disabledByTrust.size).to.equal(0);
315
+ });
316
+
317
+ it('should add already-started plugins with untrustedWorkspacesSupport: false to disabledByTrust on re-load', () => {
318
+ support.setWorkspaceTrusted(false);
319
+ const startedBlocked = createMockDeployedPlugin('test.started-blocked', false);
320
+ support.addPlugin(startedBlocked, PluginContributions.State.STARTED);
321
+
322
+ support.simulateLoadCycle();
323
+
324
+ expect(support.disabledByTrust.has('test.started-blocked')).to.equal(true);
325
+ });
326
+
327
+ it('should fire onDidChangePlugins after loadContributions when a newly deployed plugin is disabled by trust', () => {
328
+ support.setWorkspaceTrusted(false);
329
+ const existingPlugin = createMockDeployedPlugin('test.existing', false);
330
+ support.addPlugin(existingPlugin);
331
+ support.simulateLoadCycle();
332
+
333
+ const newPlugin = createMockDeployedPlugin('test.late-deploy', false);
334
+ support.addPlugin(newPlugin);
335
+
336
+ let eventFiredCount = 0;
337
+ support.onDidChangePlugins(() => { eventFiredCount++; });
338
+
339
+ support.simulateLoadCycle();
340
+
341
+ expect(support.disabledByTrust.has('test.late-deploy')).to.equal(true);
342
+ expect(eventFiredCount).to.equal(1);
343
+ });
344
+
345
+ it('should not fire onDidChangePlugins when no plugins are disabled by trust', () => {
346
+ support.setWorkspaceTrusted(false);
347
+ const allowedPlugin = createMockDeployedPlugin('test.allowed-nodeploy', true);
348
+ support.addPlugin(allowedPlugin);
349
+
350
+ let eventFiredCount = 0;
351
+ support.onDidChangePlugins(() => { eventFiredCount++; });
352
+
353
+ support.simulateLoadCycle();
354
+
355
+ expect(support.disabledByTrust.size).to.equal(0);
356
+ expect(eventFiredCount).to.equal(0);
357
+ });
358
+
359
+ it('should include both initial and newly deployed disabled plugins in disabledByTrust when workspace is untrusted', () => {
360
+ support.setWorkspaceTrusted(false);
361
+
362
+ const builtin = createMockDeployedPlugin('test.builtin', false);
363
+ support.addPlugin(builtin);
364
+ support.simulateLoadCycle();
365
+ expect(support.disabledByTrust.size).to.equal(1);
366
+
367
+ const userInstalled = createMockDeployedPlugin('test.user-installed', false);
368
+ support.addPlugin(userInstalled);
369
+ support.simulateLoadCycle();
370
+
371
+ expect(support.disabledByTrust.size).to.equal(2);
372
+ expect(support.disabledByTrust.has('test.builtin')).to.equal(true);
373
+ expect(support.disabledByTrust.has('test.user-installed')).to.equal(true);
374
+ });
375
+ });
376
+
377
+ });
@@ -84,6 +84,14 @@ export abstract class AbstractHostedPluginSupport<PM extends AbstractPluginManag
84
84
 
85
85
  protected readonly activationEvents = new Set<string>();
86
86
 
87
+ protected workspaceTrusted: boolean = false;
88
+
89
+ protected readonly _disabledByTrust = new Set<string>();
90
+
91
+ get disabledByTrust(): ReadonlySet<string> {
92
+ return this._disabledByTrust;
93
+ }
94
+
87
95
  protected readonly onDidChangePluginsEmitter = new Emitter<void>();
88
96
  readonly onDidChangePlugins = this.onDidChangePluginsEmitter.event;
89
97
 
@@ -150,6 +158,8 @@ export abstract class AbstractHostedPluginSupport<PM extends AbstractPluginManag
150
158
  protected async doLoad(): Promise<void> {
151
159
  const toDisconnect = new DisposableCollection(Disposable.create(() => { /* mark as connected */ }));
152
160
 
161
+ this._disabledByTrust.clear();
162
+
153
163
  await this.beforeSyncPlugins(toDisconnect);
154
164
 
155
165
  // process empty plugins as well in order to properly remove stale plugin widgets
@@ -166,6 +176,9 @@ export abstract class AbstractHostedPluginSupport<PM extends AbstractPluginManag
166
176
  return;
167
177
  }
168
178
  const contributionsByHost = this.loadContributions(toDisconnect);
179
+ if (this._disabledByTrust.size > 0) {
180
+ this.onDidChangePluginsEmitter.fire(undefined);
181
+ }
169
182
 
170
183
  await this.afterLoadContributions(toDisconnect);
171
184
 
@@ -301,9 +314,13 @@ export abstract class AbstractHostedPluginSupport<PM extends AbstractPluginManag
301
314
  const hostContributions = new Map<PluginHost, PluginContributions[]>();
302
315
  console.log(`[${this.clientId}] Loading plugin contributions`);
303
316
  for (const contributions of this.contributions.values()) {
317
+ if (!this.workspaceTrusted && contributions.plugin.metadata.model.untrustedWorkspacesSupport === false) {
318
+ this._disabledByTrust.add(PluginIdentifiers.componentsToUnversionedId(contributions.plugin.metadata.model));
319
+ continue;
320
+ }
321
+
304
322
  const plugin = contributions.plugin.metadata;
305
323
  const pluginId = plugin.model.id;
306
-
307
324
  if (contributions.state === PluginContributions.State.INITIALIZING) {
308
325
  contributions.state = PluginContributions.State.LOADING;
309
326
  contributions.push(Disposable.create(() => console.log(`[${pluginId}]: Unloaded plugin.`)));
@@ -22,6 +22,7 @@ import { inject, injectable, named } from '@theia/core/shared/inversify';
22
22
  import * as cp from 'child_process';
23
23
  import { Duplex } from 'stream';
24
24
  import { HostedPluginClient, PLUGIN_HOST_BACKEND, PluginHostEnvironmentVariable, ServerPluginRunner } from '../../common/plugin-protocol';
25
+ import { PluginHostNavigatorState } from '../../main/common/plugin-host-environment-preferences';
25
26
  import { HostedPluginCliContribution } from './hosted-plugin-cli-contribution';
26
27
  import { HostedPluginLocalizationService } from './hosted-plugin-localization-service';
27
28
  import { ProcessTerminateMessage, ProcessTerminatedMessage } from './hosted-plugin-protocol';
@@ -64,6 +65,9 @@ export class HostedPluginProcess implements ServerPluginRunner {
64
65
  @inject(ProcessUtils)
65
66
  protected readonly processUtils: ProcessUtils;
66
67
 
68
+ @inject(PluginHostNavigatorState)
69
+ protected readonly navigatorState: PluginHostNavigatorState;
70
+
67
71
  private childProcess: cp.ChildProcess | undefined;
68
72
  private messagePipe?: BinaryMessagePipe;
69
73
  private client: HostedPluginClient;
@@ -179,6 +183,9 @@ export class HostedPluginProcess implements ServerPluginRunner {
179
183
  env['VSCODE_NLS_CONFIG'] = JSON.stringify(this.localizationService.getNlsConfig());
180
184
  // apply external env variables
181
185
  this.pluginHostEnvironmentVariables.getContributions().forEach(envVar => envVar.process(env));
186
+ if (this.navigatorState.supportNodeGlobalNavigator) {
187
+ env['THEIA_SUPPORT_NODE_GLOBAL_NAVIGATOR'] = 'true';
188
+ }
182
189
  if (this.cli.extensionTestsPath) {
183
190
  env.extensionTestsPath = this.cli.extensionTestsPath;
184
191
  }
@@ -0,0 +1,53 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 STMicroelectronics and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ import { expect } from 'chai';
18
+ import { suppressNodeNavigator } from './plugin-host-navigator-override';
19
+
20
+ describe('suppressNodeNavigator', () => {
21
+
22
+ it('should override navigator to return undefined when env var is not set', () => {
23
+ const target = { navigator: { userAgent: 'test' } } as typeof globalThis;
24
+ suppressNodeNavigator({}, target);
25
+ expect(target.navigator).to.be.undefined;
26
+ });
27
+
28
+ it('should override navigator to return undefined when env var is set to a non-true value', () => {
29
+ const target = { navigator: { userAgent: 'test' } } as typeof globalThis;
30
+ suppressNodeNavigator({ 'THEIA_SUPPORT_NODE_GLOBAL_NAVIGATOR': 'false' }, target);
31
+ expect(target.navigator).to.be.undefined;
32
+ });
33
+
34
+ it('should preserve navigator when env var is set to true', () => {
35
+ const original = { userAgent: 'test' };
36
+ const target = { navigator: original } as typeof globalThis;
37
+ suppressNodeNavigator({ 'THEIA_SUPPORT_NODE_GLOBAL_NAVIGATOR': 'true' }, target);
38
+ expect(target.navigator).to.equal(original);
39
+ });
40
+
41
+ it('should make the override configurable so it can be restored', () => {
42
+ const target = { navigator: { userAgent: 'test' } } as typeof globalThis;
43
+ suppressNodeNavigator({}, target);
44
+ expect(target.navigator).to.be.undefined;
45
+
46
+ const restored = { userAgent: 'restored' };
47
+ Object.defineProperty(target, 'navigator', {
48
+ value: restored,
49
+ configurable: true,
50
+ });
51
+ expect(target.navigator).to.equal(restored);
52
+ });
53
+ });
@@ -0,0 +1,34 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 STMicroelectronics and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ /**
18
+ * Workaround: Node 21+ defines `globalThis.navigator`, which breaks VS Code
19
+ * extensions that use its absence to detect a Node.js environment.
20
+ * Unless explicitly opted in via `THEIA_SUPPORT_NODE_GLOBAL_NAVIGATOR=true`,
21
+ * override it with a getter that returns `undefined` to match VS Code behavior.
22
+ * See https://github.com/eclipse-theia/theia/issues/16233
23
+ *
24
+ * @param env the environment variables to read the opt-in flag from
25
+ * @param target the object on which to override the `navigator` property (defaults to `globalThis`)
26
+ */
27
+ export function suppressNodeNavigator(env: Record<string, string | undefined> = process.env, target: typeof globalThis = globalThis): void {
28
+ if (env['THEIA_SUPPORT_NODE_GLOBAL_NAVIGATOR'] !== 'true') {
29
+ Object.defineProperty(target, 'navigator', {
30
+ get: () => undefined,
31
+ configurable: true,
32
+ });
33
+ }
34
+ }
@@ -14,6 +14,7 @@
14
14
  // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
15
  // *****************************************************************************
16
16
  import '@theia/core/shared/reflect-metadata';
17
+ import { suppressNodeNavigator } from './plugin-host-navigator-override';
17
18
  import { Container } from '@theia/core/shared/inversify';
18
19
  import { URI as VSCodeURI } from '@theia/core/shared/vscode-uri';
19
20
  import { MsgPackExtensionManager } from '@theia/core/lib/common/message-rpc/msg-pack-extension-manager';
@@ -23,6 +24,9 @@ import { PluginHostRPC } from './plugin-host-rpc';
23
24
  import pluginHostModule from './plugin-host-module';
24
25
  import { URI } from '../../plugin/types-impl';
25
26
 
27
+ // Undefine globalThis.navigator unless opted in, see https://github.com/eclipse-theia/theia/issues/16233
28
+ suppressNodeNavigator();
29
+
26
30
  console.log('PLUGIN_HOST(' + process.pid + ') starting instance');
27
31
 
28
32
  // override exit() function, to do not allow plugin kill this node
@@ -0,0 +1,112 @@
1
+ // *****************************************************************************
2
+ // Copyright (C) 2026 EclipseSource and others.
3
+ //
4
+ // This program and the accompanying materials are made available under the
5
+ // terms of the Eclipse Public License v. 2.0 which is available at
6
+ // http://www.eclipse.org/legal/epl-2.0.
7
+ //
8
+ // This Source Code may also be made available under the following Secondary
9
+ // Licenses when the conditions for such availability set forth in the Eclipse
10
+ // Public License v. 2.0 are satisfied: GNU General Public License, version 2
11
+ // with the GNU Classpath Exception which is available at
12
+ // https://www.gnu.org/software/classpath/license.html.
13
+ //
14
+ // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
15
+ // *****************************************************************************
16
+
17
+ /* eslint-disable @typescript-eslint/no-explicit-any */
18
+
19
+ import { expect } from 'chai';
20
+ import * as os from 'os';
21
+ import * as path from 'path';
22
+ import * as fs from 'fs';
23
+ import { AbstractPluginScanner } from './scanner-theia';
24
+ import { PluginPackage, PluginEntryPoint } from '../../../common/plugin-protocol';
25
+ import URI from '@theia/core/lib/common/uri';
26
+
27
+ class TestPluginScanner extends AbstractPluginScanner {
28
+ constructor() {
29
+ // Use a dummy api type; no backend init path needed
30
+ super('theiaPlugin' as any);
31
+ }
32
+
33
+ protected getEntryPoint(_plugin: PluginPackage): PluginEntryPoint {
34
+ return { backend: 'main.js' };
35
+ }
36
+ }
37
+
38
+ function createMinimalPluginPackage(
39
+ capabilities?: PluginPackage['capabilities'],
40
+ packagePath?: string
41
+ ): PluginPackage {
42
+ return {
43
+ name: 'test-plugin',
44
+ publisher: 'test-publisher',
45
+ version: '1.0.0',
46
+ engines: { theiaPlugin: '1.0.0' },
47
+ displayName: 'Test Plugin',
48
+ description: 'A test plugin',
49
+ packagePath: packagePath ?? os.tmpdir(),
50
+ capabilities
51
+ } as PluginPackage;
52
+ }
53
+
54
+ describe('AbstractPluginScanner', () => {
55
+ let scanner: TestPluginScanner;
56
+ let tmpDir: string;
57
+
58
+ before(() => {
59
+ scanner = new TestPluginScanner();
60
+ // Inject a mock pluginUriFactory
61
+ (scanner as any).pluginUriFactory = {
62
+ createUri: (_pkg: PluginPackage, _relativePath?: string) => new URI('file:///dummy')
63
+ };
64
+ // Create a real temp directory so readdirSync in getLicenseUrl/getReadmeUrl works
65
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'scanner-test-'));
66
+ });
67
+
68
+ after(() => {
69
+ fs.rmSync(tmpDir, { recursive: true, force: true });
70
+ });
71
+
72
+ it('should set untrustedWorkspacesSupport to false when capabilities.untrustedWorkspaces.supported is false', () => {
73
+ const pkg = createMinimalPluginPackage({
74
+ untrustedWorkspaces: { supported: false }
75
+ }, tmpDir);
76
+
77
+ const model = scanner.getModel(pkg);
78
+ expect(model.untrustedWorkspacesSupport).to.equal(false);
79
+ });
80
+
81
+ it('should set untrustedWorkspacesSupport to true when capabilities.untrustedWorkspaces.supported is true', () => {
82
+ const pkg = createMinimalPluginPackage({
83
+ untrustedWorkspaces: { supported: true }
84
+ }, tmpDir);
85
+
86
+ const model = scanner.getModel(pkg);
87
+ expect(model.untrustedWorkspacesSupport).to.equal(true);
88
+ });
89
+
90
+ it('should set untrustedWorkspacesSupport to "limited" when capabilities.untrustedWorkspaces.supported is "limited"', () => {
91
+ const pkg = createMinimalPluginPackage({
92
+ untrustedWorkspaces: { supported: 'limited' }
93
+ }, tmpDir);
94
+
95
+ const model = scanner.getModel(pkg);
96
+ expect(model.untrustedWorkspacesSupport).to.equal('limited');
97
+ });
98
+
99
+ it('should leave untrustedWorkspacesSupport undefined when no capabilities field is present', () => {
100
+ const pkg = createMinimalPluginPackage(undefined, tmpDir);
101
+
102
+ const model = scanner.getModel(pkg);
103
+ expect(model.untrustedWorkspacesSupport).to.equal(undefined);
104
+ });
105
+
106
+ it('should leave untrustedWorkspacesSupport undefined when capabilities has no untrustedWorkspaces', () => {
107
+ const pkg = createMinimalPluginPackage({} as any, tmpDir);
108
+
109
+ const model = scanner.getModel(pkg);
110
+ expect(model.untrustedWorkspacesSupport).to.equal(undefined);
111
+ });
112
+ });