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