Skip to content

[Bug]: Kiro provider profileArn required 400 for Builder ID accounts on gated models #993

Closed

Description

@redeye1011

Summary

Kiro's GenerateAssistantResponse backend rejects requests from AWS Builder
ID / social-login accounts with 400 ValidationException: profileArn is required for this request for at least claude-opus-5 and
claude-sonnet-5. No local store ever caches a profileArn for that account
type, resolveKiroProfileArn() in src/oauth/kiro.ts only reads a cached
value, and src/adapters/kiro.ts's build() silently omits profileArn
from the wire payload when nothing was cached — so every request from a
Builder ID account against those models fails.

I expected the request to either succeed (matching what the official
kiro-cli does for the same account) or fail with a clearer, provider-level
error rather than a raw upstream ValidationException.

Investigation update — a candidate fix I proposed below turned out to be
insufficient; see "What I tried" for why. Filing as a bug report with the
open question rather than a verified fix.

Reproduction

  1. Configure the kiro provider with authMode: oauth, importing a
    Builder-ID (not IAM Identity Center) kiro-cli session — not an API key
    (ksk_...) login.
  2. POST /v1/chat/completions with "model": "kiro/claude-opus-5" (or
    claude-sonnet-5) and any prompt.
  3. Observe:
Provider error 400: Kiro invalid request:
com.amazon.kiro.runtimeservice#ValidationException: profileArn is required for this request.

Debug logging (OCX_DEBUG=1) on the failing request:

[ocx:kiro:request] {"region":"us-east-1","requestedModel":"claude-opus-5","completionMode":"disabled","bodyBytes":362,"messageCount":1,"toolCount":0,"hasProfileArn":false,"wireClient":"cli","hasPreviousResponseId":false}

For comparison, the official kiro-cli (v2.16.0) against the same account
succeeds on the same model:

$ kiro-cli chat --model claude-opus-5 --effort high --no-interactive "Say OK"
> OK
 ▸ Credits: 0.25 • Time: 2s

Root cause

src/oauth/kiro.ts — resolveKiroProfileArn():

export function resolveKiroProfileArn(account?: Pick<KiroOAuthMetadata, "profileArn">): string | undefined {
  if (account !== undefined) return account.profileArn;
  const env = process.env.KIRO_PROFILE_ARN;
  if (env) return env;
  return readImportedKiroCredential()?.profileArn;
}

This only returns a profileArn if one is already cached in account
metadata, the KIRO_PROFILE_ARN env var, or the locally imported kiro-cli
credential. For a Builder ID account none of these exist — the OAuth account
metadata opencodex stores for Kiro is {ssoRegion, apiRegion, clientId, clientSecret} with no profileArn field, and the local kiro-cli SQLite
store (~/.local/share/kiro-cli/data.sqlite3) has no row for
api.codewhisperer.profile either.

src/adapters/kiro.ts (build(), ~line 1699) treats the absence of a
cached profileArn as proof the account doesn't need one:

const resolvedProfileArn = resolveKiroProfileArn(parsed._kiroAuthContext);
const isApiKey = provider.apiKey.trim().startsWith("ksk_");
const profileArn = isApiKey ? undefined : resolvedProfileArn;
const wireClient: KiroWireClient = isApiKey || !profileArn ? "cli" : "ide";
...
if (profileArn) payload.profileArn = profileArn;

That assumption holds for most models but not for the ones Kiro gates
server-side.

What I tried (fix candidate — insufficient, do not merge as-is)

Initial (wrong) theory: strings on the kiro-cli 2.16.0 binary surfaced
a literal profile ARN compiled into
fig_api_client::profile_resolver::ProfileResolver::for_builder_id, and a
trace-level (KIRO_LOG_LEVEL=trace) kiro-cli run showed that exact ARN
attached to every outgoing request on my test account, including
ListAvailableModels. No ListAvailableProfiles RPC ever fires in the
trace on either run described below. I concluded this was a single
constant, shared by every Builder ID account, and proposed hardcoding it as
a fallback in the adapter (diff below, KIRO_PROFILE_PLACEHOLDER stands in
for the literal value from that binary).

