Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/services/accounts/browser/defaultAccount.ts
5241 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 { Emitter } from '../../../../base/common/event.js';
7
import { Disposable } from '../../../../base/common/lifecycle.js';
8
import { IProductService } from '../../../../platform/product/common/productService.js';
9
import { AuthenticationSession, AuthenticationSessionAccount, IAuthenticationExtensionsService, IAuthenticationService } from '../../authentication/common/authentication.js';
10
import { asJson, IRequestService } from '../../../../platform/request/common/request.js';
11
import { CancellationToken } from '../../../../base/common/cancellation.js';
12
import { IExtensionService } from '../../extensions/common/extensions.js';
13
import { ILogService } from '../../../../platform/log/common/log.js';
14
import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
15
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
16
import { Barrier, RunOnceScheduler, ThrottledDelayer, timeout } from '../../../../base/common/async.js';
17
import { IHostService } from '../../host/browser/host.js';
18
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
19
import { getErrorMessage } from '../../../../base/common/errors.js';
20
import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IEntitlementsData, IPolicyData } from '../../../../base/common/defaultAccount.js';
21
import { isString, Mutable } from '../../../../base/common/types.js';
22
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
23
import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';
24
import { isWeb } from '../../../../base/common/platform.js';
25
import { IDefaultAccountProvider, IDefaultAccountService } from '../../../../platform/defaultAccount/common/defaultAccount.js';
26
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
27
import { distinct } from '../../../../base/common/arrays.js';
28
import { equals } from '../../../../base/common/objects.js';
29
import { IDefaultChatAgent } from '../../../../base/common/product.js';
30
import { IRequestContext } from '../../../../base/parts/request/common/request.js';
31
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
32
33
interface IDefaultAccountConfig {
34
readonly preferredExtensions: string[];
35
readonly authenticationProvider: {
36
readonly default: {
37
readonly id: string;
38
readonly name: string;
39
};
40
readonly enterprise: {
41
readonly id: string;
42
readonly name: string;
43
};
44
readonly enterpriseProviderConfig: string;
45
readonly enterpriseProviderUriSetting: string;
46
readonly scopes: string[][];
47
};
48
readonly tokenEntitlementUrl: string;
49
readonly entitlementUrl: string;
50
readonly mcpRegistryDataUrl: string;
51
}
52
53
export const DEFAULT_ACCOUNT_SIGN_IN_COMMAND = 'workbench.actions.accounts.signIn';
54
55
const enum DefaultAccountStatus {
56
Uninitialized = 'uninitialized',
57
Unavailable = 'unavailable',
58
Available = 'available',
59
}
60
61
const CONTEXT_DEFAULT_ACCOUNT_STATE = new RawContextKey<string>('defaultAccountStatus', DefaultAccountStatus.Uninitialized);
62
const CACHED_POLICY_DATA_KEY = 'defaultAccount.cachedPolicyData';
63
const ACCOUNT_DATA_POLL_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
64
65
interface ITokenEntitlementsResponse {
66
token: string;
67
}
68
69
interface IMcpRegistryProvider {
70
readonly url: string;
71
readonly registry_access: 'allow_all' | 'registry_only';
72
readonly owner: {
73
readonly login: string;
74
readonly id: number;
75
readonly type: string;
76
readonly parent_login: string | null;
77
readonly priority: number;
78
};
79
}
80
81
interface IMcpRegistryResponse {
82
readonly mcp_registries: ReadonlyArray<IMcpRegistryProvider>;
83
}
84
85
function toDefaultAccountConfig(defaultChatAgent: IDefaultChatAgent): IDefaultAccountConfig {
86
return {
87
preferredExtensions: [
88
defaultChatAgent.chatExtensionId,
89
defaultChatAgent.extensionId,
90
],
91
authenticationProvider: {
92
default: {
93
id: defaultChatAgent.provider.default.id,
94
name: defaultChatAgent.provider.default.name,
95
},
96
enterprise: {
97
id: defaultChatAgent.provider.enterprise.id,
98
name: defaultChatAgent.provider.enterprise.name,
99
},
100
enterpriseProviderConfig: `${defaultChatAgent.completionsAdvancedSetting}.authProvider`,
101
enterpriseProviderUriSetting: defaultChatAgent.providerUriSetting,
102
scopes: defaultChatAgent.providerScopes,
103
},
104
entitlementUrl: defaultChatAgent.entitlementUrl,
105
tokenEntitlementUrl: defaultChatAgent.tokenEntitlementUrl,
106
mcpRegistryDataUrl: defaultChatAgent.mcpRegistryDataUrl,
107
};
108
}
109
110
export class DefaultAccountService extends Disposable implements IDefaultAccountService {
111
declare _serviceBrand: undefined;
112
113
private defaultAccount: IDefaultAccount | null = null;
114
get policyData(): IPolicyData | null { return this.defaultAccountProvider?.policyData ?? null; }
115
116
private readonly initBarrier = new Barrier();
117
118
private readonly _onDidChangeDefaultAccount = this._register(new Emitter<IDefaultAccount | null>());
119
readonly onDidChangeDefaultAccount = this._onDidChangeDefaultAccount.event;
120
121
private readonly _onDidChangePolicyData = this._register(new Emitter<IPolicyData | null>());
122
readonly onDidChangePolicyData = this._onDidChangePolicyData.event;
123
124
private readonly defaultAccountConfig: IDefaultAccountConfig;
125
private defaultAccountProvider: IDefaultAccountProvider | null = null;
126
127
constructor(
128
@IProductService productService: IProductService,
129
) {
130
super();
131
this.defaultAccountConfig = toDefaultAccountConfig(productService.defaultChatAgent);
132
}
133
134
async getDefaultAccount(): Promise<IDefaultAccount | null> {
135
await this.initBarrier.wait();
136
return this.defaultAccount;
137
}
138
139
getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider {
140
if (this.defaultAccountProvider) {
141
return this.defaultAccountProvider.getDefaultAccountAuthenticationProvider();
142
}
143
return {
144
...this.defaultAccountConfig.authenticationProvider.default,
145
enterprise: false
146
};
147
}
148
149
setDefaultAccountProvider(provider: IDefaultAccountProvider): void {
150
if (this.defaultAccountProvider) {
151
throw new Error('Default account provider is already set');
152
}
153
154
this.defaultAccountProvider = provider;
155
if (this.defaultAccountProvider.policyData) {
156
this._onDidChangePolicyData.fire(this.defaultAccountProvider.policyData);
157
}
158
provider.refresh().then(account => {
159
this.defaultAccount = account;
160
}).finally(() => {
161
this.initBarrier.open();
162
this._register(provider.onDidChangeDefaultAccount(account => this.setDefaultAccount(account)));
163
this._register(provider.onDidChangePolicyData(policyData => this._onDidChangePolicyData.fire(policyData)));
164
});
165
}
166
167
async refresh(): Promise<IDefaultAccount | null> {
168
await this.initBarrier.wait();
169
170
const account = await this.defaultAccountProvider?.refresh();
171
this.setDefaultAccount(account ?? null);
172
return this.defaultAccount;
173
}
174
175
async signIn(options?: { additionalScopes?: readonly string[];[key: string]: unknown }): Promise<IDefaultAccount | null> {
176
await this.initBarrier.wait();
177
return this.defaultAccountProvider?.signIn(options) ?? null;
178
}
179
180
private setDefaultAccount(account: IDefaultAccount | null): void {
181
if (equals(this.defaultAccount, account)) {
182
return;
183
}
184
this.defaultAccount = account;
185
this._onDidChangeDefaultAccount.fire(this.defaultAccount);
186
}
187
}
188
189
interface IAccountPolicyData {
190
readonly accountId: string;
191
readonly policyData: IPolicyData;
192
}
193
194
interface IDefaultAccountData {
195
defaultAccount: IDefaultAccount;
196
policyData: IAccountPolicyData | null;
197
}
198
199
type DefaultAccountStatusTelemetry = {
200
status: string;
201
initial: boolean;
202
};
203
204
type DefaultAccountStatusTelemetryClassification = {
205
owner: 'sandy081';
206
comment: 'Log default account availability status';
207
status: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Indicates whether default account is available or not.' };
208
initial: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'Indicates whether this is the initial status report.' };
209
};
210
211
class DefaultAccountProvider extends Disposable implements IDefaultAccountProvider {
212
213
private _defaultAccount: IDefaultAccountData | null = null;
214
get defaultAccount(): IDefaultAccount | null { return this._defaultAccount?.defaultAccount ?? null; }
215
216
private _policyData: IAccountPolicyData | null = null;
217
get policyData(): IPolicyData | null { return this._policyData?.policyData ?? null; }
218
219
private readonly _onDidChangeDefaultAccount = this._register(new Emitter<IDefaultAccount | null>());
220
readonly onDidChangeDefaultAccount = this._onDidChangeDefaultAccount.event;
221
222
private readonly _onDidChangePolicyData = this._register(new Emitter<IPolicyData | null>());
223
readonly onDidChangePolicyData = this._onDidChangePolicyData.event;
224
225
private readonly accountStatusContext: IContextKey<string>;
226
private initialized = false;
227
private readonly initPromise: Promise<void>;
228
private readonly updateThrottler = this._register(new ThrottledDelayer(100));
229
private readonly accountDataPollScheduler = this._register(new RunOnceScheduler(() => this.updateDefaultAccount(), ACCOUNT_DATA_POLL_INTERVAL_MS));
230
231
constructor(
232
private readonly defaultAccountConfig: IDefaultAccountConfig,
233
@IConfigurationService private readonly configurationService: IConfigurationService,
234
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
235
@IAuthenticationExtensionsService private readonly authenticationExtensionsService: IAuthenticationExtensionsService,
236
@ITelemetryService private readonly telemetryService: ITelemetryService,
237
@IExtensionService private readonly extensionService: IExtensionService,
238
@IRequestService private readonly requestService: IRequestService,
239
@ILogService private readonly logService: ILogService,
240
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
241
@IContextKeyService contextKeyService: IContextKeyService,
242
@IStorageService private readonly storageService: IStorageService,
243
@IHostService private readonly hostService: IHostService,
244
) {
245
super();
246
this.accountStatusContext = CONTEXT_DEFAULT_ACCOUNT_STATE.bindTo(contextKeyService);
247
this._policyData = this.getCachedPolicyData();
248
this.initPromise = this.init()
249
.finally(() => {
250
this.telemetryService.publicLog2<DefaultAccountStatusTelemetry, DefaultAccountStatusTelemetryClassification>('defaultaccount:status', { status: this.defaultAccount ? 'available' : 'unavailable', initial: true });
251
this.initialized = true;
252
});
253
}
254
255
private getCachedPolicyData(): IAccountPolicyData | null {
256
const cached = this.storageService.get(CACHED_POLICY_DATA_KEY, StorageScope.APPLICATION);
257
if (cached) {
258
try {
259
const { accountId, policyData } = JSON.parse(cached);
260
if (accountId && policyData) {
261
this.logService.debug('[DefaultAccount] Initializing with cached policy data');
262
return { accountId, policyData };
263
}
264
} catch (error) {
265
this.logService.error('[DefaultAccount] Failed to parse cached policy data', getErrorMessage(error));
266
}
267
}
268
return null;
269
}
270
271
private async init(): Promise<void> {
272
if (isWeb && !this.environmentService.remoteAuthority) {
273
this.logService.debug('[DefaultAccount] Running in web without remote, skipping initialization');
274
return;
275
}
276
277
try {
278
await this.extensionService.whenInstalledExtensionsRegistered();
279
this.logService.debug('[DefaultAccount] Installed extensions registered.');
280
} catch (error) {
281
this.logService.error('[DefaultAccount] Error while waiting for installed extensions to be registered', getErrorMessage(error));
282
}
283
284
this.logService.debug('[DefaultAccount] Starting initialization');
285
await this.doUpdateDefaultAccount();
286
this.logService.debug('[DefaultAccount] Initialization complete');
287
288
this._register(this.onDidChangeDefaultAccount(account => {
289
this.telemetryService.publicLog2<DefaultAccountStatusTelemetry, DefaultAccountStatusTelemetryClassification>('defaultaccount:status', { status: account ? 'available' : 'unavailable', initial: false });
290
}));
291
292
this._register(this.authenticationService.onDidChangeSessions(e => {
293
const defaultAccountProvider = this.getDefaultAccountAuthenticationProvider();
294
if (e.providerId !== defaultAccountProvider.id) {
295
return;
296
}
297
if (this.defaultAccount && e.event.removed?.some(session => session.id === this.defaultAccount?.sessionId)) {
298
this.setDefaultAccount(null);
299
} else {
300
this.logService.debug('[DefaultAccount] Sessions changed for default account provider, updating default account');
301
this.updateDefaultAccount();
302
}
303
}));
304
305
this._register(this.authenticationExtensionsService.onDidChangeAccountPreference(async e => {
306
const defaultAccountProvider = this.getDefaultAccountAuthenticationProvider();
307
if (e.providerId !== defaultAccountProvider.id) {
308
return;
309
}
310
this.logService.debug('[DefaultAccount] Account preference changed for default account provider, updating default account');
311
this.updateDefaultAccount();
312
}));
313
314
this._register(this.authenticationService.onDidRegisterAuthenticationProvider(e => {
315
const defaultAccountProvider = this.getDefaultAccountAuthenticationProvider();
316
if (e.id !== defaultAccountProvider.id) {
317
return;
318
}
319
this.logService.debug('[DefaultAccount] Default account provider registered, updating default account');
320
this.updateDefaultAccount();
321
}));
322
323
this._register(this.authenticationService.onDidUnregisterAuthenticationProvider(e => {
324
const defaultAccountProvider = this.getDefaultAccountAuthenticationProvider();
325
if (e.id !== defaultAccountProvider.id) {
326
return;
327
}
328
this.logService.debug('[DefaultAccount] Default account provider unregistered, updating default account');
329
this.updateDefaultAccount();
330
}));
331
332
this._register(this.hostService.onDidChangeFocus(focused => {
333
if (focused && this._defaultAccount) {
334
// Update default account when window gets focused
335
this.accountDataPollScheduler.cancel();
336
this.logService.debug('[DefaultAccount] Window focused, updating default account');
337
this.updateDefaultAccount();
338
}
339
}));
340
}
341
342
async refresh(): Promise<IDefaultAccount | null> {
343
if (!this.initialized) {
344
await this.initPromise;
345
return this.defaultAccount;
346
}
347
348
this.logService.debug('[DefaultAccount] Refreshing default account');
349
await this.updateDefaultAccount();
350
return this.defaultAccount;
351
}
352
353
private async updateDefaultAccount(): Promise<void> {
354
await this.updateThrottler.trigger(() => this.doUpdateDefaultAccount());
355
}
356
357
private async doUpdateDefaultAccount(): Promise<void> {
358
try {
359
const defaultAccount = await this.fetchDefaultAccount();
360
this.setDefaultAccount(defaultAccount);
361
this.scheduleAccountDataPoll();
362
} catch (error) {
363
this.logService.error('[DefaultAccount] Error while updating default account', getErrorMessage(error));
364
}
365
}
366
367
private async fetchDefaultAccount(): Promise<IDefaultAccountData | null> {
368
const defaultAccountProvider = this.getDefaultAccountAuthenticationProvider();
369
this.logService.debug('[DefaultAccount] Default account provider ID:', defaultAccountProvider.id);
370
371
const declaredProvider = this.authenticationService.declaredProviders.find(provider => provider.id === defaultAccountProvider.id);
372
if (!declaredProvider) {
373
this.logService.info(`[DefaultAccount] Authentication provider is not declared.`, defaultAccountProvider);
374
return null;
375
}
376
377
return await this.getDefaultAccountForAuthenticationProvider(defaultAccountProvider);
378
}
379
380
private setDefaultAccount(account: IDefaultAccountData | null): void {
381
if (equals(this._defaultAccount, account)) {
382
return;
383
}
384
385
this.logService.trace('[DefaultAccount] Updating default account:', account);
386
if (account) {
387
this._defaultAccount = account;
388
this.setPolicyData(account.policyData);
389
this._onDidChangeDefaultAccount.fire(this._defaultAccount.defaultAccount);
390
this.accountStatusContext.set(DefaultAccountStatus.Available);
391
this.logService.debug('[DefaultAccount] Account status set to Available');
392
} else {
393
this._defaultAccount = null;
394
this.setPolicyData(null);
395
this._onDidChangeDefaultAccount.fire(null);
396
this.accountDataPollScheduler.cancel();
397
this.accountStatusContext.set(DefaultAccountStatus.Unavailable);
398
this.logService.debug('[DefaultAccount] Account status set to Unavailable');
399
}
400
}
401
402
private setPolicyData(accountPolicyData: IAccountPolicyData | null): void {
403
if (equals(this._policyData, accountPolicyData)) {
404
return;
405
}
406
this._policyData = accountPolicyData;
407
this.cachePolicyData(accountPolicyData);
408
this._onDidChangePolicyData.fire(this._policyData?.policyData ?? null);
409
}
410
411
private cachePolicyData(accountPolicyData: IAccountPolicyData | null): void {
412
if (accountPolicyData) {
413
this.logService.debug('[DefaultAccount] Caching policy data for account:', accountPolicyData.accountId);
414
this.storageService.store(CACHED_POLICY_DATA_KEY, JSON.stringify(accountPolicyData), StorageScope.APPLICATION, StorageTarget.MACHINE);
415
} else {
416
this.logService.debug('[DefaultAccount] Removing cached policy data');
417
this.storageService.remove(CACHED_POLICY_DATA_KEY, StorageScope.APPLICATION);
418
}
419
}
420
421
private scheduleAccountDataPoll(): void {
422
if (!this._defaultAccount) {
423
return;
424
}
425
this.accountDataPollScheduler.schedule(ACCOUNT_DATA_POLL_INTERVAL_MS);
426
}
427
428
private extractFromToken(token: string): Map<string, string> {
429
const result = new Map<string, string>();
430
const firstPart = token?.split(':')[0];
431
const fields = firstPart?.split(';');
432
for (const field of fields) {
433
const [key, value] = field.split('=');
434
result.set(key, value);
435
}
436
this.logService.debug(`[DefaultAccount] extractFromToken: ${JSON.stringify(Object.fromEntries(result))}`);
437
return result;
438
}
439
440
private async getDefaultAccountForAuthenticationProvider(authenticationProvider: IDefaultAccountAuthenticationProvider): Promise<IDefaultAccountData | null> {
441
try {
442
this.logService.debug('[DefaultAccount] Getting Default Account from authenticated sessions for provider:', authenticationProvider.id);
443
const sessions = await this.findMatchingProviderSession(authenticationProvider.id, this.defaultAccountConfig.authenticationProvider.scopes);
444
445
if (!sessions?.length) {
446
this.logService.debug('[DefaultAccount] No matching session found for provider:', authenticationProvider.id);
447
return null;
448
}
449
450
return this.getDefaultAccountFromAuthenticatedSessions(authenticationProvider, sessions);
451
} catch (error) {
452
this.logService.error('[DefaultAccount] Failed to get default account for provider:', authenticationProvider.id, getErrorMessage(error));
453
return null;
454
}
455
}
456
457
private async getDefaultAccountFromAuthenticatedSessions(authenticationProvider: IDefaultAccountAuthenticationProvider, sessions: AuthenticationSession[]): Promise<IDefaultAccountData | null> {
458
try {
459
const accountId = sessions[0].account.id;
460
const [entitlementsData, tokenEntitlementsData] = await Promise.all([
461
this.getEntitlements(sessions),
462
this.getTokenEntitlements(sessions),
463
]);
464
465
let policyData: Mutable<IPolicyData> | undefined = this._policyData?.accountId === accountId ? { ...this._policyData.policyData } : undefined;
466
if (tokenEntitlementsData) {
467
policyData = policyData ?? {};
468
policyData.chat_agent_enabled = tokenEntitlementsData.chat_agent_enabled;
469
policyData.chat_preview_features_enabled = tokenEntitlementsData.chat_preview_features_enabled;
470
policyData.mcp = tokenEntitlementsData.mcp;
471
if (policyData.mcp) {
472
const mcpRegistryProvider = await this.getMcpRegistryProvider(sessions);
473
if (mcpRegistryProvider) {
474
policyData.mcpRegistryUrl = mcpRegistryProvider.url;
475
policyData.mcpAccess = mcpRegistryProvider.registry_access;
476
}
477
}
478
}
479
480
const defaultAccount: IDefaultAccount = {
481
authenticationProvider,
482
sessionId: sessions[0].id,
483
enterprise: authenticationProvider.enterprise || sessions[0].account.label.includes('_'),
484
entitlementsData,
485
};
486
this.logService.debug('[DefaultAccount] Successfully created default account for provider:', authenticationProvider.id);
487
return { defaultAccount, policyData: policyData ? { accountId, policyData } : null };
488
} catch (error) {
489
this.logService.error('[DefaultAccount] Failed to create default account for provider:', authenticationProvider.id, getErrorMessage(error));
490
return null;
491
}
492
}
493
494
private async findMatchingProviderSession(authProviderId: string, allScopes: string[][]): Promise<AuthenticationSession[] | undefined> {
495
const sessions = await this.getSessions(authProviderId);
496
const matchingSessions = [];
497
for (const session of sessions) {
498
this.logService.debug('[DefaultAccount] Checking session with scopes', session.scopes);
499
for (const scopes of allScopes) {
500
if (this.scopesMatch(session.scopes, scopes)) {
501
matchingSessions.push(session);
502
}
503
}
504
}
505
return matchingSessions.length > 0 ? matchingSessions : undefined;
506
}
507
508
private async getSessions(authProviderId: string): Promise<readonly AuthenticationSession[]> {
509
for (let attempt = 1; attempt <= 3; attempt++) {
510
try {
511
let preferredAccount: AuthenticationSessionAccount | undefined;
512
let preferredAccountName: string | undefined;
513
for (const preferredExtension of this.defaultAccountConfig.preferredExtensions) {
514
preferredAccountName = this.authenticationExtensionsService.getAccountPreference(preferredExtension, authProviderId);
515
if (preferredAccountName) {
516
break;
517
}
518
}
519
for (const account of await this.authenticationService.getAccounts(authProviderId)) {
520
if (account.label === preferredAccountName) {
521
preferredAccount = account;
522
break;
523
}
524
}
525
526
return await this.authenticationService.getSessions(authProviderId, undefined, { account: preferredAccount }, true);
527
} catch (error) {
528
this.logService.warn(`[DefaultAccount] Attempt ${attempt} to get sessions failed:`, getErrorMessage(error));
529
if (attempt === 3) {
530
throw error;
531
}
532
await timeout(500);
533
}
534
}
535
throw new Error('Unable to get sessions after multiple attempts');
536
}
537
538
private scopesMatch(scopes: ReadonlyArray<string>, expectedScopes: string[]): boolean {
539
return expectedScopes.every(scope => scopes.includes(scope));
540
}
541
542
private async getTokenEntitlements(sessions: AuthenticationSession[]): Promise<Partial<IPolicyData> | undefined> {
543
const tokenEntitlementsUrl = this.getTokenEntitlementUrl();
544
if (!tokenEntitlementsUrl) {
545
this.logService.debug('[DefaultAccount] No token entitlements URL found');
546
return undefined;
547
}
548
549
this.logService.debug('[DefaultAccount] Fetching token entitlements from:', tokenEntitlementsUrl);
550
const response = await this.request(tokenEntitlementsUrl, 'GET', undefined, sessions, CancellationToken.None);
551
if (!response) {
552
return undefined;
553
}
554
555
if (response.res.statusCode && response.res.statusCode !== 200) {
556
this.logService.trace(`[DefaultAccount] unexpected status code ${response.res.statusCode} while fetching token entitlements`);
557
return undefined;
558
}
559
560
try {
561
const chatData = await asJson<ITokenEntitlementsResponse>(response);
562
if (chatData) {
563
const tokenMap = this.extractFromToken(chatData.token);
564
return {
565
// Editor preview features are disabled if the flag is present and set to 0
566
chat_preview_features_enabled: tokenMap.get('editor_preview_features') !== '0',
567
chat_agent_enabled: tokenMap.get('agent_mode') !== '0',
568
// MCP is disabled if the flag is present and set to 0
569
mcp: tokenMap.get('mcp') !== '0',
570
};
571
}
572
this.logService.error('Failed to fetch token entitlements', 'No data returned');
573
} catch (error) {
574
this.logService.error('Failed to fetch token entitlements', getErrorMessage(error));
575
}
576
577
return undefined;
578
}
579
580
private async getEntitlements(sessions: AuthenticationSession[]): Promise<IEntitlementsData | undefined | null> {
581
const entitlementUrl = this.getEntitlementUrl();
582
if (!entitlementUrl) {
583
this.logService.debug('[DefaultAccount] No chat entitlements URL found');
584
return undefined;
585
}
586
587
this.logService.debug('[DefaultAccount] Fetching entitlements from:', entitlementUrl);
588
const response = await this.request(entitlementUrl, 'GET', undefined, sessions, CancellationToken.None);
589
if (!response) {
590
return undefined;
591
}
592
593
if (response.res.statusCode && response.res.statusCode !== 200) {
594
this.logService.trace(`[DefaultAccount] unexpected status code ${response.res.statusCode} while fetching entitlements`);
595
return (
596
response.res.statusCode === 401 || // oauth token being unavailable (expired/revoked)
597
response.res.statusCode === 404 // missing scopes/permissions, service pretends the endpoint doesn't exist
598
) ? null : undefined;
599
}
600
601
try {
602
const data = await asJson<IEntitlementsData>(response);
603
if (data) {
604
return data;
605
}
606
this.logService.error('[DefaultAccount] Failed to fetch entitlements', 'No data returned');
607
} catch (error) {
608
this.logService.error('[DefaultAccount] Failed to fetch entitlements', getErrorMessage(error));
609
}
610
return undefined;
611
}
612
613
private async getMcpRegistryProvider(sessions: AuthenticationSession[]): Promise<IMcpRegistryProvider | undefined> {
614
const mcpRegistryDataUrl = this.getMcpRegistryDataUrl();
615
if (!mcpRegistryDataUrl) {
616
this.logService.debug('[DefaultAccount] No MCP registry data URL found');
617
return undefined;
618
}
619
620
this.logService.debug('[DefaultAccount] Fetching MCP registry data from:', mcpRegistryDataUrl);
621
const response = await this.request(mcpRegistryDataUrl, 'GET', undefined, sessions, CancellationToken.None);
622
if (!response) {
623
return undefined;
624
}
625
626
if (response.res.statusCode && response.res.statusCode !== 200) {
627
this.logService.trace(`[DefaultAccount] unexpected status code ${response.res.statusCode} while fetching MCP registry data`);
628
return undefined;
629
}
630
631
try {
632
const data = await asJson<IMcpRegistryResponse>(response);
633
if (data) {
634
this.logService.debug('Fetched MCP registry providers', data.mcp_registries);
635
return data.mcp_registries[0];
636
}
637
this.logService.debug('Failed to fetch MCP registry providers', 'No data returned');
638
} catch (error) {
639
this.logService.error('Failed to fetch MCP registry providers', getErrorMessage(error));
640
}
641
return undefined;
642
}
643
644
private async request(url: string, type: 'GET', body: undefined, sessions: AuthenticationSession[], token: CancellationToken): Promise<IRequestContext | undefined>;
645
private async request(url: string, type: 'POST', body: object, sessions: AuthenticationSession[], token: CancellationToken): Promise<IRequestContext | undefined>;
646
private async request(url: string, type: 'GET' | 'POST', body: object | undefined, sessions: AuthenticationSession[], token: CancellationToken): Promise<IRequestContext | undefined> {
647
let lastResponse: IRequestContext | undefined;
648
649
for (const session of sessions) {
650
if (token.isCancellationRequested) {
651
return lastResponse;
652
}
653
654
try {
655
const response = await this.requestService.request({
656
type,
657
url,
658
data: type === 'POST' ? JSON.stringify(body) : undefined,
659
disableCache: true,
660
headers: {
661
'Authorization': `Bearer ${session.accessToken}`
662
}
663
}, token);
664
665
const status = response.res.statusCode;
666
if (status && status !== 200) {
667
lastResponse = response;
668
continue; // try next session
669
}
670
671
return response;
672
} catch (error) {
673
if (!token.isCancellationRequested) {
674
this.logService.error(`[chat entitlement] request: error ${error}`);
675
}
676
}
677
}
678
679
if (!lastResponse) {
680
this.logService.trace('[DefaultAccount]: No response received for request', url);
681
return undefined;
682
}
683
684
if (lastResponse.res.statusCode && lastResponse.res.statusCode !== 200) {
685
this.logService.trace(`[DefaultAccount]: unexpected status code ${lastResponse.res.statusCode} for request`, url);
686
return undefined;
687
}
688
689
return lastResponse;
690
}
691
692
private getEntitlementUrl(): string | undefined {
693
if (this.getDefaultAccountAuthenticationProvider().enterprise) {
694
try {
695
const enterpriseUrl = this.getEnterpriseUrl();
696
if (!enterpriseUrl) {
697
return undefined;
698
}
699
return `${enterpriseUrl.protocol}//api.${enterpriseUrl.hostname}${enterpriseUrl.port ? ':' + enterpriseUrl.port : ''}/copilot_internal/user`;
700
} catch (error) {
701
this.logService.error(error);
702
}
703
}
704
705
return this.defaultAccountConfig.entitlementUrl;
706
}
707
708
private getTokenEntitlementUrl(): string | undefined {
709
if (this.getDefaultAccountAuthenticationProvider().enterprise) {
710
try {
711
const enterpriseUrl = this.getEnterpriseUrl();
712
if (!enterpriseUrl) {
713
return undefined;
714
}
715
return `${enterpriseUrl.protocol}//api.${enterpriseUrl.hostname}${enterpriseUrl.port ? ':' + enterpriseUrl.port : ''}/copilot_internal/v2/token`;
716
} catch (error) {
717
this.logService.error(error);
718
}
719
}
720
721
return this.defaultAccountConfig.tokenEntitlementUrl;
722
}
723
724
private getMcpRegistryDataUrl(): string | undefined {
725
if (this.getDefaultAccountAuthenticationProvider().enterprise) {
726
try {
727
const enterpriseUrl = this.getEnterpriseUrl();
728
if (!enterpriseUrl) {
729
return undefined;
730
}
731
return `${enterpriseUrl.protocol}//api.${enterpriseUrl.hostname}${enterpriseUrl.port ? ':' + enterpriseUrl.port : ''}/copilot/mcp_registry`;
732
} catch (error) {
733
this.logService.error(error);
734
}
735
}
736
737
return this.defaultAccountConfig.mcpRegistryDataUrl;
738
}
739
740
getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider {
741
if (this.configurationService.getValue<string | undefined>(this.defaultAccountConfig.authenticationProvider.enterpriseProviderConfig) === this.defaultAccountConfig.authenticationProvider.enterprise.id) {
742
return {
743
...this.defaultAccountConfig.authenticationProvider.enterprise,
744
enterprise: true
745
};
746
}
747
return {
748
...this.defaultAccountConfig.authenticationProvider.default,
749
enterprise: false
750
};
751
}
752
753
private getEnterpriseUrl(): URL | undefined {
754
const value = this.configurationService.getValue(this.defaultAccountConfig.authenticationProvider.enterpriseProviderUriSetting);
755
if (!isString(value)) {
756
return undefined;
757
}
758
return new URL(value);
759
}
760
761
async signIn(options?: { additionalScopes?: readonly string[];[key: string]: unknown }): Promise<IDefaultAccount | null> {
762
const authProvider = this.getDefaultAccountAuthenticationProvider();
763
if (!authProvider) {
764
throw new Error('No default account provider configured');
765
}
766
const { additionalScopes, ...sessionOptions } = options ?? {};
767
const defaultAccountScopes = this.defaultAccountConfig.authenticationProvider.scopes[0];
768
const scopes = additionalScopes ? distinct([...defaultAccountScopes, ...additionalScopes]) : defaultAccountScopes;
769
const session = await this.authenticationService.createSession(authProvider.id, scopes, sessionOptions);
770
for (const preferredExtension of this.defaultAccountConfig.preferredExtensions) {
771
this.authenticationExtensionsService.updateAccountPreference(preferredExtension, authProvider.id, session.account);
772
}
773
await this.updateDefaultAccount();
774
return this.defaultAccount;
775
}
776
777
}
778
779
class DefaultAccountProviderContribution extends Disposable implements IWorkbenchContribution {
780
781
static ID = 'workbench.contributions.defaultAccountProvider';
782
783
constructor(
784
@IProductService productService: IProductService,
785
@IInstantiationService instantiationService: IInstantiationService,
786
@IDefaultAccountService defaultAccountService: IDefaultAccountService,
787
) {
788
super();
789
const defaultAccountProvider = this._register(instantiationService.createInstance(DefaultAccountProvider, toDefaultAccountConfig(productService.defaultChatAgent)));
790
defaultAccountService.setDefaultAccountProvider(defaultAccountProvider);
791
}
792
}
793
794
registerWorkbenchContribution2(DefaultAccountProviderContribution.ID, DefaultAccountProviderContribution, WorkbenchPhase.BlockStartup);
795
796