feat: distinct sign-in error when Amazon Q Developer access is blocked - #159
Conversation
…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" | |||
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
LGTM, but we may need todo a bugbash for this
|
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. WhyThe rejection this PR classifies comes from 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 ( The gate only polices language-server traffic. It matches on the user-agent token Two other measurements worth recording
Where it movedDetection 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 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 PRThe reviewed design mostly transfers, which is why I'm drafting rather than closing:
@laileni-aws — flagging since you approved this. Happy to walk through the service-side detail if useful. |
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.
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.
…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.
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:ListFeatureEvaluationssucceeds while the language server's identical call is denied, in the same second.restoreProfileSelection()only runs behindisValidEnterpriseSsoInUse(), so a Builder ID user never reaches that path — and IdC identities are exempt from the gate.ListAvailableProfilesreturns 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
Notificationfeature and this PR reacts to it.Changes
lsp/qDevAccessBlockedHandler.ts(new)aws/window/showNotification, persists the service message, signs the user out. Never throwscodewhisperer/util/qDevAccessBlocked.ts(new)authUtil.ts,backend_amazonq.tslistRegionProfilesshort-circuits rather than calling an API that cannot succeed; Try a different login method clears state and firesonActiveConnectionModifiedregionProfileSelector.vueregionProfileManager.tsnameandreasonon AccessDenied so a misclassification is diagnosable from customer logsFour things reviewers should look at, because each was a real bug found in testing
Identification is by
id, never by title.RouterByServerNamerewrites the notification id into base64 of{"serverName":...,"id":...}, so the raw id never arrives and an earlier revision was matching oncontent.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 andsignout()is skipped — androot.vueonly re-evaluates the auth stage on that event. Without firing it the state clears but the screen never changes.loadMetadatamust be initialised when entering the profile-selection stage.setDidLoaddereferencesloadMetadata!.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
packages/amazonqunit suite: 858 tests, 0 failures, identical tomainNot included
The blank-chat-panel webpack regression (
esbuild-loaderv4 emitting IIFE and breakinglibraryTarget: 'this') is unrelated and still needs its own PR. Telemetry for block detection is agreed as a follow-up.