Path: blob/main/extensions/copilot/src/platform/authentication/common/authenticationUpgradeService.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as l10n from '@vscode/l10n';6import type { ChatContext, ChatRequest, ChatResponseStream } from 'vscode';7import { coalesce } from '../../../util/vs/base/common/arrays';8import { findLast } from '../../../util/vs/base/common/arraysFind';9import { Emitter } from '../../../util/vs/base/common/event';10import { Disposable } from '../../../util/vs/base/common/lifecycle';11import { URI } from '../../../util/vs/base/common/uri';12import { ChatRequestTurn } from '../../../vscodeTypes';13import { AuthPermissionMode, ConfigKey, IConfigurationService } from '../../configuration/common/configurationService';14import { getGitHubRepoInfoFromContext, IGitService } from '../../git/common/gitService';15import { IGithubRepositoryService } from '../../github/common/githubService';16import { ILogService } from '../../log/common/logService';17import { IAuthenticationService } from './authentication';18import { IAuthenticationChatUpgradeService } from './authenticationUpgrade';1920export class AuthenticationChatUpgradeService extends Disposable implements IAuthenticationChatUpgradeService {21declare _serviceBrand: undefined;2223private hasRequestedPermissiveSessionUpgrade = false;2425//#region Localization26private _permissionRequest = l10n.t('Permission Request');27private _permissionRequestGrant = l10n.t('Grant');28private _permissionRequestNotNow = l10n.t('Not Now');29private _permissionRequestNeverAskAgain = l10n.t('Never Ask Again');3031private readonly _onDidGrantAuthUpgrade = this._register(new Emitter<void>());32public readonly onDidGrantAuthUpgrade = this._onDidGrantAuthUpgrade.event;3334//#endregion35constructor(36@IAuthenticationService private readonly _authenticationService: IAuthenticationService,37@IGitService private readonly gitService: IGitService,38@ILogService private readonly logService: ILogService,39@IGithubRepositoryService private readonly ghRepoService: IGithubRepositoryService,40@IConfigurationService private readonly configurationService: IConfigurationService,41) {42super();43// If the user signs out, reset the upgrade state44this._register(this._authenticationService.onDidAuthenticationChange(() => {45if (this._authenticationService.anyGitHubSession) {46this.hasRequestedPermissiveSessionUpgrade = false;47}48}));49}5051async shouldRequestPermissiveSessionUpgrade(): Promise<boolean> {52let reason: string = 'true';53try {54// We don't want to be annoying55if (this.hasRequestedPermissiveSessionUpgrade) {56reason = 'false - already requested';57return false;58}59// The user does not want to be asked60if (this._authenticationService.isMinimalMode) {61reason = 'false - minimal mode';62return false;63}64// We already have a permissive session65if (await this._authenticationService.getGitHubSession('permissive', { silent: true })) {66reason = 'false - already have permissive session';67return false;68}69// The user is not signed in at all70if (!(await this._authenticationService.getGitHubSession('any', { silent: true }))) {71reason = 'false - not signed in';72return false;73}74// The user has access to all repositories75if (await this._canAccessAllRepositories()) {76reason = 'false - access to all repositories';77return false;78}79return true;80} finally {81this.logService.trace(`Should request permissive session upgrade: ${reason}`);82}83}8485async showPermissiveSessionModal(skipRepeatCheck = false): Promise<boolean> {86if (this.hasRequestedPermissiveSessionUpgrade && !skipRepeatCheck) {87this.logService.trace('Already requested permissive session upgrade');88return false;89}90this.logService.trace('Requesting permissive session upgrade');91this.hasRequestedPermissiveSessionUpgrade = true;92try {93await this._authenticationService.getGitHubSession('permissive', {94forceNewSession: {95detail: l10n.t('To get more relevant Chat results, we need permission to read the contents of your repository on GitHub.'),96learnMore: URI.parse('https://aka.ms/copilotRepoScope'),97},98clearSessionPreference: true99});100return true;101} catch (e) {102// User cancelled so show the badge103await this._authenticationService.getGitHubSession('permissive', {});104return false;105}106}107108showPermissiveSessionUpgradeInChat(109stream: ChatResponseStream,110data: ChatRequest,111detail?: string,112context?: ChatContext113): void {114this.logService.trace('Requesting permissive session upgrade in chat');115this.hasRequestedPermissiveSessionUpgrade = true;116stream.confirmation(117this._permissionRequest,118detail || l10n.t('To get more relevant Chat results, we need permission to read the contents of your repository on GitHub.'),119// TODO: Change this shape to include request via a dedicated field120{ authPermissionPrompted: true, ...data, context },121[122this._permissionRequestGrant,123this._permissionRequestNotNow,124this._permissionRequestNeverAskAgain125]126);127}128129async handleConfirmationRequest(stream: ChatResponseStream, request: ChatRequest, history: ChatContext['history']): Promise<ChatRequest> {130const findConfirmationRequested: ChatRequest | undefined = request.acceptedConfirmationData?.find(ref => ref?.authPermissionPrompted);131if (!findConfirmationRequested) {132return request;133}134this.logService.trace('Handling confirmation request');135switch (request.prompt) {136case `${this._permissionRequestGrant}: "${this._permissionRequest}"`:137this.logService.trace('User granted permission');138try {139await this._authenticationService.getGitHubSession('permissive', { createIfNone: { detail: l10n.t('Sign in to GitHub with additional permissions for enhanced features.') } });140this._onDidGrantAuthUpgrade.fire();141} catch (e) {142// User cancelled so show the badge143await this._authenticationService.getGitHubSession('permissive', {});144}145break;146case `${this._permissionRequestNotNow}: "${this._permissionRequest}"`:147this.logService.trace('User declined permission');148stream.markdown(l10n.t("Ok. I won't bother you again for now. If you change your mind, you can react to the authentication request in the Account menu.") + '\n\n');149await this._authenticationService.getGitHubSession('permissive', {});150break;151case `${this._permissionRequestNeverAskAgain}: "${this._permissionRequest}"`:152this.logService.trace('User chose never ask again for permission');153await this.configurationService.setConfig(ConfigKey.Shared.AuthPermissions, AuthPermissionMode.Minimal);154// Change this back to false to handle if the user changes back to allowing permissive tokens.155this.hasRequestedPermissiveSessionUpgrade = false;156stream.markdown(l10n.t('Ok. I saved this decision to the `{0}` setting', ConfigKey.Shared.AuthPermissions.fullyQualifiedId) + '\n\n');157break;158}159160const previousRequest = findLast(history, item => item instanceof ChatRequestTurn) as ChatRequestTurn | undefined;161// Simple types can be used from the findConfirmationRequested request. Classes will have been serialized and not deserialized into class instances.162// Props that exist on the history entry are used, otherwise fall back to either the current request or the saved request.163if (previousRequest) {164return {165prompt: previousRequest.prompt,166command: previousRequest.command,167references: previousRequest.references,168toolReferences: previousRequest.toolReferences,169170toolInvocationToken: request.toolInvocationToken,171attempt: request.attempt,172enableCommandDetection: request.enableCommandDetection,173isParticipantDetected: findConfirmationRequested.isParticipantDetected,174location: request.location,175location2: request.location2,176model: request.model,177tools: new Map(),178id: request.id,179sessionId: '1',180sessionResource: request.sessionResource,181hasHooksEnabled: request.hasHooksEnabled,182};183} else {184// Something went wrong, history item was deleted or lost?185return {186prompt: findConfirmationRequested.prompt,187command: findConfirmationRequested.command,188references: [],189toolReferences: [],190191toolInvocationToken: request.toolInvocationToken,192attempt: request.attempt,193enableCommandDetection: request.enableCommandDetection,194isParticipantDetected: findConfirmationRequested.isParticipantDetected,195location: request.location,196location2: request.location2,197model: request.model,198tools: new Map(),199id: request.id,200sessionId: '1',201sessionResource: request.sessionResource,202hasHooksEnabled: request.hasHooksEnabled,203};204}205}206207private async _canAccessAllRepositories(): Promise<boolean> {208const repoContexts = this.gitService?.repositories;209if (!repoContexts) {210this.logService.debug('No git repositories found');211return false;212}213214const repoIds = coalesce(repoContexts.map(x => getGitHubRepoInfoFromContext(x)?.id));215const result = await Promise.all(repoIds.map(repoId => {216return this.ghRepoService.isAvailable(repoId.org, repoId.repo);217}));218219return result.every(level => level);220}221}222223224