mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-03 16:15:28 +01:00
chat: negotiate managed settings client compatibility (#329688)
* chat: negotiate managed settings client compatibility Report the VS Code managed-settings client version, securely transport the User-Agent, parse compatibility responses, and preserve fail-closed state across refreshes.\n\nCo-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chat: simplify managed settings client identity Use the existing browser-safe Editor-Version convention from productService and remove the dedicated User-Agent IPC transport. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * chat: report bundled Copilot runtime version Send the runtime version from product metadata alongside the VS Code editor identity and include both values in policy diagnostics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: align peer chat sandbox expectation Use the shared sandbox config builder introduced on main so the peer-chat assertion follows the current sandbox semantics. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -298,6 +298,8 @@ export async function withAsyncTestCodeEditorAndInlineCompletionsModel<T>(
|
||||
managedSettingsFetchStatus: null,
|
||||
managedSettingsFetchedAt: null,
|
||||
managedSettingsRawResponse: null,
|
||||
managedSettingsCompatibilityError: null,
|
||||
onDidChangeManagedSettingsCompatibilityError: Event.None,
|
||||
getDefaultAccount: async () => null,
|
||||
setDefaultAccountProvider: () => { },
|
||||
getDefaultAccountAuthenticationProvider: () => { return { id: 'mockProvider', name: 'Mock Provider', enterprise: false }; },
|
||||
|
||||
@@ -1134,6 +1134,8 @@ class StandaloneDefaultAccountService implements IDefaultAccountService {
|
||||
readonly managedSettingsFetchStatus: null = null;
|
||||
readonly managedSettingsFetchedAt: null = null;
|
||||
readonly managedSettingsRawResponse: unknown = null;
|
||||
readonly managedSettingsCompatibilityError = null;
|
||||
readonly onDidChangeManagedSettingsCompatibilityError = Event.None;
|
||||
|
||||
async getDefaultAccount(): Promise<IDefaultAccount | null> {
|
||||
return null;
|
||||
|
||||
@@ -27,6 +27,14 @@ export const GitHubPaths = {
|
||||
*/
|
||||
export type ManagedSettingsFetchStatus = number | 'ok' | 'no-url' | 'no-response' | 'parse-error' | null;
|
||||
|
||||
export const MANAGED_SETTINGS_UPDATE_REQUIRED_ERROR_CODE = 'client_update_required';
|
||||
|
||||
export interface IManagedSettingsCompatibilityError {
|
||||
readonly errorCode: typeof MANAGED_SETTINGS_UPDATE_REQUIRED_ERROR_CODE;
|
||||
readonly clientVersion?: string;
|
||||
readonly minimumClientVersion?: string;
|
||||
}
|
||||
|
||||
export interface IDefaultAccountProvider {
|
||||
readonly defaultAccount: IDefaultAccount | null;
|
||||
readonly onDidChangeDefaultAccount: Event<IDefaultAccount | null>;
|
||||
@@ -39,6 +47,8 @@ export interface IDefaultAccountProvider {
|
||||
readonly managedSettingsFetchedAt: number | null;
|
||||
/** The raw JSON response from the managed-settings endpoint, for diagnostics. */
|
||||
readonly managedSettingsRawResponse: unknown;
|
||||
readonly managedSettingsCompatibilityError: IManagedSettingsCompatibilityError | null;
|
||||
readonly onDidChangeManagedSettingsCompatibilityError: Event<IManagedSettingsCompatibilityError | null>;
|
||||
getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider;
|
||||
|
||||
/**
|
||||
@@ -70,6 +80,8 @@ export interface IDefaultAccountService {
|
||||
readonly managedSettingsFetchedAt: number | null;
|
||||
/** The raw JSON response from the managed-settings endpoint, for diagnostics. */
|
||||
readonly managedSettingsRawResponse: unknown;
|
||||
readonly managedSettingsCompatibilityError: IManagedSettingsCompatibilityError | null;
|
||||
readonly onDidChangeManagedSettingsCompatibilityError: Event<IManagedSettingsCompatibilityError | null>;
|
||||
getDefaultAccount(): Promise<IDefaultAccount | null>;
|
||||
getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider;
|
||||
setDefaultAccountProvider(provider: IDefaultAccountProvider): void;
|
||||
|
||||
@@ -133,6 +133,8 @@ class MockDefaultAccountService implements IDefaultAccountService {
|
||||
readonly managedSettingsFetchStatus: null = null;
|
||||
readonly managedSettingsFetchedAt: null = null;
|
||||
readonly managedSettingsRawResponse: unknown = null;
|
||||
readonly managedSettingsCompatibilityError = null;
|
||||
readonly onDidChangeManagedSettingsCompatibilityError = Event.None;
|
||||
|
||||
async getDefaultAccount(): Promise<IDefaultAccount | null> { return MOCK_ACCOUNT; }
|
||||
getDefaultAccountAuthenticationProvider(): IDefaultAccountAuthenticationProvider { return MOCK_ACCOUNT.authenticationProvider; }
|
||||
|
||||
@@ -53,7 +53,7 @@ import { IPolicyService, PolicyValueSource } from '../../../platform/policy/comm
|
||||
import { COPILOT_ENABLED_PLUGINS_KEY, COPILOT_EXTRA_MARKETPLACES_KEY, COPILOT_STRICT_MARKETPLACES_KEY, INativeManagedSettingsService, IFileManagedSettingsService, ManagedSettingsChannel, ManagedSettingsSource, normalizeManagedSettings, projectManagedSettings, pickManagedSettings } from '../../../platform/policy/common/copilotManagedSettings.js';
|
||||
import { IManagedSettingPolicyDefinition, ManagedSettingsData } from '../../../base/common/policy.js';
|
||||
import { APPROVED_ACCOUNT_ORGANIZATIONS_POLICY_NAME, IAccountPolicyGateService } from '../../services/policies/common/accountPolicyService.js';
|
||||
import { adaptManagedSettings, IManagedSettingsResponse } from '../../services/accounts/browser/managedSettings.js';
|
||||
import { adaptManagedSettings, getManagedSettingsClientHeaders, IManagedSettingsResponse } from '../../services/accounts/browser/managedSettings.js';
|
||||
import { isObject } from '../../../base/common/types.js';
|
||||
import * as json from '../../../base/common/json.js';
|
||||
import { getParseErrorMessage } from '../../../base/common/jsonErrorMessages.js';
|
||||
@@ -1062,13 +1062,17 @@ class PolicyDiagnosticsAction extends Action2 {
|
||||
|
||||
const fetchStatus = defaultAccountService.managedSettingsFetchStatus;
|
||||
const fetchedAt = defaultAccountService.managedSettingsFetchedAt;
|
||||
const clientHeaders = getManagedSettingsClientHeaders(productService);
|
||||
const compatibilityError = defaultAccountService.managedSettingsCompatibilityError;
|
||||
content += '#### GitHub Server API\n\n';
|
||||
content += markdownTable(
|
||||
['Property', 'Value'],
|
||||
[
|
||||
['Endpoint', '/copilot_internal/managed_settings'],
|
||||
['Last fetch', fetchStatus === null ? 'never' : String(fetchStatus)],
|
||||
['Last successful fetch', fetchedAt ? new Date(fetchedAt).toLocaleString() : 'n/a'],
|
||||
['Last fetch', fetchStatus === null ? 'never' : `${fetchStatus}${fetchedAt ? ` at ${new Date(fetchedAt).toLocaleString()}` : ''}`],
|
||||
['Editor-Version', String(clientHeaders['Editor-Version'])],
|
||||
['Copilot-Runtime-Version', String(clientHeaders['Copilot-Runtime-Version'] ?? 'not available')],
|
||||
['Compatibility', compatibilityError ? `update required (${compatibilityError.clientVersion ?? '?'} → ${compatibilityError.minimumClientVersion ?? '?'})` : 'compatible or not evaluated'],
|
||||
['Contributes winning keys', channelContributes('server') ? 'yes' : 'no']
|
||||
]
|
||||
);
|
||||
|
||||
@@ -14,18 +14,17 @@ import { equals } from '../../../../base/common/objects.js';
|
||||
import { isWeb } from '../../../../base/common/platform.js';
|
||||
import { IDefaultChatAgent } from '../../../../base/common/product.js';
|
||||
import { isString, isUndefined, Mutable } from '../../../../base/common/types.js';
|
||||
import { IRequestContext } from '../../../../base/parts/request/common/request.js';
|
||||
import { IHeaders, IRequestContext } from '../../../../base/parts/request/common/request.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { ICommandService } from '../../../../platform/commands/common/commands.js';
|
||||
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
|
||||
import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';
|
||||
import { IDefaultAccountProvider, IDefaultAccountService, ManagedSettingsFetchStatus } from '../../../../platform/defaultAccount/common/defaultAccount.js';
|
||||
import { IDefaultAccountProvider, IDefaultAccountService, IManagedSettingsCompatibilityError, MANAGED_SETTINGS_UPDATE_REQUIRED_ERROR_CODE, ManagedSettingsFetchStatus } from '../../../../platform/defaultAccount/common/defaultAccount.js';
|
||||
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { ILogService } from '../../../../platform/log/common/log.js';
|
||||
import { IProductService } from '../../../../platform/product/common/productService.js';
|
||||
import { asJson, IRequestService, isClientError, isSuccess, readHeader, retryAfterFromHeaders } from '../../../../platform/request/common/request.js';
|
||||
import { INativeManagedSettingsService, shouldForceRemoteSettingsRefresh } from '../../../../platform/policy/common/copilotManagedSettings.js';
|
||||
import { asJson, asText, IRequestService, isClientError, isSuccess, readHeader, retryAfterFromHeaders } from '../../../../platform/request/common/request.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
|
||||
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
|
||||
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
|
||||
@@ -33,7 +32,7 @@ import { AuthenticationSession, AuthenticationSessionAccount, IAuthenticationExt
|
||||
import { IWorkbenchEnvironmentService } from '../../environment/common/environmentService.js';
|
||||
import { IExtensionService } from '../../extensions/common/extensions.js';
|
||||
import { IHostService } from '../../host/browser/host.js';
|
||||
import { adaptManagedSettings, IManagedSettingsResponse } from './managedSettings.js';
|
||||
import { adaptManagedSettings, getManagedSettingsClientHeaders, IManagedSettingsResponse, parseManagedSettingsCompatibilityError } from './managedSettings.js';
|
||||
|
||||
interface IDefaultAccountConfig {
|
||||
readonly preferredExtensions: string[];
|
||||
@@ -126,6 +125,7 @@ export class DefaultAccountService extends Disposable implements IDefaultAccount
|
||||
get managedSettingsFetchStatus(): ManagedSettingsFetchStatus { return this.defaultAccountProvider?.managedSettingsFetchStatus ?? null; }
|
||||
get managedSettingsFetchedAt(): number | null { return this.defaultAccountProvider?.managedSettingsFetchedAt ?? null; }
|
||||
get managedSettingsRawResponse(): unknown { return this.defaultAccountProvider?.managedSettingsRawResponse ?? null; }
|
||||
get managedSettingsCompatibilityError(): IManagedSettingsCompatibilityError | null { return this.defaultAccountProvider?.managedSettingsCompatibilityError ?? null; }
|
||||
|
||||
private readonly initBarrier = new Barrier();
|
||||
|
||||
@@ -138,6 +138,9 @@ export class DefaultAccountService extends Disposable implements IDefaultAccount
|
||||
private readonly _onDidChangeCopilotTokenInfo = this._register(new Emitter<ICopilotTokenInfo | null>());
|
||||
readonly onDidChangeCopilotTokenInfo = this._onDidChangeCopilotTokenInfo.event;
|
||||
|
||||
private readonly _onDidChangeManagedSettingsCompatibilityError = this._register(new Emitter<IManagedSettingsCompatibilityError | null>());
|
||||
readonly onDidChangeManagedSettingsCompatibilityError = this._onDidChangeManagedSettingsCompatibilityError.event;
|
||||
|
||||
private readonly defaultAccountConfig: IDefaultAccountConfig;
|
||||
private defaultAccountProvider: IDefaultAccountProvider | null = null;
|
||||
|
||||
@@ -169,9 +172,13 @@ export class DefaultAccountService extends Disposable implements IDefaultAccount
|
||||
}
|
||||
|
||||
this.defaultAccountProvider = provider;
|
||||
this._register(provider.onDidChangeManagedSettingsCompatibilityError(error => this._onDidChangeManagedSettingsCompatibilityError.fire(error)));
|
||||
if (this.defaultAccountProvider.policyData) {
|
||||
this._onDidChangePolicyData.fire(this.defaultAccountProvider.policyData);
|
||||
}
|
||||
if (this.defaultAccountProvider.managedSettingsCompatibilityError) {
|
||||
this._onDidChangeManagedSettingsCompatibilityError.fire(this.defaultAccountProvider.managedSettingsCompatibilityError);
|
||||
}
|
||||
provider.refresh().then(account => {
|
||||
this.defaultAccount = account;
|
||||
}).finally(() => {
|
||||
@@ -224,6 +231,7 @@ interface IAccountPolicyData {
|
||||
readonly tokenEntitlementsFetchedAt?: number;
|
||||
readonly mcpRegistryDataFetchedAt?: number;
|
||||
readonly managedSettingsFetchedAt?: number;
|
||||
readonly managedSettingsCompatibilityError?: IManagedSettingsCompatibilityError;
|
||||
}
|
||||
|
||||
interface ICachedAccountData {
|
||||
@@ -238,6 +246,12 @@ interface IDefaultAccountData {
|
||||
copilotTokenInfo: ICopilotTokenInfo | null;
|
||||
}
|
||||
|
||||
type ManagedSettingsRequestResult =
|
||||
| { readonly kind: 'success'; readonly data: Partial<IPolicyData> }
|
||||
| { readonly kind: 'noSettings' }
|
||||
| { readonly kind: 'updateRequired'; readonly error: IManagedSettingsCompatibilityError }
|
||||
| { readonly kind: 'unavailable' };
|
||||
|
||||
type DefaultAccountStatusTelemetry = {
|
||||
status: string;
|
||||
initial: boolean;
|
||||
@@ -280,6 +294,9 @@ export class DefaultAccountProvider extends Disposable implements IDefaultAccoun
|
||||
private _managedSettingsRawResponse: unknown = null;
|
||||
get managedSettingsRawResponse(): unknown { return this._managedSettingsRawResponse; }
|
||||
|
||||
private _managedSettingsCompatibilityError: IManagedSettingsCompatibilityError | null = null;
|
||||
get managedSettingsCompatibilityError(): IManagedSettingsCompatibilityError | null { return this._managedSettingsCompatibilityError; }
|
||||
|
||||
private readonly _onDidChangeDefaultAccount = this._register(new Emitter<IDefaultAccount | null>());
|
||||
readonly onDidChangeDefaultAccount = this._onDidChangeDefaultAccount.event;
|
||||
|
||||
@@ -289,6 +306,9 @@ export class DefaultAccountProvider extends Disposable implements IDefaultAccoun
|
||||
private readonly _onDidChangeCopilotTokenInfo = this._register(new Emitter<ICopilotTokenInfo | null>());
|
||||
readonly onDidChangeCopilotTokenInfo = this._onDidChangeCopilotTokenInfo.event;
|
||||
|
||||
private readonly _onDidChangeManagedSettingsCompatibilityError = this._register(new Emitter<IManagedSettingsCompatibilityError | null>());
|
||||
readonly onDidChangeManagedSettingsCompatibilityError = this._onDidChangeManagedSettingsCompatibilityError.event;
|
||||
|
||||
private readonly accountStatusContext: IContextKey<string>;
|
||||
private initialized = false;
|
||||
private readonly initPromise: Promise<void>;
|
||||
@@ -306,17 +326,18 @@ export class DefaultAccountProvider extends Disposable implements IDefaultAccoun
|
||||
@IRequestService private readonly requestService: IRequestService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IWorkbenchEnvironmentService private readonly environmentService: IWorkbenchEnvironmentService,
|
||||
@IProductService private readonly productService: IProductService,
|
||||
@IContextKeyService contextKeyService: IContextKeyService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
@IHostService private readonly hostService: IHostService,
|
||||
@ICommandService private readonly commandService: ICommandService,
|
||||
@INativeManagedSettingsService private readonly nativeManagedSettingsService: INativeManagedSettingsService,
|
||||
) {
|
||||
super();
|
||||
this.accountStatusContext = CONTEXT_DEFAULT_ACCOUNT_STATE.bindTo(contextKeyService);
|
||||
const cachedAccountData = this.getCachedAccountData();
|
||||
this._policyData = cachedAccountData?.accountPolicyData ?? null;
|
||||
this._copilotTokenInfo = cachedAccountData?.copilotTokenInfo ?? null;
|
||||
this._managedSettingsCompatibilityError = cachedAccountData?.accountPolicyData.managedSettingsCompatibilityError ?? null;
|
||||
this.initPromise = this.init()
|
||||
.finally(() => {
|
||||
this.telemetryService.publicLog2<DefaultAccountStatusTelemetry, DefaultAccountStatusTelemetryClassification>('defaultaccount:status', { status: this.defaultAccount ? 'available' : 'unavailable', initial: true });
|
||||
@@ -539,12 +560,14 @@ export class DefaultAccountProvider extends Disposable implements IDefaultAccoun
|
||||
this._defaultAccount = account;
|
||||
this.setCopilotTokenInfo(account.copilotTokenInfo);
|
||||
this.setPolicyData(account.policyData);
|
||||
this.setManagedSettingsCompatibilityError(account.policyData?.managedSettingsCompatibilityError ?? null);
|
||||
this._onDidChangeDefaultAccount.fire(this._defaultAccount.defaultAccount);
|
||||
this.accountStatusContext.set(DefaultAccountStatus.Available);
|
||||
this.logService.debug('[DefaultAccount] Account status set to Available');
|
||||
} else {
|
||||
this._defaultAccount = null;
|
||||
this.setPolicyData(null);
|
||||
this.setManagedSettingsCompatibilityError(null);
|
||||
this.setCopilotTokenInfo(null);
|
||||
this._onDidChangeDefaultAccount.fire(null);
|
||||
this.accountDataPollScheduler.cancel();
|
||||
@@ -562,6 +585,14 @@ export class DefaultAccountProvider extends Disposable implements IDefaultAccoun
|
||||
this._onDidChangePolicyData.fire(this._policyData?.policyData ?? null);
|
||||
}
|
||||
|
||||
private setManagedSettingsCompatibilityError(error: IManagedSettingsCompatibilityError | null): void {
|
||||
if (equals(this._managedSettingsCompatibilityError, error)) {
|
||||
return;
|
||||
}
|
||||
this._managedSettingsCompatibilityError = error;
|
||||
this._onDidChangeManagedSettingsCompatibilityError.fire(error);
|
||||
}
|
||||
|
||||
private setCopilotTokenInfo(copilotTokenInfo: ICopilotTokenInfo | null): void {
|
||||
if (equals(this._copilotTokenInfo, copilotTokenInfo)) {
|
||||
return;
|
||||
@@ -636,6 +667,9 @@ export class DefaultAccountProvider extends Disposable implements IDefaultAccoun
|
||||
|
||||
const tokenEntitlementsFetchedAt: number | undefined = tokenEntitlementsResult?.fetchedAt;
|
||||
const managedSettingsFetchedAt: number | undefined = managedSettingsResult?.fetchedAt;
|
||||
const managedSettingsCompatibilityError = managedSettingsResult
|
||||
? managedSettingsResult.compatibilityError
|
||||
: this._managedSettingsCompatibilityError;
|
||||
let mcpRegistryDataFetchedAt: number | undefined;
|
||||
let policyData: Mutable<IPolicyData> | undefined = accountPolicyData?.policyData ? { ...accountPolicyData.policyData } : undefined;
|
||||
if (entitlementsData) {
|
||||
@@ -671,7 +705,15 @@ export class DefaultAccountProvider extends Disposable implements IDefaultAccoun
|
||||
};
|
||||
this.logService.debug('[DefaultAccount] Successfully created default account for provider:', authenticationProvider.id);
|
||||
const accountPolicyResult: IAccountPolicyData | null = policyData || entitlementsFetchedAt
|
||||
? { accountId, policyData: policyData ?? {}, entitlementsFetchedAt, tokenEntitlementsFetchedAt, mcpRegistryDataFetchedAt, managedSettingsFetchedAt }
|
||||
? {
|
||||
accountId,
|
||||
policyData: policyData ?? {},
|
||||
entitlementsFetchedAt,
|
||||
tokenEntitlementsFetchedAt,
|
||||
mcpRegistryDataFetchedAt,
|
||||
managedSettingsFetchedAt,
|
||||
managedSettingsCompatibilityError: managedSettingsCompatibilityError ?? undefined,
|
||||
}
|
||||
: null;
|
||||
return {
|
||||
defaultAccount,
|
||||
@@ -873,7 +915,7 @@ export class DefaultAccountProvider extends Disposable implements IDefaultAccoun
|
||||
}
|
||||
}
|
||||
|
||||
private async getManagedSettings(sessions: AuthenticationSession[], accountPolicyData: IAccountPolicyData | undefined, options?: { forceRefresh?: boolean }): Promise<{ data: Partial<IPolicyData> | undefined; fetchedAt: number }> {
|
||||
private async getManagedSettings(sessions: AuthenticationSession[], accountPolicyData: IAccountPolicyData | undefined, options?: { forceRefresh?: boolean }): Promise<{ data: Partial<IPolicyData> | undefined; fetchedAt: number; compatibilityError: IManagedSettingsCompatibilityError | null }> {
|
||||
const accountId = sessions[0].account.id;
|
||||
const cachedManagedSettings = accountPolicyData?.managedSettingsFetchedAt !== undefined && !this.isDataStale(accountPolicyData.managedSettingsFetchedAt)
|
||||
? {
|
||||
@@ -883,58 +925,64 @@ export class DefaultAccountProvider extends Disposable implements IDefaultAccoun
|
||||
fetchedAt: accountPolicyData.managedSettingsFetchedAt,
|
||||
}
|
||||
: undefined;
|
||||
let forceRemoteSettingsRefresh = false;
|
||||
if (!options?.forceRefresh && cachedManagedSettings && !this.managedSettingsFetchAttemptedAccounts.has(accountId)) {
|
||||
let nativeManagedSettings = this.nativeManagedSettingsService.managedSettings;
|
||||
try {
|
||||
nativeManagedSettings = await this.nativeManagedSettingsService.initialize();
|
||||
} catch (error) {
|
||||
this.logService.warn('[DefaultAccount] Failed to initialize native managed settings before resolving forceRemoteSettingsRefresh; using available values', getErrorMessage(error));
|
||||
nativeManagedSettings = this.nativeManagedSettingsService.managedSettings;
|
||||
}
|
||||
forceRemoteSettingsRefresh = shouldForceRemoteSettingsRefresh(nativeManagedSettings, accountPolicyData?.policyData.managedSettings);
|
||||
const hasFetchedThisProcess = this.managedSettingsFetchAttemptedAccounts.has(accountId);
|
||||
if (!options?.forceRefresh && cachedManagedSettings && hasFetchedThisProcess) {
|
||||
this.logService.debug('[DefaultAccount] Using last fetched managed settings data');
|
||||
return { ...cachedManagedSettings, compatibilityError: this._managedSettingsCompatibilityError };
|
||||
}
|
||||
|
||||
if (!options?.forceRefresh && cachedManagedSettings && !forceRemoteSettingsRefresh) {
|
||||
this.logService.debug('[DefaultAccount] Using last fetched managed settings data');
|
||||
// Seed status so Policy Diagnostics reflects "applied" rather than
|
||||
// "not yet fetched" after a process restart that warm-starts from
|
||||
// the cached policy payload.
|
||||
this._managedSettingsFetchStatus = 'ok';
|
||||
return cachedManagedSettings;
|
||||
}
|
||||
if (forceRemoteSettingsRefresh) {
|
||||
this.logService.info('[DefaultAccount] forceRemoteSettingsRefresh is set; fetching fresh managed settings instead of using the cached response');
|
||||
}
|
||||
this.managedSettingsFetchAttemptedAccounts.add(accountId);
|
||||
const data = await this.requestManagedSettings(sessions);
|
||||
return { data: data ?? cachedManagedSettings?.data, fetchedAt: Date.now() };
|
||||
const result = await this.requestManagedSettings(sessions);
|
||||
const fetchedAt = Date.now();
|
||||
switch (result.kind) {
|
||||
case 'success':
|
||||
return { data: result.data, fetchedAt, compatibilityError: null };
|
||||
case 'noSettings':
|
||||
return { data: { managedSettings: undefined }, fetchedAt, compatibilityError: null };
|
||||
case 'updateRequired':
|
||||
return { data: { managedSettings: undefined }, fetchedAt, compatibilityError: result.error };
|
||||
case 'unavailable':
|
||||
return {
|
||||
data: this._managedSettingsCompatibilityError ? { managedSettings: undefined } : cachedManagedSettings?.data,
|
||||
fetchedAt,
|
||||
compatibilityError: this._managedSettingsCompatibilityError,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private async requestManagedSettings(sessions: AuthenticationSession[]): Promise<Partial<IPolicyData> | undefined> {
|
||||
private async requestManagedSettings(sessions: AuthenticationSession[]): Promise<ManagedSettingsRequestResult> {
|
||||
const managedSettingsUrl = this.getManagedSettingsUrl();
|
||||
if (!managedSettingsUrl) {
|
||||
this.logService.debug('[DefaultAccount] No managed settings URL configured; skipping enterprise policy fetch');
|
||||
this._managedSettingsFetchStatus = 'no-url';
|
||||
return undefined;
|
||||
return { kind: 'unavailable' };
|
||||
}
|
||||
|
||||
this.logService.debug('[DefaultAccount] Fetching managed settings from:', managedSettingsUrl);
|
||||
const rateLimitBackoffActive = Date.now() < this._rateLimitBackoffUntil;
|
||||
const response = await this.request(managedSettingsUrl, 'GET', undefined, sessions, CancellationToken.None, 'defaultAccount.managedSettings', MANAGED_SETTINGS_REQUEST_TIMEOUT_MS);
|
||||
const response = await this.request(managedSettingsUrl, 'GET', undefined, sessions, CancellationToken.None, 'defaultAccount.managedSettings', MANAGED_SETTINGS_REQUEST_TIMEOUT_MS, getManagedSettingsClientHeaders(this.productService));
|
||||
if (!response) {
|
||||
this.logService.debug('[DefaultAccount] Managed settings fetch returned no response (network error, all sessions rejected, or active rate-limit backoff); falling back to local-only policy');
|
||||
this.reportManagedSettingsOutcome('no-response', rateLimitBackoffActive);
|
||||
return undefined;
|
||||
return { kind: 'unavailable' };
|
||||
}
|
||||
|
||||
const status = response.res.statusCode ?? 0;
|
||||
if (status === 404) {
|
||||
this.reportManagedSettingsOutcome(status, rateLimitBackoffActive);
|
||||
return { kind: 'noSettings' };
|
||||
}
|
||||
if (status === 466) {
|
||||
const error = await this.readManagedSettingsCompatibilityError(response);
|
||||
this.setManagedSettingsCompatibilityError(error);
|
||||
this.reportManagedSettingsOutcome(status, rateLimitBackoffActive);
|
||||
return { kind: 'updateRequired', error };
|
||||
}
|
||||
|
||||
// Any non-2xx response means "fall back to local settings only and continue
|
||||
// operating normally" — silent fallback, no policy.
|
||||
if (!isSuccess(response)) {
|
||||
const status = response.res.statusCode ?? 0;
|
||||
this.logService.warn(`[DefaultAccount] Managed settings fetch returned non-success status ${status}; falling back to local-only policy`);
|
||||
this.reportManagedSettingsOutcome(status, rateLimitBackoffActive);
|
||||
return undefined;
|
||||
return { kind: 'unavailable' };
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -951,14 +999,29 @@ export class DefaultAccountProvider extends Disposable implements IDefaultAccoun
|
||||
this.logService.trace('[DefaultAccount] Managed settings payload:', JSON.stringify(adapted));
|
||||
}
|
||||
this.reportManagedSettingsOutcome('ok', rateLimitBackoffActive);
|
||||
return adapted;
|
||||
return { kind: 'success', data: adapted };
|
||||
} catch (error) {
|
||||
this.logService.error('[DefaultAccount] Failed to parse managed settings response', getErrorMessage(error));
|
||||
this.reportManagedSettingsOutcome('parse-error', rateLimitBackoffActive);
|
||||
return undefined;
|
||||
return { kind: 'unavailable' };
|
||||
}
|
||||
}
|
||||
|
||||
private async readManagedSettingsCompatibilityError(response: IRequestContext): Promise<IManagedSettingsCompatibilityError> {
|
||||
try {
|
||||
const text = await asText(response);
|
||||
const body: unknown = text ? JSON.parse(text) : undefined;
|
||||
const parsed = parseManagedSettingsCompatibilityError(body);
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
}
|
||||
this.logService.error('[DefaultAccount] Managed settings compatibility response did not contain the expected error code');
|
||||
} catch (error) {
|
||||
this.logService.error('[DefaultAccount] Failed to parse managed settings compatibility response', getErrorMessage(error));
|
||||
}
|
||||
return { errorCode: MANAGED_SETTINGS_UPDATE_REQUIRED_ERROR_CODE };
|
||||
}
|
||||
|
||||
private reportManagedSettingsOutcome(status: Exclude<ManagedSettingsFetchStatus, null | 'no-url'>, rateLimitBackoffActive: boolean): void {
|
||||
this._managedSettingsFetchStatus = status;
|
||||
this.telemetryService.publicLog2<ManagedSettingsFetchTelemetry, ManagedSettingsFetchTelemetryClassification>('defaultaccount:managedSettings:fetch', {
|
||||
@@ -994,9 +1057,9 @@ export class DefaultAccountProvider extends Disposable implements IDefaultAccoun
|
||||
|
||||
private _rateLimitBackoffUntil = 0;
|
||||
|
||||
private async request(url: string, type: 'GET', body: undefined, sessions: AuthenticationSession[], token: CancellationToken, callSite: string, requestTimeoutMs?: number): Promise<IRequestContext | undefined>;
|
||||
private async request(url: string, type: 'POST', body: object, sessions: AuthenticationSession[], token: CancellationToken, callSite: string, requestTimeoutMs?: number): Promise<IRequestContext | undefined>;
|
||||
private async request(url: string, type: 'GET' | 'POST', body: object | undefined, sessions: AuthenticationSession[], token: CancellationToken, callSite: string, requestTimeoutMs?: number): Promise<IRequestContext | undefined> {
|
||||
private async request(url: string, type: 'GET', body: undefined, sessions: AuthenticationSession[], token: CancellationToken, callSite: string, requestTimeoutMs?: number, headers?: IHeaders): Promise<IRequestContext | undefined>;
|
||||
private async request(url: string, type: 'POST', body: object, sessions: AuthenticationSession[], token: CancellationToken, callSite: string, requestTimeoutMs?: number, headers?: IHeaders): Promise<IRequestContext | undefined>;
|
||||
private async request(url: string, type: 'GET' | 'POST', body: object | undefined, sessions: AuthenticationSession[], token: CancellationToken, callSite: string, requestTimeoutMs?: number, headers?: IHeaders): Promise<IRequestContext | undefined> {
|
||||
// Rate-limit backoff: when any prior `/copilot_internal/*` request was
|
||||
// throttled (429 or 403 + `X-RateLimit-Remaining: 0`), every subsequent
|
||||
// request is short-circuited until the parsed `Retry-After` elapses.
|
||||
@@ -1025,6 +1088,7 @@ export class DefaultAccountProvider extends Disposable implements IDefaultAccoun
|
||||
disableCache: true,
|
||||
timeout: requestTimeoutMs,
|
||||
headers: {
|
||||
...headers,
|
||||
'Authorization': `Bearer ${session.accessToken}`
|
||||
},
|
||||
callSite
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IPolicyData } from '../../../../base/common/defaultAccount.js';
|
||||
import { IProductConfiguration } from '../../../../base/common/product.js';
|
||||
import { isString } from '../../../../base/common/types.js';
|
||||
import { IHeaders } from '../../../../base/parts/request/common/request.js';
|
||||
import { IManagedSettingsCompatibilityError, MANAGED_SETTINGS_UPDATE_REQUIRED_ERROR_CODE } from '../../../../platform/defaultAccount/common/defaultAccount.js';
|
||||
import { normalizeManagedSettings } from '../../../../platform/policy/common/copilotManagedSettings.js';
|
||||
|
||||
/**
|
||||
@@ -58,6 +62,41 @@ export interface IManagedSettingsResponse {
|
||||
readonly [key: string]: unknown;
|
||||
}
|
||||
|
||||
export function getManagedSettingsClientHeaders(product: Pick<IProductConfiguration, 'version' | 'copilotVersions'>): IHeaders {
|
||||
const headers: IHeaders = {
|
||||
'Editor-Version': `vscode/${product.version}`,
|
||||
};
|
||||
const runtimeVersion = product.copilotVersions?.runtime;
|
||||
if (runtimeVersion) {
|
||||
headers['Copilot-Runtime-Version'] = `copilot-runtime/${runtimeVersion}`;
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
interface IManagedSettingsCompatibilityErrorResponse {
|
||||
readonly error_code?: unknown;
|
||||
readonly client_version?: unknown;
|
||||
readonly minimum_client_version?: unknown;
|
||||
}
|
||||
|
||||
function isManagedSettingsCompatibilityErrorResponse(response: unknown): response is IManagedSettingsCompatibilityErrorResponse {
|
||||
return typeof response === 'object' && response !== null;
|
||||
}
|
||||
|
||||
export function parseManagedSettingsCompatibilityError(response: unknown): IManagedSettingsCompatibilityError | undefined {
|
||||
if (!isManagedSettingsCompatibilityErrorResponse(response) || response.error_code !== MANAGED_SETTINGS_UPDATE_REQUIRED_ERROR_CODE) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const clientVersion = isString(response.client_version) ? response.client_version : undefined;
|
||||
const minimumClientVersion = isString(response.minimum_client_version) ? response.minimum_client_version : undefined;
|
||||
return {
|
||||
errorCode: MANAGED_SETTINGS_UPDATE_REQUIRED_ERROR_CODE,
|
||||
...(clientVersion ? { clientVersion } : {}),
|
||||
...(minimumClientVersion ? { minimumClientVersion } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapt the `managed_settings` API response into the `managedSettings` slice of
|
||||
* {@link IPolicyData} that the policy framework consumes. This is a thin wrapper
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
import assert from 'assert';
|
||||
import { bufferToStream, VSBuffer } from '../../../../../base/common/buffer.js';
|
||||
import { Event } from '../../../../../base/common/event.js';
|
||||
import { IRequestContext } from '../../../../../base/parts/request/common/request.js';
|
||||
import { IRequestContext, IRequestOptions } from '../../../../../base/parts/request/common/request.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
|
||||
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
|
||||
@@ -15,7 +15,8 @@ import { IContextKeyService } from '../../../../../platform/contextkey/common/co
|
||||
import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';
|
||||
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
|
||||
import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';
|
||||
import { COPILOT_FORCE_REMOTE_SETTINGS_REFRESH_KEY, INativeManagedSettingsService } from '../../../../../platform/policy/common/copilotManagedSettings.js';
|
||||
import { COPILOT_FORCE_REMOTE_SETTINGS_REFRESH_KEY } from '../../../../../platform/policy/common/copilotManagedSettings.js';
|
||||
import { IProductService } from '../../../../../platform/product/common/productService.js';
|
||||
import { IRequestService } from '../../../../../platform/request/common/request.js';
|
||||
import { InMemoryStorageService, IStorageService } from '../../../../../platform/storage/common/storage.js';
|
||||
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
|
||||
@@ -25,7 +26,7 @@ import { IWorkbenchEnvironmentService } from '../../../environment/common/enviro
|
||||
import { IExtensionService } from '../../../extensions/common/extensions.js';
|
||||
import { IHostService } from '../../../host/browser/host.js';
|
||||
import { DefaultAccountProvider } from '../../browser/defaultAccount.js';
|
||||
import { IManagedSettingsResponse } from '../../browser/managedSettings.js';
|
||||
import { TestProductService } from '../../../../test/common/workbenchTestServices.js';
|
||||
|
||||
suite('DefaultAccountProvider managed settings', () => {
|
||||
|
||||
@@ -38,12 +39,12 @@ suite('DefaultAccountProvider managed settings', () => {
|
||||
scopes: ['user:email'],
|
||||
}];
|
||||
|
||||
test('cached server control forces only the first fetch', async () => {
|
||||
test('cached settings perform one startup compatibility fetch', async () => {
|
||||
const requestService = new TestRequestService(async () => jsonResponse({
|
||||
forceRemoteSettingsRefresh: true,
|
||||
permissions: { disableBypassPermissionsMode: 'disable' },
|
||||
}));
|
||||
const provider = await createProvider({}, requestService);
|
||||
const provider = await createProvider(requestService);
|
||||
const cachedPolicy = createCachedPolicy(true);
|
||||
|
||||
const first = await provider['getManagedSettings'](sessions, cachedPolicy);
|
||||
@@ -51,38 +52,86 @@ suite('DefaultAccountProvider managed settings', () => {
|
||||
|
||||
assert.deepStrictEqual({
|
||||
requestCount: requestService.requestCount,
|
||||
editorVersion: requestService.requests[0].headers?.['Editor-Version'],
|
||||
runtimeVersion: requestService.requests[0].headers?.['Copilot-Runtime-Version'],
|
||||
first: first.data,
|
||||
second: second.data,
|
||||
}, {
|
||||
requestCount: 1,
|
||||
editorVersion: 'vscode/1.132.0',
|
||||
runtimeVersion: 'copilot-runtime/0.0.344',
|
||||
first: cachedPolicy.policyData,
|
||||
second: cachedPolicy.policyData,
|
||||
});
|
||||
});
|
||||
|
||||
test('native false suppresses cached server true', async () => {
|
||||
const requestService = new TestRequestService(async () => {
|
||||
throw new Error('unexpected request');
|
||||
});
|
||||
const provider = await createProvider({ [COPILOT_FORCE_REMOTE_SETTINGS_REFRESH_KEY]: false }, requestService);
|
||||
test('404 clears cached server managed settings', async () => {
|
||||
const requestService = new TestRequestService(async () => jsonResponse({}, 404));
|
||||
const provider = await createProvider(requestService);
|
||||
const cachedPolicy = createCachedPolicy(true);
|
||||
|
||||
const result = await provider['getManagedSettings'](sessions, cachedPolicy);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
requestCount: requestService.requestCount,
|
||||
status: provider.managedSettingsFetchStatus,
|
||||
data: result.data,
|
||||
compatibilityError: provider.managedSettingsCompatibilityError,
|
||||
}, {
|
||||
requestCount: 0,
|
||||
data: cachedPolicy.policyData,
|
||||
requestCount: 1,
|
||||
status: 404,
|
||||
data: { managedSettings: undefined },
|
||||
compatibilityError: null,
|
||||
});
|
||||
});
|
||||
|
||||
test('failed forced fetch retains cached managed settings', async () => {
|
||||
test('466 blocks and does not fall back to cached server managed settings', async () => {
|
||||
const requestService = new TestRequestService(async () => jsonResponse({
|
||||
error_code: 'client_update_required',
|
||||
client_id: 'vscode',
|
||||
client_version: '1.132.0',
|
||||
minimum_client_version: '1.133.0',
|
||||
}, 466));
|
||||
const provider = await createProvider(requestService);
|
||||
const cachedPolicy = createCachedPolicy(false);
|
||||
|
||||
const result = await provider['getManagedSettings'](sessions, cachedPolicy);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
status: provider.managedSettingsFetchStatus,
|
||||
data: result.data,
|
||||
compatibilityError: provider.managedSettingsCompatibilityError,
|
||||
}, {
|
||||
status: 466,
|
||||
data: { managedSettings: undefined },
|
||||
compatibilityError: {
|
||||
errorCode: 'client_update_required',
|
||||
clientVersion: '1.132.0',
|
||||
minimumClientVersion: '1.133.0',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('malformed 466 still blocks without compatibility metadata', async () => {
|
||||
const requestService = new TestRequestService(async () => jsonResponse({ error_code: 'unexpected' }, 466));
|
||||
const provider = await createProvider(requestService);
|
||||
|
||||
const result = await provider['getManagedSettings'](sessions, createCachedPolicy(false));
|
||||
|
||||
assert.deepStrictEqual({
|
||||
data: result.data,
|
||||
compatibilityError: provider.managedSettingsCompatibilityError,
|
||||
}, {
|
||||
data: { managedSettings: undefined },
|
||||
compatibilityError: { errorCode: 'client_update_required' },
|
||||
});
|
||||
});
|
||||
|
||||
test('failed startup fetch retains cached managed settings when no rejection is known', async () => {
|
||||
const requestService = new TestRequestService(async () => {
|
||||
throw new Error('managed settings unavailable');
|
||||
});
|
||||
const provider = await createProvider({ [COPILOT_FORCE_REMOTE_SETTINGS_REFRESH_KEY]: true }, requestService);
|
||||
const provider = await createProvider(requestService);
|
||||
const cachedPolicy = createCachedPolicy(false);
|
||||
|
||||
const result = await provider['getManagedSettings'](sessions, cachedPolicy);
|
||||
@@ -98,7 +147,63 @@ suite('DefaultAccountProvider managed settings', () => {
|
||||
});
|
||||
});
|
||||
|
||||
async function createProvider(nativeManagedSettings: Record<string, boolean>, requestService: TestRequestService): Promise<DefaultAccountProvider> {
|
||||
test('transient failure does not clear an update-required state', async () => {
|
||||
let requestCount = 0;
|
||||
const requestService = new TestRequestService(async () => {
|
||||
requestCount++;
|
||||
if (requestCount === 1) {
|
||||
return jsonResponse({ error_code: 'client_update_required', client_id: 'vscode' }, 466);
|
||||
}
|
||||
throw new Error('managed settings unavailable');
|
||||
});
|
||||
const provider = await createProvider(requestService);
|
||||
const cachedPolicy = createCachedPolicy(false);
|
||||
|
||||
await provider['getManagedSettings'](sessions, cachedPolicy);
|
||||
const result = await provider['getManagedSettings'](sessions, cachedPolicy, { forceRefresh: true });
|
||||
|
||||
assert.deepStrictEqual({
|
||||
status: provider.managedSettingsFetchStatus,
|
||||
data: result.data,
|
||||
compatibilityError: provider.managedSettingsCompatibilityError,
|
||||
}, {
|
||||
status: 'no-response',
|
||||
data: { managedSettings: undefined },
|
||||
compatibilityError: { errorCode: 'client_update_required' },
|
||||
});
|
||||
});
|
||||
|
||||
test('successful full refresh clears a prior update-required state', async () => {
|
||||
const requestService = new TestRequestService(async options => {
|
||||
if (options.url?.endsWith('/copilot_internal/user')) {
|
||||
return jsonResponse({ chat_enabled: true });
|
||||
}
|
||||
if (options.url?.endsWith('/copilot_internal/managed_settings')) {
|
||||
return jsonResponse({});
|
||||
}
|
||||
throw new Error(`Unexpected request: ${options.url}`);
|
||||
});
|
||||
const provider = await createProvider(requestService);
|
||||
provider['setManagedSettingsCompatibilityError']({ errorCode: 'client_update_required' });
|
||||
|
||||
const account = await provider['getDefaultAccountFromAuthenticatedSessions'](
|
||||
{ id: 'github', name: 'GitHub', enterprise: false },
|
||||
sessions,
|
||||
{ forceRefresh: true }
|
||||
);
|
||||
assert.ok(account);
|
||||
provider['setDefaultAccount'](account);
|
||||
|
||||
assert.deepStrictEqual({
|
||||
compatibilityError: provider.managedSettingsCompatibilityError,
|
||||
managedSettings: provider.policyData?.managedSettings,
|
||||
}, {
|
||||
compatibilityError: null,
|
||||
managedSettings: {},
|
||||
});
|
||||
});
|
||||
|
||||
async function createProvider(requestService: TestRequestService): Promise<DefaultAccountProvider> {
|
||||
const instantiationService = disposables.add(new TestInstantiationService());
|
||||
instantiationService.stub(IConfigurationService, new TestConfigurationService());
|
||||
instantiationService.stub(IAuthenticationService, {
|
||||
@@ -123,6 +228,11 @@ suite('DefaultAccountProvider managed settings', () => {
|
||||
remoteAuthority: undefined,
|
||||
isSessionsWindow: false,
|
||||
});
|
||||
instantiationService.stub(IProductService, {
|
||||
...TestProductService,
|
||||
version: '1.132.0',
|
||||
copilotVersions: { runtime: '0.0.344', sdk: '0.1.0' },
|
||||
});
|
||||
instantiationService.stub(IContextKeyService, new MockContextKeyService());
|
||||
instantiationService.stub(IStorageService, disposables.add(new InMemoryStorageService()));
|
||||
instantiationService.stub(IHostService, {
|
||||
@@ -130,11 +240,6 @@ suite('DefaultAccountProvider managed settings', () => {
|
||||
onDidChangeFocus: Event.None,
|
||||
});
|
||||
instantiationService.stub(ICommandService, {});
|
||||
instantiationService.stub(INativeManagedSettingsService, {
|
||||
managedSettings: nativeManagedSettings,
|
||||
initialize: async () => nativeManagedSettings,
|
||||
onDidChangeManagedSettings: Event.None,
|
||||
});
|
||||
|
||||
const provider = disposables.add(instantiationService.createInstance(DefaultAccountProvider, {
|
||||
preferredExtensions: [],
|
||||
@@ -146,7 +251,7 @@ suite('DefaultAccountProvider managed settings', () => {
|
||||
scopes: [['user:email']],
|
||||
},
|
||||
tokenEntitlementUrl: '',
|
||||
entitlementUrl: '',
|
||||
entitlementUrl: 'https://api.github.com/copilot_internal/user',
|
||||
mcpRegistryDataUrl: '',
|
||||
managedSettingsUrl: 'https://api.github.com/copilot_internal/managed_settings',
|
||||
}));
|
||||
@@ -172,12 +277,14 @@ class TestRequestService implements IRequestService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly onDidCompleteRequest = Event.None;
|
||||
requestCount = 0;
|
||||
readonly requests: IRequestOptions[] = [];
|
||||
|
||||
constructor(private readonly requestHandler: () => Promise<IRequestContext>) { }
|
||||
constructor(private readonly requestHandler: (options: IRequestOptions) => Promise<IRequestContext>) { }
|
||||
|
||||
request(): Promise<IRequestContext> {
|
||||
request(options: IRequestOptions): Promise<IRequestContext> {
|
||||
this.requestCount++;
|
||||
return this.requestHandler();
|
||||
this.requests.push(options);
|
||||
return this.requestHandler(options);
|
||||
}
|
||||
|
||||
async resolveProxy(): Promise<string | undefined> {
|
||||
@@ -197,9 +304,9 @@ class TestRequestService implements IRequestService {
|
||||
}
|
||||
}
|
||||
|
||||
function jsonResponse(data: IManagedSettingsResponse): IRequestContext {
|
||||
function jsonResponse(data: unknown, statusCode = 200): IRequestContext {
|
||||
return {
|
||||
res: { statusCode: 200, headers: {} },
|
||||
res: { statusCode, headers: {} },
|
||||
stream: bufferToStream(VSBuffer.fromString(JSON.stringify(data))),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
|
||||
import assert from 'assert';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
|
||||
import { adaptManagedSettings, IManagedSettingsResponse } from '../../browser/managedSettings.js';
|
||||
import { adaptManagedSettings, getManagedSettingsClientHeaders, IManagedSettingsResponse, parseManagedSettingsCompatibilityError } from '../../browser/managedSettings.js';
|
||||
|
||||
suite('adaptManagedSettings', () => {
|
||||
|
||||
@@ -17,6 +17,24 @@ suite('adaptManagedSettings', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('builds available client identity headers', () => {
|
||||
assert.deepStrictEqual({
|
||||
withRuntime: getManagedSettingsClientHeaders({
|
||||
version: '1.132.0',
|
||||
copilotVersions: { runtime: '0.0.344', sdk: '0.1.0' },
|
||||
}),
|
||||
withoutRuntime: getManagedSettingsClientHeaders({ version: '1.132.0' }),
|
||||
}, {
|
||||
withRuntime: {
|
||||
'Editor-Version': 'vscode/1.132.0',
|
||||
'Copilot-Runtime-Version': 'copilot-runtime/0.0.344',
|
||||
},
|
||||
withoutRuntime: {
|
||||
'Editor-Version': 'vscode/1.132.0',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('normalizes permissions into a dot-path managed setting', () => {
|
||||
assert.deepStrictEqual(adaptManagedSettings({
|
||||
permissions: { disableBypassPermissionsMode: 'disable' },
|
||||
@@ -27,6 +45,23 @@ suite('adaptManagedSettings', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('parses the stable compatibility error and optional versions', () => {
|
||||
assert.deepStrictEqual(parseManagedSettingsCompatibilityError({
|
||||
error_code: 'client_update_required',
|
||||
client_id: 'vscode',
|
||||
client_version: '1.132.0',
|
||||
minimum_client_version: '1.133.0',
|
||||
}), {
|
||||
errorCode: 'client_update_required',
|
||||
clientVersion: '1.132.0',
|
||||
minimumClientVersion: '1.133.0',
|
||||
});
|
||||
});
|
||||
|
||||
test('rejects an unrecognized compatibility error shape', () => {
|
||||
assert.strictEqual(parseManagedSettingsCompatibilityError({ error_code: 'unexpected' }), undefined);
|
||||
});
|
||||
|
||||
test('carries enabledPlugins as a canonical JSON string under a single key', () => {
|
||||
const response: IManagedSettingsResponse = {
|
||||
enabledPlugins: {
|
||||
|
||||
@@ -39,6 +39,8 @@ class DefaultAccountProvider implements IDefaultAccountProvider {
|
||||
readonly managedSettingsFetchStatus: null = null;
|
||||
readonly managedSettingsFetchedAt: null = null;
|
||||
readonly managedSettingsRawResponse: unknown = null;
|
||||
readonly managedSettingsCompatibilityError = null;
|
||||
readonly onDidChangeManagedSettingsCompatibilityError = Event.None;
|
||||
|
||||
constructor(
|
||||
readonly defaultAccount: IDefaultAccount,
|
||||
|
||||
@@ -45,6 +45,8 @@ class DefaultAccountProvider implements IDefaultAccountProvider {
|
||||
readonly managedSettingsFetchStatus: null = null;
|
||||
readonly managedSettingsFetchedAt: null = null;
|
||||
readonly managedSettingsRawResponse: unknown = null;
|
||||
readonly managedSettingsCompatibilityError = null;
|
||||
readonly onDidChangeManagedSettingsCompatibilityError = Event.None;
|
||||
|
||||
constructor(
|
||||
readonly defaultAccount: IDefaultAccount,
|
||||
|
||||
@@ -592,6 +592,8 @@ export function createEditorServices(disposables: DisposableStore, options?: Cre
|
||||
managedSettingsFetchStatus: null,
|
||||
managedSettingsFetchedAt: null,
|
||||
managedSettingsRawResponse: null,
|
||||
managedSettingsCompatibilityError: null,
|
||||
onDidChangeManagedSettingsCompatibilityError: Event.None,
|
||||
getDefaultAccount: async () => null,
|
||||
getDefaultAccountAuthenticationProvider: () => ({ id: 'test', name: 'Test', scopes: [], enterprise: false }),
|
||||
resolveGitHubUrl: (path: string) => `https://github.com/${path}`,
|
||||
|
||||
Reference in New Issue
Block a user