Skip to content

feat: distinct sign-in error when Amazon Q Developer access is blocked - #159

Merged
ashishrp-aws merged 8 commits into
Amazon-Q-Developer:mainfrom
ashishrp-aws:feat/qdev-not-accepting-new-customers
Aug 18, 2026
Merged

feat: distinct sign-in error when Amazon Q Developer access is blocked#159
ashishrp-aws merged 8 commits into
Amazon-Q-Developer:mainfrom
ashishrp-aws:feat/qdev-not-accepting-new-customers

Conversation

@ashishrp-aws

@ashishrp-aws ashishrp-aws commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Problem

Amazon Q Developer no longer accepts new Builder ID customers. Those users sign in successfully and then find Q silently non-functional: the service's rejection arrives as if it were a chat reply, with no explanation and no way forward.

What the user sees now

Sign-in succeeds (it is OIDC and never gated). Within a couple of seconds the block is reported and the user is signed out onto a screen that explains the situation: a heading, three cards covering why this happened, what to use instead (Kiro), and that pre-cutoff Builder IDs can still sign in, then Get started with Kiro, Try a different login method, and a link to the announcement.

The screen follows a design from @saurabh, replacing an earlier revision that showed only the service's sentence. The service message is still stored — its presence is what marks the identity as blocked — but is no longer displayed. The screen therefore hardcodes two dates (sign-ups stopped May 15 2026, support ends April 30 2027), taken from the public announcement, so it will not follow the service if the service changes its cutoff.

Detection: why it cannot be done client-side

The first revision classified the block from ListAvailableProfiles. That cannot work:

  • RTS gates on User-Agent. The gate applies only to traffic carrying the shared language server's token. The extension's own SDK calls are allowed unconditionally — verified against prod: with a blocked Builder ID the extension's own ListFeatureEvaluations succeeds while the language server's identical call is denied, in the same second.
  • Profiles are an IdC concept. restoreProfileSelection() only runs behind isValidEnterpriseSsoInUse(), so a Builder ID user never reaches that path — and IdC identities are exempt from the gate.
  • For Builder ID, ListAvailableProfiles returns a different error ("AWS Builder ID is not supported for this operation", reason: undefined) that is identical for healthy and blocked identities.

So the language server is the only component the service ever refuses. It reports the rejection over the existing Notification feature and this PR reacts to it.

Changes

Area Change
lsp/qDevAccessBlockedHandler.ts (new) Handles aws/window/showNotification, persists the service message, signs the user out. Never throws
codewhisperer/util/qDevAccessBlocked.ts (new) Persisted blocked state, so the message survives the sign-out that follows
authUtil.ts, backend_amazonq.ts Route a blocked identity to the blocked screen; listRegionProfiles short-circuits rather than calling an API that cannot succeed; Try a different login method clears state and fires onActiveConnectionModified
regionProfileSelector.vue The redesigned screen
regionProfileManager.ts Log name and reason on AccessDenied so a misclassification is diagnosable from customer logs

Four things reviewers should look at, because each was a real bug found in testing

Identification is by id, never by title. RouterByServerName rewrites the notification id into base64 of {"serverName":...,"id":...}, so the raw id never arrives and an earlier revision was matching on content.title == 'Amazon Q Developer' — which would have signed out a working user the first time any other error notification reused that title. The client now decodes the envelope. Title matching is removed entirely, with a test asserting a lookalike notification is ignored.

onActiveConnectionModified.fire() is required, not defensive. Reacting to the block already signs the user out, so by the time the button is pressed there is no connection and signout() is skipped — and root.vue only re-evaluates the auth stage on that event. Without firing it the state clears but the screen never changes.

loadMetadata must be initialised when entering the profile-selection stage. setDidLoad dereferences loadMetadata!.start, so routing there without it threw inside the webview (Cannot read properties of undefined (reading 'start')) — breaking the screen for exactly the users this feature exists to help. Both callers now go through one method.

The screen widens its container rather than overflowing it. The container is capped at 260px for the profile picker; a wider child cannot centre inside it, so the content sat 120px right of the icon.

Dependency

Requires a language server carrying the server-side reporting: aws/language-servers #2794, #2796, #2797, #2799 (all merged). Older servers send nothing and this code stays dormant, so it is safe to ship ahead of the server rollout.

