Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9b03d57
deps: resolve 123 Dependabot security alerts (safe, non-breaking) (#136)
laileni-aws Jul 14, 2026
1524042
deps: resolve major-version Dependabot alerts (category 2) (#137)
laileni-aws Jul 14, 2026
c4226d9
fix: resolve CodeQL code-scanning security alerts (#140)
laileni-aws Jul 15, 2026
73bdc86
deps: fix webpack SSRF alerts (#68, #69) via resolver pin (#138)
laileni-aws Jul 15, 2026
7ccfb97
deps: resolve newly-expanded Dependabot advisories (#143)
laileni-aws Jul 16, 2026
1bbf763
deps: fix critical websocket-driver advisory (#247) (#145)
laileni-aws Jul 16, 2026
2c1d9d2
deps: bump adm-zip to 0.6.0 to patch high-severity advisory (#147)
laileni-aws Jul 21, 2026
0c4d92a
test: bump @vscode/test-electron to ^3.1.0 to fix macOS insiders CI (…
laileni-aws Jul 30, 2026
9225aa8
feat: distinct sign-in error when Q Developer is not accepting new cu…
ashishrp-aws Jul 31, 2026
5d36e75
deps: fix protobufjs + newly-expanded advisories (#251-259) (#150)
laileni-aws Jul 31, 2026
0fa66ed
Merge branch 'aws:main' into feat/qdev-not-accepting-new-customers
ashishrp-aws Aug 5, 2026
3a90bb3
feat: detect Q Developer access blocks via the language server
ashishrp-aws Aug 13, 2026
6121a57
fix: identify the access-blocked notification by id, not by title
ashishrp-aws Aug 13, 2026
6a03a4a
fix: initialise load metadata when routing to the blocked screen
ashishrp-aws Aug 13, 2026
7b045fc
feat: redesign the access-blocked screen
ashishrp-aws Aug 14, 2026
0fef0fa
fix: centre the access-blocked screen instead of overflowing its cont…
ashishrp-aws Aug 14, 2026
e99912f
fix: repoint CODEOWNERS and workflow org guards to Amazon-Q-Developer
chungjac Aug 17, 2026
1fa953a
Merge pull request #171 from chungjac/chungjac/org-migration-codeowne…
chungjac Aug 17, 2026
25fc81f
Merge branch 'main' into feat/qdev-not-accepting-new-customers
ashishrp-aws Aug 17, 2026
b2c0ac2
Merge pull request #159 from ashishrp-aws/feat/qdev-not-accepting-new…
ashishrp-aws Aug 18, 2026
0305a61
build: merge release candidate version rc-20260819 (#174)
ashishrp-aws Aug 21, 2026
2b28bc0
fix: unbreak the login webview shipped in 2.5.0 (#179)
ashishrp-aws Aug 27, 2026
6790d93
resolve merge conflicts: prefer main over release/rc-20260821
ashishrp-aws Aug 27, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"type": "Feature",
"description": "Amazon Q: Clearer message when signing in with an account that is not eligible for Amazon Q Developer, instead of a generic profile loading failure"
}
4 changes: 4 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ on:
push:
branches: [main, feature/*, release/*]

# Least-privilege default for GITHUB_TOKEN; the `publish` job overrides with `contents: write`.
permissions:
contents: read

jobs:
package:
runs-on: ubuntu-latest
Expand Down
6,680 changes: 3,704 additions & 2,976 deletions package-lock.json

Large diffs are not rendered by default.

47 changes: 44 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@
"@aws-toolkits/telemetry": "^1.0.341",
"@playwright/browser-chromium": "^1.43.1",
"@stylistic/eslint-plugin": "^2.11.0",
"@types/adm-zip": "^0.4.34",
"@types/he": "^1.2.3",
"@types/jaro-winkler": "^0.2.4",
"@types/vscode": "^1.68.0",
Expand All @@ -53,9 +54,10 @@
"@typescript-eslint/eslint-plugin": "^7.14.1",
"@typescript-eslint/parser": "^7.14.1",
"@vscode/codicons": "^0.0.33",
"@vscode/test-electron": "^2.3.8",
"@vscode/test-electron": "^3.1.0",
"@vscode/test-web": "^0.0.65",
"@vscode/vsce": "^2.19.0",
"adm-zip": "^0.6.0",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-aws-toolkits": "file:plugins/eslint-plugin-aws-toolkits",
Expand All @@ -69,9 +71,9 @@
"pretty-quick": "^4.0.0",
"ts-node": "^10.9.1",
"typescript": "^5.0.4",
"webpack": "^5.95.0",
"webpack": "^5.104.1",
"webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.2",
"webpack-dev-server": "^5.2.5",
"webpack-merge": "^5.10.0"
},
"dependencies": {
Expand All @@ -80,5 +82,44 @@
"jaro-winkler": "^0.2.8",
"vscode-nls": "^5.2.0",
"vscode-nls-dev": "^4.0.4"
},
"overrides": {
"@tootallnate/once": "2.0.1",
"@xmldom/xmldom": "0.8.13",
"brace-expansion@1": "1.1.16",
"cipher-base": "1.0.5",
"diff@4": "4.0.4",
"diff@5": "5.2.2",
"enhanced-resolve": "5.17.1",
"fast-uri": "3.1.4",
"fast-xml-parser": "5.7.0",
"flatted": "3.4.2",
"follow-redirects": "1.16.0",
"form-data": "4.0.6",
"glob@10": "10.5.0",
"immutable": "4.3.9",
"js-yaml@4": "4.3.0",
"jsonata": "2.2.1",
"koa": "2.16.4",
"launch-editor": "2.14.1",
"linkify-it": "5.0.2",
"lodash": "4.18.0",
"markdown-it": "14.2.0",
"minimatch@5": "5.1.8",
"morgan": "1.11.0",
"node-forge": "1.4.0",
"on-headers": "1.1.0",
"pbkdf2": "3.1.3",
"postcss": "8.5.19",
"protobufjs": "7.6.5",
"qs": "6.15.2",
"serialize-javascript": "7.0.5",
"sha.js": "2.4.12",
"shell-quote": "1.9.0",
"tmp": "0.2.7",
"undici": "6.27.0",
"websocket-driver": "0.7.5",
"ws": "8.21.0",
"yaml": "1.10.3"
}
}
5 changes: 0 additions & 5 deletions packages/amazonq/.changes/2.6.0.json

This file was deleted.

4 changes: 0 additions & 4 deletions packages/amazonq/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
## 2.6.0 2026-08-21

- Miscellaneous non-user-facing changes

## 2.5.0 2026-08-20

- Miscellaneous non-user-facing changes
Expand Down
2 changes: 1 addition & 1 deletion packages/amazonq/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "amazon-q-vscode",
"displayName": "Amazon Q",
"description": "The most capable generative AI–powered assistant for software development.",
"version": "2.7.0-SNAPSHOT",
"version": "2.6.0-SNAPSHOT",
"extensionKind": [
"workspace"
],
Expand Down
3 changes: 3 additions & 0 deletions packages/amazonq/src/lsp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { LineTracker } from '../app/inline/stateTracker/lineTracker'
import { InlineTutorialAnnotation } from '../app/inline/tutorials/inlineTutorialAnnotation'
import { InlineChatTutorialAnnotation } from '../app/inline/tutorials/inlineChatTutorialAnnotation'
import { codeReviewInChat } from '../app/amazonqScan/models/constants'
import { registerQDevAccessBlockedHandler } from './qDevAccessBlockedHandler'

const localize = nls.loadMessageBundle()
const logger = getLogger('amazonqLsp.lspClient')
Expand Down Expand Up @@ -229,6 +230,8 @@ export async function startLanguageServer(

await client.start()

registerQDevAccessBlockedHandler(client)

// Set up connection metadata handler
client.onRequest<ConnectionMetadata, Error>(notificationTypes.getConnectionMetadata.method, () => {
// For IAM auth, provide a default startUrl
Expand Down
110 changes: 110 additions & 0 deletions packages/amazonq/src/lsp/qDevAccessBlockedHandler.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import { LanguageClient } from 'vscode-languageclient/node'
import { getLogger } from 'aws-core-vscode/shared'
import { AuthUtil, setQDevAccessBlocked } from 'aws-core-vscode/codewhisperer'

/**
* Method the language server uses to push notifications to the client.
*
* Hard-coded rather than imported from `@aws/language-server-runtimes/protocol` so that the
* extension does not take a new dependency on the runtimes package purely for a string constant.
*/
const showNotificationMethod = 'aws/window/showNotification'

/**
* Stable identifier the language server is expected to set on the access-blocked notification.
*
* Preferred over any text matching, because it is the only part of the payload that is meant to be
* machine-readable.
*/
const qDevAccessBlockedNotificationId = 'qDevPluginAccessBlocked'

interface ShowNotificationParams {
id?: string
type?: number
content?: {
title?: string
text?: string
}
}

/**
* The id the server set, recovered from what the client actually receives.
*
* The runtime does not forward the server's id verbatim: `RouterByServerName` replaces it with
* base64 of `{"serverName":...,"id":...}` so that followups can be routed back to the originating
* server. The server's own id is therefore only reachable by decoding that envelope.
*
* Returns the raw value when it is not an envelope, so a server that sends a plain id still matches.
*/
function serverNotificationId(id: string | undefined): string | undefined {
if (id === undefined) {
return undefined
}

try {
const decoded = JSON.parse(Buffer.from(id, 'base64').toString('utf-8')) as { id?: unknown }
if (typeof decoded.id === 'string') {
return decoded.id
}
} catch {
// Not an envelope; fall through to treating the value as the id itself.
}

return id
}

/**
* Whether this notification is Amazon Q Developer reporting that it has blocked access for the
* current identity.
*
* Matches on the id only. Matching on the title was tried and rejected: acting on this notification
* signs the user out, and 'Amazon Q Developer' is a plausible title for any future error the server
* sends, so a text match would eventually sign out a working user. Every server able to deliver a
* notification at all sends the id, so there is nothing to fall back for.
*/
function isQDevAccessBlockedNotification(params: ShowNotificationParams): boolean {
return serverNotificationId(params.id) === qDevAccessBlockedNotificationId
}

/**
* Reacts to the language server reporting that Amazon Q Developer has blocked this identity.
*
* The server detects this because RTS gates Q Developer plugin traffic before the activity runs, so
* every request from a blocked identity is denied -- including the ones the extension itself never
* makes. Detection therefore has to come from the server; the extension's own service calls are not
* covered by the gate and succeed even for a blocked identity.
*
* On detection the blocked state is persisted and the user is signed out. Persisting first matters:
* signing out clears the connection, which is what makes the login view render, and the stored
* message is what that view then displays. Doing it the other way round loses the explanation.
*/
export function registerQDevAccessBlockedHandler(client: LanguageClient) {
client.onNotification(showNotificationMethod, async (params: ShowNotificationParams) => {
try {
if (!isQDevAccessBlockedNotification(params)) {
return
}

const message = params.content?.text?.trim()
if (message === undefined || message.length === 0) {
getLogger().warn('qDevAccessBlocked: notification had no message, ignoring')
return
}

await setQDevAccessBlocked(message)
// Refresh first so the login view is already showing by the time the connection goes
// away, then sign out. The persisted flag keeps the blocked screen selected afterwards.
await AuthUtil.instance.setVscodeContextProps()
await AuthUtil.instance.secondaryAuth.deleteConnection()
} catch (e) {
// Never let this throw: it runs on the language client's notification dispatch, and a
// failure here must not disturb the rest of the connection.
getLogger().error('qDevAccessBlocked: failed to handle notification: %s', (e as Error)?.message)
}
})
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/*!
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
* SPDX-License-Identifier: Apache-2.0
*/

import assert from 'assert'
import sinon from 'sinon'
import { LanguageClient } from 'vscode-languageclient/node'
import { AuthUtil, clearQDevAccessBlocked, getQDevAccessBlockedMessage } from 'aws-core-vscode/codewhisperer'
import { registerQDevAccessBlockedHandler } from '../../../../src/lsp/qDevAccessBlockedHandler'

/**
* The runtime does not deliver the server's notification id verbatim -- RouterByServerName replaces it
* with base64 of {"serverName":...,"id":...} so followups can be routed back. Tests have to send what
* the client actually receives, not what the server set, or they pass against a shape that never
* reaches production.
*/
function routedId(id: string, serverName = 'AWS Language Server for Amazon Q Developer (Token)'): string {
return Buffer.from(JSON.stringify({ serverName, id }), 'utf-8').toString('base64')
}

const blockedMessage = 'Please visit https://kiro.dev/ to purchase a Kiro subscription.'

describe('qDevAccessBlockedHandler', function () {
let sandbox: sinon.SinonSandbox
let handler: (params: unknown) => Promise<void>
let deleteConnection: sinon.SinonStub

beforeEach(async function () {
sandbox = sinon.createSandbox()

deleteConnection = sandbox.stub()
sandbox.stub(AuthUtil, 'instance').value({
setVscodeContextProps: sandbox.stub().resolves(),
secondaryAuth: { deleteConnection },
})

const client = {
onNotification: (_method: string, cb: (params: unknown) => Promise<void>) => {
handler = cb
},
}
registerQDevAccessBlockedHandler(client as unknown as LanguageClient)

await clearQDevAccessBlocked()
})

afterEach(async function () {
sandbox.restore()
await clearQDevAccessBlocked()
})

it('records the block and signs out when the routed id matches', async function () {
await handler({
id: routedId('qDevPluginAccessBlocked'),
type: 1,
content: { title: 'Amazon Q Developer', text: blockedMessage },
})

// The message is stored verbatim: it is the service's own copy and the only thing that tells
// the user what to do about it.
assert.strictEqual(getQDevAccessBlockedMessage(), blockedMessage)
assert.strictEqual(deleteConnection.calledOnce, true)
})

it('also matches an unrouted id, so a server that sends it plainly still works', async function () {
await handler({ id: 'qDevPluginAccessBlocked', type: 1, content: { text: blockedMessage } })

assert.strictEqual(getQDevAccessBlockedMessage(), blockedMessage)
})

it('ignores an unrelated error notification that shares the title', async function () {
// Regression guard for the removed title-based matching. Acting on this notification signs the
// user out, so a lookalike must not match: 'Amazon Q Developer' is a plausible title for any
// future error the server sends, and matching it would sign out a working user.
await handler({
id: routedId('someOtherNotification'),
type: 1,
content: { title: 'Amazon Q Developer', text: 'Something else went wrong.' },
})

assert.strictEqual(getQDevAccessBlockedMessage(), undefined)
assert.strictEqual(deleteConnection.notCalled, true)
})

it('ignores a notification with no id at all', async function () {
await handler({ type: 1, content: { title: 'Amazon Q Developer', text: blockedMessage } })

assert.strictEqual(getQDevAccessBlockedMessage(), undefined)
assert.strictEqual(deleteConnection.notCalled, true)
})

it('does not sign out when the message is missing', async function () {
// Signing out without an explanation to show would be strictly worse than doing nothing: the
// user would be ejected with no idea why.
await handler({ id: routedId('qDevPluginAccessBlocked'), type: 1, content: { text: ' ' } })

assert.strictEqual(getQDevAccessBlockedMessage(), undefined)
assert.strictEqual(deleteConnection.notCalled, true)
})

it('never throws, even when signing out fails', async function () {
deleteConnection.rejects(new Error('sign out exploded'))

// This runs on the language client's notification dispatch; throwing would disturb the rest of
// the connection for a user who is already in a broken state.
await handler({ id: routedId('qDevPluginAccessBlocked'), type: 1, content: { text: blockedMessage } })

// The block was still recorded, so the user gets the explanation even though sign-out failed.
assert.strictEqual(getQDevAccessBlockedMessage(), blockedMessage)
})

it('is idempotent when the server reports the block more than once', async function () {
const params = { id: routedId('qDevPluginAccessBlocked'), type: 1, content: { text: blockedMessage } }

await handler(params)
await handler(params)

assert.strictEqual(getQDevAccessBlockedMessage(), blockedMessage)
assert.strictEqual(deleteConnection.callCount, 2, 'each report tears down whatever connection exists')
})
})
Loading