Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
5cb7d9b
feat(lambda): Merging Feature/lambda console2 ide to staging branch (…
laileni-aws Jul 3, 2025
07791af
Merge public/master to private/staging
aws-toolkit-automation Jul 3, 2025
f3aadd2
Merge public/master to private/staging
aws-toolkit-automation Jul 3, 2025
d7f21e0
Merge public/master to private/staging
aws-toolkit-automation Jul 3, 2025
cf2f028
Merge public/master to private/staging
aws-toolkit-automation Jul 3, 2025
212c330
Merge public/master to private/staging
aws-toolkit-automation Jul 3, 2025
f707638
Merge public/master to private/staging
aws-toolkit-automation Jul 3, 2025
839c799
Merge public/master to private/staging
aws-toolkit-automation Jul 7, 2025
cbb26a8
Merge public/master to private/staging
aws-toolkit-automation Jul 7, 2025
bdc473d
Merge public/master to private/staging
aws-toolkit-automation Jul 7, 2025
6399327
Merge public/master to private/staging
aws-toolkit-automation Jul 7, 2025
5e5400e
Merge public/master to private/staging
aws-toolkit-automation Jul 7, 2025
bef0940
Merge public/master to private/staging
aws-toolkit-automation Jul 7, 2025
c26f3b3
Merge public/master to private/staging
aws-toolkit-automation Jul 7, 2025
4deb3b3
Merge public/master to private/staging
aws-toolkit-automation Jul 8, 2025
ba8fce1
Merge public/master to private/staging
aws-toolkit-automation Jul 8, 2025
8c4a9f4
Merge public/master to private/staging
aws-toolkit-automation Jul 8, 2025
a6ee9b4
Merge public/master to private/staging
aws-toolkit-automation Jul 8, 2025
3b2a161
Merge public/master to private/staging
aws-toolkit-automation Jul 9, 2025
9c44261
Merge public/master to private/staging
aws-toolkit-automation Jul 9, 2025
1157292
feat(sagemaker): Add deeplink space reconnect logic (#2155)
aws-asolidu Jul 10, 2025
39efe43
fix(sagemaker): manual filtering of spaces per region (#2154)
NewtonDer Jul 10, 2025
92e11ee
Merge public/master to private/staging
aws-toolkit-automation Jul 10, 2025
27387c9
Merge public/master to private/staging
aws-toolkit-automation Jul 10, 2025
762d4e7
Merge public/master to private/staging
aws-toolkit-automation Jul 10, 2025
647f457
Merge public/master to private/staging
aws-toolkit-automation Jul 10, 2025
0bd4d8b
Merge public/master to private/staging
aws-toolkit-automation Jul 11, 2025
8d2fdfc
Merge public/master to private/staging
aws-toolkit-automation Jul 11, 2025
f7a7e25
Merge public/master to private/staging
aws-toolkit-automation Jul 11, 2025
8138566
Merge public/master to private/staging
aws-toolkit-automation Jul 11, 2025
117e03a
Merge public/master to private/staging
aws-toolkit-automation Jul 11, 2025
fe02ffc
Merge public/master to private/staging
aws-toolkit-automation Jul 13, 2025
93a4f9d
Merge public/master to private/staging
aws-toolkit-automation Jul 14, 2025
8d7732c
feat(sagemaker): Add Autoshutdown support and Fix connect to capitali…
aws-asolidu Jul 14, 2025
db0f3b4
Merge public/master to private/staging
aws-toolkit-automation Jul 14, 2025
dee06a0
Merge public/master to private/staging
aws-toolkit-automation Jul 14, 2025
b63bf21
Merge public/master to private/staging
aws-toolkit-automation Jul 14, 2025
7d48d48
Merge public/master to private/staging
aws-toolkit-automation Jul 14, 2025
6d9764b
Merge public/master to private/staging
aws-toolkit-automation Jul 15, 2025
3c1c282
feat(sagemaker): Show notification if instanceType has insufficient m…
NewtonDer Jul 15, 2025
ac33d67
fix(sagemaker): GetStatus error when refreshing large number of space…
aws-asolidu Jul 15, 2025
394eda9
Merge public/master to private/staging
aws-toolkit-automation Jul 15, 2025
cb1b95d
Merge public/master to private/staging
aws-toolkit-automation Jul 15, 2025
3008405
fix(sagemaker): Show error message when trying to connect remotely fr…
NewtonDer Jul 15, 2025
42f8826
Merge public/master to private/staging
aws-toolkit-automation Jul 15, 2025
2599c04
Merge public/master to private/staging
aws-toolkit-automation Jul 15, 2025
59f35fb
Merge public/master to private/staging
aws-toolkit-automation Jul 15, 2025
a9fc649
Merge branch 'aws:master' into master
laileni-aws Jul 16, 2025
563d128
Add changelog entries for bug fixes
Jul 15, 2025
76eabd1
Add changelog entries for remaining bug fixes and features
Jul 16, 2025
6f93279
fix: removing unwanted file
laileni-aws Jul 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions packages/core/src/awsService/sagemaker/activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,27 @@
* SPDX-License-Identifier: Apache-2.0
*/

import * as path from 'path'
import * as vscode from 'vscode'
import { Commands } from '../../shared/vscode/commands2'
import { SagemakerSpaceNode } from './explorer/sagemakerSpaceNode'
import { SagemakerParentNode } from './explorer/sagemakerParentNode'
import * as uriHandlers from './uriHandlers'
import { openRemoteConnect, filterSpaceAppsByDomainUserProfiles, stopSpace } from './commands'
import { updateIdleFile, startMonitoringTerminalActivity, ActivityCheckInterval } from './utils'
import { ExtContext } from '../../shared/extensions'
import { telemetry } from '../../shared/telemetry/telemetry'
import { isSageMaker, UserActivity } from '../../shared/extensionUtilities'

let terminalActivityInterval: NodeJS.Timeout | undefined

export async function activate(ctx: ExtContext): Promise<void> {
ctx.extensionContext.subscriptions.push(
uriHandlers.register(ctx),
Commands.register('aws.sagemaker.openRemoteConnection', async (node: SagemakerSpaceNode) => {
if (!validateNode(node)) {
return
}
await telemetry.sagemaker_openRemoteConnection.run(async () => {
await openRemoteConnect(node, ctx.extensionContext)
})
Expand All @@ -27,9 +36,47 @@ export async function activate(ctx: ExtContext): Promise<void> {
}),

Commands.register('aws.sagemaker.stopSpace', async (node: SagemakerSpaceNode) => {
if (!validateNode(node)) {
return
}
await telemetry.sagemaker_stopSpace.run(async () => {
await stopSpace(node, ctx.extensionContext)
})
})
)

// If running in SageMaker AI Space, track user activity for autoshutdown feature
if (isSageMaker('SMAI')) {
// Use /tmp/ directory so the file is cleared on each reboot to prevent stale timestamps.
const tmpDirectory = '/tmp/'
const idleFilePath = path.join(tmpDirectory, '.sagemaker-last-active-timestamp')

const userActivity = new UserActivity(ActivityCheckInterval)
userActivity.onUserActivity(() => updateIdleFile(idleFilePath))

terminalActivityInterval = startMonitoringTerminalActivity(idleFilePath)

// Write initial timestamp
await updateIdleFile(idleFilePath)

ctx.extensionContext.subscriptions.push(userActivity, {
dispose: () => {
if (terminalActivityInterval) {
clearInterval(terminalActivityInterval)
terminalActivityInterval = undefined
}
},
})
}
}

/**
* Checks if a node is undefined and shows a warning message if so.
*/
function validateNode(node: unknown): boolean {
if (!node) {
void vscode.window.showWarningMessage('Space information is being refreshed. Please try again shortly.')
return false
}
return true
}
59 changes: 38 additions & 21 deletions packages/core/src/awsService/sagemaker/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import { ExtContext } from '../../shared/extensions'
import { SagemakerClient } from '../../shared/clients/sagemaker'
import { ToolkitError } from '../../shared/errors'
import { showConfirmationMessage } from '../../shared/utilities/messages'
import { RemoteSessionError } from '../../shared/remoteSession'
import { ConnectFromRemoteWorkspaceMessage, InstanceTypeError } from './constants'

const localize = nls.loadMessageBundle()

Expand Down Expand Up @@ -90,34 +92,36 @@ export async function deeplinkConnect(
)

if (isRemoteWorkspace()) {
void vscode.window.showErrorMessage(
'You are in a remote workspace, skipping deeplink connect. Please open from a local workspace.'
)
void vscode.window.showErrorMessage(ConnectFromRemoteWorkspaceMessage)
return
}

const remoteEnv = await prepareDevEnvConnection(
connectionIdentifier,
ctx.extensionContext,
'sm_dl',
session,
wsUrl,
token,
domain
)

try {
const remoteEnv = await prepareDevEnvConnection(
connectionIdentifier,
ctx.extensionContext,
'sm_dl',
session,
wsUrl,
token,
domain
)

await startVscodeRemote(
remoteEnv.SessionProcess,
remoteEnv.hostname,
'/home/sagemaker-user',
remoteEnv.vscPath,
'sagemaker-user'
)
} catch (err) {
} catch (err: any) {
getLogger().error(
`sm:OpenRemoteConnect: Unable to connect to target space with arn: ${connectionIdentifier} error: ${err}`
)

if (![RemoteSessionError.MissingExtension, RemoteSessionError.ExtensionVersionTooLow].includes(err.code)) {
throw err
}
}
}

Expand Down Expand Up @@ -156,16 +160,29 @@ export async function stopSpace(node: SagemakerSpaceNode, ctx: vscode.ExtensionC
}

export async function openRemoteConnect(node: SagemakerSpaceNode, ctx: vscode.ExtensionContext) {
if (isRemoteWorkspace()) {
void vscode.window.showErrorMessage(ConnectFromRemoteWorkspaceMessage)
return
}

if (node.getStatus() === 'Stopped') {
const client = new SagemakerClient(node.regionCode)
await client.startSpace(node.spaceApp.SpaceName!, node.spaceApp.DomainId!)
await tryRefreshNode(node)
const appType = node.spaceApp.SpaceSettingsSummary?.AppType
if (!appType) {
throw new ToolkitError('AppType is undefined for the selected space. Cannot start remote connection.')

try {
await client.startSpace(node.spaceApp.SpaceName!, node.spaceApp.DomainId!)
await tryRefreshNode(node)
const appType = node.spaceApp.SpaceSettingsSummary?.AppType
if (!appType) {
throw new ToolkitError('AppType is undefined for the selected space. Cannot start remote connection.')
}
await client.waitForAppInService(node.spaceApp.DomainId!, node.spaceApp.SpaceName!, appType)
await tryRemoteConnection(node, ctx)
} catch (err: any) {
// Ignore InstanceTypeError since it means the user decided not to use an instanceType with more memory
if (err.code !== InstanceTypeError) {
throw err
}
}
await client.waitForAppInService(node.spaceApp.DomainId!, node.spaceApp.SpaceName!, appType)
await tryRemoteConnection(node, ctx)
} else if (node.getStatus() === 'Running') {
await tryRemoteConnection(node, ctx)
}
Expand Down
31 changes: 31 additions & 0 deletions packages/core/src/awsService/sagemaker/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

export const ConnectFromRemoteWorkspaceMessage =
'Unable to establish new remote connection. Your last active VS Code window is connected to a remote workspace. To open a new SageMaker Studio connection, select your local VS Code window and try again.'

export const InstanceTypeError = 'InstanceTypeError'

export const InstanceTypeMinimum = 'ml.t3.large'

export const InstanceTypeInsufficientMemory: Record<string, string> = {
'ml.t3.medium': 'ml.t3.large',
'ml.c7i.large': 'ml.c7i.xlarge',
'ml.c6i.large': 'ml.c6i.xlarge',
'ml.c6id.large': 'ml.c6id.xlarge',
'ml.c5.large': 'ml.c5.xlarge',
}

export const InstanceTypeInsufficientMemoryMessage = (
spaceName: string,
chosenInstanceType: string,
recommendedInstanceType: string
) => {
return `Unable to create app for [${spaceName}] because instanceType [${chosenInstanceType}] is not supported for remote access enabled spaces. Use instanceType with at least 8 GiB memory. Would you like to start your space with instanceType [${recommendedInstanceType}]?`
}

export const InstanceTypeNotSelectedMessage = (spaceName: string) => {
return `No instanceType specified for [${spaceName}]. ${InstanceTypeMinimum} is the default instance type, which meets minimum 8 GiB memory requirements for remote access. Continuing will start your space with instanceType [${InstanceTypeMinimum}] and remotely connect.`
}
12 changes: 8 additions & 4 deletions packages/core/src/awsService/sagemaker/credentialMapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import globals from '../../shared/extensionGlobals'
import { ToolkitError } from '../../shared/errors'
import { DevSettings } from '../../shared/settings'
import { Auth } from '../../auth/auth'
import { parseRegionFromArn } from './utils'
import { SpaceMappings, SsmConnectionInfo } from './types'
import { getLogger } from '../../shared/logger/logger'
import { parseArn } from './detached-server/utils'

const mappingFileName = '.sagemaker-space-profiles'
const mappingFilePath = path.join(os.homedir(), '.aws', mappingFileName)
Expand Down Expand Up @@ -81,9 +81,14 @@ export async function persistSSMConnection(
wsUrl?: string,
token?: string
): Promise<void> {
const region = parseRegionFromArn(appArn)
const { region } = parseArn(appArn)
const endpoint = DevSettings.instance.get('endpoints', {})['sagemaker'] ?? ''

// TODO: Hardcoded to 'jupyterlab' due to a bug in Studio that only supports refreshing
// the token for both CodeEditor and JupyterLab Apps in the jupyterlab subdomain.
// This will be fixed shortly after NYSummit launch to support refresh URL in CodeEditor subdomain.
const appSubDomain = 'jupyterlab'

let envSubdomain: string

if (endpoint.includes('beta')) {
Expand All @@ -101,8 +106,7 @@ export async function persistSSMConnection(
? `studio.${region}.sagemaker.aws`
: `${envSubdomain}.studio.${region}.asfiovnxocqpcry.com`

const refreshUrl = `https://studio-${domain}.${baseDomain}/api/remoteaccess/token`

const refreshUrl = `https://studio-${domain}.${baseDomain}/${appSubDomain}`
await setSpaceCredentials(appArn, refreshUrl, {
sessionId: session ?? '-',
url: wsUrl ?? '-',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import { IncomingMessage, ServerResponse } from 'http'
import url from 'url'
import { SessionStore } from '../sessionStore'
import { open, parseArn, readServerInfo } from '../utils'

export async function handleGetSessionAsync(req: IncomingMessage, res: ServerResponse): Promise<void> {
const parsedUrl = url.parse(req.url || '', true)
Expand Down Expand Up @@ -37,38 +38,30 @@ export async function handleGetSessionAsync(req: IncomingMessage, res: ServerRes
})
)
return
} else {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end(
`No session found for connection identifier: ${connectionIdentifier}. Reconnecting for deeplink is not supported yet.`
)
return
}

// Temporarily disabling reconnect logic for the 7/3 Phase 1 launch.
// Will re-enable in the next release around 7/14.

// const status = await store.getStatus(connectionIdentifier, requestId)
// if (status === 'pending') {
// res.writeHead(204)
// res.end()
// return
// } else if (status === 'not-started') {
// const serverInfo = await readServerInfo()
// const refreshUrl = await store.getRefreshUrl(connectionIdentifier)
const status = await store.getStatus(connectionIdentifier, requestId)
if (status === 'pending') {
res.writeHead(204)
res.end()
return
} else if (status === 'not-started') {
const serverInfo = await readServerInfo()
const refreshUrl = await store.getRefreshUrl(connectionIdentifier)
const { spaceName } = parseArn(connectionIdentifier)

// const url = `${refreshUrl}?connection_identifier=${encodeURIComponent(
// connectionIdentifier
// )}&request_id=${encodeURIComponent(requestId)}&call_back_url=${encodeURIComponent(
// `http://localhost:${serverInfo.port}/refresh_token`
// )}`
const url = `${refreshUrl}/${encodeURIComponent(spaceName)}?reconnect_identifier=${encodeURIComponent(
connectionIdentifier
)}&reconnect_request_id=${encodeURIComponent(requestId)}&reconnect_callback_url=${encodeURIComponent(
`http://localhost:${serverInfo.port}/refresh_token`
)}`

// await open(url)
// res.writeHead(202, { 'Content-Type': 'text/plain' })
// res.end('Session is not ready yet. Please retry in a few seconds.')
// await store.markPending(connectionIdentifier, requestId)
// return
// }
await open(url)
res.writeHead(202, { 'Content-Type': 'text/plain' })
res.end('Session is not ready yet. Please retry in a few seconds.')
await store.markPending(connectionIdentifier, requestId)
return
}
} catch (err) {
console.error('Error handling session async request:', err)
res.writeHead(500, { 'Content-Type': 'text/plain' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,8 @@ export class SessionStore {

const asyncEntry = requests[requestId]
if (asyncEntry?.status === 'fresh') {
await this.markConsumed(connectionId, requestId)
delete requests[requestId]
await writeMapping(mapping)
return asyncEntry
}

Expand Down
15 changes: 11 additions & 4 deletions packages/core/src/awsService/sagemaker/detached-server/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,18 @@ export async function readServerInfo(): Promise<ServerInfo> {
}

/**
* Parses a SageMaker ARN to extract region and account ID.
* Parses a SageMaker ARN to extract region, account ID, and space name.
* Supports formats like:
* arn:aws:sagemaker:<region>:<account_id>:space/<space_name>
* arn:aws:sagemaker:<region>:<account_id>:space/<domain>/<space_name>
* or sm_lc_arn:aws:sagemaker:<region>:<account_id>:space__d-xxxx__<name>
*
* If the input is prefixed with an identifier (e.g. "sagemaker-user@"), the function will strip it.
*
* @param arn - The full SageMaker ARN string
* @returns An object containing the region and accountId
* @returns An object containing the region, accountId, and spaceName
* @throws If the ARN format is invalid
*/
export function parseArn(arn: string): { region: string; accountId: string } {
export function parseArn(arn: string): { region: string; accountId: string; spaceName: string } {
const cleanedArn = arn.includes('@') ? arn.split('@')[1] : arn
const regex = /^arn:aws:sagemaker:(?<region>[^:]+):(?<account_id>\d+):space[/:].+$/i
const match = cleanedArn.match(regex)
Expand All @@ -64,9 +64,16 @@ export function parseArn(arn: string): { region: string; accountId: string } {
throw new Error(`Invalid SageMaker ARN format: "${arn}"`)
}

// Extract space name from the end of the ARN (after the last forward slash)
const spaceName = cleanedArn.split('/').pop()
if (!spaceName) {
throw new Error(`Could not extract space name from ARN: "${arn}"`)
}

return {
region: match.groups.region,
accountId: match.groups.account_id,
spaceName: spaceName,
}
}

Expand Down
Loading
Loading