From 28996a4cfb379fb1f2b337677187fae426c1ff1b Mon Sep 17 00:00:00 2001 From: byongshintv <47180856+byongshintv@users.noreply.github.com> Date: Wed, 29 Jul 2026 18:32:09 +0900 Subject: [PATCH 1/2] fix(kiro): use CLI wire contract without profiles Co-authored-by: OpenAI Codex --- src/adapters/kiro.ts | 57 +++++++++++++++++++++++++++++++++++--- tests/kiro-adapter.test.ts | 47 +++++++++++++++++++++++++++---- 2 files changed, 95 insertions(+), 9 deletions(-) diff --git a/src/adapters/kiro.ts b/src/adapters/kiro.ts index 8d022c2d74..740c2388b3 100644 --- a/src/adapters/kiro.ts +++ b/src/adapters/kiro.ts @@ -52,6 +52,24 @@ const AMZ_TARGET = "AmazonCodeWhispererStreamingService.GenerateAssistantRespons const SDK_VERSION = "1.0.27"; const NODE_VERSION = "22.21.1"; const KIRO_IDE_VERSION = "1.0.0"; +type KiroWireClient = "ide" | "cli"; + +function kiroCliPlatform(): "linux" | "macos" | "windows" { + return process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux"; +} + +function kiroCliUserAgent(includeAppVersion: boolean): string { + return [ + "aws-sdk-rust/1.3.15", + "ua/2.1", + "api/codewhispererstreaming/0.1.17975", + `os/${kiroCliPlatform()}`, + "lang/rust/1.92.0", + ...(includeAppVersion ? ["md/appVersion-2.14.2"] : []), + "m/F", + "app/AmazonQ-For-CLI", + ].join(" "); +} // Payload construction (conversationState) interface KiroToolUse { @@ -68,7 +86,11 @@ interface KiroUserInputMessage { content: string; modelId?: string; origin?: string; - userInputMessageContext?: { tools?: unknown[]; toolResults?: KiroToolResult[] }; + userInputMessageContext?: { + tools?: unknown[]; + toolResults?: KiroToolResult[]; + envState?: { operatingSystem: string; currentWorkingDirectory: string }; + }; images?: KiroImage[]; } interface KiroHistoryEntry { @@ -385,6 +407,7 @@ export function buildKiroPayload( parsed: OcxParsedRequest, profileArn: string | undefined, forcedCompletionMode?: KiroCompletionMode, + wireClient: KiroWireClient = "ide", ): { payload: Record; nameMap: Map; @@ -509,7 +532,7 @@ export function buildKiroPayload( userInputMessage: { content: turn.content, modelId, - origin: "AI_EDITOR", + origin: wireClient === "cli" ? "KIRO_CLI" : "AI_EDITOR", ...(turn.images.length > 0 ? { images: turn.images } : {}), ...(turn.toolResults.length > 0 ? { userInputMessageContext: { toolResults: turn.toolResults } } : {}), }, @@ -526,6 +549,12 @@ export function buildKiroPayload( if (kiroTools.length > 0) { currentUim.userInputMessageContext = { ...(currentUim.userInputMessageContext ?? {}), tools: kiroTools }; } + if (wireClient === "cli") { + currentUim.userInputMessageContext = { + ...(currentUim.userInputMessageContext ?? {}), + envState: { operatingSystem: kiroCliPlatform(), currentWorkingDirectory: process.cwd() }, + }; + } if (completionMode === "text_fallback") { if (currentUim.content !== KIRO_COMPLETION_RETRY_MESSAGE) { currentUim.content = appendTurnText(currentUim.content, KIRO_COMPLETION_RETRY_MESSAGE); @@ -539,6 +568,10 @@ export function buildKiroPayload( const payload: Record = { conversationState: { chatTriggerType: "MANUAL", + ...(wireClient === "cli" ? { + agentContinuationId: crypto.randomUUID(), + agentTaskType: "vibe", + } : {}), conversationId, currentMessage: { userInputMessage: currentUim }, ...(history.length > 0 ? { history } : {}), @@ -1447,8 +1480,23 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter } const region = resolveKiroApiRegion(parsed._kiroAuthContext); const profileArn = resolveKiroProfileArn(parsed._kiroAuthContext); + // Builder ID and Kiro API keys have no profile ARN and are accepted only on Kiro's CLI + // request path. Enterprise profiles retain the existing IDE-shaped request. + const wireClient: KiroWireClient = profileArn ? "ide" : "cli"; + const isApiKey = provider.apiKey.trim().startsWith("ksk_"); const fp = fingerprint().slice(0, 64); - const headers: Record = { + const headers: Record = wireClient === "cli" ? { + authorization: `Bearer ${provider.apiKey}`, + "content-type": "application/x-amz-json-1.0", + accept: "*/*", + "x-amz-target": AMZ_TARGET, + "user-agent": kiroCliUserAgent(true), + "x-amz-user-agent": kiroCliUserAgent(false), + "x-amzn-codewhisperer-optout": "false", + "amz-sdk-request": "attempt=1; max=3", + "amz-sdk-invocation-id": invocationId(), + ...(isApiKey ? { tokentype: "API_KEY" } : {}), + } : { authorization: `Bearer ${provider.apiKey}`, "content-type": "application/x-amz-json-1.0", accept: "application/vnd.amazon.eventstream", @@ -1460,7 +1508,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter "amz-sdk-invocation-id": invocationId(), }; if (profileArn) headers["x-amzn-kiro-profile-arn"] = profileArn; - const built = buildKiroPayload(parsed, profileArn, forcedCompletionMode); + const built = buildKiroPayload(parsed, profileArn, forcedCompletionMode, wireClient); await normalizeKiroImages(built.payload); const contextInputEstimate = estimateKiroPayloadInputTokens(built.payload, parsed.modelId); const body = JSON.stringify(built.payload); @@ -1472,6 +1520,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter messageCount: kiroPayloadMessages(parsed).length, toolCount: parsed.context.tools?.length ?? 0, hasProfileArn: Boolean(profileArn), + wireClient, hasPreviousResponseId: Boolean(parsed.previousResponseId), }); return { diff --git a/tests/kiro-adapter.test.ts b/tests/kiro-adapter.test.ts index 62e5782797..7a0134ac16 100644 --- a/tests/kiro-adapter.test.ts +++ b/tests/kiro-adapter.test.ts @@ -73,14 +73,49 @@ describe("kiro adapter — buildRequest", () => { } }); - test("headers carry Bearer token + CW targets", async () => { - const { url, method, headers } = await createKiroAdapter(provider).buildRequest(parsedWith([{ role: "user", content: "hi" }])); + test("Builder ID requests without a profile ARN use the Kiro CLI wire contract", async () => { + const { url, method, headers, body } = await createKiroAdapter(provider).buildRequest(parsedWith([{ role: "user", content: "hi" }])); + const payload = JSON.parse(body) as { + profileArn?: string; + conversationState: { + agentContinuationId?: string; + agentTaskType?: string; + currentMessage: { userInputMessage: Record }; + }; + }; expect(url).toBe("https://runtime.us-east-1.kiro.dev/"); expect(method).toBe("POST"); expect(headers.authorization).toBe("Bearer tok-123"); expect(headers["x-amz-target"]).toBe("AmazonCodeWhispererStreamingService.GenerateAssistantResponse"); - expect(headers.accept).toBe("application/vnd.amazon.eventstream"); - expect(headers["x-amzn-kiro-agent-mode"]).toBe("vibe"); + expect(headers.accept).toBe("*/*"); + expect(headers["user-agent"]).toContain("app/AmazonQ-For-CLI"); + expect(headers["x-amzn-kiro-agent-mode"]).toBeUndefined(); + expect(headers["x-amzn-kiro-profile-arn"]).toBeUndefined(); + expect(headers.tokentype).toBeUndefined(); + expect(payload.profileArn).toBeUndefined(); + expect(payload.conversationState.agentTaskType).toBe("vibe"); + expect(payload.conversationState.agentContinuationId).toMatch(/^[0-9a-f-]{36}$/); + expect(payload.conversationState.currentMessage.userInputMessage).toMatchObject({ + content: "hi", + origin: "KIRO_CLI", + userInputMessageContext: { + envState: { + operatingSystem: process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux", + currentWorkingDirectory: process.cwd(), + }, + }, + }); + }); + + test("Kiro API keys use the CLI token type without exposing a profile ARN", async () => { + const apiKeyProvider = { ...provider, authMode: "key", apiKey: "ksk_example" } as unknown as OcxProviderConfig; + const request = await createKiroAdapter(apiKeyProvider).buildRequest(parsedWith([{ role: "user", content: "hi" }])); + const body = JSON.parse(request.body) as { profileArn?: string }; + + expect(request.headers.authorization).toBe("Bearer ksk_example"); + expect(request.headers.tokentype).toBe("API_KEY"); + expect(request.headers["x-amzn-kiro-profile-arn"]).toBeUndefined(); + expect(body.profileArn).toBeUndefined(); }); test("runtime URL uses KIRO_API_REGION separately from auth region", async () => { @@ -104,6 +139,8 @@ describe("kiro adapter — buildRequest", () => { expect(request.url).toBe("https://runtime.eu-central-1.kiro.dev/"); expect(request.headers["x-amzn-kiro-profile-arn"]).toBe(parsed._kiroAuthContext.profileArn); + expect(request.headers.accept).toBe("application/vnd.amazon.eventstream"); + expect(request.headers["x-amzn-kiro-agent-mode"]).toBe("vibe"); expect(body.profileArn).toBe(parsed._kiroAuthContext.profileArn); }); @@ -668,7 +705,7 @@ describe("kiro adapter — buildRequest", () => { ]; const cs = JSON.parse((await createKiroAdapter(provider).buildRequest(parsedWith(messages))).body).conversationState; expect(cs.history).toEqual([ - { userInputMessage: { content: "first\n\nsecond", modelId: "claude-sonnet-4.5", origin: "AI_EDITOR" } }, + { userInputMessage: { content: "first\n\nsecond", modelId: "claude-sonnet-4.5", origin: "KIRO_CLI" } }, { assistantResponseMessage: { content: "one\n\ntwo" } }, ]); expect(cs.currentMessage.userInputMessage.content).toBe("third"); From 9b5b827ccd7c20ad7524164bc4aeacddf24e13be Mon Sep 17 00:00:00 2001 From: byongshintv <47180856+byongshintv@users.noreply.github.com> Date: Wed, 29 Jul 2026 19:26:50 +0900 Subject: [PATCH 2/2] fix(kiro): harden CLI auth metadata Co-authored-by: OpenAI Codex --- src/adapters/kiro.ts | 16 +++++----------- tests/kiro-adapter.test.ts | 22 +++++++++++++--------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/adapters/kiro.ts b/src/adapters/kiro.ts index 740c2388b3..1f093b391f 100644 --- a/src/adapters/kiro.ts +++ b/src/adapters/kiro.ts @@ -89,7 +89,6 @@ interface KiroUserInputMessage { userInputMessageContext?: { tools?: unknown[]; toolResults?: KiroToolResult[]; - envState?: { operatingSystem: string; currentWorkingDirectory: string }; }; images?: KiroImage[]; } @@ -549,12 +548,6 @@ export function buildKiroPayload( if (kiroTools.length > 0) { currentUim.userInputMessageContext = { ...(currentUim.userInputMessageContext ?? {}), tools: kiroTools }; } - if (wireClient === "cli") { - currentUim.userInputMessageContext = { - ...(currentUim.userInputMessageContext ?? {}), - envState: { operatingSystem: kiroCliPlatform(), currentWorkingDirectory: process.cwd() }, - }; - } if (completionMode === "text_fallback") { if (currentUim.content !== KIRO_COMPLETION_RETRY_MESSAGE) { currentUim.content = appendTurnText(currentUim.content, KIRO_COMPLETION_RETRY_MESSAGE); @@ -1479,11 +1472,12 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter throw new Error("kiro token missing — run ocx login kiro"); } const region = resolveKiroApiRegion(parsed._kiroAuthContext); - const profileArn = resolveKiroProfileArn(parsed._kiroAuthContext); + const resolvedProfileArn = resolveKiroProfileArn(parsed._kiroAuthContext); + const isApiKey = provider.apiKey.trim().startsWith("ksk_"); + const profileArn = isApiKey ? undefined : resolvedProfileArn; // Builder ID and Kiro API keys have no profile ARN and are accepted only on Kiro's CLI // request path. Enterprise profiles retain the existing IDE-shaped request. - const wireClient: KiroWireClient = profileArn ? "ide" : "cli"; - const isApiKey = provider.apiKey.trim().startsWith("ksk_"); + const wireClient: KiroWireClient = isApiKey || !profileArn ? "cli" : "ide"; const fp = fingerprint().slice(0, 64); const headers: Record = wireClient === "cli" ? { authorization: `Bearer ${provider.apiKey}`, @@ -1492,7 +1486,7 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter "x-amz-target": AMZ_TARGET, "user-agent": kiroCliUserAgent(true), "x-amz-user-agent": kiroCliUserAgent(false), - "x-amzn-codewhisperer-optout": "false", + "x-amzn-codewhisperer-optout": "true", "amz-sdk-request": "attempt=1; max=3", "amz-sdk-invocation-id": invocationId(), ...(isApiKey ? { tokentype: "API_KEY" } : {}), diff --git a/tests/kiro-adapter.test.ts b/tests/kiro-adapter.test.ts index 7a0134ac16..b2ad637318 100644 --- a/tests/kiro-adapter.test.ts +++ b/tests/kiro-adapter.test.ts @@ -91,6 +91,7 @@ describe("kiro adapter — buildRequest", () => { expect(headers["user-agent"]).toContain("app/AmazonQ-For-CLI"); expect(headers["x-amzn-kiro-agent-mode"]).toBeUndefined(); expect(headers["x-amzn-kiro-profile-arn"]).toBeUndefined(); + expect(headers["x-amzn-codewhisperer-optout"]).toBe("true"); expect(headers.tokentype).toBeUndefined(); expect(payload.profileArn).toBeUndefined(); expect(payload.conversationState.agentTaskType).toBe("vibe"); @@ -98,24 +99,27 @@ describe("kiro adapter — buildRequest", () => { expect(payload.conversationState.currentMessage.userInputMessage).toMatchObject({ content: "hi", origin: "KIRO_CLI", - userInputMessageContext: { - envState: { - operatingSystem: process.platform === "win32" ? "windows" : process.platform === "darwin" ? "macos" : "linux", - currentWorkingDirectory: process.cwd(), - }, - }, }); + expect(payload.conversationState.currentMessage.userInputMessage).not.toHaveProperty("userInputMessageContext.envState"); }); - test("Kiro API keys use the CLI token type without exposing a profile ARN", async () => { + test("Kiro API keys force the CLI token type and ignore unrelated profile metadata", async () => { const apiKeyProvider = { ...provider, authMode: "key", apiKey: "ksk_example" } as unknown as OcxProviderConfig; - const request = await createKiroAdapter(apiKeyProvider).buildRequest(parsedWith([{ role: "user", content: "hi" }])); - const body = JSON.parse(request.body) as { profileArn?: string }; + const parsed = parsedWith([{ role: "user", content: "hi" }]); + parsed._kiroAuthContext = { + profileArn: "arn:aws:codewhisperer:us-east-1:123456789012:profile/unrelated", + }; + const request = await createKiroAdapter(apiKeyProvider).buildRequest(parsed); + const body = JSON.parse(request.body) as { + profileArn?: string; + conversationState: { currentMessage: { userInputMessage: { origin?: string } } }; + }; expect(request.headers.authorization).toBe("Bearer ksk_example"); expect(request.headers.tokentype).toBe("API_KEY"); expect(request.headers["x-amzn-kiro-profile-arn"]).toBeUndefined(); expect(body.profileArn).toBeUndefined(); + expect(body.conversationState.currentMessage.userInputMessage.origin).toBe("KIRO_CLI"); }); test("runtime URL uses KIRO_API_REGION separately from auth region", async () => {