Testing

  • End to end in VS Code against prod RTS with a Builder ID created after the cutoff, and re-tested after each fix above
  • Negative case: healthy identities unaffected — no message, normal chat
  • 10 unit tests: 7 for the notification handler (routed id, plain id, missing id, lookalike ignored, empty message, idempotence, never throws) and 3 for the backend (routing, verbatim message, the Try-a-different-login-method path)
  • Full packages/amazonq unit suite: 858 tests, 0 failures, identical to main

Not included

The blank-chat-panel webpack regression (esbuild-loader v4 emitting IIFE and breaking libraryTarget: 'this') is unrelated and still needs its own PR. Telemetry for block detection is agreed as a follow-up.

…stomers

When RTS rejects an identity with AccessDeniedException and
reason=FEATURE_NOT_SUPPORTED, Amazon Q Developer is no longer accepting
that customer. Previously this surfaced as the generic "Failed to list Q
Developer profiles for regions: ..." error, which offers Retry and Sign
out actions -- both useless for a permanent, deliberate rejection, and
misleading because it reads as a transient outage.

RegionProfileManager now classifies this case and throws a ToolkitError
with code QDeveloperNotAcceptingNewCustomers carrying the real service
message. The login webview renders a dedicated state showing that
message with a single "Go back" action instead of Retry/Sign out.

Classification requires all three of: isAwsError (a real AWS service
error carrying code and time, not merely an object with a `reason`
field), name === 'AccessDeniedException', and reason exactly equal to
'FEATURE_NOT_SUPPORTED'. This deliberately avoids capturing the other
modeled AccessDeniedExceptionReason values --
UNAUTHORIZED_CUSTOMIZATION_RESOURCE_ACCESS,
UNAUTHORIZED_WORKSPACE_CONTEXT_FEATURE_ACCESS and TEMPORARILY_SUSPENDED
-- the last of which is transient and must keep its retry affordance.
Since the rejection is per-identity rather than per-region, the first
matching region wins and is preferred over the generic failure
regardless of which region's call settles first.

listRegionProfiles returns RegionProfile[] | string, so the specific
case is tagged for the frontend by prefixing the message with the
notAcceptingNewCustomersPrefix sentinel, which the Vue component strips
before display. That constant lives in types.ts rather than backend.ts
because backend.ts imports vscode and Auth; importing a runtime value
(not just a type) from it into a webview file bundles Node-only
dependencies into the webview bundle and blanks the view at load.

Adds signOutIfConnected() to CommonAuthWebview, backing the "Go back"
action. Unlike signout() it must never throw, because by the time the
user dismisses the error the connection may already have been cleared by
a connection-modified listener reacting to the auth failure; the action's
job is to return to a neutral login screen, not to assert a connection
existed.

Tests cover the positive case plus three negative cases that pin the
fallback to ListQDeveloperProfilesFailed: an unrelated
AccessDeniedException reason (TEMPORARILY_SUSPENDED), a
non-AccessDeniedException error coincidentally carrying
reason=FEATURE_NOT_SUPPORTED, and a generic transient failure.
@@ -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"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Improved error messaging when signing in with an account that isn't eligible for Amazon Q Developer — users now see a clear eligibility notice instead of a generic profile loading error.

@laileni-aws laileni-aws left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, but we may need todo a bugbash for this

@ashishrp-aws
ashishrp-aws marked this pull request as draft August 5, 2026 08:38
@ashishrp-aws

Copy link
Copy Markdown
Collaborator Author

Converting to draft — please don't merge this yet. Testing against prod surfaced a service-side constraint that means this code path cannot fire in production, so merging it would ship dead code.

Why

The rejection this PR classifies comes from QDevPluginAccessGateHandler in AWSVectorConsolasRuntimeService (enforcement live). Two properties of that gate undercut this change:

IdC is exempt.

if (profileIdentity.getProfileIdentityType() != ProfileIdentityType.SONO) {
    // IdC (Enterprise) Q Developer plugin callers are not subject to the cutoff check.
    job.getMetrics().addCount(METRIC_IDC_ALLOWED, true);
    return;
}

Only Builder ID (SONO) identities created on/after 2026-07-25T00:00:00Z are denied, with reason=FEATURE_NOT_SUPPORTED. But this PR hangs its classification off ListAvailableProfiles, which the extension only calls for IdC connections — requireProfileSelection() returns false for Builder ID (authUtil.ts:325-327). So we're looking for the error on the one identity type that never receives it.

