-
Notifications
You must be signed in to change notification settings - Fork 1.2k
fix(kiro): capture profileArn from whoami and classify profileArn-required 400s (#993) #1032
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0e326a9
d2dd489
0338d07
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -109,6 +109,17 @@ function classifyKiroFailure( | |||||||||||||||||||||||||||||||||||||||||
| retryable: false, | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
| // #993: a gated model demanding a profileArn gets a stable, actionable code | ||||||||||||||||||||||||||||||||||||||||||
| // instead of the generic validation bucket. Non-retryable by definition. | ||||||||||||||||||||||||||||||||||||||||||
| if (evidence.includes("profilearn") && evidence.includes("required")) { | ||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||
| message: "kiro_profile_required: Kiro requires a CodeWhisperer profileArn for this account and model. Re-login or re-import the matching Kiro account (ocx account login kiro --reauth) so the profile is captured, then retry.", | ||||||||||||||||||||||||||||||||||||||||||
| status: 400, | ||||||||||||||||||||||||||||||||||||||||||
| errorType: "invalid_request_error", | ||||||||||||||||||||||||||||||||||||||||||
| code: "kiro_profile_required", | ||||||||||||||||||||||||||||||||||||||||||
| retryable: false, | ||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+114
to
+122
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Restrict Line 114 matches independent occurrences of Require Proposed fix const headerType = headerValue(headers, ":exception-type") || headerValue(headers, ":error-type") || "";
const evidence = [headerType, ...payloadDetails(payloadText), message].join(" ").toLowerCase();
+ const profileArnRequired =
+ /\bprofile\s*arn\b\s+(?:is\s+)?required\b|\brequired\s+(?:for\s+)?profile\s*arn\b/.test(evidence);
if (isContentLengthError(evidence)) {
return {
// ...
};
}
- if (evidence.includes("profilearn") && evidence.includes("required")) {
+ if ((status === undefined || status === 400) && profileArnRequired) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||
| evidence.includes("insufficient_quota") | ||||||||||||||||||||||||||||||||||||||||||
| || evidence.includes("quota exhausted") | ||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -172,13 +172,37 @@ async function defaultKiroCliRunner(args: string[], signal?: AbortSignal): Promi | |||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| async function readKiroCliIdentity(runner: KiroCliRunner, signal?: AbortSignal): Promise<{ email?: string }> { | ||||||||||||||||||||||||||
| /** Kiro profile ARN structure: arn:<partition>:codewhisperer:<region>:<account>:profile/<id> */ | ||||||||||||||||||||||||||
| const KIRO_PROFILE_ARN_PATTERN = /^arn:[a-z0-9-]+:codewhisperer:[a-z0-9-]+:\d{12}:profile\/[A-Za-z0-9-]+$/; | ||||||||||||||||||||||||||
| const KIRO_PROFILE_ARN_MAX_LENGTH = 256; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| function parseKiroProfileArn(value: unknown): string | undefined { | ||||||||||||||||||||||||||
| if (typeof value !== "string") return undefined; | ||||||||||||||||||||||||||
| const trimmed = value.trim(); | ||||||||||||||||||||||||||
| if (trimmed.length === 0 || trimmed.length > KIRO_PROFILE_ARN_MAX_LENGTH) return undefined; | ||||||||||||||||||||||||||
| return KIRO_PROFILE_ARN_PATTERN.test(trimmed) ? trimmed : undefined; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| function profileArnFromWhoami(parsed: Record<string, unknown>): string | undefined { | ||||||||||||||||||||||||||
| // Only narrowly-named documented-ish shapes; never invent an ARN (#993). | ||||||||||||||||||||||||||
| return parseKiroProfileArn(parsed.profileArn) | ||||||||||||||||||||||||||
| ?? parseKiroProfileArn(parsed.profile_arn) | ||||||||||||||||||||||||||
| ?? (parsed.profile && typeof parsed.profile === "object" && !Array.isArray(parsed.profile) | ||||||||||||||||||||||||||
| ? parseKiroProfileArn((parsed.profile as Record<string, unknown>).arn) | ||||||||||||||||||||||||||
| : undefined); | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| async function readKiroCliIdentity(runner: KiroCliRunner, signal?: AbortSignal): Promise<{ email?: string; profileArn?: string }> { | ||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||
| const result = await runner(["whoami", "--format", "json"], signal); | ||||||||||||||||||||||||||
| if (result.exitCode !== 0) return {}; | ||||||||||||||||||||||||||
| const parsed = JSON.parse(result.stdout) as { email?: unknown }; | ||||||||||||||||||||||||||
| const parsed = JSON.parse(result.stdout) as Record<string, unknown>; | ||||||||||||||||||||||||||
| const email = typeof parsed.email === "string" ? parsed.email.trim().toLowerCase() : ""; | ||||||||||||||||||||||||||
| return email && email.length <= 320 ? { email } : {}; | ||||||||||||||||||||||||||
| const profileArn = profileArnFromWhoami(parsed); | ||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||
| ...(email && email.length <= 320 ? { email } : {}), | ||||||||||||||||||||||||||
| ...(profileArn ? { profileArn } : {}), | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
| } catch { | ||||||||||||||||||||||||||
| return {}; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
@@ -251,14 +275,34 @@ async function oauthCredentialFromImported( | |||||||||||||||||||||||||
| runner: KiroCliRunner, | ||||||||||||||||||||||||||
| signal?: AbortSignal, | ||||||||||||||||||||||||||
| ): Promise<OAuthCredentials> { | ||||||||||||||||||||||||||
| const identity = imported.source === "sqlite" ? await readKiroCliIdentity(runner, signal) : {}; | ||||||||||||||||||||||||||
| const metadata = metadataFromImported(imported); | ||||||||||||||||||||||||||
| let identity: { email?: string; profileArn?: string } = {}; | ||||||||||||||||||||||||||
| if (imported.source === "sqlite") { | ||||||||||||||||||||||||||
| identity = await readKiroCliIdentity(runner, signal); | ||||||||||||||||||||||||||
| // Session-switch race (#993 review): another process may have switched the | ||||||||||||||||||||||||||
| // active Kiro CLI session between the SQLite read and whoami. Accept | ||||||||||||||||||||||||||
| // whoami's identity only when the session token STILL matches the import — | ||||||||||||||||||||||||||
| // refresh token, or access token when refresh is absent. | ||||||||||||||||||||||||||
| if (identity.profileArn !== undefined) { | ||||||||||||||||||||||||||
| const current = readKiroCliSqliteCredential(); | ||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When a user imports Kiro from Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||
| const importedKey = imported.refresh || imported.access; | ||||||||||||||||||||||||||
| const currentKey = current ? current.refresh || current.access : ""; | ||||||||||||||||||||||||||
| if (!current || currentKey !== importedKey) identity = {}; | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
Comment on lines
+285
to
+290
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Revalidate every Line 285 revalidates the SQLite session only when Run the token comparison when either Proposed fix- if (identity.profileArn !== undefined) {
+ if (identity.email !== undefined || identity.profileArn !== undefined) {
const current = readKiroCliSqliteCredential();
const importedKey = imported.refresh || imported.access;
const currentKey = current ? current.refresh || current.access : "";
if (!current || currentKey !== importedKey) identity = {};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
| // Builder ID imports often lack a profileArn in SQLite; whoami against the | ||||||||||||||||||||||||||
| // SAME active CLI session can supply it (#993). Imported stays authoritative. | ||||||||||||||||||||||||||
| const resolvedProfileArn = imported.profileArn ?? identity.profileArn; | ||||||||||||||||||||||||||
| const metadata: KiroOAuthMetadata | undefined = (() => { | ||||||||||||||||||||||||||
| const base = metadataFromImported(imported) ?? {}; | ||||||||||||||||||||||||||
| if (resolvedProfileArn && !base.profileArn) base.profileArn = resolvedProfileArn; | ||||||||||||||||||||||||||
| return Object.keys(base).length > 0 ? base : undefined; | ||||||||||||||||||||||||||
| })(); | ||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||
| access: imported.access, | ||||||||||||||||||||||||||
| refresh: imported.refresh, | ||||||||||||||||||||||||||
| expires: imported.expires, | ||||||||||||||||||||||||||
| source: imported.source === "json" ? "credential-file" : "local-cli", | ||||||||||||||||||||||||||
| ...(imported.profileArn ? { accountId: imported.profileArn } : {}), | ||||||||||||||||||||||||||
| ...(resolvedProfileArn ? { accountId: resolvedProfileArn } : {}), | ||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When an existing Kiro account was saved before it had a profile ARN, its stored identity is usually the email. Returning the new credential with Useful? React with 👍 / 👎. |
||||||||||||||||||||||||||
| ...(identity.email ? { email: identity.email } : {}), | ||||||||||||||||||||||||||
| ...(metadata ? { kiro: metadata } : {}), | ||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new
kiro_profile_requiredcode is only carried by stream/parser error events; the ordinary non-stream HTTP path callssafeKiroHttpErrorMessage(), then wraps this response asformatErrorResponse(status, "upstream_error", ...), so the/v1/chat/completionsreproduction for this 400 still returns top-levelcode: "upstream_error"instead of the stable code added here. Return a structured classification to the HTTP wrapper or map this message beforeformatErrorResponseso non-stream clients can handle the same condition.Useful? React with 👍 / 👎.