feat: distinct sign-in error when Amazon Q Developer access is blocked - #159
feat: distinct sign-in error when Amazon Q Developer access is blocked#159ashishrp-aws wants to merge 4 commits into
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: aws/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.
Problem
Amazon Q Developer no longer accepts new Builder ID customers. Those users can sign in successfully, then find Q silently non-functional: chat returns the service's rejection as if it were a chat reply, and nothing explains what happened or offers a way out.
What the user sees now
Sign-in succeeds (it is OIDC and never gated). Within a couple of seconds the block is reported, the user is signed out, and the login view shows the service's own message — including a clickable link to kiro.dev — with a single Go back that returns them to sign-in so they can try a different account.
Important: the detection approach changed
The first revision of this PR classified the block from
ListAvailableProfiles. That cannot work, and the reasons are worth recording because they constrain any client-side approach:AWS-Language-Servers-AWS-CodeWhisperer). The extension's own SDK calls carry a different UA and are allowed unconditionally. Verified against prod with a blocked Builder ID: the extension's ownListFeatureEvaluationssucceeds while the language server's identical call is denied, in the same second.restoreProfileSelection()only runs behindisValidEnterpriseSsoInUse()(authUtil.ts), so a Builder ID user never reaches that path — and IdC identities are exempt from the gate, so the only identity type that does reach it is never denied.ListAvailableProfilesrejects every Builder ID caller with "AWS Builder ID is not supported for this operation" andreason: undefined— identical for healthy and blocked identities.So the language server is the only component the service ever says "no" to. It now reports the rejection over the existing
Notificationfeature, and this PR reacts to that.Changes
lsp/qDevAccessBlockedHandler.ts(new)aws/window/showNotification, persists the service message, signs the user out. Never throws.codewhisperer/util/qDevAccessBlocked.ts(new)authUtil.ts,backend_amazonq.tslistRegionProfilesshort-circuits to the stored message instead of calling an API that cannot succeed.backend_amazonq.tsonActiveConnectionModified.regionProfileSelector.vueregionProfileManager.tsnameandreasonon AccessDenied so a misclassification is diagnosable from customer logs.Two details worth reviewer attention:
Firing
onActiveConnectionModifiedis required, not defensive. Reacting to the block already signs the user out, so by the time Go back runs there is no connection andsignout()is skipped — androot.vueonly re-evaluates the auth stage on that event. Without firing it, the state is cleared correctly but the screen never changes. This was a real bug found in testing.The message is rendered as segments, not
v-html. It comes from a service response; interpolating it as markup would let that response inject into the login webview. Splitting into text/URL segments keeps Vue's escaping and puts the URL only inhref.Dependency
Requires a language server carrying the server-side reporting: aws/language-servers #2794, #2796, #2797. Older servers send nothing and this code stays dormant, so this is safe to ship ahead of the server rollout.
Testing
LOGIN).packages/amazonqunit suite: 858 tests, 0 failures, identical tomain(832 passing / 26 pending on both), so no regressions and no tests lost.Not included
The blank-chat-panel webpack regression (
esbuild-loaderv4 emitting IIFE and breakinglibraryTarget: 'this') is a separate, unrelated bug that would ship in 2.5.0. It is deliberately kept out of this PR and will be raised on its own.