From 8791650e81f612e38cc6c9479757f1ba0f5c47fb Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Tue, 26 Aug 2025 15:38:22 +0200 Subject: [PATCH] allow policy config to compute the policy value for account based policies (#263410) --- src/vs/base/common/defaultAccount.ts | 26 ++++++ src/vs/base/common/policy.ts | 25 ++---- .../configuration/common/configurations.ts | 5 +- src/vs/platform/policy/common/policy.ts | 8 +- .../contrib/chat/browser/chat.contribution.ts | 8 +- .../accounts/common/defaultAccount.ts | 23 +----- .../extensionGalleryManifestService.ts | 3 +- .../policies/common/accountPolicyService.ts | 80 +++++-------------- .../test/common/accountPolicyService.test.ts | 13 ++- .../common/multiplexPolicyService.test.ts | 13 ++- 10 files changed, 75 insertions(+), 129 deletions(-) create mode 100644 src/vs/base/common/defaultAccount.ts diff --git a/src/vs/base/common/defaultAccount.ts b/src/vs/base/common/defaultAccount.ts new file mode 100644 index 00000000000..8b10d42b6ea --- /dev/null +++ b/src/vs/base/common/defaultAccount.ts @@ -0,0 +1,26 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface IDefaultAccount { + readonly sessionId: string; + readonly enterprise: boolean; + readonly access_type_sku?: string; + readonly assigned_date?: string; + readonly can_signup_for_limited?: boolean; + readonly chat_enabled?: boolean; + readonly chat_preview_features_enabled?: boolean; + readonly mcp?: boolean; + readonly analytics_tracking_id?: string; + readonly limited_user_quotas?: { + readonly chat: number; + readonly completions: number; + }; + readonly monthly_quotas?: { + readonly chat: number; + readonly completions: number; + }; + readonly limited_user_reset_date?: string; + readonly chat_agent_enabled?: boolean; +} diff --git a/src/vs/base/common/policy.ts b/src/vs/base/common/policy.ts index b255d28d7b0..1e97392d5e2 100644 --- a/src/vs/base/common/policy.ts +++ b/src/vs/base/common/policy.ts @@ -3,14 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -export type PolicyName = string; +import { IDefaultAccount } from './defaultAccount.js'; -export enum PolicyTag { - Account = 'ACCOUNT', - MCP = 'MCP', - Preview = 'PREVIEW', - Agent = 'AGENT', -} +export type PolicyName = string; export interface IPolicy { @@ -36,18 +31,10 @@ export interface IPolicy { * this value determines what value the feature receives. * * For example: - * - If `defaultValue: true`, the feature's setting is locked to `true` WHEN the policy is in effect. - * - If `defaultValue: 'foo'`, the feature's setting is locked to 'foo' WHEN the policy is in effect. + * - If evaluated value is `true`, the feature's setting is locked to `true` WHEN the policy is in effect. + * - If evaluated value is `foo`, the feature's setting is locked to 'foo' WHEN the policy is in effect. * - * If omitted, 'false' is the assumed value. - * - * Note: This is unrelated to the default value of the VS Code setting itself. This specifically controls - * the value of an account-based feature's setting WHEN the policy is overriding it. + * If `undefined`, the feature's setting is not locked and can be overridden by other means. */ - readonly defaultValue?: string | number | boolean; - - /** - * Tags for categorizing policies - */ - readonly tags?: PolicyTag[]; + readonly value?: (account: IDefaultAccount) => string | number | boolean | undefined; } diff --git a/src/vs/platform/configuration/common/configurations.ts b/src/vs/platform/configuration/common/configurations.ts index 43fec1665b6..86917d69484 100644 --- a/src/vs/platform/configuration/common/configurations.ts +++ b/src/vs/platform/configuration/common/configurations.ts @@ -137,12 +137,11 @@ export class PolicyConfiguration extends Disposable implements IPolicyConfigurat this.logService.warn(`Policy ${config.policy.name} has unsupported type ${config.type}`); continue; } - const { defaultValue, tags } = config.policy; + const { value } = config.policy; keys.push(key); policyDefinitions[config.policy.name] = { type: config.type === 'number' ? 'number' : config.type === 'boolean' ? 'boolean' : 'string', - tags, - defaultValue, + value, }; } } diff --git a/src/vs/platform/policy/common/policy.ts b/src/vs/platform/policy/common/policy.ts index 29b31a62484..a02d49e28d9 100644 --- a/src/vs/platform/policy/common/policy.ts +++ b/src/vs/platform/policy/common/policy.ts @@ -4,14 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { IStringDictionary } from '../../../base/common/collections.js'; +import { IDefaultAccount } from '../../../base/common/defaultAccount.js'; import { Emitter, Event } from '../../../base/common/event.js'; import { Iterable } from '../../../base/common/iterator.js'; import { Disposable } from '../../../base/common/lifecycle.js'; -import { PolicyName, PolicyTag } from '../../../base/common/policy.js'; +import { PolicyName } from '../../../base/common/policy.js'; import { createDecorator } from '../../instantiation/common/instantiation.js'; export type PolicyValue = string | number | boolean; -export type PolicyDefinition = { type: 'string' | 'number' | 'boolean'; defaultValue?: string | number | boolean; tags?: PolicyTag[] }; +export type PolicyDefinition = { + type: 'string' | 'number' | 'boolean'; + value?: (account: IDefaultAccount) => string | number | boolean | undefined; +}; export const IPolicyService = createDecorator('policy'); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index bbae6822aff..95302621f9d 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -9,7 +9,6 @@ import { MarkdownString, isMarkdownString } from '../../../../base/common/htmlCo import { Disposable } from '../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../base/common/network.js'; import { isMacintosh } from '../../../../base/common/platform.js'; -import { PolicyTag } from '../../../../base/common/policy.js'; import { assertDefined } from '../../../../base/common/types.js'; import { registerEditorFeature } from '../../../../editor/common/editorFeatures.js'; import * as nls from '../../../../nls.js'; @@ -245,8 +244,7 @@ configurationRegistry.registerConfiguration({ policy: { name: 'ChatToolsAutoApprove', minimumVersion: '1.99', - defaultValue: false, - tags: [PolicyTag.Account, PolicyTag.Preview] + value: (account) => account.chat_preview_features_enabled === false ? false : undefined, } }, [ChatConfiguration.AutoApproveEdits]: { @@ -316,7 +314,7 @@ configurationRegistry.registerConfiguration({ policy: { name: 'ChatMCP', minimumVersion: '1.99', - tags: [PolicyTag.Account, PolicyTag.MCP] + value: (account) => account.mcp === false ? false : undefined, } }, [mcpAutoStartConfig]: { @@ -399,7 +397,7 @@ configurationRegistry.registerConfiguration({ policy: { name: 'ChatAgentMode', minimumVersion: '1.99', - tags: [PolicyTag.Account, PolicyTag.Agent] + value: (account) => account.chat_agent_enabled === false ? false : undefined, } }, [ChatConfiguration.EnableMath]: { diff --git a/src/vs/workbench/services/accounts/common/defaultAccount.ts b/src/vs/workbench/services/accounts/common/defaultAccount.ts index 713a7121d04..807ad7ad56f 100644 --- a/src/vs/workbench/services/accounts/common/defaultAccount.ts +++ b/src/vs/workbench/services/accounts/common/defaultAccount.ts @@ -19,6 +19,7 @@ import { IWorkbenchContribution } from '../../../common/contributions.js'; import { Barrier } from '../../../../base/common/async.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { getErrorMessage } from '../../../../base/common/errors.js'; +import { IDefaultAccount } from '../../../../base/common/defaultAccount.js'; export const DEFAULT_ACCOUNT_SIGN_IN_COMMAND = 'workbench.actions.accounts.signIn'; @@ -30,28 +31,6 @@ const enum DefaultAccountStatus { const CONTEXT_DEFAULT_ACCOUNT_STATE = new RawContextKey('defaultAccountStatus', DefaultAccountStatus.Uninitialized); -export interface IDefaultAccount { - readonly sessionId: string; - readonly enterprise: boolean; - readonly access_type_sku?: string; - readonly assigned_date?: string; - readonly can_signup_for_limited?: boolean; - readonly chat_enabled?: boolean; - readonly chat_preview_features_enabled?: boolean; - readonly mcp?: boolean; - readonly analytics_tracking_id?: string; - readonly limited_user_quotas?: { - readonly chat: number; - readonly completions: number; - }; - readonly monthly_quotas?: { - readonly chat: number; - readonly completions: number; - }; - readonly limited_user_reset_date?: string; - readonly chat_agent_enabled?: boolean; -} - interface IChatEntitlementsResponse { readonly access_type_sku: string; readonly assigned_date: string; diff --git a/src/vs/workbench/services/extensionManagement/electron-browser/extensionGalleryManifestService.ts b/src/vs/workbench/services/extensionManagement/electron-browser/extensionGalleryManifestService.ts index dd03183f726..ade3bd512f5 100644 --- a/src/vs/workbench/services/extensionManagement/electron-browser/extensionGalleryManifestService.ts +++ b/src/vs/workbench/services/extensionManagement/electron-browser/extensionGalleryManifestService.ts @@ -20,10 +20,11 @@ import { IProductService } from '../../../../platform/product/common/productServ import { asJson, IRequestService } from '../../../../platform/request/common/request.js'; import { IStorageService } from '../../../../platform/storage/common/storage.js'; import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IDefaultAccount, IDefaultAccountService } from '../../accounts/common/defaultAccount.js'; +import { IDefaultAccountService } from '../../accounts/common/defaultAccount.js'; import { IRemoteAgentService } from '../../remote/common/remoteAgentService.js'; import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; import { IHostService } from '../../host/browser/host.js'; +import { IDefaultAccount } from '../../../../base/common/defaultAccount.js'; export class WorkbenchExtensionGalleryManifestService extends ExtensionGalleryManifestService implements IExtensionGalleryManifestService { diff --git a/src/vs/workbench/services/policies/common/accountPolicyService.ts b/src/vs/workbench/services/policies/common/accountPolicyService.ts index 8ca4b6cabe7..d81da4dffe1 100644 --- a/src/vs/workbench/services/policies/common/accountPolicyService.ts +++ b/src/vs/workbench/services/policies/common/accountPolicyService.ts @@ -4,24 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { IStringDictionary } from '../../../../base/common/collections.js'; -import { equals } from '../../../../base/common/objects.js'; -import { PolicyTag } from '../../../../base/common/policy.js'; +import { IDefaultAccount } from '../../../../base/common/defaultAccount.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { AbstractPolicyService, IPolicyService, PolicyDefinition } from '../../../../platform/policy/common/policy.js'; import { IDefaultAccountService } from '../../accounts/common/defaultAccount.js'; -interface IAccountPolicy { - readonly chatPreviewFeaturesEnabled: boolean; - readonly mcpEnabled: boolean; - readonly chatAgentEnabled: boolean; -} export class AccountPolicyService extends AbstractPolicyService implements IPolicyService { - private accountPolicy: IAccountPolicy = { - chatPreviewFeaturesEnabled: true, - mcpEnabled: true, - chatAgentEnabled: true - }; + + private account: IDefaultAccount | null = null; + constructor( @ILogService private readonly logService: ILogService, @IDefaultAccountService private readonly defaultAccountService: IDefaultAccountService @@ -30,66 +22,32 @@ export class AccountPolicyService extends AbstractPolicyService implements IPoli this.defaultAccountService.getDefaultAccount() .then(account => { - this._update({ - chatPreviewFeaturesEnabled: account?.chat_preview_features_enabled ?? true, - mcpEnabled: account?.mcp ?? true, - chatAgentEnabled: account?.chat_agent_enabled ?? true - }); - this._register(this.defaultAccountService.onDidChangeDefaultAccount( - account => this._update({ - chatPreviewFeaturesEnabled: account?.chat_preview_features_enabled ?? true, - mcpEnabled: account?.mcp ?? true, - chatAgentEnabled: account?.chat_agent_enabled ?? true - }) - )); + this.account = account; + this._updatePolicyDefinitions(this.policyDefinitions); + this._register(this.defaultAccountService.onDidChangeDefaultAccount(account => { + this.account = account; + this._updatePolicyDefinitions(this.policyDefinitions); + })); }); } - private _update(updatedPolicy: IAccountPolicy): void { - if (!equals(this.accountPolicy, updatedPolicy)) { - this.accountPolicy = updatedPolicy; - this._updatePolicyDefinitions(this.policyDefinitions); - } - } - protected async _updatePolicyDefinitions(policyDefinitions: IStringDictionary): Promise { this.logService.trace(`AccountPolicyService#_updatePolicyDefinitions: Got ${Object.keys(policyDefinitions).length} policy definitions`); const updated: string[] = []; - const updateIfNeeded = (key: string, policy: PolicyDefinition, isFeatureEnabled: boolean): void => { - if (isFeatureEnabled) { - // Clear the policy if it is set + for (const key in policyDefinitions) { + const policy = policyDefinitions[key]; + const policyValue = this.account && policy.value ? policy.value(this.account) : undefined; + if (policyValue !== undefined) { + if (this.policies.get(key) !== policyValue) { + this.policies.set(key, policyValue); + updated.push(key); + } + } else { if (this.policies.has(key)) { this.policies.delete(key); updated.push(key); } - } else { - // Enforce the defaultValue if not already set - const updatedValue = policy.defaultValue === undefined ? false : policy.defaultValue; - if (this.policies.get(key) !== updatedValue) { - this.policies.set(key, updatedValue); - updated.push(key); - } - } - }; - - const hasAllTags = (policy: PolicyDefinition, tags: PolicyTag[]): boolean | undefined => { - return policy.tags && tags.every(tag => policy.tags!.includes(tag)); - }; - - for (const key in policyDefinitions) { - const policy = policyDefinitions[key]; - - // Map chat preview features with ACCOUNT + PREVIEW tags - if (hasAllTags(policy, [PolicyTag.Account, PolicyTag.Preview])) { - updateIfNeeded(key, policy, this.accountPolicy?.chatPreviewFeaturesEnabled); - } - // Map MCP feature with MCP tag - else if (hasAllTags(policy, [PolicyTag.Account, PolicyTag.MCP])) { - updateIfNeeded(key, policy, this.accountPolicy?.mcpEnabled); - } - else if (hasAllTags(policy, [PolicyTag.Account, PolicyTag.Agent])) { - updateIfNeeded(key, policy, this.accountPolicy?.chatAgentEnabled); } } diff --git a/src/vs/workbench/services/policies/test/common/accountPolicyService.test.ts b/src/vs/workbench/services/policies/test/common/accountPolicyService.test.ts index 4fc335e37fb..1e7621472f5 100644 --- a/src/vs/workbench/services/policies/test/common/accountPolicyService.test.ts +++ b/src/vs/workbench/services/policies/test/common/accountPolicyService.test.ts @@ -4,14 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { PolicyTag } from '../../../../../base/common/policy.js'; import { NullLogService } from '../../../../../platform/log/common/log.js'; -import { DefaultAccountService, IDefaultAccount, IDefaultAccountService } from '../../../accounts/common/defaultAccount.js'; +import { DefaultAccountService, IDefaultAccountService } from '../../../accounts/common/defaultAccount.js'; import { AccountPolicyService } from '../../common/accountPolicyService.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; import { Extensions, IConfigurationNode, IConfigurationRegistry } from '../../../../../platform/configuration/common/configurationRegistry.js'; import { DefaultConfiguration, PolicyConfiguration } from '../../../../../platform/configuration/common/configurations.js'; +import { IDefaultAccount } from '../../../../../base/common/defaultAccount.js'; const BASE_DEFAULT_ACCOUNT: IDefaultAccount = { enterprise: false, @@ -47,8 +47,7 @@ suite('AccountPolicyService', () => { policy: { name: 'PolicySettingB', minimumVersion: '1.0.0', - defaultValue: "policyValueB", - tags: [PolicyTag.Account, PolicyTag.Preview] + value: account => account.chat_preview_features_enabled === false ? 'policyValueB' : undefined, } }, 'setting.C': { @@ -57,8 +56,7 @@ suite('AccountPolicyService', () => { policy: { name: 'PolicySettingC', minimumVersion: '1.0.0', - defaultValue: JSON.stringify(['policyValueC1', 'policyValueC2']), - tags: [PolicyTag.Account, PolicyTag.Preview] + value: account => account.chat_preview_features_enabled === false ? JSON.stringify(['policyValueC1', 'policyValueC2']) : undefined, } }, 'setting.D': { @@ -67,8 +65,7 @@ suite('AccountPolicyService', () => { policy: { name: 'PolicySettingD', minimumVersion: '1.0.0', - defaultValue: false, - tags: [PolicyTag.Account, PolicyTag.Preview] + value: account => account.chat_preview_features_enabled === false ? false : undefined, } }, 'setting.E': { diff --git a/src/vs/workbench/services/policies/test/common/multiplexPolicyService.test.ts b/src/vs/workbench/services/policies/test/common/multiplexPolicyService.test.ts index fb739caf76f..4484b9d01b2 100644 --- a/src/vs/workbench/services/policies/test/common/multiplexPolicyService.test.ts +++ b/src/vs/workbench/services/policies/test/common/multiplexPolicyService.test.ts @@ -4,9 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import assert from 'assert'; -import { PolicyTag } from '../../../../../base/common/policy.js'; import { NullLogService } from '../../../../../platform/log/common/log.js'; -import { DefaultAccountService, IDefaultAccount, IDefaultAccountService } from '../../../accounts/common/defaultAccount.js'; +import { DefaultAccountService, IDefaultAccountService } from '../../../accounts/common/defaultAccount.js'; import { AccountPolicyService } from '../../common/accountPolicyService.js'; import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; import { Registry } from '../../../../../platform/registry/common/platform.js'; @@ -19,6 +18,7 @@ import { IFileService } from '../../../../../platform/files/common/files.js'; import { InMemoryFileSystemProvider } from '../../../../../platform/files/common/inMemoryFilesystemProvider.js'; import { FileService } from '../../../../../platform/files/common/fileService.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; +import { IDefaultAccount } from '../../../../../base/common/defaultAccount.js'; const BASE_DEFAULT_ACCOUNT: IDefaultAccount = { enterprise: false, @@ -56,8 +56,7 @@ suite('MultiplexPolicyService', () => { policy: { name: 'PolicySettingB', minimumVersion: '1.0.0', - defaultValue: "policyValueB", - tags: [PolicyTag.Account, PolicyTag.Preview] + value: account => account.chat_preview_features_enabled === false ? 'policyValueB' : undefined, } }, 'setting.C': { @@ -66,8 +65,7 @@ suite('MultiplexPolicyService', () => { policy: { name: 'PolicySettingC', minimumVersion: '1.0.0', - defaultValue: JSON.stringify(['policyValueC1', 'policyValueC2']), - tags: [PolicyTag.Account, PolicyTag.Preview] + value: account => account.chat_preview_features_enabled === false ? JSON.stringify(['policyValueC1', 'policyValueC2']) : undefined, } }, 'setting.D': { @@ -76,8 +74,7 @@ suite('MultiplexPolicyService', () => { policy: { name: 'PolicySettingD', minimumVersion: '1.0.0', - defaultValue: false, - tags: [PolicyTag.Account, PolicyTag.Preview] + value: account => account.chat_preview_features_enabled === false ? false : undefined, } }, 'setting.E': {