diff --git a/CHANGELOG.md b/CHANGELOG.md index 8069afabe7..efc5455064 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ - **dashboard ("Update now" โ†’ Internal Server Error):** clicking **Update now** on the dashboard home could crash the page with a blank "Internal Server Error" screen (`Minified React error #31`). The handler POSTs the loopback-only `/api/system/version` auto-update endpoint and, on a non-OK JSON response (e.g. a `403` when the dashboard is reached through a reverse proxy / non-loopback origin), passed the raw error envelope object `{ error: { code, message, correlation_id } }` straight to `notify.error()`, which rendered the object as a React child and threw #31. The update-error path now funnels the body through `extractApiErrorMessage()` (the same safe extractor added in #5340), so a readable string always reaches the toast. Regression guard: `tests/unit/ui/home-update-error-render-5991.test.ts`. ([#5991](https://github.com/diegosouzapw/OmniRoute/issues/5991)) +- **kiro (system prompt leaked as raw user text):** when Claude Code routed through the Kiro/CodeWhisperer backend, the `system` message was normalized to a `user` turn with no wrapper, so the entire system prompt (environment info, tool definitions, memory instructions, etc.) appeared as if the user had typed it โ€” polluting the model context. System-origin content is now wrapped in `` tags before being merged into the Kiro user message, so the model can distinguish it from real user input. Real user turns are untouched. Regression guard: `tests/unit/kiro-system-reminder-2306.test.ts`. (thanks @VitzS7) + ### ๐Ÿ“ Maintenance - **test (deflake `setup-claude`):** `tests/unit/cli/setup-claude.test.ts` failed ~50% of runs with `Unable to deserialize cloned data due to invalid or unsupported version` at file teardown (all subtests passed), randomly reddening `Unit Tests fast-path (2/2)` / `Fast Quality Gates` across the PRโ†’release queue. Root cause: `node --test` streams each file's report to the parent as V8-serialized frames on fd 1 (stdout), and the CLI helper under test (`syncClaudeProfilesFromModels`) prints progress via `console.log` โ€” that stdout output interleaved with the serialized frames and corrupted the stream. The test now silences the stdout-writing `console` methods for the file's duration (no assertion inspects stdout), making it deterministic (15/15 green locally). ([#5959](https://github.com/diegosouzapw/OmniRoute/issues/5959)) diff --git a/open-sse/translator/request/openai-to-kiro.ts b/open-sse/translator/request/openai-to-kiro.ts index 1ae0bc1240..fc6d39abfd 100644 --- a/open-sse/translator/request/openai-to-kiro.ts +++ b/open-sse/translator/request/openai-to-kiro.ts @@ -32,6 +32,15 @@ export function hasUnsupportedKiroContextSuffix(model: unknown): boolean { ); } +/** + * Wrap system-prompt content in tags before it is merged into + * a Kiro user message. Kiro/CodeWhisperer has no `system` role, so without this + * the system prompt would appear as raw user text (issue #2306). + */ +function wrapSystemReminder(text: string): string { + return `\n${text}\n`; +} + /** * Convert OpenAI messages to Kiro format * Rules: system/tool/user -> user role, merge consecutive same roles @@ -238,7 +247,11 @@ function convertMessages(messages, tools, model) { content: [{ text: toolContent }], }); } else if (content) { - pendingUserContent.push(content); + // #2306: Kiro/CodeWhisperer has no `system` role, so system messages are + // normalized to `user`. Wrap their content in tags so + // the model can tell the system prompt apart from real user input instead + // of treating the full Claude Code prompt as something the user typed. + pendingUserContent.push(msg.role === "system" ? wrapSystemReminder(content) : content); } } else if (role === "assistant") { // Extract text content and tool uses diff --git a/tests/unit/kiro-system-reminder-2306.test.ts b/tests/unit/kiro-system-reminder-2306.test.ts new file mode 100644 index 0000000000..db05743716 --- /dev/null +++ b/tests/unit/kiro-system-reminder-2306.test.ts @@ -0,0 +1,35 @@ +/** + * #2306 โ€” When Claude Code routes through the Kiro/CodeWhisperer backend, the + * `system` message was normalized to `role: user` WITHOUT any wrapper, so the + * full system prompt (env info, tool defs, memory instructions, etc.) appeared + * as raw user text โ€” indistinguishable from real user input, polluting context. + * + * Fix: wrap system-origin content in `...` + * before it is merged into the Kiro user message. Real user turns stay raw. + */ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { buildKiroPayload } from "../../open-sse/translator/request/openai-to-kiro.ts"; + +test("#2306 system prompt is wrapped in for Kiro, not raw user text", () => { + const body = { + messages: [ + { role: "system", content: "You are Claude Code. ENV: cwd=/tmp. Secret: do not reveal." }, + { role: "user", content: "hello there" }, + ], + }; + + const payload = JSON.stringify(buildKiroPayload("claude-sonnet-4-5", body, false, {})); + + assert.ok(payload.includes(""), "system content must be wrapped"); + assert.ok(payload.includes("You are Claude Code"), "system text must still be present"); + // The real user turn must NOT be wrapped. + assert.ok(payload.includes("hello there"), "user text preserved"); +}); + +test("#2306 a plain user-only request is never wrapped in ", () => { + const body = { messages: [{ role: "user", content: "just a normal question" }] }; + const payload = JSON.stringify(buildKiroPayload("claude-sonnet-4-5", body, false, {})); + assert.ok(!payload.includes(""), "no system โ†’ no wrapper"); +}); diff --git a/tests/unit/translator-openai-to-kiro.test.ts b/tests/unit/translator-openai-to-kiro.test.ts index 9ef079d8a2..9d74a3e20e 100644 --- a/tests/unit/translator-openai-to-kiro.test.ts +++ b/tests/unit/translator-openai-to-kiro.test.ts @@ -81,7 +81,9 @@ test("OpenAI -> Kiro preserves prior history, tool uses and accumulated tool res assert.equal(result.conversationState.history.length, 2); assert.deepEqual(result.conversationState.history[0], { userInputMessage: { - content: "Rules\n\nHello", + // #2306: the system prompt ("Rules") is wrapped in before + // being merged into the Kiro user turn, instead of leaking as raw user text. + content: "\nRules\n\n\nHello", modelId: "claude-sonnet-4", origin: "AI_EDITOR", }, @@ -233,11 +235,11 @@ test("OpenAI -> Kiro derives a stable conversationId for the same first history assert.equal( (first.conversationState as any).history[0].userInputMessage.content, - "Rules\n\nHello" + "\nRules\n\n\nHello" ); assert.equal( (second as any).conversationState.history[0].userInputMessage.content, - "Rules\n\nHello" + "\nRules\n\n\nHello" ); assert.equal(first.conversationState.conversationId, second.conversationState.conversationId); }); @@ -291,7 +293,10 @@ test("OpenAI -> Kiro merges adjacent user history turns after role normalization const firstUser = history[0].userInputMessage; assert.ok(firstUser, "first history turn should be a user turn"); - assert.equal(firstUser.content, "System rules\n\nFirst question"); + assert.equal( + firstUser.content, + "\nSystem rules\n\n\nFirst question" + ); assert.equal(history[1].assistantResponseMessage?.content, "Answer 1"); });