Skip to content

Commit 21e5b80

Browse files
Fix
Signed-off-by: Roman Nikitenko <rnikiten@redhat.com>
1 parent f53495b commit 21e5b80

7 files changed

Lines changed: 82 additions & 10 deletions

File tree

code/extensions/che-api/src/api/github-service.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,7 @@ export interface GithubService {
3434

3535
getUser(): Promise<GithubUser>;
3636
getTokenScopes(token: string): Promise<string[]>;
37+
38+
/** True when the active token comes from the device-authentication secret (OAuth), not a workspace PAT. */
39+
isDeviceAuthToken(): Promise<boolean>;
3740
}

code/extensions/che-api/src/impl/github-service-impl.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ export class GithubServiceImpl implements GithubService {
6767
return result.scopes;
6868
}
6969

70+
async isDeviceAuthToken(): Promise<boolean> {
71+
return (await this.getDeviceAuthToken()) !== undefined;
72+
}
73+
7074
private async fetchGithubUser(token: string): Promise<{ user: GithubUser; scopes: string[] }> {
7175
try {
7276
this.logger.info('Github Service: fetching GitHub user using fetch...');

code/extensions/che-github-authentication/src/github.ts

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import type { DeviceAuthentication } from './device-authentication';
1717
import { ErrorHandler } from './error-handler';
1818
import { ExtensionContext } from './extension-context';
1919
import { Logger } from './logger';
20-
import { arrayEquals, getMatchingHydrationScopeBundles, hasAllScopes, isUnauthorizedError } from './utils';
20+
import { getMatchingHydrationScopeBundles, hasAllScopes, isUnauthorizedError, isWorkspacePatSession, sessionMatchesRequestedScopes, WORKSPACE_PAT_SCOPE } from './utils';
2121

2222
export interface GithubUser {
2323
login: string;
@@ -32,6 +32,7 @@ export interface GithubService {
3232
removeDeviceAuthToken(): Promise<void>;
3333
getUser(): Promise<GithubUser>;
3434
getTokenScopes(token: string): Promise<string[]>;
35+
isDeviceAuthToken(): Promise<boolean>;
3536
}
3637

3738
@injectable()
@@ -64,8 +65,15 @@ export class GitHubAuthProvider implements vscode.AuthenticationProvider {
6465
if (sessions.length > 0) {
6566
try {
6667
await this.githubService.getTokenScopes(sessions[0].accessToken);
67-
this.logger.trace('GitHubAuthProvider: existing session token is valid');
68-
return;
68+
const isDeviceAuthToken = await this.githubService.isDeviceAuthToken();
69+
const sessionsNeedRetag = isDeviceAuthToken
70+
? sessions.some(session => isWorkspacePatSession(session.scopes))
71+
: sessions.some(session => !isWorkspacePatSession(session.scopes));
72+
if (!sessionsNeedRetag) {
73+
this.logger.trace('GitHubAuthProvider: existing session token is valid');
74+
return;
75+
}
76+
this.logger.info('GitHubAuthProvider: re-hydrating sessions to match current token source');
6977
} catch (error) {
7078
if (isUnauthorizedError(error)) {
7179
this.logger.warn('GitHubAuthProvider: existing session token is not valid, clearing sessions');
@@ -95,17 +103,19 @@ export class GitHubAuthProvider implements vscode.AuthenticationProvider {
95103
return;
96104
}
97105

106+
const isDeviceAuthToken = await this.githubService.isDeviceAuthToken();
98107
const account = { label: githubUser.login, id: githubUser.id.toString() };
99108
const hydratedSessions = matchingBundles.map(scopes => ({
100109
id: v4(),
101110
accessToken: token,
102111
account,
103-
scopes,
112+
scopes: isDeviceAuthToken ? scopes : [...scopes, WORKSPACE_PAT_SCOPE],
104113
}));
105114

106115
await this.storeSessions(hydratedSessions);
107116
this.sessionChangeEmitter.fire({ added: hydratedSessions, removed: [], changed: [] });
108-
this.logger.info(`GitHubAuthProvider: hydrated ${hydratedSessions.length} session(s) from K8s token`);
117+
const tokenSource = isDeviceAuthToken ? 'device authentication' : 'workspace PAT';
118+
this.logger.info(`GitHubAuthProvider: hydrated ${hydratedSessions.length} session(s) from K8s ${tokenSource}`);
109119
} catch (error) {
110120
if (isUnauthorizedError(error)) {
111121
this.logger.warn('GitHubAuthProvider: hydrate failed, token is not valid');
@@ -121,7 +131,7 @@ export class GitHubAuthProvider implements vscode.AuthenticationProvider {
121131
const sessions = await this.sessionsPromise;
122132
const sortedScopes = sessionScopes ? [...sessionScopes].sort() : [];
123133
const filteredSessions = sortedScopes.length
124-
? sessions.filter(session => arrayEquals([...session.scopes].sort(), sortedScopes))
134+
? sessions.filter(session => sessionMatchesRequestedScopes(session.scopes, sortedScopes))
125135
: [...sessions];
126136

127137
this.logger.trace(`GitHubAuthProvider: GET sessions - found ${filteredSessions.length} sessions for scopes: ${sessionScopes}`);
@@ -160,14 +170,15 @@ export class GitHubAuthProvider implements vscode.AuthenticationProvider {
160170
}
161171

162172
const sessions = await this.sessionsPromise;
173+
const isDeviceAuthToken = await this.githubService.isDeviceAuthToken();
163174
const session: vscode.AuthenticationSession = {
164175
id: v4(),
165176
accessToken: token,
166177
account: { label: githubUser.login, id: githubUser.id.toString() },
167-
scopes,
178+
scopes: isDeviceAuthToken ? scopes : [...scopes, WORKSPACE_PAT_SCOPE],
168179
};
169180

170-
const sessionIndex = sessions.findIndex(s => arrayEquals([...s.scopes].sort(), sortedScopes));
181+
const sessionIndex = sessions.findIndex(s => sessionMatchesRequestedScopes(s.scopes, sortedScopes));
171182
const removed: vscode.AuthenticationSession[] = [];
172183
const updatedSessions = [...sessions];
173184
if (sessionIndex > -1) {

code/extensions/che-github-authentication/src/utils.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,24 @@ export function hasAllScopes(existingScopes: string[], requestedScopes: string[]
3838
return requestedScopes.every(scope => existingScopes.includes(scope));
3939
}
4040

41+
/**
42+
* Marker scope on sessions backed by a workspace PAT. Copilot requests exact scope lists and
43+
* will not match these sessions; other extensions use superset matching in getSessions.
44+
*/
45+
export const WORKSPACE_PAT_SCOPE = 'che:workspace-pat';
46+
47+
export function isWorkspacePatSession(scopes: readonly string[]): boolean {
48+
return scopes.includes(WORKSPACE_PAT_SCOPE);
49+
}
50+
51+
export function sessionMatchesRequestedScopes(sessionScopes: readonly string[], requestedScopes: readonly string[]): boolean {
52+
if (isWorkspacePatSession(sessionScopes)) {
53+
const tokenScopes = sessionScopes.filter(scope => scope !== WORKSPACE_PAT_SCOPE);
54+
return hasAllScopes(tokenScopes, [...requestedScopes]);
55+
}
56+
return arrayEquals([...sessionScopes].sort(), [...requestedScopes].sort());
57+
}
58+
4159
/** Scope bundles used by Copilot / github / GHPRI / Agent Host — session keys match vanilla createSession. */
4260
export const HYDRATION_SCOPE_BUNDLES: readonly string[][] = [
4361
['read:user', 'user:email', 'repo', 'workflow', 'project', 'read:org'],

code/extensions/copilot/src/platform/authentication/vscode-node/copilotTokenManager.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ import { getAnyAuthSession } from './session';
2121
//Flag if we've shown message about broken oauth token.
2222
let shown401Message = false;
2323

24+
/** Workspace PATs cannot be exchanged for a Copilot token; device OAuth is required. */
25+
function isWorkspacePersonalAccessToken(token: string): boolean {
26+
return token.startsWith('ghp_') || token.startsWith('github_pat_');
27+
}
28+
2429
export class NotSignedUpError extends Error { }
2530
export class SubscriptionExpiredError extends Error { }
2631
export class ContactSupportError extends Error { }
@@ -78,12 +83,20 @@ export class VSCodeCopilotTokenManager extends BaseCopilotTokenManager {
7883
return { kind: 'failure', reason: 'GitHubLoginFailed' };
7984
}
8085
if (session) {
86+
if (isWorkspacePersonalAccessToken(session.accessToken)) {
87+
this._logService.info('Copilot token exchange skipped (workspace PAT cannot be used for Copilot; device authentication is required)');
88+
this._telemetryService.sendGHTelemetryErrorEvent('auth.github_login_failed');
89+
return { kind: 'failure', reason: 'GitHubLoginFailed' };
90+
}
8191
// Log the steps by default, but only log actual token values when the log level is set to debug.
8292
this._logService.info(`Logged in as ${session.account.label}`);
8393
const tokenResult = await this.authFromGitHubToken(session.accessToken, session.account.label);
8494
if (tokenResult.kind === 'success') {
8595
this._logService.info(`Got Copilot token for ${session.account.label}`);
8696
this._logService.info(`Copilot Chat: ${this._envService.getVersion()}, VS Code: ${this._envService.vscodeVersion}`);
97+
} else if (tokenResult.kind === 'failure' && (tokenResult.reason === 'ParseFailed' || tokenResult.reason === 'RequestFailed')) {
98+
this._logService.info('Copilot token exchange failed (workspace token cannot be exchanged for Copilot token), triggering sign-in flow');
99+
return { kind: 'failure', reason: 'GitHubLoginFailed' };
87100
}
88101
return tokenResult;
89102
} else {

code/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -136,10 +136,29 @@ export class ChatSetupContribution extends Disposable implements IWorkbenchContr
136136
panelAgentDisposables.dispose();
137137
}
138138
}));
139+
}
140+
141+
// Proactively clear panel agents when Copilot reports GitHub login failure
142+
// (e.g. workspace PAT cannot be exchanged for a Copilot token)
143+
const gitHubLoginFailedKey = 'github.copilot.interactiveSession.gitHubLoginFailed';
144+
const checkGitHubLoginFailed = () => {
145+
if (this.contextKeyService.getContextKeyValue<boolean>(gitHubLoginFailedKey)) {
146+
const panelAgentHasGuidance = chatViewsWelcomeRegistry.get().some(descriptor => this.contextKeyService.contextMatchesRules(descriptor.when));
147+
if (panelAgentHasGuidance) {
148+
this.logService.error('[chat setup] GitHub login failed detected, clearing panel agent registration to show welcome view.');
149+
panelAgentDisposables.dispose();
150+
}
139151
}
152+
};
153+
panelAgentDisposables.add(this.contextKeyService.onDidChangeContext(e => {
154+
if (e.affectsSome(new Set([gitHubLoginFailedKey]))) {
155+
checkGitHubLoginFailed();
156+
}
157+
}));
158+
checkGitHubLoginFailed();
140159

141-
// Inline Agents
142-
disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Terminal, ChatModeKind.Ask, context, controller).disposable);
160+
// Inline Agents
161+
disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Terminal, ChatModeKind.Ask, context, controller).disposable);
143162
disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.Notebook, ChatModeKind.Ask, context, controller).disposable);
144163
disposables.add(SetupAgent.registerDefaultAgents(this.instantiationService, ChatAgentLocation.EditorInline, ChatModeKind.Ask, context, controller).disposable);
145164
}