The gate only polices language-server traffic. It matches on the user-agent token AWS-Language-Servers-AWS-CodeWhisperer (ClientMetadataUtil.isQDevPluginUserAgent); everything else takes an unconditional-allow early return. The extension's own SDK v2 clients don't carry that token, so extension-side detection isn't possible at all. That's confirmed empirically — for a blocked Builder ID, the extension's ListFeatureEvaluations succeeds while the language server's identical call is denied.

Two other measurements worth recording

  • ListAvailableProfiles rejects every Builder ID caller with AccessDeniedException, reason=undefined, message "AWS Builder ID is not supported for this operation." — a request-shape rejection, not the gate, and identical for blocked and healthy identities. The narrow three-part check in this PR correctly declined to misclassify it and fell through to ListQDeveloperProfilesFailed. A looser check would have shown every Builder ID user the not-accepting-new-customers message.
  • Sign-in never touches RTS (OIDC /client/register + /token only), so a blocked identity always signs in successfully. The observable symptom today is a blank chat panel, because every subsequent language-server request is denied.

Where it moved

Detection now lives in the language server, where the denial actually arrives: Amazon-Q-Developer/language-servers#2794. It classifies centrally and surfaces the service message over the existing showNotification channel, which is gated on the client-advertised window.notifications capability — so plugins already in the market are unaffected.

Once that ships, the plugin-side work is: consume the notification, sign out, and show the message with a route back to the auth screen.

What carries over from this PR

The reviewed design mostly transfers, which is why I'm drafting rather than closing:

  • the narrow classifier (reason must be exactly FEATURE_NOT_SUPPORTED, so TEMPORARILY_SUSPENDED keeps its retry affordance) — reused as-is in #2794
  • showing the service message verbatim rather than canned copy, since FEATURE_NOT_SUPPORTED is reused across several RTS gates and only the message says why
  • the dedicated screen with a single action instead of Retry/Sign out
  • signOutIfConnected() and the reset path

@laileni-aws — flagging since you approved this. Happy to walk through the service-side detail if useful.

@ashishrp-aws
ashishrp-aws marked this pull request as ready for review August 5, 2026 20:57
Replaces the detection half of this change. The UI is unchanged.

The original approach classified the block from ListAvailableProfiles, which cannot
work for the population being blocked:

- RTS gates on the User-Agent of the shared language server
  (AWS-Language-Servers-AWS-CodeWhisperer). The extension's own SDK calls carry a
  different UA and are allowed unconditionally, so the extension is never told "no".
  Verified against prod: with a blocked Builder ID the extension's own
  ListFeatureEvaluations succeeds while the language server's identical call is denied.
- Profiles are an IdC concept. restoreProfileSelection() only runs behind
  isValidEnterpriseSsoInUse(), so a Builder ID user never reaches that path -- and IdC
  identities are exempt from the gate, so the one type that does reach it is never
  denied.
