From 678e6077e49e7ea5ab5a6cda5a87fcbc57a78985 Mon Sep 17 00:00:00 2001 From: Deftera <55022020+Deftera186@users.noreply.github.com> Date: Wed, 2 Sep 2026 06:02:31 +0300 Subject: [PATCH] fix(kiro): do not permanently ban on 'User is not authorized to make this call' (#11809) * fix(kiro): do not permanently ban on 'User is not authorized to make this call' * test(kiro): regression cover the 403 'User is not authorized' non-ban classification --------- Co-authored-by: Deftera186 Co-authored-by: Diego Rodrigues de Sa e Souza --- open-sse/services/errorClassifier.ts | 11 ++++ ...kiro-403-missing-profile-arn-11809.test.ts | 59 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 tests/unit/kiro-403-missing-profile-arn-11809.test.ts diff --git a/open-sse/services/errorClassifier.ts b/open-sse/services/errorClassifier.ts index 8ae4edc763..210bb01aba 100644 --- a/open-sse/services/errorClassifier.ts +++ b/open-sse/services/errorClassifier.ts @@ -363,6 +363,17 @@ export function classifyProviderError( if (recoverableProject403) { return PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR; } + // Kiro IDC missing profileArn — AWS returns 403 "User is not authorized to make this call" + // when the request is sent without a profileArn or to the wrong Q Developer region. + // This is a recoverable configuration issue, not a ban: the account still works in Kiro IDE. + // Do NOT classify as FORBIDDEN (which bans permanently). Treat as PROJECT_ROUTE_ERROR + // so the connection stays active and can be retried after profile discovery (#10725). + const isKiroProfile403 = + (p === "kiro" || p === "amazon-q") && + bodyStr.includes("User is not authorized to make this call"); + if (isKiroProfile403) { + return PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR; + } // A Cloudflare Sentinel/Turnstile 403 is a TERMINAL block for browser-session // providers: the user's IP/session needs a browser Turnstile challenge, and // retrying the same connection will keep 403ing. Classify as FORBIDDEN so diff --git a/tests/unit/kiro-403-missing-profile-arn-11809.test.ts b/tests/unit/kiro-403-missing-profile-arn-11809.test.ts new file mode 100644 index 0000000000..f34eb7379e --- /dev/null +++ b/tests/unit/kiro-403-missing-profile-arn-11809.test.ts @@ -0,0 +1,59 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { + classifyProviderError, + PROVIDER_ERROR_TYPES, +} from "../../open-sse/services/errorClassifier.ts"; + +// #11809 (follow-up to #10725) — a Kiro/Amazon Q IdC account whose Identity Center +// lives outside the Q Developer profile regions is stored without a profileArn, so +// CodeWhisperer answers 403 "User is not authorized to make this call". That is a +// RECOVERABLE configuration issue (the same token succeeds once the profile ARN is +// discovered, and the account keeps working in Kiro IDE) — not a ban. Before the fix +// it fell through to FORBIDDEN, which markAccountUnavailable turns into the terminal +// "banned" state (is_active=0) and required a full re-auth on every authentication. + +const KIRO_MISSING_ARN_403 = { + message: "User is not authorized to make this call", +}; + +test("#11809: kiro 403 'User is not authorized to make this call' -> PROJECT_ROUTE_ERROR, not FORBIDDEN", () => { + assert.equal( + classifyProviderError(403, KIRO_MISSING_ARN_403, "kiro"), + PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR, + ); +}); + +test("#11809: amazon-q shares the Kiro executor/credentials -> same recoverable classification", () => { + assert.equal( + classifyProviderError(403, KIRO_MISSING_ARN_403, "amazon-q"), + PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR, + ); +}); + +test("#11809: the message is matched inside a raw CodeWhisperer error body too", () => { + const body = + '{"__type":"AccessDeniedException","message":"User is not authorized to make this call."}'; + assert.equal( + classifyProviderError(403, body, "kiro"), + PROVIDER_ERROR_TYPES.PROJECT_ROUTE_ERROR, + ); +}); + +test("control: an unrelated kiro 403 still bans (FORBIDDEN) — carve-out is message-scoped", () => { + assert.equal(classifyProviderError(403, "Forbidden", "kiro"), PROVIDER_ERROR_TYPES.FORBIDDEN); +}); + +test("control: the same message on a non-Kiro oauth provider keeps FORBIDDEN — carve-out is provider-scoped", () => { + assert.equal( + classifyProviderError(403, KIRO_MISSING_ARN_403, "claude"), + PROVIDER_ERROR_TYPES.FORBIDDEN, + ); +}); + +test("control: a real Kiro ban signal still classifies as ACCOUNT_DEACTIVATED", () => { + assert.equal( + classifyProviderError(403, "your account has been suspended", "kiro"), + PROVIDER_ERROR_TYPES.ACCOUNT_DEACTIVATED, + ); +});