mirror of https://github.com/microsoft/vscode.git
Initial refactoring of Language Models Access Management (#251773)
To clean things up... this still has the association that you must get access to language models provided by a particular extension.
This commit is contained in:
parent
0140ab3f1b
commit
66cc872a0a
|
@ -515,6 +515,7 @@ export class QuickInputController extends Disposable {
|
|||
input.ignoreFocusOut = !!options.ignoreFocusLost;
|
||||
input.matchOnDescription = !!options.matchOnDescription;
|
||||
input.matchOnDetail = !!options.matchOnDetail;
|
||||
input.sortByLabel = !!options.sortByLabel;
|
||||
input.matchOnLabel = (options.matchOnLabel === undefined) || options.matchOnLabel; // default to true
|
||||
input.quickNavigate = options.quickNavigate;
|
||||
input.hideInput = !!options.hideInput;
|
||||
|
|
|
@ -195,6 +195,7 @@ class QuickPickItemElement extends BaseQuickPickItemElement {
|
|||
|
||||
constructor(
|
||||
index: number,
|
||||
readonly childIndex: number,
|
||||
hasCheckbox: boolean,
|
||||
readonly fireButtonTriggered: (event: IQuickPickItemButtonEvent<IQuickPickItem>) => void,
|
||||
private _onChecked: Emitter<{ element: IQuickPickElement; checked: boolean }>,
|
||||
|
@ -520,7 +521,7 @@ class QuickPickItemElementRenderer extends BaseQuickInputListRenderer<QuickPickI
|
|||
} else {
|
||||
data.separator.style.display = 'none';
|
||||
}
|
||||
data.entry.classList.toggle('quick-input-list-separator-border', !!element.separator);
|
||||
data.entry.classList.toggle('quick-input-list-separator-border', !!element.separator && element.childIndex !== 0);
|
||||
|
||||
// Actions
|
||||
const buttons = mainItem.buttons;
|
||||
|
@ -1098,12 +1099,13 @@ export class QuickInputTree extends Disposable {
|
|||
const previous = index > 0 ? inputElements[index - 1] : undefined;
|
||||
let separator: IQuickPickSeparator | undefined;
|
||||
if (previous && previous.type === 'separator' && !previous.buttons) {
|
||||
// Found an inline separator so we clear out the current separator element
|
||||
currentSeparatorElement = undefined;
|
||||
separator = previous;
|
||||
}
|
||||
const qpi = new QuickPickItemElement(
|
||||
index,
|
||||
currentSeparatorElement?.children
|
||||
? currentSeparatorElement.children.length
|
||||
: index,
|
||||
this._hasCheckboxes && item.pickable !== false,
|
||||
e => this._onButtonTriggered.fire(e),
|
||||
this._elementChecked,
|
||||
|
|
|
@ -109,6 +109,11 @@ export interface IPickOptions<T extends IQuickPickItem> {
|
|||
*/
|
||||
matchOnLabel?: boolean;
|
||||
|
||||
/**
|
||||
* an optional flag to sort the picks based by the label.
|
||||
*/
|
||||
sortByLabel?: boolean;
|
||||
|
||||
/**
|
||||
* an optional flag to not close the picker on focus lost
|
||||
*/
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js';
|
||||
import * as nls from '../../../nls.js';
|
||||
import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
|
||||
import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService, INTERNAL_AUTH_PROVIDER_PREFIX as INTERNAL_MODEL_AUTH_PROVIDER_PREFIX, AuthenticationSessionAccount, IAuthenticationProviderSessionOptions } from '../../services/authentication/common/authentication.js';
|
||||
import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationProvider, IAuthenticationService, IAuthenticationExtensionsService, AuthenticationSessionAccount, IAuthenticationProviderSessionOptions } from '../../services/authentication/common/authentication.js';
|
||||
import { ExtHostAuthenticationShape, ExtHostContext, MainContext, MainThreadAuthenticationShape } from '../common/extHost.protocol.js';
|
||||
import { IDialogService, IPromptButton } from '../../../platform/dialogs/common/dialogs.js';
|
||||
import Severity from '../../../base/common/severity.js';
|
||||
|
@ -236,9 +236,10 @@ export class MainThreadAuthentication extends Disposable implements MainThreadAu
|
|||
private async loginPrompt(provider: IAuthenticationProvider, extensionName: string, recreatingSession: boolean, options?: AuthenticationInteractiveOptions): Promise<boolean> {
|
||||
let message: string;
|
||||
|
||||
// An internal provider is a special case which is for model access only.
|
||||
if (provider.id.startsWith(INTERNAL_MODEL_AUTH_PROVIDER_PREFIX)) {
|
||||
message = nls.localize('confirmModelAccess', "The extension '{0}' wants to access the language models provided by {1}.", extensionName, provider.label);
|
||||
// Check if the provider has a custom confirmation message
|
||||
const customMessage = provider.confirmation?.(extensionName, recreatingSession);
|
||||
if (customMessage) {
|
||||
message = customMessage;
|
||||
} else {
|
||||
message = recreatingSession
|
||||
? nls.localize('confirmRelogin', "The extension '{0}' wants you to sign in again using {1}.", extensionName, provider.label)
|
||||
|
|
|
@ -258,6 +258,10 @@ class LanguageModelAccessAuthProvider implements IAuthenticationProvider {
|
|||
return Promise.resolve();
|
||||
}
|
||||
|
||||
confirmation(extensionName: string, _recreatingSession: boolean): string {
|
||||
return localize('confirmLanguageModelAccess', "The extension '{0}' wants to access the language models provided by {1}.", extensionName, this.label);
|
||||
}
|
||||
|
||||
private _createFakeSession(scopes: string[]): AuthenticationSession {
|
||||
return {
|
||||
id: 'fake-session',
|
||||
|
|
|
@ -33,7 +33,7 @@ import { ILogService } from '../../../platform/log/common/log.js';
|
|||
import { IProductService } from '../../../platform/product/common/productService.js';
|
||||
import { ISecretStorageService } from '../../../platform/secrets/common/secrets.js';
|
||||
import { AuthenticationSessionInfo, getCurrentAuthenticationSessionInfo } from '../../services/authentication/browser/authenticationService.js';
|
||||
import { AuthenticationSessionAccount, IAuthenticationService } from '../../services/authentication/common/authentication.js';
|
||||
import { AuthenticationSessionAccount, IAuthenticationService, INTERNAL_AUTH_PROVIDER_PREFIX } from '../../services/authentication/common/authentication.js';
|
||||
import { IWorkbenchEnvironmentService } from '../../services/environment/common/environmentService.js';
|
||||
import { IHoverService } from '../../../platform/hover/browser/hover.js';
|
||||
import { ILifecycleService, LifecyclePhase } from '../../services/lifecycle/common/lifecycle.js';
|
||||
|
@ -363,16 +363,19 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction
|
|||
let menus: IAction[] = [];
|
||||
|
||||
for (const providerId of providers) {
|
||||
if (providerId.startsWith(INTERNAL_AUTH_PROVIDER_PREFIX)) {
|
||||
continue;
|
||||
}
|
||||
if (!this.initialized) {
|
||||
const noAccountsAvailableAction = disposables.add(new Action('noAccountsAvailable', localize('loading', "Loading..."), undefined, false));
|
||||
menus.push(noAccountsAvailableAction);
|
||||
break;
|
||||
}
|
||||
const providerLabel = this.authenticationService.getProvider(providerId).label;
|
||||
const provider = this.authenticationService.getProvider(providerId);
|
||||
const accounts = this.groupedAccounts.get(providerId);
|
||||
if (!accounts) {
|
||||
if (this.problematicProviders.has(providerId)) {
|
||||
const providerUnavailableAction = disposables.add(new Action('providerUnavailable', localize('authProviderUnavailable', '{0} is currently unavailable', providerLabel), undefined, false));
|
||||
const providerUnavailableAction = disposables.add(new Action('providerUnavailable', localize('authProviderUnavailable', '{0} is currently unavailable', provider.label), undefined, false));
|
||||
menus.push(providerUnavailableAction);
|
||||
// try again in the background so that if the failure was intermittent, we can resolve it on the next showing of the menu
|
||||
try {
|
||||
|
@ -384,6 +387,7 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction
|
|||
continue;
|
||||
}
|
||||
|
||||
const canUseMcp = !!provider.authorizationServers?.length;
|
||||
for (const account of accounts) {
|
||||
const manageExtensionsAction = toAction({
|
||||
id: `configureSessions${account.label}`,
|
||||
|
@ -392,15 +396,17 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction
|
|||
run: () => this.commandService.executeCommand('_manageTrustedExtensionsForAccount', { providerId, accountLabel: account.label })
|
||||
});
|
||||
|
||||
const manageMCPAction = toAction({
|
||||
id: `configureSessions${account.label}`,
|
||||
label: localize('manageTrustedMCPServers', "Manage Trusted MCP Servers"),
|
||||
enabled: true,
|
||||
run: () => this.commandService.executeCommand('_manageTrustedMCPServersForAccount', { providerId, accountLabel: account.label })
|
||||
});
|
||||
|
||||
const providerSubMenuActions: IAction[] = [manageExtensionsAction, manageMCPAction];
|
||||
|
||||
const providerSubMenuActions: IAction[] = [manageExtensionsAction];
|
||||
if (canUseMcp) {
|
||||
const manageMCPAction = toAction({
|
||||
id: `configureSessions${account.label}`,
|
||||
label: localize('manageTrustedMCPServers', "Manage Trusted MCP Servers"),
|
||||
enabled: true,
|
||||
run: () => this.commandService.executeCommand('_manageTrustedMCPServersForAccount', { providerId, accountLabel: account.label })
|
||||
});
|
||||
providerSubMenuActions.push(manageMCPAction);
|
||||
}
|
||||
if (account.canSignOut) {
|
||||
providerSubMenuActions.push(toAction({
|
||||
id: 'signOut',
|
||||
|
@ -410,7 +416,7 @@ export class AccountsActivityActionViewItem extends AbstractGlobalActivityAction
|
|||
}));
|
||||
}
|
||||
|
||||
const providerSubMenu = new SubmenuAction('activitybar.submenu', `${account.label} (${providerLabel})`, providerSubMenuActions);
|
||||
const providerSubMenu = new SubmenuAction('activitybar.submenu', `${account.label} (${provider.label})`, providerSubMenuActions);
|
||||
menus.push(providerSubMenu);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,218 @@
|
|||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
|
||||
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IQuickInputService, IQuickPickItem, QuickPickInput } from '../../../../../platform/quickinput/common/quickInput.js';
|
||||
import { ILanguageModelsService } from '../../common/languageModels.js';
|
||||
import { IAuthenticationAccessService } from '../../../../services/authentication/browser/authenticationAccessService.js';
|
||||
import { localize, localize2 } from '../../../../../nls.js';
|
||||
import { AllowedExtension, INTERNAL_AUTH_PROVIDER_PREFIX } from '../../../../services/authentication/common/authentication.js';
|
||||
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
|
||||
import { CHAT_CATEGORY } from './chatActions.js';
|
||||
import { IExtensionService } from '../../../../services/extensions/common/extensions.js';
|
||||
import { IExtensionsWorkbenchService } from '../../../extensions/common/extensions.js';
|
||||
import { IProductService } from '../../../../../platform/product/common/productService.js';
|
||||
import { Codicon } from '../../../../../base/common/codicons.js';
|
||||
import { ThemeIcon } from '../../../../../base/common/themables.js';
|
||||
|
||||
class ManageLanguageModelAuthenticationAction extends Action2 {
|
||||
static readonly ID = 'workbench.action.chat.manageLanguageModelAuthentication';
|
||||
|
||||
constructor() {
|
||||
super({
|
||||
id: ManageLanguageModelAuthenticationAction.ID,
|
||||
title: localize2('manageLanguageModelAuthentication', 'Manage Language Model Access...'),
|
||||
category: CHAT_CATEGORY,
|
||||
menu: [{
|
||||
id: MenuId.AccountsContext,
|
||||
order: 100,
|
||||
}],
|
||||
f1: true
|
||||
});
|
||||
}
|
||||
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const quickInputService = accessor.get(IQuickInputService);
|
||||
const languageModelsService = accessor.get(ILanguageModelsService);
|
||||
const authenticationAccessService = accessor.get(IAuthenticationAccessService);
|
||||
const dialogService = accessor.get(IDialogService);
|
||||
const extensionService = accessor.get(IExtensionService);
|
||||
const extensionsWorkbenchService = accessor.get(IExtensionsWorkbenchService);
|
||||
const productService = accessor.get(IProductService);
|
||||
|
||||
// Get all registered language models
|
||||
const modelIds = languageModelsService.getLanguageModelIds();
|
||||
|
||||
// Group models by owning extension and collect all allowed extensions
|
||||
const extensionAuth = new Map<string, AllowedExtension[]>();
|
||||
|
||||
const ownerToAccountLabel = new Map<string, string>();
|
||||
for (const modelId of modelIds) {
|
||||
const model = languageModelsService.lookupLanguageModel(modelId);
|
||||
if (!model?.auth) {
|
||||
continue; // Skip if model is not found
|
||||
}
|
||||
const ownerId = model.extension.value;
|
||||
if (extensionAuth.has(ownerId)) {
|
||||
// If the owner already exists, just continue
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get allowed extensions for this model's auth provider
|
||||
try {
|
||||
// Use providerLabel as the providerId and accountLabel (or default)
|
||||
const providerId = INTERNAL_AUTH_PROVIDER_PREFIX + ownerId;
|
||||
const accountLabel = model.auth.accountLabel || 'Language Models';
|
||||
ownerToAccountLabel.set(ownerId, accountLabel);
|
||||
const allowedExtensions = authenticationAccessService.readAllowedExtensions(
|
||||
providerId,
|
||||
accountLabel
|
||||
).filter(ext => !ext.trusted); // Filter out trusted extensions because those should not be modified
|
||||
|
||||
if (productService.trustedExtensionAuthAccess && !Array.isArray(productService.trustedExtensionAuthAccess)) {
|
||||
const trustedExtensions = productService.trustedExtensionAuthAccess[providerId];
|
||||
// If the provider is trusted, add all trusted extensions to the allowed list
|
||||
for (const ext of trustedExtensions) {
|
||||
const index = allowedExtensions.findIndex(a => a.id === ext);
|
||||
if (index !== -1) {
|
||||
allowedExtensions.splice(index, 1);
|
||||
}
|
||||
const extension = await extensionService.getExtension(ext);
|
||||
if (!extension) {
|
||||
continue; // Skip if the extension is not found
|
||||
}
|
||||
allowedExtensions.push({
|
||||
id: ext,
|
||||
name: extension.displayName || extension.name,
|
||||
allowed: true, // Assume trusted extensions are allowed by default
|
||||
trusted: true // Mark as trusted
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Only grab extensions that are gettable from the extension service
|
||||
const filteredExtensions = new Array<AllowedExtension>();
|
||||
for (const ext of allowedExtensions) {
|
||||
if (await extensionService.getExtension(ext.id)) {
|
||||
filteredExtensions.push(ext);
|
||||
}
|
||||
}
|
||||
|
||||
extensionAuth.set(ownerId, filteredExtensions);
|
||||
// Add all allowed extensions to the set for this owner
|
||||
} catch (error) {
|
||||
// Handle error by ensuring the owner is in the map
|
||||
if (!extensionAuth.has(ownerId)) {
|
||||
extensionAuth.set(ownerId, []);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionAuth.size === 0) {
|
||||
dialogService.prompt({
|
||||
type: 'info',
|
||||
message: localize('noLanguageModels', 'No language models requiring authentication found.'),
|
||||
detail: localize('noLanguageModelsDetail', 'There are currently no language models that require authentication.')
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const items: QuickPickInput<IQuickPickItem & { extension?: AllowedExtension; ownerId?: string }>[] = [];
|
||||
// Create QuickPick items grouped by owner extension
|
||||
for (const [ownerId, allowedExtensions] of extensionAuth) {
|
||||
const extension = await extensionService.getExtension(ownerId);
|
||||
if (!extension) {
|
||||
// If the extension is not found, skip it
|
||||
continue;
|
||||
}
|
||||
// Add separator for the owning extension
|
||||
items.push({
|
||||
type: 'separator',
|
||||
id: ownerId,
|
||||
label: localize('extensionOwner', '{0}', extension.displayName || extension.name),
|
||||
buttons: [{
|
||||
iconClass: ThemeIcon.asClassName(Codicon.info),
|
||||
tooltip: localize('openExtension', 'Open Extension'),
|
||||
}]
|
||||
});
|
||||
|
||||
// Add allowed extensions as checkboxes (visual representation)
|
||||
let addedTrustedSeparator = false;
|
||||
if (allowedExtensions.length > 0) {
|
||||
for (const allowedExt of allowedExtensions) {
|
||||
if (allowedExt.trusted && !addedTrustedSeparator) {
|
||||
items.push({
|
||||
type: 'separator',
|
||||
label: localize('trustedExtension', 'Trusted by Microsoft'),
|
||||
});
|
||||
addedTrustedSeparator = true;
|
||||
}
|
||||
items.push({
|
||||
label: allowedExt.name,
|
||||
ownerId,
|
||||
id: allowedExt.id,
|
||||
picked: allowedExt.allowed ?? false,
|
||||
extension: allowedExt,
|
||||
disabled: allowedExt.trusted, // Don't allow toggling trusted extensions
|
||||
});
|
||||
}
|
||||
} else {
|
||||
items.push({
|
||||
label: localize('noAllowedExtensions', 'No extensions have access'),
|
||||
description: localize('noAccessDescription', 'No extensions are currently allowed to use models from {0}', ownerId),
|
||||
pickable: false
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Show the QuickPick
|
||||
const result = await quickInputService.pick(
|
||||
items,
|
||||
{
|
||||
canPickMany: true,
|
||||
sortByLabel: true,
|
||||
onDidTriggerSeparatorButton(context) {
|
||||
// Handle separator button clicks
|
||||
const extId = context.separator.id!;
|
||||
if (extId) {
|
||||
// Open the extension in the editor
|
||||
void extensionsWorkbenchService.open(extId);
|
||||
}
|
||||
},
|
||||
title: localize('languageModelAuthTitle', 'Manage Language Model Access'),
|
||||
placeHolder: localize('languageModelAuthPlaceholder', 'Choose which extensions can access language models'),
|
||||
}
|
||||
);
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [ownerId, allowedExtensions] of extensionAuth) {
|
||||
// diff with result to find out which extensions are allowed or not
|
||||
// but we need to only look at the result items that have the ownerId
|
||||
const allowedSet = new Set(result
|
||||
.filter(item => item.ownerId === ownerId)
|
||||
// only save items that are not trusted automatically
|
||||
.filter(item => !item.extension?.trusted)
|
||||
.map(item => item.id!));
|
||||
|
||||
for (const allowedExt of allowedExtensions) {
|
||||
allowedExt.allowed = allowedSet.has(allowedExt.id);
|
||||
}
|
||||
|
||||
authenticationAccessService.updateAllowedExtensions(
|
||||
INTERNAL_AUTH_PROVIDER_PREFIX + ownerId,
|
||||
ownerToAccountLabel.get(ownerId) || 'Language Models',
|
||||
allowedExtensions
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export function registerLanguageModelActions() {
|
||||
registerAction2(ManageLanguageModelAuthenticationAction);
|
||||
}
|
|
@ -112,6 +112,7 @@ import { ChatResponseResourceFileSystemProvider } from '../common/chatResponseRe
|
|||
import { runSaveToPromptAction, SAVE_TO_PROMPT_SLASH_COMMAND_NAME } from './promptSyntax/saveToPromptAction.js';
|
||||
import { ChatDynamicVariableModel } from './contrib/chatDynamicVariables.js';
|
||||
import { ChatAttachmentResolveService, IChatAttachmentResolveService } from './chatAttachmentResolveService.js';
|
||||
import { registerLanguageModelActions } from './actions/chatLanguageModelActions.js';
|
||||
|
||||
// Register configuration
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
|
||||
|
@ -736,6 +737,7 @@ registerChatContextActions();
|
|||
registerChatDeveloperActions();
|
||||
registerChatEditorActions();
|
||||
registerChatToolActions();
|
||||
registerLanguageModelActions();
|
||||
|
||||
registerEditorFeature(ChatPasteProvidersFeature);
|
||||
|
||||
|
|
|
@ -361,6 +361,15 @@ export interface IAuthenticationProvider {
|
|||
*/
|
||||
readonly supportsMultipleAccounts: boolean;
|
||||
|
||||
/**
|
||||
* Optional function to provide a custom confirmation message for authentication prompts.
|
||||
* If not implemented, the default confirmation messages will be used.
|
||||
* @param extensionName - The name of the extension requesting authentication.
|
||||
* @param recreatingSession - Whether this is recreating an existing session.
|
||||
* @returns A custom confirmation message or undefined to use the default message.
|
||||
*/
|
||||
readonly confirmation?: (extensionName: string, recreatingSession: boolean) => string | undefined;
|
||||
|
||||
/**
|
||||
* An {@link Event} which fires when the array of sessions has changed, or data
|
||||
* within a session has changed.
|
||||
|
|
Loading…
Reference in New Issue