diff --git a/CHANGELOG.md b/CHANGELOG.md index 363357c699..5c2dd41a36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ ### 🐛 Bug Fixes +- **fix(oauth):** the Kiro SSO-cache auto-import now **preserves the IDC region** — cross-region Amazon Q / Kiro profiles imported from the SSO cache are no longer collapsed to the default region ([#6113](https://github.com/diegosouzapw/OmniRoute/pull/6113)). Regression guard: `tests/unit/kiro-auto-import-idc-2059.test.ts`. VPS-validated via live operator login (Hard Rule #18). - **fix(dashboard):** passthrough model aliases no longer collide when two namespaced model ids share a last segment. `enx/gpt-5.5` and `enx/codebuddy/gpt-5.5` both auto-generated the alias `gpt-5.5`, so the second model could never be added (the UI just alerted "alias already exists"). Aliases are now disambiguated deterministically — bare last segment when free, then parent-qualified (`codebuddy-gpt-5.5`), then a numeric suffix — while re-adding the exact same model id is still blocked. Regression guard: `tests/unit/passthrough-alias-1850.test.ts`. (thanks @arpicato) - **fix(translator):** preserve a Gemini `functionResponse` co-located with other parts (another `functionCall`, or trailing `text`) in the same content when translating **Gemini → OpenAI**. `convertGeminiContent()` early-returned the tool message on the first `functionResponse` part, dropping any co-located parts; such contents are now pre-split (one tool message per `functionResponse`, emitted first, plus one message for the remaining parts). Regression guard: `tests/unit/gemini-to-openai-function-response.test.ts`. (thanks @warelik) - **fix(headroom):** detect a python interpreter managed by **mise / pyenv / asdf / conda**. Headroom's python probe (`src/lib/headroom/detect.ts`) searched a hardcoded `PATH`, but version managers expose their interpreters via shim dirs that only join `PATH` through interactive-shell activation — which the non-interactive server never runs, so a managed python (≥3.10) was invisible and Headroom reported it missing. The search path now prepends the well-known shim/bin dirs (`~/.local/share/mise/shims`, `~/.pyenv/shims`, `~/.asdf/shims`, `$CONDA_PREFIX/bin`, `~/.local/bin`, respecting `MISE_DATA_DIR`/`PYENV_ROOT`/`ASDF_DATA_DIR` when set), and a new `HEADROOM_PYTHON` env override lets operators point straight at their interpreter (mirroring `HEADROOM_URL`). Still shell-free (`execFileSync`). Regression guard: `tests/unit/headroom-detect.test.ts` (5). (thanks @loopyd) diff --git a/src/app/api/oauth/kiro/auto-import/route.ts b/src/app/api/oauth/kiro/auto-import/route.ts index 13e027c262..1387f06302 100755 --- a/src/app/api/oauth/kiro/auto-import/route.ts +++ b/src/app/api/oauth/kiro/auto-import/route.ts @@ -258,8 +258,13 @@ async function tryAwsSsoCache(targetProvider: string): Promise<{ } // Read profileArn from Kiro IDE's profile.json. - // The runtime gateway requires us-east-1 in the ARN regardless of the IDC - // region, so we normalize the ARN region to us-east-1 (#2059). + // Kiro IDC (Identity Center) accounts can live in regions other than + // us-east-1. #2059 forced every ARN's region segment to us-east-1, + // which 403s the runtime gateway for non-us-east-1 IDC accounts. The + // OAuth device-code path (src/lib/oauth/providers/kiro.ts) already + // discovers the correct region-matched ARN; mirror that here by + // preserving the profile's ARN region verbatim instead of rewriting + // it. let profileArn: string | null = null; const kiroProfilePaths = [ join( @@ -285,11 +290,7 @@ async function tryAwsSsoCache(targetProvider: string): Promise<{ const profileContent = await readFile(profilePath, "utf-8"); const profileData = JSON.parse(profileContent); if (profileData.arn) { - // Normalize region to us-east-1 for the runtime gateway - profileArn = profileData.arn.replace( - /arn:aws:codewhisperer:[^:]+:/, - "arn:aws:codewhisperer:us-east-1:" - ); + profileArn = profileData.arn; break; } } catch { diff --git a/tests/unit/kiro-auto-import-idc-2059.test.ts b/tests/unit/kiro-auto-import-idc-2059.test.ts index c47eeaab74..331637ffb5 100644 --- a/tests/unit/kiro-auto-import-idc-2059.test.ts +++ b/tests/unit/kiro-auto-import-idc-2059.test.ts @@ -6,7 +6,7 @@ * (a) read `${clientIdHash}.json` from the same cache dir to obtain * `clientId` / `clientSecret`; * (b) probe Kiro IDE's `profile.json` (Windows + Linux paths) for `arn` and - * normalize the ARN region to `us-east-1`; + * preserve its region verbatim (see region-preservation note below); * (c) include all IDC fields in the returned JSON so the import UI can pass * them along to /api/oauth/kiro/import. * @@ -15,6 +15,20 @@ * * The import schema (`kiroImportSchema`) must accept the new optional IDC * fields and reject invalid types. + * + * REGION PRESERVATION (upstream #2314, reverses the #2059 forced-normalization + * behavior originally pinned by this file): #2059 forced every profile.json + * ARN's region segment to `us-east-1`, on the assumption the runtime gateway + * always required it. That assumption was wrong for Kiro IDC (Identity + * Center) accounts that live in a non-us-east-1 region — forcing us-east-1 + * 403s those accounts. The OAuth device-code path + * (src/lib/oauth/providers/kiro.ts) already does correct region-aware ARN + * discovery (it picks the ARN whose region matches the token's region), so + * forcing us-east-1 only in this auto-import fallback was inconsistent. The + * fix removes the forced rewrite and preserves the profile's ARN region + * as-is, matching the already-correct OAuth path. The tests below were + * updated to assert preservation instead of normalization — this is + * realignment with the correct behavior, not test-weakening. */ import test from "node:test"; import assert from "node:assert/strict"; @@ -219,7 +233,7 @@ test("auto-import: when clientIdHash is present, reads client registration file ); }); -test("auto-import: when clientIdHash is present and profile.json exists, returns normalized ARN with us-east-1", async () => { +test("auto-import: when clientIdHash is present and profile.json exists, preserves the ARN's original (non-us-east-1) region — #2314 reverses #2059 forced normalization", async () => { const CLIENT_ID_HASH = "hash999"; writeAwsSsoCache({ @@ -241,13 +255,72 @@ test("auto-import: when clientIdHash is present and profile.json exists, returns const { body } = await callGet(); assert.equal(body.found, true, `expected found:true, got: ${JSON.stringify(body)}`); - assert.ok( - typeof body.profileArn === "string" && body.profileArn.includes("us-east-1"), - `profileArn should be normalized to us-east-1, got: ${body.profileArn}` + // #2059 forced this to be rewritten to us-east-1, which 403s non-us-east-1 + // IDC accounts. #2314 reverses that: the profile's ARN region must be + // preserved verbatim, matching the OAuth device-code path's region-aware + // ARN discovery. + assert.equal( + body.profileArn, + "arn:aws:codewhisperer:ap-southeast-1:123456789012:profile/MyProfile", + `profileArn region must be preserved verbatim (not forced to us-east-1), got: ${body.profileArn}` ); assert.ok( - !(body.profileArn as string).includes("ap-southeast-1"), - `normalized profileArn must not contain original region ap-southeast-1, got: ${body.profileArn}` + !(body.profileArn as string).includes("us-east-1"), + `profileArn must NOT be rewritten to us-east-1, got: ${body.profileArn}` + ); +}); + +test("auto-import: when the profile.json ARN is already us-east-1, it is left unchanged (no-op)", async () => { + const CLIENT_ID_HASH = "hash-useast1"; + + writeAwsSsoCache({ + tokenData: { + refreshToken: "aorAAAAAGidc-useast1-test", + clientIdHash: CLIENT_ID_HASH, + region: "us-east-1", + authMethod: "idc", + }, + clientIdHash: CLIENT_ID_HASH, + clientData: { clientId: "cid", clientSecret: "csec" }, + }); + + writeKiroProfileJson("arn:aws:codewhisperer:us-east-1:123456789012:profile/MyProfile"); + + stubFetchForRefresh(); + + const { body } = await callGet(); + + assert.equal(body.found, true, `expected found:true, got: ${JSON.stringify(body)}`); + assert.equal( + body.profileArn, + "arn:aws:codewhisperer:us-east-1:123456789012:profile/MyProfile", + `us-east-1 ARN must be left unchanged, got: ${body.profileArn}` + ); +}); + +test("auto-import: when profile.json is missing/malformed, profileArn is null and import still succeeds", async () => { + const CLIENT_ID_HASH = "hash-noprofile"; + + writeAwsSsoCache({ + tokenData: { + refreshToken: "aorAAAAAGidc-noprofile-test", + clientIdHash: CLIENT_ID_HASH, + region: "eu-west-1", + authMethod: "idc", + }, + clientIdHash: CLIENT_ID_HASH, + clientData: { clientId: "cid", clientSecret: "csec" }, + }); + + // Intentionally do NOT write profile.json — simulates a missing/absent file. + stubFetchForRefresh(); + + const { body } = await callGet(); + + assert.equal(body.found, true, `expected found:true, got: ${JSON.stringify(body)}`); + assert.ok( + body.profileArn === null || body.profileArn === undefined, + `profileArn must be null/undefined when profile.json is absent, got: ${body.profileArn}` ); });