Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions src/vs/platform/agentHost/browser/agentHostProtocolClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { ITelemetryService, TelemetryLevel, TELEMETRY_CRASH_REPORTER_SETTING_ID,
import { getTelemetryLevel } from '../../telemetry/common/telemetryUtils.js';
import { AgentHostAutoApprovePolicyRestrictedConfigKey, AgentHostTelemetryLevelConfigKey, AgentHostTerminalAutoApproveEnabledConfigKey, AgentHostTerminalAutoApproveRulesConfigKey, AgentHostDisableRepoInfoTelemetryConfigKey, AgentHostWorkspaceTrustConfigKey, getAgentHostTerminalAutoApproveRulesConfig, GLOBAL_AUTO_APPROVE_SETTING_ID, TERMINAL_AUTO_APPROVE_ENABLED_SETTING_ID, TERMINAL_AUTO_APPROVE_SETTING_ID, TERMINAL_IGNORE_DEFAULT_AUTO_APPROVE_RULES_SETTING_ID, DISABLE_REPO_INFO_TELEMETRY_SETTING_ID, telemetryLevelToAgentHostConfigValue } from '../common/agentHostSchema.js';
import { formatAgentHostConfigurationSyncValueForLog, getAgentHostConfigurationSyncEntries, getAgentHostConfigurationSyncTarget, resolveAgentHostConfigurationSyncPatch, resolveAgentHostConfigurationSyncValue } from '../common/agentHostConfigurationSync.js';
import { managedPermissionsConfigurationIds, resolveManagedSettingsPermissions, type IAgentHostManagedSettingsPermissions } from '../common/agentHostManagedSettings.js';
import { AgentHostManagedPluginsSettingId, managedPermissionsConfigurationIds, resolveManagedPluginEnablement, resolveManagedSettingsPermissions, type IAgentHostManagedPluginEnablement, type IAgentHostManagedSettingsPermissions } from '../common/agentHostManagedSettings.js';
import { AgentHostClientConnectionKind, toAgentHostClientMeta } from '../common/agentHostTelemetry.js';
import type { OtlpExportLogsParams } from '../common/state/protocol/channels-otlp/notifications.js';
import type { TelemetryCapabilities } from '../common/state/protocol/channels-otlp/state.js';
Expand Down Expand Up @@ -117,7 +117,7 @@ function isConnectionClosedError(error: unknown): boolean {
}

interface IRemoteAgentHostExtensionNotificationMap {
'setClientManagedSettingsPermissions': { params: { permissions: IAgentHostManagedSettingsPermissions } };
'setClientManagedSettingsPermissions': { params: { permissions: IAgentHostManagedSettingsPermissions; enabledPlugins: IAgentHostManagedPluginEnablement } };
}

interface IPendingRequest {
Expand Down Expand Up @@ -522,7 +522,7 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
if (e.affectsConfiguration(DISABLE_REPO_INFO_TELEMETRY_SETTING_ID)) {
this._updateDisableRepoInfoTelemetry();
}
if (managedPermissionsConfigurationIds.some(settingId => e.affectsConfiguration(settingId))) {
if (managedPermissionsConfigurationIds.some(settingId => e.affectsConfiguration(settingId)) || e.affectsConfiguration(AgentHostManagedPluginsSettingId)) {
void this._updateManagedSettingsPermissions();
}
}));
Expand Down Expand Up @@ -2347,10 +2347,10 @@ export class AgentHostProtocolClient extends Disposable implements IAgentConnect
}

private _updateManagedSettingsPermissions(sendDuringReconnect = false): void {
const permissions = this._resourceIdentity === LOCAL_AGENT_HOST_RESOURCE_IDENTITY
? resolveManagedSettingsPermissions(this._configurationService)
: {};
this._sendExtensionNotification('setClientManagedSettingsPermissions', { permissions }, sendDuringReconnect);
const local = this._resourceIdentity === LOCAL_AGENT_HOST_RESOURCE_IDENTITY;
const permissions = local ? resolveManagedSettingsPermissions(this._configurationService) : {};
const enabledPlugins = local ? resolveManagedPluginEnablement(this._configurationService) : {};
this._sendExtensionNotification('setClientManagedSettingsPermissions', { permissions, enabledPlugins }, sendDuringReconnect);
}

