mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-19 00:21:38 -05:00
do not fetch policy data if already fetched (#293996)
* do not fetch policy data if already fetched * reset mcp data when not configured and refetch on polling * refetch always * reschedule when window has no focus * another change * - handle 429 for mcp registry - do not fetch entitlements on refreshing policies
This commit is contained in:
committed by
GitHub
parent
7cc9148a3b
commit
591199df40
@@ -18,7 +18,7 @@ import { IHostService } from '../../host/browser/host.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { getErrorMessage } from '../../../../base/common/errors.js';
|
||||
import { IDefaultAccount, IDefaultAccountAuthenticationProvider, IEntitlementsData, IPolicyData } from '../../../../base/common/defaultAccount.js';
|
||||
import { isString, Mutable } from '../../../../base/common/types.js';
|
||||
import { isString, isUndefined, Mutable } from '../../../../base/common/types.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';
|
||||
import { isWeb } from '../../../../base/common/platform.js';
|
||||
@@ -60,7 +60,7 @@ const enum DefaultAccountStatus {
|
||||
|
||||
const CONTEXT_DEFAULT_ACCOUNT_STATE = new RawContextKey<string>('defaultAccountStatus', DefaultAccountStatus.Uninitialized);
|
||||
const CACHED_POLICY_DATA_KEY = 'defaultAccount.cachedPolicyData';
|
||||
const ACCOUNT_DATA_POLL_INTERVAL_MS = 15 * 60 * 1000; // 15 minutes
|
||||
const ACCOUNT_DATA_POLL_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
||||
|
||||
interface ITokenEntitlementsResponse {
|
||||
token: string;
|
||||
@@ -69,7 +69,7 @@ interface ITokenEntitlementsResponse {
|
||||
interface IMcpRegistryProvider {
|
||||
readonly url: string;
|
||||
readonly registry_access: 'allow_all' | 'registry_only';
|
||||
readonly owner: {
|
||||
readonly owner?: {
|
||||
readonly login: string;
|
||||
readonly id: number;
|
||||
readonly type: string;
|
||||
@@ -189,6 +189,8 @@ export class DefaultAccountService extends Disposable implements IDefaultAccount
|
||||
interface IAccountPolicyData {
|
||||
readonly accountId: string;
|
||||
readonly policyData: IPolicyData;
|
||||
readonly isTokenEntitlementsDataFetched: boolean;
|
||||
readonly isMcpRegistryDataFetched: boolean;
|
||||
}
|
||||
|
||||
interface IDefaultAccountData {
|
||||
@@ -226,7 +228,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
|
||||
private initialized = false;
|
||||
private readonly initPromise: Promise<void>;
|
||||
private readonly updateThrottler = this._register(new ThrottledDelayer(100));
|
||||
private readonly accountDataPollScheduler = this._register(new RunOnceScheduler(() => this.updateDefaultAccount(), ACCOUNT_DATA_POLL_INTERVAL_MS));
|
||||
private readonly accountDataPollScheduler = this._register(new RunOnceScheduler(() => this.refetchPolicyData(), ACCOUNT_DATA_POLL_INTERVAL_MS));
|
||||
|
||||
constructor(
|
||||
private readonly defaultAccountConfig: IDefaultAccountConfig,
|
||||
@@ -259,7 +261,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
|
||||
const { accountId, policyData } = JSON.parse(cached);
|
||||
if (accountId && policyData) {
|
||||
this.logService.debug('[DefaultAccount] Initializing with cached policy data');
|
||||
return { accountId, policyData };
|
||||
return { accountId, policyData, isTokenEntitlementsDataFetched: false, isMcpRegistryDataFetched: false };
|
||||
}
|
||||
} catch (error) {
|
||||
this.logService.error('[DefaultAccount] Failed to parse cached policy data', getErrorMessage(error));
|
||||
@@ -282,7 +284,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
|
||||
}
|
||||
|
||||
this.logService.debug('[DefaultAccount] Starting initialization');
|
||||
await this.doUpdateDefaultAccount();
|
||||
await this.doUpdateDefaultAccount(false, false);
|
||||
this.logService.debug('[DefaultAccount] Initialization complete');
|
||||
|
||||
this._register(this.onDidChangeDefaultAccount(account => {
|
||||
@@ -330,33 +332,43 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
|
||||
}));
|
||||
|
||||
this._register(this.hostService.onDidChangeFocus(focused => {
|
||||
if (focused && this._defaultAccount) {
|
||||
// Update default account when window gets focused
|
||||
// Refresh default account when window gets focused and we have cached policy data, which likely means we haven't successfully fetched data since the last time the window was focused (e.g. due to network issues), so we should try again to fetch the data.
|
||||
if (focused && this._defaultAccount && this._policyData && (!this._policyData.isMcpRegistryDataFetched || !this._policyData.isTokenEntitlementsDataFetched)) {
|
||||
this.accountDataPollScheduler.cancel();
|
||||
this.logService.debug('[DefaultAccount] Window focused, updating default account');
|
||||
this.updateDefaultAccount();
|
||||
this.refresh(true);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
async refresh(): Promise<IDefaultAccount | null> {
|
||||
async refresh(donotFetchEntitlements: boolean = false): Promise<IDefaultAccount | null> {
|
||||
if (!this.initialized) {
|
||||
await this.initPromise;
|
||||
return this.defaultAccount;
|
||||
}
|
||||
|
||||
this.logService.debug('[DefaultAccount] Refreshing default account');
|
||||
await this.updateDefaultAccount();
|
||||
await this.updateDefaultAccount(false, donotFetchEntitlements);
|
||||
return this.defaultAccount;
|
||||
}
|
||||
|
||||
private async updateDefaultAccount(): Promise<void> {
|
||||
await this.updateThrottler.trigger(() => this.doUpdateDefaultAccount());
|
||||
private async refetchPolicyData(): Promise<void> {
|
||||
if (!this.hostService.hasFocus) {
|
||||
this.scheduleAccountDataPoll();
|
||||
this.logService.debug('[DefaultAccount] Skipping refetching policy data because window is not focused');
|
||||
return;
|
||||
}
|
||||
this.logService.debug('[DefaultAccount] Refetching policy data for current default account');
|
||||
await this.updateDefaultAccount(true);
|
||||
}
|
||||
|
||||
private async doUpdateDefaultAccount(): Promise<void> {
|
||||
private async updateDefaultAccount(donotUseCache: boolean = false, donotFetchEntitlements: boolean = false): Promise<void> {
|
||||
await this.updateThrottler.trigger(() => this.doUpdateDefaultAccount(donotUseCache, donotFetchEntitlements));
|
||||
}
|
||||
|
||||
private async doUpdateDefaultAccount(donotUseCache: boolean, donotFetchEntitlements: boolean): Promise<void> {
|
||||
try {
|
||||
const defaultAccount = await this.fetchDefaultAccount();
|
||||
const defaultAccount = await this.fetchDefaultAccount(donotUseCache, donotFetchEntitlements);
|
||||
this.setDefaultAccount(defaultAccount);
|
||||
this.scheduleAccountDataPoll();
|
||||
} catch (error) {
|
||||
@@ -364,7 +376,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
|
||||
}
|
||||
}
|
||||
|
||||
private async fetchDefaultAccount(): Promise<IDefaultAccountData | null> {
|
||||
private async fetchDefaultAccount(donotUseCache: boolean, donotFetchEntitlements: boolean): Promise<IDefaultAccountData | null> {
|
||||
const defaultAccountProvider = this.getDefaultAccountAuthenticationProvider();
|
||||
this.logService.debug('[DefaultAccount] Default account provider ID:', defaultAccountProvider.id);
|
||||
|
||||
@@ -374,7 +386,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this.getDefaultAccountForAuthenticationProvider(defaultAccountProvider);
|
||||
return await this.getDefaultAccountForAuthenticationProvider(defaultAccountProvider, donotUseCache, donotFetchEntitlements);
|
||||
}
|
||||
|
||||
private setDefaultAccount(account: IDefaultAccountData | null): void {
|
||||
@@ -437,7 +449,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
|
||||
return result;
|
||||
}
|
||||
|
||||
private async getDefaultAccountForAuthenticationProvider(authenticationProvider: IDefaultAccountAuthenticationProvider): Promise<IDefaultAccountData | null> {
|
||||
private async getDefaultAccountForAuthenticationProvider(authenticationProvider: IDefaultAccountAuthenticationProvider, donotUseCache: boolean, donotFetchEntitlements: boolean): Promise<IDefaultAccountData | null> {
|
||||
try {
|
||||
this.logService.debug('[DefaultAccount] Getting Default Account from authenticated sessions for provider:', authenticationProvider.id);
|
||||
const sessions = await this.findMatchingProviderSession(authenticationProvider.id, this.defaultAccountConfig.authenticationProvider.scopes);
|
||||
@@ -447,33 +459,42 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.getDefaultAccountFromAuthenticatedSessions(authenticationProvider, sessions);
|
||||
return this.getDefaultAccountFromAuthenticatedSessions(authenticationProvider, sessions, donotUseCache, donotFetchEntitlements);
|
||||
} catch (error) {
|
||||
this.logService.error('[DefaultAccount] Failed to get default account for provider:', authenticationProvider.id, getErrorMessage(error));
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async getDefaultAccountFromAuthenticatedSessions(authenticationProvider: IDefaultAccountAuthenticationProvider, sessions: AuthenticationSession[]): Promise<IDefaultAccountData | null> {
|
||||
private async getDefaultAccountFromAuthenticatedSessions(authenticationProvider: IDefaultAccountAuthenticationProvider, sessions: AuthenticationSession[], donotUseCache: boolean, donotFetchEntitlements: boolean): Promise<IDefaultAccountData | null> {
|
||||
try {
|
||||
const accountId = sessions[0].account.id;
|
||||
const accountPolicyData = this._policyData?.accountId === accountId ? this._policyData : undefined;
|
||||
|
||||
const [entitlementsData, tokenEntitlementsData] = await Promise.all([
|
||||
this.getEntitlements(sessions),
|
||||
this.getTokenEntitlements(sessions),
|
||||
donotFetchEntitlements && accountPolicyData && this._defaultAccount ? this._defaultAccount.defaultAccount.entitlementsData : this.getEntitlements(sessions),
|
||||
this.getTokenEntitlements(sessions, donotUseCache ? undefined : accountPolicyData),
|
||||
]);
|
||||
|
||||
let policyData: Mutable<IPolicyData> | undefined = this._policyData?.accountId === accountId ? { ...this._policyData.policyData } : undefined;
|
||||
let isTokenEntitlementsDataFetched = false;
|
||||
let isMcpRegistryDataFetched = false;
|
||||
let policyData: Mutable<IPolicyData> | undefined = accountPolicyData?.policyData ? { ...accountPolicyData.policyData } : undefined;
|
||||
if (tokenEntitlementsData) {
|
||||
isTokenEntitlementsDataFetched = true;
|
||||
policyData = policyData ?? {};
|
||||
policyData.chat_agent_enabled = tokenEntitlementsData.chat_agent_enabled;
|
||||
policyData.chat_preview_features_enabled = tokenEntitlementsData.chat_preview_features_enabled;
|
||||
policyData.mcp = tokenEntitlementsData.mcp;
|
||||
if (policyData.mcp) {
|
||||
const mcpRegistryProvider = await this.getMcpRegistryProvider(sessions);
|
||||
if (mcpRegistryProvider) {
|
||||
policyData.mcpRegistryUrl = mcpRegistryProvider.url;
|
||||
policyData.mcpAccess = mcpRegistryProvider.registry_access;
|
||||
const mcpRegistryProvider = await this.getMcpRegistryProvider(sessions, donotUseCache ? undefined : accountPolicyData);
|
||||
if (!isUndefined(mcpRegistryProvider)) {
|
||||
isMcpRegistryDataFetched = true;
|
||||
policyData.mcpRegistryUrl = mcpRegistryProvider?.url;
|
||||
policyData.mcpAccess = mcpRegistryProvider?.registry_access;
|
||||
}
|
||||
} else {
|
||||
policyData.mcpRegistryUrl = undefined;
|
||||
policyData.mcpAccess = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -484,7 +505,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
|
||||
entitlementsData,
|
||||
};
|
||||
this.logService.debug('[DefaultAccount] Successfully created default account for provider:', authenticationProvider.id);
|
||||
return { defaultAccount, policyData: policyData ? { accountId, policyData } : null };
|
||||
return { defaultAccount, policyData: policyData ? { accountId, policyData, isTokenEntitlementsDataFetched, isMcpRegistryDataFetched } : null };
|
||||
} catch (error) {
|
||||
this.logService.error('[DefaultAccount] Failed to create default account for provider:', authenticationProvider.id, getErrorMessage(error));
|
||||
return null;
|
||||
@@ -539,7 +560,15 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
|
||||
return expectedScopes.every(scope => scopes.includes(scope));
|
||||
}
|
||||
|
||||
private async getTokenEntitlements(sessions: AuthenticationSession[]): Promise<Partial<IPolicyData> | undefined> {
|
||||
private async getTokenEntitlements(sessions: AuthenticationSession[], accountPolicyData: IAccountPolicyData | undefined): Promise<Partial<IPolicyData> | undefined> {
|
||||
if (accountPolicyData?.isTokenEntitlementsDataFetched) {
|
||||
this.logService.debug('[DefaultAccount] Using last fetched token entitlements data');
|
||||
return accountPolicyData.policyData;
|
||||
}
|
||||
return await this.requestTokenEntitlements(sessions);
|
||||
}
|
||||
|
||||
private async requestTokenEntitlements(sessions: AuthenticationSession[]): Promise<Partial<IPolicyData> | undefined> {
|
||||
const tokenEntitlementsUrl = this.getTokenEntitlementUrl();
|
||||
if (!tokenEntitlementsUrl) {
|
||||
this.logService.debug('[DefaultAccount] No token entitlements URL found');
|
||||
@@ -610,11 +639,19 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async getMcpRegistryProvider(sessions: AuthenticationSession[]): Promise<IMcpRegistryProvider | undefined> {
|
||||
private async getMcpRegistryProvider(sessions: AuthenticationSession[], accountPolicyData: IAccountPolicyData | undefined): Promise<IMcpRegistryProvider | null | undefined> {
|
||||
if (accountPolicyData?.isMcpRegistryDataFetched) {
|
||||
this.logService.debug('[DefaultAccount] Using last fetched MCP registry data');
|
||||
return accountPolicyData.policyData.mcpRegistryUrl && accountPolicyData.policyData.mcpAccess ? { url: accountPolicyData.policyData.mcpRegistryUrl, registry_access: accountPolicyData.policyData.mcpAccess } : null;
|
||||
}
|
||||
return await this.requestMcpRegistryProvider(sessions);
|
||||
}
|
||||
|
||||
private async requestMcpRegistryProvider(sessions: AuthenticationSession[]): Promise<IMcpRegistryProvider | null | undefined> {
|
||||
const mcpRegistryDataUrl = this.getMcpRegistryDataUrl();
|
||||
if (!mcpRegistryDataUrl) {
|
||||
this.logService.debug('[DefaultAccount] No MCP registry data URL found');
|
||||
return undefined;
|
||||
return null;
|
||||
}
|
||||
|
||||
this.logService.debug('[DefaultAccount] Fetching MCP registry data from:', mcpRegistryDataUrl);
|
||||
@@ -624,7 +661,19 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
|
||||
}
|
||||
|
||||
if (response.res.statusCode && response.res.statusCode !== 200) {
|
||||
this.logService.trace(`[DefaultAccount] unexpected status code ${response.res.statusCode} while fetching MCP registry data`);
|
||||
if (response.res.statusCode === 401) {
|
||||
this.logService.debug('[DefaultAccount] Unauthorized (401) when fetching MCP registry data, treating as no registry available');
|
||||
return null;
|
||||
}
|
||||
if (response.res.statusCode === 404) {
|
||||
this.logService.debug('[DefaultAccount] MCP registry endpoint not found (404), treating as no registry available');
|
||||
return null;
|
||||
}
|
||||
if (response.res.statusCode === 429) {
|
||||
this.logService.debug(`[DefaultAccount] Received 429 Too Many Requests for MCP registry data, treating as no registry available and not retrying immediately.`);
|
||||
return null;
|
||||
}
|
||||
this.logService.debug(`[DefaultAccount] unexpected status code ${response.res.statusCode} while fetching MCP registry data`);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -632,13 +681,14 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
|
||||
const data = await asJson<IMcpRegistryResponse>(response);
|
||||
if (data) {
|
||||
this.logService.debug('Fetched MCP registry providers', data.mcp_registries);
|
||||
return data.mcp_registries[0];
|
||||
return data.mcp_registries[0] ?? null;
|
||||
}
|
||||
this.logService.debug('Failed to fetch MCP registry providers', 'No data returned');
|
||||
this.logService.debug('No MCP registry providers content found in response');
|
||||
return null;
|
||||
} catch (error) {
|
||||
this.logService.error('Failed to fetch MCP registry providers', getErrorMessage(error));
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private async request(url: string, type: 'GET', body: undefined, sessions: AuthenticationSession[], token: CancellationToken): Promise<IRequestContext | undefined>;
|
||||
@@ -664,6 +714,10 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
|
||||
|
||||
const status = response.res.statusCode;
|
||||
if (status && status !== 200) {
|
||||
if (status === 429) {
|
||||
this.logService.warn(`[DefaultAccount] Received 429 Too Many Requests for ${url}.`);
|
||||
return response;
|
||||
}
|
||||
lastResponse = response;
|
||||
continue; // try next session
|
||||
}
|
||||
@@ -671,7 +725,7 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (!token.isCancellationRequested) {
|
||||
this.logService.error(`[chat entitlement] request: error ${error}`);
|
||||
this.logService.error(`[DefaultAccount] request: error ${error}`, url);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -681,11 +735,6 @@ class DefaultAccountProvider extends Disposable implements IDefaultAccountProvid
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (lastResponse.res.statusCode && lastResponse.res.statusCode !== 200) {
|
||||
this.logService.trace(`[DefaultAccount]: unexpected status code ${lastResponse.res.statusCode} for request`, url);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return lastResponse;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user