Falsified by a second, independent account. I logged a second,
unrelated Builder ID/social-login account into kiro-cli 2.16.0 on a
separate machine and ran the same trace. That account's kiro-cli attached
a different profile ARN — different AWS account-id component and
different profile-id component — from the one on the first account, again
with no ListAvailableProfiles call in the trace. That rules out "one
shared constant for all Builder ID accounts." The mechanism kiro-cli uses
to arrive at a per-account profile with no visible RPC is still unknown to
me — possibly derived from claims already present in the OAuth/OIDC token,
possibly returned inline during token exchange rather than a separate call.
I have not yet confirmed either.

Net: the "hardcode a shared default ARN" fix is wrong. It would send one
account's real profile ARN as a blanket fallback for every other Builder ID
account, which is not obviously safer than omitting profileArn entirely
(today's behavior) and could plausibly cause Kiro's backend to associate a
request with the wrong profile rather than just reject it.

Diff as proposed (do not merge without resolving the above):

--- a/src/adapters/kiro.ts
+++ b/src/adapters/kiro.ts
@@ constants near line 55
 const KIRO_FALLBACK_SERIALIZATION_ENVELOPE_BYTES = 64 * 1024;
+const KIRO_BUILDER_ID_DEFAULT_PROFILE_ARN = "<KIRO_PROFILE_PLACEHOLDER>";
 type KiroWireClient = "ide" | "cli";

@@ inside build(), ~line 1697
     const region = resolveKiroApiRegion(parsed._kiroAuthContext);
     const resolvedProfileArn = resolveKiroProfileArn(parsed._kiroAuthContext);
     const isApiKey = provider.apiKey.trim().startsWith("ksk_");
-    const profileArn = isApiKey ? undefined : resolvedProfileArn;
     // wireClient must key off the ORIGINAL lookup, not the fallback below.
-    const wireClient: KiroWireClient = isApiKey || !profileArn ? "cli" : "ide";
+    const wireClient: KiroWireClient = isApiKey || !resolvedProfileArn ? "cli" : "ide";
+    const profileArn = isApiKey ? undefined : (resolvedProfileArn ?? KIRO_BUILDER_ID_DEFAULT_PROFILE_ARN);

Client or integration

Direct HTTP/API client (also reachable via Codex CLI once opencodex
injects its openai_base_url)

Area

Provider adapter / Authentication and account pool

Version

2.10.0 (@bitkyc08/opencodex)

Operating system

Linux (Ubuntu, x86_64) — reproduced on two separate machines/accounts, same
OS family

Provider and model

kiro / claude-opus-5, claude-sonnet-5 (confirmed affected); a model not
requiring a profileArn (gpt-5.6-terra) shows no equivalent failure on the
same account, so this is model-specific on Kiro's side, not account-wide.

Logs or error output

Failing request (before any change):

$ curl -s -X POST http://127.0.0.1:PORT/v1/chat/completions \
  -d '{"model":"kiro/claude-opus-5","messages":[{"role":"user","content":"Say OK"}],"reasoning_effort":"high"}'
{"error":{"message":"Provider error 400: Kiro invalid request: com.amazon.kiro.runtimeservice#ValidationException: profileArn is required for this request.","type":"invalid_request_error","param":null,"code":"invalid_request_error"}}
[ocx:kiro:request] {"region":"us-east-1","requestedModel":"claude-opus-5","completionMode":"disabled","bodyBytes":362,"messageCount":1,"toolCount":0,"hasProfileArn":false,"wireClient":"cli","hasPreviousResponseId":false}

Same request, same account, with the candidate fix applied (succeeds, but
see "What I tried" for why this specific fix is not safe to merge):

$ curl -s -X POST http://127.0.0.1:PORT/v1/chat/completions \
  -d '{"model":"kiro/claude-opus-5","messages":[{"role":"user","content":"Say OK"}],"reasoning_effort":"high"}'
{"id":"chatcmpl-...","object":"chat.completion","model":"kiro/claude-opus-5","choices":[{"index":0,"message":{"role":"assistant","content":"OK"},"finish_reason":"stop","logprobs":null}],"usage":{"prompt_tokens":9,"completion_tokens":1,"total_tokens":10,"prompt_tokens_details":{"cached_tokens":0},"completion_tokens_details":{"reasoning_tokens":0}}}
[ocx:kiro:request] {"region":"us-east-1","requestedModel":"claude-opus-5","completionMode":"disabled","bodyBytes":466,"messageCount":1,"toolCount":0,"hasProfileArn":true,"wireClient":"cli","hasPreviousResponseId":false}
[ocx:kiro:attempt_complete] {"mode":"disabled","sawText":true,"sawReasoning":false,"sawRealTool":false,"completionCalls":0,"nativeCompletionStop":true,"stopReason":"END_TURN","assistantChars":2}

Redacted configuration

{
  "adapter": "kiro",
  "baseUrl": "https://runtime.us-east-1.kiro.dev",
  "authMode": "oauth",
  "defaultModel": "kiro-auto"
}

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and
    personal data. Specifically: no AWS account IDs, no Kiro profile IDs, no
    OAuth/access/refresh tokens, no email addresses, no hostnames/ports/paths
    from any machine used to reproduce this appear anywhere above — all such
    values are replaced with placeholders (<AWS_ACCOUNT_ID_PLACEHOLDER>,
    <KIRO_PROFILE_PLACEHOLDER>, PORT) even where they were plausible
    evidence for the bug, because two independently-tested accounts produced
    two different values for both fields and neither is reliably distinguishable
    from account-identifying data without a byte-level audit I'm not confident
    in giving a hard guarantee on.

Activity

  1. github-actions commented on Aug 4, 2026

    @github-actions
    Contributor

    Automated translation bookkeeping — detected language: unknown.

  2. github-actions commented on Aug 4, 2026

    @github-actions
    Contributor

    Issue reopened

    The report now contains the information required by the automated check. Thanks for updating it.

  3. changed the title [-]Kiro provider: profileArn is required 400 for Builder ID accounts on gated models (fix included, verified)[/-] [+][Bug]: Kiro provider profileArn required 400 for Builder ID accounts on gated models[/+] on Aug 4, 2026
  4. redeye1011 commented on Aug 4, 2026

    @redeye1011
    Author

    @lidge-jun — flagging a mistake I made on this issue that needs your admin access to fully fix.

    In my first version of this report (posted, then edited), I included a real AWS account ID and Kiro profile ID extracted from my own account while investigating this bug. I later redacted them from the current body, but I've since confirmed GitHub retains that original text in the issue's public edit history — anyone can view it via the "edited" trail or the API. I don't have repo admin access to purge that history myself.

    Could you delete this issue outright, or otherwise scrub the edit history, so that original revision isn't publicly retrievable? I can re-file a clean version of the bug report (already prepared, fully redacted) once the old history is gone — happy to do that immediately after.

    For context on the report itself: root cause is confirmed (Kiro's GenerateAssistantResponse rejects Builder ID/social-login accounts with no profileArn for at least claude-opus-5/claude-sonnet-5), but the specific fix I proposed turned out to be wrong — testing against a second, independent account showed the profile ARN is per-account, not a shared constant, so a hardcoded fallback isn't safe. That part is accurately reflected in the current body; the privacy issue above is separate and is what I need your help with.

    Apologies for the mess — appreciate you taking a look.

  5. added a commit that references this issue on Aug 5, 2026
  6. lidge-jun commented on Aug 5, 2026

    @lidge-jun
    Owner

    Fixed on dev in #1032 (merged as dddc674ca).

    profileArn is captured from kiro-cli whoami --format json when the SQLite import lacks one, and a profileArn-required 400 is classified as its own non-retryable error instead of being retried blindly.

    The identity binding is guarded: if the active CLI session changes between the SQLite read and whoami, the whole identity is discarded rather than binding account A's token to account B's ARN. An imported profileArn stays authoritative, and a malformed ARN is ignored without breaking the login.

    One caveat worth stating: the whoami JSON shapes are read from the documented-ish forms, and this was not verified against a live Builder ID account. If your whoami output uses a different shape and the ARN still comes up missing, please reopen with a redacted kiro-cli whoami --format json and I will match the real contract.

    Not in a release yet. This is on dev, and main is at v2.10.0. It ships in the next release.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    account-poolOAuth, credentials, Codex pool, quota, failover, plansbugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions