Skip to content

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

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.tsresolveKiroProfileArn():

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

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