Skip to content

Commit d0923ff

Browse files
committed
fix: Q chat stopped using IAM creds in the Amazon Q v1.63.0 release
1 parent d17c1d7 commit d0923ff

File tree

6 files changed

+816
-13
lines changed

6 files changed

+816
-13
lines changed

P261194666.md

Lines changed: 630 additions & 0 deletions
Large diffs are not rendered by default.

packages/amazonq/src/lsp/auth.ts

Lines changed: 57 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import {
77
bearerCredentialsUpdateRequestType,
8+
iamCredentialsUpdateRequestType,
89
ConnectionMetadata,
910
NotificationType,
1011
RequestType,
@@ -17,8 +18,8 @@ import { LanguageClient } from 'vscode-languageclient'
1718
import { AuthUtil } from 'aws-core-vscode/codewhisperer'
1819
import { Writable } from 'stream'
1920
import { onceChanged } from 'aws-core-vscode/utils'
20-
import { getLogger, oneMinute } from 'aws-core-vscode/shared'
21-
import { isSsoConnection } from 'aws-core-vscode/auth'
21+
import { getLogger, oneMinute, isSageMaker } from 'aws-core-vscode/shared'
22+
import { isSsoConnection, isIamConnection } from 'aws-core-vscode/auth'
2223

2324
export const encryptionKey = crypto.randomBytes(32)
2425

@@ -78,10 +79,16 @@ export class AmazonQLspAuth {
7879
*/
7980
async refreshConnection(force: boolean = false) {
8081
const activeConnection = this.authUtil.conn
81-
if (this.authUtil.isConnectionValid() && isSsoConnection(activeConnection)) {
82-
// send the token to the language server
83-
const token = await this.authUtil.getBearerToken()
84-
await (force ? this._updateBearerToken(token) : this.updateBearerToken(token))
82+
if (this.authUtil.isConnectionValid()) {
83+
if (isSsoConnection(activeConnection)) {
84+
// Existing SSO path
85+
const token = await this.authUtil.getBearerToken()
86+
await (force ? this._updateBearerToken(token) : this.updateBearerToken(token))
87+
} else if (isSageMaker() && isIamConnection(activeConnection)) {
88+
// New SageMaker IAM path
89+
const credentials = await this.authUtil.getCredentials()
90+
await (force ? this._updateIamCredentials(credentials) : this.updateIamCredentials(credentials))
91+
}
8592
}
8693
}
8794

@@ -92,9 +99,7 @@ export class AmazonQLspAuth {
9299

93100
public updateBearerToken = onceChanged(this._updateBearerToken.bind(this))
94101
private async _updateBearerToken(token: string) {
95-
const request = await this.createUpdateCredentialsRequest({
96-
token,
97-
})
102+
const request = await this.createUpdateBearerCredentialsRequest(token)
98103

99104
// "aws/credentials/token/update"
100105
// https://github.com/aws/language-servers/blob/44d81f0b5754747d77bda60b40cc70950413a737/core/aws-lsp-core/src/credentials/credentialsProvider.ts#L27
@@ -103,15 +108,36 @@ export class AmazonQLspAuth {
103108
this.client.info(`UpdateBearerToken: ${JSON.stringify(request)}`)
104109
}
105110

111+
public updateIamCredentials = onceChanged(this._updateIamCredentials.bind(this))
112+
private async _updateIamCredentials(credentials: any) {
113+
getLogger().info(
114+
`[SageMaker Debug] Updating IAM credentials - credentials received: ${credentials ? 'YES' : 'NO'}`
115+
)
116+
if (credentials) {
117+
getLogger().info(
118+
`[SageMaker Debug] IAM credentials structure: accessKeyId=${credentials.accessKeyId ? 'present' : 'missing'}, secretAccessKey=${credentials.secretAccessKey ? 'present' : 'missing'}, sessionToken=${credentials.sessionToken ? 'present' : 'missing'}`
119+
)
120+
}
121+
122+
const request = await this.createUpdateIamCredentialsRequest(credentials)
123+
124+
// "aws/credentials/iam/update"
125+
await this.client.sendRequest(iamCredentialsUpdateRequestType.method, request)
126+
127+
this.client.info(`UpdateIamCredentials: ${JSON.stringify(request)}`)
128+
getLogger().info(`[SageMaker Debug] IAM credentials update request sent successfully`)
129+
}
130+
106131
public startTokenRefreshInterval(pollingTime: number = oneMinute / 2) {
107132
const interval = setInterval(async () => {
108133
await this.refreshConnection().catch((e) => this.logRefreshError(e))
109134
}, pollingTime)
110135
return interval
111136
}
112137

113-
private async createUpdateCredentialsRequest(data: any): Promise<UpdateCredentialsParams> {
114-
const payload = new TextEncoder().encode(JSON.stringify({ data }))
138+
private async createUpdateBearerCredentialsRequest(token: string): Promise<UpdateCredentialsParams> {
139+
const bearerCredentials = { token }
140+
const payload = new TextEncoder().encode(JSON.stringify({ data: bearerCredentials }))
115141

116142
const jwt = await new jose.CompactEncrypt(payload)
117143
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
@@ -127,4 +153,24 @@ export class AmazonQLspAuth {
127153
encrypted: true,
128154
}
129155
}
156+
157+
private async createUpdateIamCredentialsRequest(credentials: any): Promise<UpdateCredentialsParams> {
158+
// Extract IAM credentials structure
159+
const iamCredentials = {
160+
accessKeyId: credentials.accessKeyId,
161+
secretAccessKey: credentials.secretAccessKey,
162+
sessionToken: credentials.sessionToken,
163+
}
164+
const payload = new TextEncoder().encode(JSON.stringify({ data: iamCredentials }))
165+
166+
const jwt = await new jose.CompactEncrypt(payload)
167+
.setProtectedHeader({ alg: 'dir', enc: 'A256GCM' })
168+
.encrypt(encryptionKey)
169+
170+
return {
171+
data: jwt,
172+
// Omit metadata for IAM credentials since startUrl is undefined for non-SSO connections
173+
encrypted: true,
174+
}
175+
}
130176
}

packages/amazonq/src/lsp/chat/messages.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,31 @@ export function registerMessageListeners(
285285
}
286286

287287
const chatRequest = await encryptRequest<ChatParams>(chatParams, encryptionKey)
288+
289+
// Add detailed logging for SageMaker debugging
290+
if (process.env.USE_IAM_AUTH === 'true') {
291+
languageClient.info(`[SageMaker Debug] Making chat request with IAM auth`)
292+
languageClient.info(`[SageMaker Debug] Chat request method: ${chatRequestType.method}`)
293+
languageClient.info(
294+
`[SageMaker Debug] Original chat params: ${JSON.stringify(
295+
{
296+
tabId: chatParams.tabId,
297+
prompt: chatParams.prompt,
298+
// Don't log full textDocument content, just metadata
299+
textDocument: chatParams.textDocument
300+
? { uri: chatParams.textDocument.uri }
301+
: undefined,
302+
context: chatParams.context ? `${chatParams.context.length} context items` : undefined,
303+
},
304+
null,
305+
2
306+
)}`
307+
)
308+
languageClient.info(
309+
`[SageMaker Debug] Environment context: USE_IAM_AUTH=${process.env.USE_IAM_AUTH}, AWS_REGION=${process.env.AWS_REGION}`
310+
)
311+
}
312+
288313
try {
289314
const chatResult = await languageClient.sendRequest<string | ChatResult>(
290315
chatRequestType.method,
@@ -294,6 +319,26 @@ export function registerMessageListeners(
294319
},
295320
cancellationToken.token
296321
)
322+
323+
// Add response content logging for SageMaker debugging
324+
if (process.env.USE_IAM_AUTH === 'true') {
325+
languageClient.info(`[SageMaker Debug] Chat response received - type: ${typeof chatResult}`)
326+
if (typeof chatResult === 'string') {
327+
languageClient.info(
328+
`[SageMaker Debug] Chat response (string): ${chatResult.substring(0, 200)}...`
329+
)
330+
} else if (chatResult && typeof chatResult === 'object') {
331+
languageClient.info(
332+
`[SageMaker Debug] Chat response (object keys): ${Object.keys(chatResult)}`
333+
)
334+
if ('message' in chatResult) {
335+
languageClient.info(
336+
`[SageMaker Debug] Chat response message: ${JSON.stringify(chatResult.message).substring(0, 200)}...`
337+
)
338+
}
339+
}
340+
}
341+
297342
await handleCompleteResult<ChatResult>(
298343
chatResult,
299344
encryptionKey,

packages/amazonq/src/lsp/client.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import {
1616
RenameFilesParams,
1717
ResponseMessage,
1818
WorkspaceFolder,
19+
ConnectionMetadata,
1920
} from '@aws/language-server-runtimes/protocol'
2021
import {
2122
AuthUtil,
@@ -181,10 +182,11 @@ export async function startLanguageServer(
181182
contextConfiguration: {
182183
workspaceIdentifier: extensionContext.storageUri?.path,
183184
},
184-
logLevel: toAmazonQLSPLogLevel(globals.logOutputChannel.logLevel),
185+
logLevel: isSageMaker() ? 'debug' : toAmazonQLSPLogLevel(globals.logOutputChannel.logLevel),
185186
},
186187
credentials: {
187188
providesBearerToken: true,
189+
providesIam: isSageMaker(), // Enable IAM credentials for SageMaker environments
188190
},
189191
},
190192
/**
@@ -210,6 +212,32 @@ export async function startLanguageServer(
210212
toDispose.push(disposable)
211213
await client.onReady()
212214

215+
// Set up connection metadata handler
216+
client.onRequest<ConnectionMetadata, Error>(notificationTypes.getConnectionMetadata.method, () => {
217+
// For IAM auth, provide a default startUrl
218+
if (process.env.USE_IAM_AUTH === 'true') {
219+
getLogger().info(
220+
`[SageMaker Debug] Connection metadata requested - returning hardcoded startUrl for IAM auth`
221+
)
222+
return {
223+
sso: {
224+
// TODO P261194666 Replace with correct startUrl once identified
225+
startUrl: 'https://amzn.awsapps.com/start', // Default for IAM auth
226+
},
227+
}
228+
}
229+
230+
// For SSO auth, use the actual startUrl
231+
getLogger().info(
232+
`[SageMaker Debug] Connection metadata requested - returning actual startUrl for SSO auth: ${AuthUtil.instance.auth.startUrl}`
233+
)
234+
return {
235+
sso: {
236+
startUrl: AuthUtil.instance.auth.startUrl,
237+
},
238+
}
239+
})
240+
213241
const auth = await initializeAuth(client)
214242

215243
await onLanguageServerReady(extensionContext, auth, client, resourcePaths, toDispose)

packages/core/src/auth/auth.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,6 +1032,16 @@ export class Auth implements AuthService, ConnectionManager {
10321032
}
10331033
}
10341034
}
1035+
1036+
// Add conditional auto-login logic for SageMaker (jmkeyes@ guidance)
1037+
if (hasVendedIamCredentials() && isSageMaker()) {
1038+
// SageMaker auto-login logic - use 'ec2' source since SageMaker uses EC2-like instance credentials
1039+
const sagemakerProfileId = asString({ credentialSource: 'ec2', credentialTypeId: 'sagemaker-instance' })
1040+
if ((await tryConnection(sagemakerProfileId)) === true) {
1041+
getLogger().info(`auth: automatically connected with SageMaker credentials`)
1042+
return
1043+
}
1044+
}
10351045
}
10361046

10371047
/**

packages/core/src/shared/lsp/utils/platform.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55

6+
import * as vscode from 'vscode'
67
import { ToolkitError } from '../../errors'
78
import { Logger } from '../../logger/logger'
89
import { ChildProcess } from '../../utilities/processUtils'
910
import { waitUntil } from '../../utilities/timeoutUtils'
1011
import { isDebugInstance } from '../../vscode/env'
12+
import { isSageMaker } from '../../extensionUtilities'
13+
import { getLogger } from '../../logger'
14+
15+
interface SagemakerCookie {
16+
authMode?: 'Sso' | 'Iam'
17+
}
1118

1219
export function getNodeExecutableName(): string {
1320
return process.platform === 'win32' ? 'node.exe' : 'node'
@@ -101,7 +108,44 @@ export function createServerOptions({
101108
args.unshift('--inspect=6080')
102109
}
103110

104-
const lspProcess = new ChildProcess(bin, args, { warnThresholds })
111+
// Set USE_IAM_AUTH environment variable for SageMaker environments based on cookie detection
112+
// This tells the language server to use IAM authentication mode instead of SSO mode
113+
const env = { ...process.env }
114+
if (isSageMaker()) {
115+
try {
116+
// The command `sagemaker.parseCookies` is registered in VS Code SageMaker environment
117+
const result = (await vscode.commands.executeCommand('sagemaker.parseCookies')) as SagemakerCookie
118+
if (result.authMode !== 'Sso') {
119+
env.USE_IAM_AUTH = 'true'
120+
getLogger().info(
121+
`[SageMaker Debug] Setting USE_IAM_AUTH=true for language server process (authMode: ${result.authMode})`
122+
)
123+
} else {
124+
getLogger().info(`[SageMaker Debug] Using SSO auth mode, not setting USE_IAM_AUTH`)
125+
}
126+
} catch (err) {
127+
getLogger().warn(`[SageMaker Debug] Failed to parse SageMaker cookies, defaulting to IAM auth: ${err}`)
128+
env.USE_IAM_AUTH = 'true'
129+
}
130+
131+
// Enable verbose logging for Mynah backend to help debug the generic error response
132+
env.RUST_LOG = 'debug'
133+
134+
// Log important environment variables for debugging
135+
getLogger().info(`[SageMaker Debug] Environment variables for language server:`)
136+
getLogger().info(`[SageMaker Debug] USE_IAM_AUTH: ${env.USE_IAM_AUTH}`)
137+
getLogger().info(
138+
`[SageMaker Debug] AWS_CONTAINER_CREDENTIALS_RELATIVE_URI: ${env.AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}`
139+
)
140+
getLogger().info(`[SageMaker Debug] AWS_DEFAULT_REGION: ${env.AWS_DEFAULT_REGION}`)
141+
getLogger().info(`[SageMaker Debug] AWS_REGION: ${env.AWS_REGION}`)
142+
getLogger().info(`[SageMaker Debug] RUST_LOG: ${env.RUST_LOG}`)
143+
}
144+
145+
const lspProcess = new ChildProcess(bin, args, {
146+
warnThresholds,
147+
spawnOptions: { env },
148+
})
105149

106150
// this is a long running process, awaiting it will never resolve
107151
void lspProcess.run()

0 commit comments

Comments
 (0)