rebase.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -440,6 +440,8 @@ resolve_conflicts() {
440440
apply_changes_multi_line "$conflictingFile"
441441
elif [[ "$conflictingFile" == "code/src/vs/workbench/browser/parts/titlebar/commandCenterControl.ts" ]]; then
442442
apply_changes_multi_line "$conflictingFile"
443+
elif [[ "$conflictingFile" == "code/src/vs/workbench/contrib/chat/browser/chatSetup/chatSetupContributions.ts" ]]; then
444+
apply_changes_multi_line "$conflictingFile"
443445
elif [[ "$conflictingFile" == "code/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/agentTitleBarStatusWidget.ts" ]]; then
444446
apply_changes_multi_line "$conflictingFile"
445447
elif [[ "$conflictingFile" == "code/src/vs/workbench/contrib/chat/browser/agentSessions/experiments/media/agenttitlebarstatuswidget.css" ]]; then
@@ -486,6 +488,8 @@ resolve_conflicts() {
486488
apply_code_vs_base_browser_dompurify_d_changes
487489
elif [[ "$conflictingFile" == "code/src/vs/base/browser/dompurify/dompurify.js" ]]; then
488490
apply_code_vs_base_browser_dompurify_changes
491+
elif [[ "$conflictingFile" == "code/extensions/copilot/src/platform/authentication/vscode-node/copilotTokenManager.ts" ]]; then
492+
apply_changes_multi_line "$conflictingFile"
489493
elif [[ "$conflictingFile" == "code/extensions/markdown-language-features/package.json" ]]; then
490494
apply_package_changes_by_path "$conflictingFile"
491495
elif [[ "$conflictingFile" == "code/extensions/mermaid-chat-features/package.json" ]]; then

0 commit comments

Comments
 (0)