From abd5d2a592f24a75779a0ec8c80a8690b551da95 Mon Sep 17 00:00:00 2001 From: aws-ajangg Date: Thu, 22 Jan 2026 12:33:22 -0800 Subject: [PATCH 1/5] fix(sagemaker): revert presign url changes (#8506) ## Problem - workspace connection is currently failing via presigned url due to change of attributes ## Solution - reverting change and using eks cluster attr for hostname ## Testing - updated unit tests - tested locally with new vsix --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --- .../core/src/awsService/sagemaker/commands.ts | 21 ++++++++----------- .../src/awsService/sagemaker/uriHandlers.ts | 4 ++-- .../core/src/shared/clients/kubectlClient.ts | 15 ++----------- 3 files changed, 13 insertions(+), 27 deletions(-) diff --git a/packages/core/src/awsService/sagemaker/commands.ts b/packages/core/src/awsService/sagemaker/commands.ts index dd67ee04804..62c0940c017 100644 --- a/packages/core/src/awsService/sagemaker/commands.ts +++ b/packages/core/src/awsService/sagemaker/commands.ts @@ -100,7 +100,7 @@ export async function deeplinkConnect( appType?: string, workspaceName?: string, namespace?: string, - clusterArn?: string, + eksClusterArn?: string, isSMUS: boolean = false ) { getLogger().debug( @@ -118,11 +118,7 @@ export async function deeplinkConnect( appType: ${appType}, workspaceName: ${workspaceName}, namespace: ${namespace}, - clusterArn: ${clusterArn}` - ) - - getLogger().info( - `sm:deeplinkConnect: domain: ${domain}, appType: ${appType}, workspaceName: ${workspaceName}, namespace: ${namespace}, clusterArn: ${clusterArn}` + eksClusterArn: ${eksClusterArn}` ) if (isRemoteWorkspace()) { @@ -132,8 +128,8 @@ export async function deeplinkConnect( try { let connectionType = 'sm_dl' - if (!domain && clusterArn && workspaceName && namespace) { - const { accountId, region, clusterName } = parseArn(clusterArn) + if (!domain && eksClusterArn && workspaceName && namespace) { + const { accountId, region, clusterName } = parseArn(eksClusterArn) connectionType = 'sm_hp' const proposedSession = `${workspaceName}_${namespace}_${clusterName}_${region}_${accountId}` session = isValidSshHostname(proposedSession) @@ -224,13 +220,14 @@ function createValidSshSession( const components = [ sanitize(workspaceName, 63), // K8s limit sanitize(namespace, 63), // K8s limit - sanitize(clusterName, 63), // HP cluster limit + sanitize(clusterName, 100), // EKS limit sanitize(region, 16), // Longest AWS region limit sanitize(accountId, 12), // Fixed ].filter((c) => c.length > 0) - // Total: 63 + 63 + 63 + 16 + 12 + 4 separators + 3 chars for hostname header = 224 < 253 (max limit) + // Total: 63 + 63 + 100 + 16 + 12 + 4 separators + 3 chars for hostname header = 261 > 253 (max limit) + // If all attributes max out char limit, then accountId will be truncated to the first 4 char. - const session = components.join('_').substring(0, 224) + const session = components.join('_').substring(0, 253) return session } @@ -238,7 +235,7 @@ function createValidSshSession( * Validates if a string meets SSH hostname naming convention */ function isValidSshHostname(label: string): boolean { - return /^[a-z0-9]([a-z0-9.-_]{0,222}[a-z0-9])?$/.test(label) + return /^[a-z0-9]([a-z0-9.-_]{0,251}[a-z0-9])?$/.test(label) } export async function stopSpace( diff --git a/packages/core/src/awsService/sagemaker/uriHandlers.ts b/packages/core/src/awsService/sagemaker/uriHandlers.ts index 6a189698472..411fe5a789c 100644 --- a/packages/core/src/awsService/sagemaker/uriHandlers.ts +++ b/packages/core/src/awsService/sagemaker/uriHandlers.ts @@ -38,7 +38,7 @@ export function register(ctx: ExtContext) { undefined, params.workspaceName, params.namespace, - params.clusterArn + params.eksClusterArn ) }) } @@ -51,7 +51,7 @@ export function register(ctx: ExtContext) { export function parseHyperpodConnectParams(query: SearchParams) { const requiredParams = query.getFromKeysOrThrow('sessionId', 'streamUrl', 'sessionToken', 'cell-number') - const optionalParams = query.getFromKeys('workspaceName', 'namespace', 'clusterArn') + const optionalParams = query.getFromKeys('workspaceName', 'namespace', 'eksClusterArn') return { ...requiredParams, ...optionalParams } } export function parseConnectParams(query: SearchParams) { diff --git a/packages/core/src/shared/clients/kubectlClient.ts b/packages/core/src/shared/clients/kubectlClient.ts index a04f8111faf..e4fe11c6545 100644 --- a/packages/core/src/shared/clients/kubectlClient.ts +++ b/packages/core/src/shared/clients/kubectlClient.ts @@ -35,11 +35,9 @@ export interface HyperpodCluster { export class KubectlClient { private kubeConfig: k8s.KubeConfig private k8sApi: k8s.CustomObjectsApi - private hyperpodCluster: HyperpodCluster public constructor(eksCluster: Cluster, hyperpodCluster: HyperpodCluster) { this.kubeConfig = new k8s.KubeConfig() - this.hyperpodCluster = hyperpodCluster this.loadKubeConfig(eksCluster, hyperpodCluster) this.k8sApi = this.kubeConfig.makeApiClient(k8s.CustomObjectsApi) } @@ -262,18 +260,9 @@ export class KubectlClient { throw new Error('No workspace connection URL returned') } - const url = new URL(presignedUrl) - - // If eksClusterArn exists, remove it and add clusterArn instead - if (url.searchParams.has('eksClusterArn') && this.hyperpodCluster.clusterArn) { - url.searchParams.delete('eksClusterArn') - url.searchParams.set('clusterArn', this.hyperpodCluster.clusterArn) - } - - const modifiedUrl = url.toString() getLogger().info(`Connection Type: ${connectionType}`) - getLogger().info(`Modified Presigned URL: ${modifiedUrl}`) - return { type: connectionType || 'vscode-remote', url: modifiedUrl } + getLogger().info(`Presigned URL: ${presignedUrl}`) + return { type: connectionType || 'vscode-remote', url: presignedUrl } } catch (error) { getLogger().error(`[Hyperpod] Failed to create workspace connection: ${error}`) throw new Error( From cc2e00d06679248499458ed559f94c4113ac4498 Mon Sep 17 00:00:00 2001 From: Keen Wilson <40321520+keenwilson@users.noreply.github.com> Date: Thu, 22 Jan 2026 12:35:02 -0800 Subject: [PATCH 2/5] fix(auth): prompt window reload for stale console session credentials (#8514) ## Problem Console session credentials fail to work properly in two scenarios: 1. After token refresh or profile overwrite: Users encounter "Your session has expired" errors even after successfully running aws login, requiring manual VS Code restart without clear guidance (reported in #8488) 2. Immediate connection use after CLI login: When users try to use a connection immediately after aws login completes, the credential provider was created before the CLI wrote the new login session to disk, causing authentication failures ## Solution - Enhanced makeConsoleSessionCredentialsProvider() to detect stale credential scenarios and prompt for window reload final-2-after-succeeds - Added user-friendly messages showing identity ARN after successful login final-1-before-retry - Improved inline documentation explaining AWS CLI vs SDK credential handling differences - Added handling for does not contain login_session error when provider is created too early - Removed try-catch wrapper when verifying that connection exists after CLI succeeds ## Tradeoffs Reloading the VS Code window is a heavier UX, but it is the only deterministic way to fully reinitialize credential providers and avoid using stale credentials. Reloading guarantees that all in0memory credential providers, Auth state, and AWS SDK clients are fully reinitialized. This avoids subtle, hard-to-debug states where credentials appear refreshed on disk but stale credentials are still used at runtime. The reload is user-initiated via confirmation, not automatic. For future direction, this needs a supported Auth/provider reset mechanism to refresh credentials without requiring a window reload. ## Testing 1. Run `npm run compile` to verify build. 2. Run `npm run package`. Manual verification of end-to-end flow - Verified token refresh flow prompts for reload and works after reload - Tested immediate connection use after aws login triggers appropriate reload prompt - Confirmed profile overwrite scenarios handle credential refresh correctly --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --- packages/core/src/auth/consoleSessionUtils.ts | 73 +++------ .../providers/sharedCredentialsProvider.ts | 151 ++++++++++++------ .../webview/vue/toolkit/backend_toolkit.ts | 9 ++ .../sharedCredentialsProvider.test.ts | 122 +++++++++++++- ...-d54f0a97-febc-4b81-8956-0e0f3d9936a3.json | 4 + 5 files changed, 258 insertions(+), 101 deletions(-) create mode 100644 packages/toolkit/.changes/next-release/Bug Fix-d54f0a97-febc-4b81-8956-0e0f3d9936a3.json diff --git a/packages/core/src/auth/consoleSessionUtils.ts b/packages/core/src/auth/consoleSessionUtils.ts index fcda5de6a12..e05396de6a8 100644 --- a/packages/core/src/auth/consoleSessionUtils.ts +++ b/packages/core/src/auth/consoleSessionUtils.ts @@ -217,14 +217,6 @@ export async function authenticateWithConsoleLogin(profileName?: string, region? if (result.exitCode === 0) { telemetry.aws_consoleLoginCLISuccess.emit({ result: 'Succeeded' }) - // Show generic success message - void vscode.window.showInformationMessage( - localize( - 'AWS.message.success.consoleLogin', - 'Login with console credentials command completed. Profile "{0}" is now available.', - profileName - ) - ) logger.info('Login with console credentials command completed. Exit code: %d', result.exitCode) } else if (result.exitCode === 254) { logger.error( @@ -291,49 +283,36 @@ export async function authenticateWithConsoleLogin(profileName?: string, region? code: 'ConsoleLoginConfigError', }) } + // Show success message after seeing newly created session in the disk + void vscode.window.showInformationMessage( + `Profile "${profileName}" ready with credentials from ${profile.login_session} console session` + ) - // Activate the newly created profile - try { - logger.info(`Activating profile: ${profileName}`) - // Connection ID format is "profile:profileName" - const credentialsId: CredentialsId = { - credentialSource: 'profile', - credentialTypeId: profileName, - } - const connectionId = asString(credentialsId) - // Invalidate cached credentials to force fresh fetch - getLogger().info(`Invalidated cached credentials for ${connectionId}`) - globals.loginManager.store.invalidateCredentials(credentialsId) - logger.info(`Looking for connection with ID: ${connectionId}`) - - const connection = await Auth.instance.getConnection({ id: connectionId }) - if (connection === undefined) { - // Log available connections for debugging - const availableConnections = await Auth.instance.listConnections() - logger.error( - 'Connection not found. Available connections: %O', - availableConnections.map((c) => c.id) - ) - throw new ToolkitError(`Failed to get connection from profile: ${connectionId}`, { - code: 'MissingConnection', - }) - } + logger.info(`Activating profile: ${profileName}`) + const credentialsId: CredentialsId = { + credentialSource: 'profile', + credentialTypeId: profileName, + } + const connectionId = asString(credentialsId) + // Invalidate cached credentials to force fresh fetch + getLogger().info(`Invalidated cached credentials for ${connectionId}`) + globals.loginManager.store.invalidateCredentials(credentialsId) + logger.info(`Looking for connection with ID: ${connectionId}`) - // Don't call useConnection() - let credentials be fetched naturally when needed - await Auth.instance.updateConnectionState(connectionId, 'valid') - } catch (error: any) { - logger.error('Failed to activate profile: %O', error) - void vscode.window.showErrorMessage( - localize( - 'AWS.message.error.consoleLogin.profileActivationFailed', - 'Failed to activate profile: {0}', - error instanceof Error ? error.message : String(error) - ) + // Make sure that connection exists before letting other part use connection + const connection = await Auth.instance.getConnection({ id: connectionId }) + if (connection === undefined) { + // Log available connections for debugging + const availableConnections = await Auth.instance.listConnections() + logger.error( + 'Connection not found. Available connections: %O', + availableConnections.map((c) => c.id) ) - throw new ToolkitError('Failed to activate profile', { - code: 'ProfileActivationFailed', - cause: error as Error, + throw new ToolkitError(`Failed to get connection from profile: ${connectionId}`, { + code: 'MissingConnection', }) } + // Don't call useConnection() - let credentials be fetched naturally when needed + await Auth.instance.updateConnectionState(connectionId, 'valid') }) } diff --git a/packages/core/src/auth/providers/sharedCredentialsProvider.ts b/packages/core/src/auth/providers/sharedCredentialsProvider.ts index 19baa7d5dd1..c74eca406d8 100644 --- a/packages/core/src/auth/providers/sharedCredentialsProvider.ts +++ b/packages/core/src/auth/providers/sharedCredentialsProvider.ts @@ -11,6 +11,7 @@ import { ParsedIniData } from '@smithy/types' import { chain } from '@aws-sdk/property-provider' import { fromInstanceMetadata, fromContainerMetadata } from '@smithy/credential-provider-imds' import { fromEnv } from '@aws-sdk/credential-provider-env' +import * as localizedText from '../../shared/localizedText' import { getLogger } from '../../shared/logger/logger' import { getStringHash } from '../../shared/utilities/textUtilities' import { getMfaTokenFromUser, resolveProviderWithCancel } from '../credentials/utils' @@ -59,6 +60,96 @@ function isSsoProfile(profile: Profile): boolean { ) } +export async function handleInvalidConsoleCredentials( + error: Error, + profileName: string, + region: string +): Promise { + getLogger().error('Console login authentication failed for profile %s in region %s: %O', profileName, region, error) + + // Indicates that a VS Code window reload is required to reinitialize credential providers + // and avoid using stale console session credentials when login cache and in-memory state diverge. + let requiresVscodeReloadForCredentials = false + if ( + error.message.includes('Your session has expired') || + error.message.includes('Failed to load a token for session') || + error.message.includes('Failed to load token from') + ) { + requiresVscodeReloadForCredentials = true + // Ask for user confirmation before refreshing + const response = await vscode.window.showInformationMessage( + `Unable to use your console credentials for profile "${profileName}". Would you like to retry?`, + localizedText.retry, + localizedText.cancel + ) + + if (response !== localizedText.retry) { + throw ToolkitError.chain(error, 'User cancelled console credentials token refresh.', { + code: 'LoginSessionRefreshCancelled', + cancelled: true, + }) + } + + getLogger().info('Re-authenticating using console credentials for profile %s', profileName) + // Execute the console login command with the existing profile and region + try { + await vscode.commands.executeCommand('aws.toolkit.auth.consoleLogin', profileName, region) + } catch (_) { + void vscode.window.showErrorMessage( + `Unable to refresh your AWS credentials. Please run 'aws login --profile ${profileName}' in your terminal, then reload VS Code to continue.` + ) + } + } + + if (error.message.includes('does not contain login_session')) { + // The credential provider was created before the CLI wrote the new login session to disk. + // This happens when you run console login and immediately try to use the connection. + // A window reload is needed to pick up the newly created session. + requiresVscodeReloadForCredentials = true + } + + if (requiresVscodeReloadForCredentials) { + getLogger().info( + `Reloading window to sync with updated credentials cache using connection for profile: ${profileName}` + ) + const reloadResponse = await vscode.window.showInformationMessage( + `Credentials for "${profileName}" were updated. A window reload is required to apply them. Save your work before continuing. Reload now?`, + localizedText.yes, + localizedText.no + ) + if (reloadResponse === localizedText.yes) { + // At this point, the console credential cache on disk has been updated (via AWS CLI login), + // but the in-memory credential providers used by the Toolkit / AWS SDK were already + // constructed earlier and continue to reference stale credentials. + // + // Notes on behavior: + // - Console credentials are read once when the provider is created and are not reloaded + // dynamically at runtime. + // - Removing or recreating connections/profiles does not rebuild the underlying provider. + // - Filesystem watchers may detect cache changes, but provider instances still hold + // the originally loaded credentials. + // - Attempting to swap providers at runtime can introduce incompatibilities between + // legacy credential shims and AWS SDK v3 providers. + // + // Authentication flow (simplified): + // aws login (CLI) -> writes ~/.aws/login/cache + // Toolkit -> constructs credential provider (snapshots credentials in memory) + // SDK calls -> continue using in-memory credentials until provider is reinitialized + // + // A VS Code window reload is the only safe and deterministic way to fully reinitialize + // credential providers and ensure the updated console session credentials are used. + await vscode.commands.executeCommand('workbench.action.reloadWindow') + } + throw ToolkitError.chain(error, 'Console credentials require window reload', { + code: 'FromLoginCredentialProviderError', + }) + } + + throw ToolkitError.chain(error, 'Console credentials error', { + code: 'FromLoginCredentialProviderError', + }) +} + /** * Represents one profile from the AWS Shared Credentials files. */ @@ -400,6 +491,10 @@ export class SharedCredentialsProvider implements CredentialsProvider { const baseProvider = fromLoginCredentials({ profile: this.profileName, clientConfig: { + // Console session profiles created by 'aws login' may not have a region property + // The AWS CLI's philosophy is to treat global options like --region as per-invocation overrides + // rather than persistent configuration, minimizing what gets permanently stored in profiles + // and deferring configuration decisions until the actual command execution. region: defaultRegion, }, }) @@ -407,60 +502,10 @@ export class SharedCredentialsProvider implements CredentialsProvider { try { return await baseProvider() } catch (error) { - getLogger().error( - 'Console login authentication failed for profile %s in region %s: %O', - this.profileName, - defaultRegion, - error - ) - - if ( - error instanceof Error && - (error.message.includes('Your session has expired') || - error.message.includes('Failed to load a token for session') || - error.message.includes('Failed to load token from')) - ) { - // Ask for user confirmation before refreshing - const response = await vscode.window.showInformationMessage( - `Unable to use your console credentials for profile "${this.profileName}". Would you like to refresh it?`, - 'Refresh', - 'Cancel' - ) - - if (response !== 'Refresh') { - throw ToolkitError.chain(error, 'User cancelled console credentials token refresh.', { - code: 'LoginSessionRefreshCancelled', - cancelled: true, - }) - } - - getLogger().info('Re-authenticating using console credentials for profile %s', this.profileName) - // Execute the console login command with the existing profile and region - try { - await vscode.commands.executeCommand( - 'aws.toolkit.auth.consoleLogin', - this.profileName, - defaultRegion - ) - } catch (reAuthError) { - throw ToolkitError.chain( - reAuthError, - `Failed to refresh credentials for profile ${this.profileName}. Run 'aws login --profile ${this.profileName}' to authenticate.`, - { code: 'LoginSessionReAuthError' } - ) - } - - getLogger().info( - 'Authentication completed for profile %s, refreshing credentials...', - this.profileName - ) - - // Use the same provider instance but get fresh credentials - return await baseProvider() + if (error instanceof Error) { + await handleInvalidConsoleCredentials(error, this.profileName, defaultRegion) } - throw ToolkitError.chain(error, `Failed to get console credentials`, { - code: 'FromLoginCredentialProviderError', - }) + throw error } } } diff --git a/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts b/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts index 10a6c98a3c9..aea498071cb 100644 --- a/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts +++ b/packages/core/src/login/webview/vue/toolkit/backend_toolkit.ts @@ -21,6 +21,7 @@ import { CodeCatalystAuthenticationProvider } from '../../../../codecatalyst/aut import { AuthError, AuthFlowState } from '../types' import { setContext } from '../../../../shared/vscode/setContext' import { builderIdStartUrl } from '../../../../auth/sso/constants' +import { CredentialsId, asString } from '../../../../auth/providers/credentials' import { RegionProfile } from '../../../../codewhisperer/models/model' import { ProfileSwitchIntent } from '../../../../codewhisperer/region/regionProfileManager' import globals from '../../../../shared/extensionGlobals' @@ -93,6 +94,14 @@ export class ToolkitLoginWebview extends CommonAuthWebview { // Execute AWS CLI login command await vscode.commands.executeCommand('aws.toolkit.auth.consoleLogin', profileName, region) + // Use profile as active connection + const credentialsId: CredentialsId = { + credentialSource: 'profile', + credentialTypeId: profileName, + } + const connectionId = asString(credentialsId) + await Auth.instance.useConnection({ id: connectionId }) + // Hide auth view and show resource explorer await setContext('aws.explorer.showAuthView', false) await this.showResourceExplorer() diff --git a/packages/core/src/test/auth/providers/sharedCredentialsProvider.test.ts b/packages/core/src/test/auth/providers/sharedCredentialsProvider.test.ts index 71abe8f86b2..92a0d58466a 100644 --- a/packages/core/src/test/auth/providers/sharedCredentialsProvider.test.ts +++ b/packages/core/src/test/auth/providers/sharedCredentialsProvider.test.ts @@ -4,13 +4,20 @@ */ import assert from 'assert' -import { SharedCredentialsProvider } from '../../../auth/providers/sharedCredentialsProvider' +import { + SharedCredentialsProvider, + handleInvalidConsoleCredentials, +} from '../../../auth/providers/sharedCredentialsProvider' import { createTestSections } from '../../credentials/testUtil' import { DefaultStsClient } from '../../../shared/clients/stsClient' import { oneDay } from '../../../shared/datetime' import sinon from 'sinon' import { SsoAccessTokenProvider } from '../../../auth/sso/ssoAccessTokenProvider' import { SsoClient } from '../../../auth/sso/clients' +import * as vscode from 'vscode' +import * as localizedText from '../../../shared/localizedText' +import { ToolkitError } from '../../../shared/errors' +import { getTestWindow } from '../../shared/vscode/window' describe('SharedCredentialsProvider - Role Chaining with SSO', function () { let sandbox: sinon.SinonSandbox @@ -212,4 +219,117 @@ describe('SharedCredentialsProvider - Console Session', function () { assert.notStrictEqual(provider.validate(), undefined) assert.strictEqual(await provider.isAvailable(), false) }) + + describe('SharedCredentialsProvider - Console Session - handleInvalidConsoleCredentials', function () { + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + + afterEach(function () { + sandbox.restore() + }) + + it('throws error with expired session message and user cancels retry', async function () { + const error = new Error('Your session has expired') + getTestWindow().onDidShowMessage((m) => m.selectItem(localizedText.cancel)) + + await assert.rejects( + () => handleInvalidConsoleCredentials(error, 'test-profile', 'us-east-1'), + (err: ToolkitError) => { + assert.strictEqual(err.code, 'LoginSessionRefreshCancelled') + assert.strictEqual(err.cancelled, true) + return true + } + ) + }) + + it('executes console login and prompts reload when user retries expired session', async function () { + const error = new Error('Your session has expired') + let messageCount = 0 + getTestWindow().onDidShowMessage((m) => { + messageCount++ + m.selectItem(messageCount === 1 ? localizedText.retry : localizedText.yes) + }) + const executeCommandStub = sandbox.stub(vscode.commands, 'executeCommand') + + await assert.rejects(() => handleInvalidConsoleCredentials(error, 'test-profile', 'us-east-1')) + + assert.ok(executeCommandStub.calledWith('aws.toolkit.auth.consoleLogin', 'test-profile', 'us-east-1')) + assert.ok(executeCommandStub.calledWith('workbench.action.reloadWindow')) + }) + + it('handles failed token load error and prompts reload', async function () { + const error = new Error('Failed to load a token for session') + let messageCount = 0 + getTestWindow().onDidShowMessage((m) => { + messageCount++ + m.selectItem(messageCount === 1 ? localizedText.retry : localizedText.no) + }) + sandbox.stub(vscode.commands, 'executeCommand') + + await assert.rejects( + () => handleInvalidConsoleCredentials(error, 'test-profile', 'us-east-1'), + (err: ToolkitError) => { + assert.strictEqual(err.code, 'FromLoginCredentialProviderError') + return true + } + ) + }) + + it('handles missing login_session error and prompts reload', async function () { + const error = new Error('does not contain login_session') + getTestWindow().onDidShowMessage((m) => m.selectItem(localizedText.yes)) + const executeCommandStub = sandbox.stub(vscode.commands, 'executeCommand') + + await assert.rejects(() => handleInvalidConsoleCredentials(error, 'test-profile', 'us-east-1')) + + assert.ok(executeCommandStub.calledWith('workbench.action.reloadWindow')) + }) + + it('shows error message when console login command fails', async function () { + const error = new Error('Your session has expired') + getTestWindow().onDidShowMessage((m) => { + if (m.items.length > 0) { + m.selectItem(m.message.includes('retry') ? localizedText.retry : localizedText.yes) + } + }) + const executeCommandStub = sandbox.stub(vscode.commands, 'executeCommand') + executeCommandStub.withArgs('aws.toolkit.auth.consoleLogin').rejects(new Error('Login failed')) + + await assert.rejects(() => handleInvalidConsoleCredentials(error, 'test-profile', 'us-east-1')) + + const errorMessage = getTestWindow().shownMessages.find((m) => m.message.includes('aws login --profile')) + assert.ok(errorMessage) + }) + + it('does not prompt reload for non-session errors', async function () { + const error = new Error('Some other error') + + await assert.rejects( + () => handleInvalidConsoleCredentials(error, 'test-profile', 'us-east-1'), + (err: ToolkitError) => { + assert.strictEqual(err.code, 'FromLoginCredentialProviderError') + return true + } + ) + + assert.strictEqual(getTestWindow().shownMessages.length, 0) + }) + + it('handles Failed to load token from error message', async function () { + const error = new Error('Failed to load token from cache') + let messageCount = 0 + getTestWindow().onDidShowMessage((m) => { + messageCount++ + m.selectItem(messageCount === 1 ? localizedText.retry : localizedText.no) + }) + sandbox.stub(vscode.commands, 'executeCommand') + + await assert.rejects(() => handleInvalidConsoleCredentials(error, 'test-profile', 'us-east-1')) + + assert.strictEqual(messageCount, 2) + }) + }) }) diff --git a/packages/toolkit/.changes/next-release/Bug Fix-d54f0a97-febc-4b81-8956-0e0f3d9936a3.json b/packages/toolkit/.changes/next-release/Bug Fix-d54f0a97-febc-4b81-8956-0e0f3d9936a3.json new file mode 100644 index 00000000000..4742b23eab9 --- /dev/null +++ b/packages/toolkit/.changes/next-release/Bug Fix-d54f0a97-febc-4b81-8956-0e0f3d9936a3.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "Console session credentials now prompt for window reload when stale, eliminating manual VS Code restarts after token refresh or profile updates (#8488)" +} From dae88c8344c34f6693b42cfeb3849c5829fb05d7 Mon Sep 17 00:00:00 2001 From: invictus <149003065+ashishrp-aws@users.noreply.github.com> Date: Thu, 22 Jan 2026 17:47:20 -0800 Subject: [PATCH 3/5] build(amazonq): merge release candidate version rc-20260122 (#8526) This merges the released changes for rc-20260122 into main. MCM-XXX --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: aws-toolkit-automation <> --- package-lock.json | 7 ++----- packages/toolkit/.changes/3.94.0.json | 10 ++++++++++ .../Bug Fix-d54f0a97-febc-4b81-8956-0e0f3d9936a3.json | 4 ---- packages/toolkit/CHANGELOG.md | 4 ++++ packages/toolkit/package.json | 2 +- 5 files changed, 17 insertions(+), 10 deletions(-) create mode 100644 packages/toolkit/.changes/3.94.0.json delete mode 100644 packages/toolkit/.changes/next-release/Bug Fix-d54f0a97-febc-4b81-8956-0e0f3d9936a3.json diff --git a/package-lock.json b/package-lock.json index b144b43e595..a0a7e36a064 100644 --- a/package-lock.json +++ b/package-lock.json @@ -32861,10 +32861,6 @@ "ajv": "^6.9.1" } }, - "node_modules/amazon-q-vscode": { - "resolved": "packages/amazonq", - "link": true - }, "node_modules/amazon-states-language-service": { "version": "1.16.1", "license": "MIT", @@ -44734,6 +44730,7 @@ "packages/amazonq": { "name": "amazon-q-vscode", "version": "1.108.0-SNAPSHOT", + "extraneous": true, "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" @@ -47029,7 +47026,7 @@ }, "packages/toolkit": { "name": "aws-toolkit-vscode", - "version": "3.94.0-SNAPSHOT", + "version": "3.95.0-SNAPSHOT", "license": "Apache-2.0", "dependencies": { "aws-core-vscode": "file:../core/" diff --git a/packages/toolkit/.changes/3.94.0.json b/packages/toolkit/.changes/3.94.0.json new file mode 100644 index 00000000000..fec7ce9dbb1 --- /dev/null +++ b/packages/toolkit/.changes/3.94.0.json @@ -0,0 +1,10 @@ +{ + "date": "2026-01-23", + "version": "3.94.0", + "entries": [ + { + "type": "Bug Fix", + "description": "Console session credentials now prompt for window reload when stale, eliminating manual VS Code restarts after token refresh or profile updates (#8488)" + } + ] +} \ No newline at end of file diff --git a/packages/toolkit/.changes/next-release/Bug Fix-d54f0a97-febc-4b81-8956-0e0f3d9936a3.json b/packages/toolkit/.changes/next-release/Bug Fix-d54f0a97-febc-4b81-8956-0e0f3d9936a3.json deleted file mode 100644 index 4742b23eab9..00000000000 --- a/packages/toolkit/.changes/next-release/Bug Fix-d54f0a97-febc-4b81-8956-0e0f3d9936a3.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "type": "Bug Fix", - "description": "Console session credentials now prompt for window reload when stale, eliminating manual VS Code restarts after token refresh or profile updates (#8488)" -} diff --git a/packages/toolkit/CHANGELOG.md b/packages/toolkit/CHANGELOG.md index 95ca0e98298..c5471d57869 100644 --- a/packages/toolkit/CHANGELOG.md +++ b/packages/toolkit/CHANGELOG.md @@ -1,3 +1,7 @@ +## 3.94.0 2026-01-23 + +- **Bug Fix** Console session credentials now prompt for window reload when stale, eliminating manual VS Code restarts after token refresh or profile updates (#8488) + ## 3.93.0 2026-01-15 - **Bug Fix** AWS CLI update process could enter an infinite retry loop when outdated CLI is detected during console login. The Toolkit now attempts the update once and prompts users to manually reload and retry, preventing continuous failed authentication attempts. diff --git a/packages/toolkit/package.json b/packages/toolkit/package.json index b84194b2e9c..f3b965dd6b2 100644 --- a/packages/toolkit/package.json +++ b/packages/toolkit/package.json @@ -2,7 +2,7 @@ "name": "aws-toolkit-vscode", "displayName": "AWS Toolkit", "description": "Including CodeCatalyst, Infrastructure Composer, and support for Lambda, S3, CloudWatch Logs, CloudFormation, and many other services.", - "version": "3.94.0-SNAPSHOT", + "version": "3.95.0-SNAPSHOT", "extensionKind": [ "workspace" ], From f1f08273d1786d967593e856433074850e44e9ac Mon Sep 17 00:00:00 2001 From: Keen Wilson <40321520+keenwilson@users.noreply.github.com> Date: Mon, 26 Jan 2026 10:53:31 -0800 Subject: [PATCH 4/5] fix(auth): show actual AWS CLI path in update success message (#8519) ## Problem After updating AWS CLI for console credentials, the success message didn't show users which AWS CLI installation the Toolkit was actually using. This made it unclear whether the update was successful or if multiple CLI installations existed on the system. This is particularly problematic for users who encountered repeated update prompts during console credentials authentication, as they can't verify if the correct CLI version is being used. ## Solution - Added child process execution to run the OS-specific command (which on Unix/macOS, where on Windows) after CLI update - Retrieved the actual AWS CLI path that the Toolkit uses for console credentials - Updated the success message to display: 'AWS CLI updated successfully to "{path}"' This helps users immediately verify the CLI installation location and confirms which CLI binary the Toolkit will use for console login. ### Notes We make a display message clearer that this is about what the Toolkit will be used going forward, not necessarily what the installer was just installed. This helps distinguish between: - What the installer just updated/installed - What the Toolkit will actually use (which could be a different installation if multiple exist) cli-1-detect-need-to-update cli-2-installer-proceeds cli-3-show-path-in-message --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license. --------- Co-authored-by: invictus <149003065+ashishrp-aws@users.noreply.github.com> --- .../src/shared/telemetry/vscodeTelemetry.json | 3 +- .../core/src/shared/utilities/cliUtils.ts | 32 ++++++- .../test/shared/utilities/cliUtils.test.ts | 85 ++++++++++++++++++- ...-b1fd8c29-a44d-46a5-8b88-8dcdc2e4dab3.json | 4 + 4 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 packages/toolkit/.changes/next-release/Bug Fix-b1fd8c29-a44d-46a5-8b88-8dcdc2e4dab3.json diff --git a/packages/core/src/shared/telemetry/vscodeTelemetry.json b/packages/core/src/shared/telemetry/vscodeTelemetry.json index 29aaec12ed1..a54d3ae460f 100644 --- a/packages/core/src/shared/telemetry/vscodeTelemetry.json +++ b/packages/core/src/shared/telemetry/vscodeTelemetry.json @@ -11,7 +11,8 @@ "aws-cli", "sam-cli", "docker", - "finch" + "finch", + "path-resolver" ] }, { diff --git a/packages/core/src/shared/utilities/cliUtils.ts b/packages/core/src/shared/utilities/cliUtils.ts index 67615ef0374..1f785d056f4 100644 --- a/packages/core/src/shared/utilities/cliUtils.ts +++ b/packages/core/src/shared/utilities/cliUtils.ts @@ -55,7 +55,10 @@ interface Cli { exec?: string } -export type AwsClis = Extract +export type AwsClis = Extract< + ToolId, + 'session-manager-plugin' | 'aws-cli' | 'sam-cli' | 'docker' | 'finch' | 'path-resolver' +> /** * CLIs and their full filenames and download paths for their respective OSes @@ -185,6 +188,15 @@ export const awsClis: { [cli in AwsClis]: Cli } = { manualInstallLink: 'https://runfinch.com/docs/getting-started/installation/', exec: 'finch', }, + 'path-resolver': { + command: { + windows: ['where'], + unix: ['which'], + }, + source: {}, // OS utilities used to locate files and executables + name: 'Path resolver', + manualInstallLink: '', + }, } /** @@ -579,6 +591,24 @@ export async function updateAwsCli(): Promise { } const result = await installCli('aws-cli', false) + + // Get the which/ where command, run it to find the AWS CLI path, and display it to the user + const whichCommands = getOsCommands(awsClis['path-resolver']) + if (whichCommands && whichCommands.length > 0) { + const whichCmd = whichCommands[0] + const cliExec = awsClis['aws-cli'].exec + if (cliExec) { + getLogger().info(`Running "${whichCmd} ${cliExec}" to find AWS CLI path`) + const whichResult = await new ChildProcess(whichCmd, [cliExec]).run() + if (whichResult.exitCode === 0 && whichResult.stdout) { + const cliPath = whichResult.stdout.trim().split('\n')[0] + const cliInUseMessage = `Toolkit is using AWS CLI at "${cliPath}".` + getLogger().info(cliInUseMessage) + void vscode.window.showInformationMessage(cliInUseMessage) + } + } + } + return result } diff --git a/packages/core/src/test/shared/utilities/cliUtils.test.ts b/packages/core/src/test/shared/utilities/cliUtils.test.ts index 9fa7270abb1..5bda7de2647 100644 --- a/packages/core/src/test/shared/utilities/cliUtils.test.ts +++ b/packages/core/src/test/shared/utilities/cliUtils.test.ts @@ -5,16 +5,25 @@ import assert from 'assert' import * as path from 'path' -import { installCli } from '../../../shared/utilities/cliUtils' +import sinon from 'sinon' +import { installCli, updateAwsCli } from '../../../shared/utilities/cliUtils' import globals from '../../../shared/extensionGlobals' import { ChildProcess } from '../../../shared/utilities/processUtils' import { SeverityLevel } from '../vscode/message' import { assertTelemetryCurried } from '../../testUtil' import { getTestWindow } from '../../shared/vscode/window' import { fs } from '../../../shared' +import { getOpenExternalStub } from '../../globalSetup.test' describe('cliUtils', async function () { + let sandbox: sinon.SinonSandbox + + beforeEach(function () { + sandbox = sinon.createSandbox() + }) + afterEach(async function () { + sandbox.restore() await fs.delete(path.join(globals.context.globalStorageUri.fsPath, 'tools'), { recursive: true, force: true }) }) @@ -61,4 +70,78 @@ describe('cliUtils', async function () { }) }) }) + + describe('updateAwsCli', function () { + it('cancels when user does not confirm update', async function () { + getTestWindow().onDidShowMessage((m) => m.close()) + + await assert.rejects(() => updateAwsCli(), /cancelled/) + }) + + it('installs CLI and shows path when user confirms', async function () { + let messageCount = 0 + getTestWindow().onDidShowMessage((m) => { + messageCount++ + if (messageCount === 1 && m.items.some((i) => i.title === 'Update')) { + m.selectItem('Update') + } else if (m.message.includes('not supported')) { + // Toolkit shows "Auto install of AWS CLI is not supported on your platform" + // with "Install manually..." button. We close this message to simulate the user dismissing it. + m.close() + } + }) + sandbox.stub(ChildProcess.prototype, 'run').resolves({ + exitCode: 0, + stdout: '/usr/local/bin/aws\n', + stderr: '', + } as any) + + if (process.platform === 'linux') { + await assert.rejects(() => updateAwsCli(), /cancelled/) + } else { + const result = await updateAwsCli() + assert.ok(result) + const messages = getTestWindow().shownMessages + assert.ok(messages.some((m) => m.message.includes('/usr/local/bin/aws'))) + } + }) + + it('opens manual install link when user selects "Install manually" on Linux', async function () { + let messageCount = 0 + const openExternalStub = getOpenExternalStub().resolves(true) + + getTestWindow().onDidShowMessage((m) => { + messageCount++ + if (messageCount === 1 && m.items.some((i) => i.title === 'Update')) { + m.selectItem('Update') + } else if (m.message.includes('not supported')) { + // Linux: no installer source defined, so auto-install shows "not supported" message. + // Simulate user clicking "Install manually..." button + const manualInstallButton = m.items.find((i) => i.title === 'Install manually...') + if (manualInstallButton) { + m.selectItem(manualInstallButton.title) + } + } + }) + + if (process.platform === 'linux') { + await assert.rejects(() => updateAwsCli(), /cancelled/) + + // Verify the manual install link was opened + assert.ok(openExternalStub.calledOnce) + const openedUrl = openExternalStub.firstCall.args.toString() + assert.ok(openedUrl.includes('getting-started-install.html')) + } else { + // Non-Linux platforms should succeed (existing behavior) + sandbox.stub(ChildProcess.prototype, 'run').resolves({ + exitCode: 0, + stdout: '/usr/local/bin/aws', + stderr: '', + } as any) + + const result = await updateAwsCli() + assert.ok(result) + } + }) + }) }) diff --git a/packages/toolkit/.changes/next-release/Bug Fix-b1fd8c29-a44d-46a5-8b88-8dcdc2e4dab3.json b/packages/toolkit/.changes/next-release/Bug Fix-b1fd8c29-a44d-46a5-8b88-8dcdc2e4dab3.json new file mode 100644 index 00000000000..1c61937efa4 --- /dev/null +++ b/packages/toolkit/.changes/next-release/Bug Fix-b1fd8c29-a44d-46a5-8b88-8dcdc2e4dab3.json @@ -0,0 +1,4 @@ +{ + "type": "Bug Fix", + "description": "AWS CLI update success message now shows the actual CLI installation path that the Toolkit uses for console credentials." +} From afddd9d47d902f7a35726b03c886744a82b82357 Mon Sep 17 00:00:00 2001 From: laileni Date: Tue, 27 Jan 2026 15:41:00 -0800 Subject: [PATCH 5/5] fix(toolkit): fix CI builds --- .github/workflows/node.js.yml | 4 ++-- .github/workflows/release.yml | 8 +------- .github/workflows/release_notes.md | 1 - packages/core/src/test/shared/vscode/env.test.ts | 6 ------ packages/core/src/testLint/eslint.test.ts | 1 - 5 files changed, 3 insertions(+), 17 deletions(-) diff --git a/.github/workflows/node.js.yml b/.github/workflows/node.js.yml index 97cb46fd71f..705be4630ff 100644 --- a/.github/workflows/node.js.yml +++ b/.github/workflows/node.js.yml @@ -126,7 +126,7 @@ jobs: matrix: node-version: [18.x] vscode-version: [minimum, stable, insiders] - package: [amazonq, toolkit] + package: [toolkit] env: VSCODE_TEST_VERSION: ${{ matrix.vscode-version }} NODE_OPTIONS: '--max-old-space-size=8192' @@ -251,7 +251,7 @@ jobs: matrix: node-version: [18.x] vscode-version: [stable, insiders] - package: [amazonq, toolkit] + package: [toolkit] env: VSCODE_TEST_VERSION: ${{ matrix.vscode-version }} NODE_OPTIONS: '--max-old-space-size=8192' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 60863f3b5a4..64d76c32247 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -25,9 +25,7 @@ jobs: feature: ${{ steps.build.outputs.feature }} tagname: ${{ steps.build.outputs.tagname }} toolkit_version: ${{ steps.build.outputs.toolkit_version }} - amazonq_version: ${{ steps.build.outputs.amazonq_version }} toolkit_changes: ${{ steps.build.outputs.toolkit_changes }} - amazonq_changes: ${{ steps.build.outputs.amazonq_changes }} steps: - uses: actions/checkout@v4 with: @@ -57,9 +55,8 @@ jobs: - run: npm ci - name: vsix run: | - npm run createRelease -w packages/toolkit -w packages/amazonq # Generate CHANGELOG.md + npm run createRelease -w packages/toolkit # Generate CHANGELOG.md npm run -w packages/toolkit package -- --feature "$FEAT_NAME" - npm run -w packages/amazonq package -- --feature "$FEAT_NAME" - uses: actions/upload-artifact@v4 with: name: artifacts @@ -80,7 +77,6 @@ jobs: echo "feature=$FEAT_NAME" >> $GITHUB_OUTPUT echo "tagname=$TAG_NAME" >> $GITHUB_OUTPUT write_package_info toolkit - write_package_info amazonq publish: needs: [package] @@ -94,13 +90,11 @@ jobs: FEAT_NAME: ${{ needs.package.outputs.feature }} TAG_NAME: ${{ needs.package.outputs.tagname }} AWS_TOOLKIT_VERSION: ${{ needs.package.outputs.toolkit_version }} - AMAZON_Q_VERSION: ${{ needs.package.outputs.amazonq_version }} # # Used in release_notes.md # BRANCH: ${{ github.ref_name }} AWS_TOOLKIT_CHANGES: ${{ needs.package.outputs.toolkit_changes }} - AMAZON_Q_CHANGES: ${{ needs.package.outputs.amazonq_changes }} permissions: contents: write steps: diff --git a/.github/workflows/release_notes.md b/.github/workflows/release_notes.md index cd9bc191983..2a799783854 100644 --- a/.github/workflows/release_notes.md +++ b/.github/workflows/release_notes.md @@ -3,7 +3,6 @@ This is an **unsupported preview build** of the `${BRANCH}` branch of AWS IDE Ex # Install 1. Download the vsix file(s) from "Assets" below. - - Amazon Q $AMAZON_Q_VERSION is provided by `amazon-q-vscode….vsix` - AWS Toolkit $AWS_TOOLKIT_VERSION is provided by `aws-toolkit-vscode….vsix` 2. Run `Extensions: Install from VSIX...` from the VSCode [command palette](https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette) and choose the vsix file(s). diff --git a/packages/core/src/test/shared/vscode/env.test.ts b/packages/core/src/test/shared/vscode/env.test.ts index a71aca33e8d..21b3c7bb380 100644 --- a/packages/core/src/test/shared/vscode/env.test.ts +++ b/packages/core/src/test/shared/vscode/env.test.ts @@ -92,15 +92,9 @@ describe('env', function () { it('isBeta', async () => { // HACK: read each package.json because env.ts thinks version is "testPluginVersion" during testing. const toolkitPath = path.join(__dirname, '../../../../../../toolkit/package.json') - const amazonqPath = path.join(__dirname, '../../../../../../amazonq/package.json') const toolkit = JSON.parse(await fs.readFileText(toolkitPath)) - const amazonq = JSON.parse(await fs.readFileText(amazonqPath)) const toolkitVer = toolkit.version as string - const amazonqVer = amazonq.version as string const toolkitBeta = toolkitVer.startsWith('99.') - const amazonqBeta = amazonqVer.startsWith('99.') - - assert(toolkitBeta === amazonqBeta) const expected = toolkitBeta assert.strictEqual(isBeta(), expected) }) diff --git a/packages/core/src/testLint/eslint.test.ts b/packages/core/src/testLint/eslint.test.ts index fc3607008bb..040553f92bf 100644 --- a/packages/core/src/testLint/eslint.test.ts +++ b/packages/core/src/testLint/eslint.test.ts @@ -31,7 +31,6 @@ describe('eslint', function () { '**/src/testFixtures/**', '--ext', '.ts', - '../amazonq', '../core', '../toolkit', // TODO: fix lint issues in scripts/