/**
Expand Down
20 changes: 20 additions & 0 deletions src/vs/platform/agentHost/common/agentHostManagedSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,15 @@ export interface IAgentHostManagedSettingsPermissions {
ask?: string[];
}

export type IAgentHostManagedPluginEnablement = Readonly<Record<string, boolean>>;

export interface IAgentHostManagedSettingsContribution {
readonly permissions: IAgentHostManagedSettingsPermissions;
readonly enabledPlugins: IAgentHostManagedPluginEnablement;
}

export const AgentHostMapLegacySettingsToManagedSettingsSettingId = 'chat.agentHost.copilot.mapLegacySettingsToManagedSettings';
export const AgentHostManagedPluginsSettingId = 'chat.plugins.enabledPlugins';

/**
* Which configuration layers may drive a mapping.
Expand Down Expand Up @@ -293,6 +301,13 @@ export function isManagedSettingsPermissions(value: unknown): value is IAgentHos
&& isStringArrayOrUndefined(permissions.ask);
}

export function isManagedPluginEnablement(value: unknown): value is IAgentHostManagedPluginEnablement {
return typeof value === 'object'
&& value !== null
&& !Array.isArray(value)
&& Object.values(value).every(enabled => typeof enabled === 'boolean');
}

function isStringArrayOrUndefined(value: unknown): boolean {
return value === undefined || (Array.isArray(value) && value.every(item => typeof item === 'string'));
}
Expand Down Expand Up @@ -339,3 +354,8 @@ export function resolveManagedSettingsPermissions(configurationService: IConfigu
}
return permissions;
}

export function resolveManagedPluginEnablement(configurationService: IConfigurationService): IAgentHostManagedPluginEnablement {
const value = configurationService.inspect<Readonly<Record<string, boolean>>>(AgentHostManagedPluginsSettingId)?.policyValue;
return isManagedPluginEnablement(value) ? value : {};
}
25 changes: 25 additions & 0 deletions src/vs/platform/agentHost/common/meta/clientPluginIdentityMeta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import type { ClientPluginCustomization } from '../state/sessionState.js';

const clientPluginIdentityKey = 'vscode.clientPluginIdentity';

interface IClientPluginIdentity {
readonly pluginId: string;
}

export function toClientPluginIdentityMeta(pluginId: string): Record<string, unknown> {
return { [clientPluginIdentityKey]: { pluginId } satisfies IClientPluginIdentity };
}

export function readClientPluginIdentity(customization: Pick<ClientPluginCustomization, '_meta'>): string | undefined {
const value = customization._meta?.[clientPluginIdentityKey];
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return undefined;
}
const pluginId = (value as Record<string, unknown>).pluginId;
return typeof pluginId === 'string' && pluginId.length > 0 ? pluginId : undefined;
}
53 changes: 33 additions & 20 deletions src/vs/platform/agentHost/node/agentHostManagedSettingsService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@ import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { equals } from '../../../base/common/objects.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import type { IAgentHostManagedSettingsPermissions } from '../common/agentHostManagedSettings.js';
import type { IAgentHostManagedPluginEnablement, IAgentHostManagedSettingsContribution, IAgentHostManagedSettingsPermissions } from '../common/agentHostManagedSettings.js';

export const IAgentHostManagedSettingsService = createDecorator<IAgentHostManagedSettingsService>('agentHostManagedSettingsService');

export interface IAgentHostManagedSettingsService {
readonly _serviceBrand: undefined;
readonly onDidChange: Event<void>;
readonly permissions: IAgentHostManagedSettingsPermissions;
setClientPermissions(clientId: string, permissions: IAgentHostManagedSettingsPermissions): void;
removeClientPermissions(clientId: string): void;
readonly enabledPlugins: IAgentHostManagedPluginEnablement;
setClientContribution(clientId: string, contribution: IAgentHostManagedSettingsContribution): void;
removeClientContribution(clientId: string): void;
}

export class AgentHostManagedSettingsService extends Disposable implements IAgentHostManagedSettingsService {
Expand All @@ -25,41 +26,51 @@ export class AgentHostManagedSettingsService extends Disposable implements IAgen
private readonly _onDidChange = this._register(new Emitter<void>());
readonly onDidChange = this._onDidChange.event;

private readonly _permissionsByClient = new Map<string, IAgentHostManagedSettingsPermissions>();
private readonly _contributionsByClient = new Map<string, IAgentHostManagedSettingsContribution>();
private _permissions: IAgentHostManagedSettingsPermissions = {};
private _enabledPlugins: IAgentHostManagedPluginEnablement = {};

get permissions(): IAgentHostManagedSettingsPermissions {
return this._permissions;
}

setClientPermissions(clientId: string, permissions: IAgentHostManagedSettingsPermissions): void {
if (Object.keys(permissions).length === 0) {
this._permissionsByClient.delete(clientId);
get enabledPlugins(): IAgentHostManagedPluginEnablement {
return this._enabledPlugins;
}

setClientContribution(clientId: string, contribution: IAgentHostManagedSettingsContribution): void {
if (Object.keys(contribution.permissions).length === 0 && Object.keys(contribution.enabledPlugins).length === 0) {
this._contributionsByClient.delete(clientId);
} else {
this._permissionsByClient.set(clientId, permissions);
this._contributionsByClient.set(clientId, contribution);
}
this._updatePermissions();
this._update();
}

removeClientPermissions(clientId: string): void {
if (this._permissionsByClient.delete(clientId)) {
this._updatePermissions();
removeClientContribution(clientId: string): void {
if (this._contributionsByClient.delete(clientId)) {
this._update();
}
}

private _updatePermissions(): void {
private _update(): void {
const permissions: IAgentHostManagedSettingsPermissions = {};
const deny = new Set<string>();
const ask = new Set<string>();
for (const contribution of this._permissionsByClient.values()) {
if (contribution.disableBypassPermissionsMode === 'disable') {
const pluginValues = new Map<string, boolean>();
for (const contribution of this._contributionsByClient.values()) {
if (contribution.permissions.disableBypassPermissionsMode === 'disable') {
permissions.disableBypassPermissionsMode = 'disable';
}
if (contribution.deny) {
contribution.deny.forEach(rule => deny.add(rule));
if (contribution.permissions.deny) {
contribution.permissions.deny.forEach(rule => deny.add(rule));
}
if (contribution.permissions.ask) {
contribution.permissions.ask.forEach(rule => ask.add(rule));
}
if (contribution.ask) {
contribution.ask.forEach(rule => ask.add(rule));
for (const [pluginId, enabled] of Object.entries(contribution.enabledPlugins)) {
const previous = pluginValues.get(pluginId);
pluginValues.set(pluginId, previous === false ? false : enabled);
}
}
if (deny.size > 0) {
Expand All @@ -68,8 +79,10 @@ export class AgentHostManagedSettingsService extends Disposable implements IAgen
if (ask.size > 0) {
permissions.ask = [...ask];
}
if (!equals(this._permissions, permissions)) {
const enabledPlugins = Object.fromEntries(pluginValues);
if (!equals(this._permissions, permissions) || !equals(this._enabledPlugins, enabledPlugins)) {
this._permissions = permissions;
this._enabledPlugins = enabledPlugins;
this._onDidChange.fire();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CheckpointAndChangesetContribution } from './checkpointAndChangeset/che
import { ExternalSessionAdoptionContribution } from './externalSessionAdoption/externalSessionAdoptionContribution.js';
import { GitHubReferencesContribution } from './githubReferences/githubReferencesContribution.js';
import { LocalCommandContribution } from './localCommand/localCommandContribution.js';
import { ManagedPluginAdmissionContribution } from './managedPluginAdmission/managedPluginAdmissionContribution.js';
import { MarkdownPlanRichLinksContribution } from './markdownPlanRichLinks/markdownPlanRichLinksContribution.js';
import { MarkUnreadContribution } from './markUnread/markUnreadContribution.js';
import { PersistedTurnUsageContribution } from './persistedTurnUsage/persistedTurnUsageContribution.js';
Expand All @@ -36,6 +37,7 @@ export function registerBuiltInChatContributions(
): IDisposable {
const registrations = new DisposableStore();
registrations.add(contributions.registerContribution(LocalCommandContribution));
registrations.add(contributions.registerContribution(ManagedPluginAdmissionContribution));
registrations.add(contributions.registerContribution(ExternalSessionAdoptionContribution));
registrations.add(contributions.registerContribution(TurnAdmissionContribution));
registrations.add(contributions.registerContribution(ChatInputContribution));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Disposable } from '../../../../../base/common/lifecycle.js';
import { localize } from '../../../../../nls.js';
import { ILogService } from '../../../../log/common/log.js';
import { type IAgentHostChatContribution, type IAgentHostChatContributionContext, type IIncomingRequest, type IncomingRequestDisposition } from '../../../common/agentHostChatContributionsService.js';
import { isCustomizationEnabled } from '../../../common/customizationEnablement.js';
import { readClientPluginIdentity } from '../../../common/meta/clientPluginIdentityMeta.js';
import { CustomizationLoadStatus, CustomizationType } from '../../../common/state/protocol/state.js';
import { AgentHostManagedSettingsService, IAgentHostManagedSettingsService } from '../../agentHostManagedSettingsService.js';
import { AgentHostStateManager, IAgentHostStateManager } from '../../agentHostStateManager.js';

export class ManagedPluginAdmissionContribution extends Disposable implements IAgentHostChatContribution {

static readonly id = 'managedPluginAdmission';
readonly order = 75;

constructor(
protected readonly _context: IAgentHostChatContributionContext,
@IAgentHostManagedSettingsService private readonly _managedSettingsService: AgentHostManagedSettingsService,
@IAgentHostStateManager private readonly _stateManager: AgentHostStateManager,
@ILogService private readonly _logService: ILogService,
) {
super();
}

onIncomingRequest(request: IIncomingRequest): IncomingRequestDisposition | undefined {
const required = Object.entries(this._managedSettingsService.enabledPlugins)
.filter(([, enabled]) => enabled)
.map(([pluginId]) => pluginId);
Comment on lines +31 to +33
if (required.length === 0) {
return undefined;
}

const loaded = new Set<string>();
for (const customization of this._stateManager.getSessionState(request.session)?.customizations ?? []) {
if (customization.type !== CustomizationType.Plugin
|| customization.load?.kind !== CustomizationLoadStatus.Loaded
|| !isCustomizationEnabled(customization)) {
continue;
}
const pluginId = readClientPluginIdentity(customization);
if (pluginId) {
loaded.add(pluginId);
}
}

const unavailable = required.filter(pluginId => !loaded.has(pluginId));
if (unavailable.length === 0) {
return undefined;
}

this._logService.warn(`[ManagedPluginAdmissionContribution] Rejecting turn because required plugins are unavailable: ${unavailable.join(', ')}`);
return {
kind: 'reject',
error: {
errorType: 'managedPluginUnavailable',
message: unavailable.length === 1
? localize('managedPluginUnavailable', "The plugin \"{0}\" is required by your organization but is not available. Install or repair the plugin before continuing.", unavailable[0])
: localize('managedPluginsUnavailable', "The following plugins are required by your organization but are not available: {0}. Install or repair the plugins before continuing.", unavailable.join(', ')),
},
stage: 'validation',
};
}
}
17 changes: 10 additions & 7 deletions src/vs/platform/agentHost/node/protocolServerHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { AHPFileSystemProvider } from '../common/agentHostFileSystemProvider.js'
import { getAgentHostClientType } from '../common/agentHostClientInfo.js';
import { AgentHostClientConnectionKind, AgentHostLaunchKind, AgentHostTransportKind, readClientConnectionKind, readClientDevDeviceId, readClientMachineId, readClientTelemetryLevel, type IAgentHostClientTelemetryContext } from '../common/agentHostTelemetry.js';
import { AgentSession, type IAgentCreateChatRequestOptions, type IMcpNotification } from '../common/agent.js';
import { isManagedSettingsPermissions } from '../common/agentHostManagedSettings.js';
import { isManagedPluginEnablement, isManagedSettingsPermissions } from '../common/agentHostManagedSettings.js';
import { isAnnotationsUri } from '../common/annotationsUri.js';
import { parseChangesetUri } from '../common/changesetUri.js';
import { type IAgentService } from '../common/agentService.js';
Expand Down Expand Up @@ -514,11 +514,14 @@ export class ProtocolServerHandler extends Disposable implements IAgentHostClien
this._logService.trace(`[ProtocolServer] notification: method=${msg.method}`);
if ((msg as { method: string }).method === 'setClientManagedSettingsPermissions') {
if (client) {
const permissions = ((msg as { params?: { permissions?: unknown } }).params)?.permissions;
if (isManagedSettingsPermissions(permissions)) {
this._managedSettingsService.setClientPermissions(this._managedSettingsContributionId(client.clientId), permissions);
const params = (msg as { params?: { permissions?: unknown; enabledPlugins?: unknown } }).params;
if (isManagedSettingsPermissions(params?.permissions) && isManagedPluginEnablement(params.enabledPlugins)) {
this._managedSettingsService.setClientContribution(this._managedSettingsContributionId(client.clientId), {
permissions: params.permissions,
enabledPlugins: params.enabledPlugins,
});
} else {
this._logService.warn('[ProtocolServer] Ignoring invalid managed settings permissions contribution.');
this._logService.warn('[ProtocolServer] Ignoring invalid managed settings contribution.');
}
}
return;
Expand Down Expand Up @@ -1108,7 +1111,7 @@ export class ProtocolServerHandler extends Disposable implements IAgentHostClien
if (record?.state === 'grace') {
record.disconnectTimeouts.set('managed-settings', disposableTimeout(() => {
record.disconnectTimeouts.deleteAndDispose('managed-settings');
this._managedSettingsService.removeClientPermissions(this._managedSettingsContributionId(clientId));
this._managedSettingsService.removeClientContribution(this._managedSettingsContributionId(clientId));
}, CLIENT_TOOL_CALL_DISCONNECT_TIMEOUT));
}
for (const session of this._stateManager.getSessionUris()) {
Expand Down Expand Up @@ -2366,7 +2369,7 @@ export class ProtocolServerHandler extends Disposable implements IAgentHostClien

override dispose(): void {
for (const [clientId, record] of this._clients) {
this._managedSettingsService.removeClientPermissions(this._managedSettingsContributionId(clientId));
this._managedSettingsService.removeClientContribution(this._managedSettingsContributionId(clientId));
if (record.state === 'active') {
for (const connection of [...record.connections]) {
const subscriptionCount = connection.subscriptions.size;
Expand Down
Loading