- For Builder ID, ListAvailableProfiles returns a different error ("AWS Builder ID is
  not supported for this operation", reason undefined) which is identical for healthy
  and blocked identities, so it cannot be used as a signal either.

The language server is therefore the only component that observes the rejection. It now
reports it over the existing Notification feature, and this change reacts to that:

- qDevAccessBlockedHandler listens for aws/window/showNotification, persists the
  service's message, and signs the user out.
- The blocked state is persisted so the message survives the sign-out that follows.
- showLoginView and refreshAuthState route a blocked identity to the existing blocked
  screen; listRegionProfiles short-circuits to the stored message rather than calling an
  API that cannot succeed.
- Go back clears the state and fires onActiveConnectionModified so the webview returns
  to sign-in. Firing is required, not incidental: reacting to the block already signed
  the user out, so signout() -- which would normally trigger the re-render -- is
  skipped, and without this the screen never changes.
- URLs in the message render as links. The message is the service's copy and contains
  the action the user must take, which is useless as inert text. Split into segments
  rather than v-html so a service response can never inject markup.

Requires a language server carrying the server-side reporting (aws/language-servers
 #2794, #2796, #2797). Older servers send nothing and this code stays dormant.

Tested end to end in VS Code against prod RTS with a blocked Builder ID: sign-in
succeeds, the block is reported seconds later, the user is signed out, the message
renders with a working kiro.dev link, and Go back returns to sign-in. Also verified a
healthy identity is unaffected.
@ashishrp-aws ashishrp-aws changed the title feat: distinct sign-in error when Q Developer is not accepting new customers feat: distinct sign-in error when Amazon Q Developer access is blocked Aug 13, 2026
Review follow-up. The id check could never match and the title check was doing all the
work, which meant any future error notification titled "Amazon Q Developer" would have
signed a working user out.

The runtime does not forward the server's id verbatim: RouterByServerName replaces it
with base64 of {"serverName":...,"id":...} so followups can be routed back to the
originating server. So `params.id === 'qDevPluginAccessBlocked'` never matched, and the
title fallback was the only live path.

Decode the envelope and match on the inner id, falling back to the raw value so a server
sending a plain id still works. Title matching is removed entirely rather than kept as a
fallback: every server able to deliver a notification at all sends the id, so there is
nothing to fall back for, and the cost of a false positive here is signing out a user
who is not blocked.

Adds the tests this file should have had. One asserts an unrelated error notification
sharing the title is ignored, which is the regression that motivated the change; the
others cover the routed id, a plain id, a missing id, an empty message, idempotency on
repeated reports, and that the handler never throws when sign-out fails. 7 passing.
Routing a blocked identity to PENDING_PROFILE_SELECTION returned early without setting
loadMetadata, which the existing pendingProfileSelection branch does. When the webview
then reported readiness it broke:

    webviewId="aws.amazonq.AmazonCommonAuth": Error: Webview error
     -> Error: Webview backend command failed: "setUiReady()"
     -> TypeError: Cannot read properties of undefined (reading 'start')

setDidLoad dereferences loadMetadata!.start non-optionally, so entering that stage
without it throws inside the webview. The failure lands on exactly the users this feature
exists to help: they would see a broken login view instead of the explanation.

Both callers now go through enterProfileSelection() rather than duplicating the setup, so
a future third caller cannot miss it. The comment records why the metadata is required
rather than leaving it as unexplained bookkeeping.

Added a regression test asserting setUiReady does not throw after routing to the blocked
screen. Verified it bites: reintroducing the early return fails it (8 passing/1 failing
vs 9 passing).
Replaces the single-message screen with the layout Saurabh proposed, which explains why
access is blocked and what to do about it instead of only relaying the service's sentence.

- Heading and subheading stating that sign-ups have stopped, with the date
- Three cards: why this is happening (with a link to the announcement), what to use
  instead (Kiro), and that pre-cutoff Builder IDs can still sign in
- Primary action opens kiro.dev, secondary returns to sign-in, footer links to the
  announcement

The service message is no longer displayed. It is still stored, because its presence is
what marks the identity as blocked, but the screen now carries its own copy. Worth
knowing for review: that copy hardcodes two dates, so if the service changes its cutoff
the screen will not follow. The dates are Saurabh's, taken from the public announcement.

The container widens for this screen only, rather than changing the 260px cap the profile
picker relies on. Colours come from VS Code theme variables so the screen follows light
and dark themes; only the accent hues and the primary button gradient are fixed, since
those carry meaning rather than chrome. Icons are inline SVG rather than an icon font, to
avoid adding a webview dependency.

Removes the URL-splitting computed, which existed only to linkify the service message.
@ashishrp-aws
ashishrp-aws requested a review from a team as a code owner August 14, 2026 04:33
ashishrp-aws and others added 2 commits August 14, 2026 10:07
…ainer

The container is capped at 260px and absolutely positioned for the profile picker. The
blocked screen is wider, and a wider child cannot centre inside it: with negative
available space the auto margins resolve to 0, so the content started at the container's
left edge and overflowed to the right while the Q icon above stayed centred in the 260px
box. The whole screen read as misaligned.

The container itself now widens for this state rather than the child overflowing it, so
the icon, heading, cards and buttons share one centre axis. Static positioning also drops
the fixed top offset, which only made sense for the short picker and left a large gap
above this taller screen.
@ashishrp-aws
ashishrp-aws requested a review from a team as a code owner August 17, 2026 23:54
@ashishrp-aws
ashishrp-aws merged commit b2c0ac2 into Amazon-Q-Developer:main Aug 18, 2026
17 of 26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants