Skip to content

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

Open
ashishrp-aws wants to merge 4 commits into
aws:mainfrom
ashishrp-aws:feat/qdev-not-accepting-new-customers
Open

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

Conversation

@ashishrp-aws

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

Copy link
Copy Markdown
Contributor

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:

  • RTS gates on User-Agent, not on identity alone. The gate applies only to traffic carrying the shared language server's token (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 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() (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.
  • The Builder ID error is not distinguishable. ListAvailableProfiles rejects every Builder ID caller with "AWS Builder ID is not supported for this operation" and reason: 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 Notification feature, and this PR reacts to that.

Changes

Area Change
lsp/qDevAccessBlockedHandler.ts (new) Listens for 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 existing blocked screen; listRegionProfiles short-circuits to the stored message instead of calling an API that cannot succeed.
backend_amazonq.ts Go back clears the state and fires onActiveConnectionModified.
regionProfileSelector.vue URLs in the message render as links.
regionProfileManager.ts Log name and reason on AccessDenied so a misclassification is diagnosable from customer logs.

Two details worth reviewer attention:

Firing onActiveConnectionModified is required, not defensive. Reacting to the block already signs the user out, so by the time Go back runs 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 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 in href.

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

  • End to end in VS Code against prod RTS with a Builder ID created after the cutoff: block reported, user signed out, message rendered with working link, Go back returns to sign-in.
  • Negative case: a healthy identity is unaffected — no message, normal chat.
  • 3 unit tests added covering the routing, the verbatim message, and the Go back path (flag cleared, event fired once, state back to LOGIN).
  • Full packages/amazonq unit suite: 858 tests, 0 failures, identical to main (832 passing / 26 pending on both), so no regressions and no tests lost.

Not included

The blank-chat-panel webpack regression (esbuild-loader v4 emitting IIFE and breaking libraryTarget: '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.

…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
Contributor

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
Contributor

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
Contributor 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: aws/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.
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