From c87517c0bf542e91a292a3fe3f49d7867cd99643 Mon Sep 17 00:00:00 2001 From: huangzhenpc Date: Tue, 12 May 2026 10:58:19 +0800 Subject: [PATCH] feat: sanitize injection blocks from conversation history before forwarding upstream When the proxy's own --- SYSTEM PROMPT --- wrapper or Claude Code's blocks appear in conversation history (e.g. echoed back by Kiro and included in the next request), strip them from user and assistant message content before building the Kiro payload. Co-Authored-By: Claude Sonnet 4.6 --- proxy/translator.go | 22 ++++++++++++- proxy/translator_test.go | 68 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/proxy/translator.go b/proxy/translator.go index 4a4d312..4c5dc62 100644 --- a/proxy/translator.go +++ b/proxy/translator.go @@ -10,6 +10,23 @@ import ( "github.com/google/uuid" ) +// reSysPromptBlock matches the --- SYSTEM PROMPT --- wrapper this proxy injects around system +// prompts before forwarding to Kiro. When Kiro echoes the content back the block can end up +// in subsequent conversation history and must be stripped before the next upstream request. +var reSysPromptBlock = regexp.MustCompile(`(?s)---+\s*SYSTEM\s+PROMPT\s*---+.*?---+\s*END\s+SYSTEM\s+PROMPT\s*---+\n?`) + +// reSystemReminder matches blocks injected by Claude Code into conversation +// context; these are internal metadata and must not be forwarded to Kiro. +var reSystemReminder = regexp.MustCompile(`(?s).*?\n?`) + +// sanitizeText removes known proxy-injected and tool-injected marker blocks from message text +// so they are not forwarded upstream where they can pollute Kiro's context. +func sanitizeText(s string) string { + s = reSysPromptBlock.ReplaceAllString(s, "") + s = reSystemReminder.ReplaceAllString(s, "") + return strings.TrimSpace(s) +} + // 模型映射(有序,长 key 优先匹配,避免 "claude-sonnet-4" 误匹配 "claude-sonnet-4.5") type modelMapping struct { key string @@ -224,6 +241,7 @@ func ClaudeToKiro(req *ClaudeRequest, thinking bool) *KiroPayload { if msg.Role == "user" { content, images, toolResults := extractClaudeUserContent(msg.Content) + content = sanitizeText(content) content = normalizeUserContent(content, len(images) > 0) if isLast { @@ -250,6 +268,7 @@ func ClaudeToKiro(req *ClaudeRequest, thinking bool) *KiroPayload { } } else if msg.Role == "assistant" { content, toolUses := extractClaudeAssistantContent(msg.Content) + content = sanitizeText(content) history = append(history, KiroHistoryMessage{ AssistantResponseMessage: &KiroAssistantResponseMessage{ Content: content, @@ -731,6 +750,7 @@ func OpenAIToKiro(req *OpenAIRequest, thinking bool) *KiroPayload { switch msg.Role { case "user": content, images := extractOpenAIUserContent(msg.Content) + content = sanitizeText(content) content = normalizeUserContent(content, len(images) > 0) // 第一条 user 消息合并 system prompt @@ -754,7 +774,7 @@ func OpenAIToKiro(req *OpenAIRequest, thinking bool) *KiroPayload { } case "assistant": - content := extractOpenAIMessageText(msg.Content) + content := sanitizeText(extractOpenAIMessageText(msg.Content)) var toolUses []KiroToolUse for _, tc := range msg.ToolCalls { diff --git a/proxy/translator_test.go b/proxy/translator_test.go index 984c97f..822c168 100644 --- a/proxy/translator_test.go +++ b/proxy/translator_test.go @@ -314,3 +314,71 @@ func TestParseModelAndThinkingNoSilentDowngrade(t *testing.T) { } } } + +func TestSanitizeTextStripsInjectionBlocks(t *testing.T) { + cases := []struct { + name string + input string + want string + }{ + { + name: "strips system prompt block", + input: "--- SYSTEM PROMPT ---\nYou are Kiro.\n--- END SYSTEM PROMPT ---\n\nHello", + want: "Hello", + }, + { + name: "strips system reminder block", + input: "\nsome metadata\n\n\nActual question", + want: "Actual question", + }, + { + name: "strips both blocks", + input: "--- SYSTEM PROMPT ---\nidentity\n--- END SYSTEM PROMPT ---\nctx\nReal content", + want: "Real content", + }, + { + name: "passes through clean text unchanged", + input: "Just a normal message", + want: "Just a normal message", + }, + { + name: "handles multiple dashes", + input: "---- SYSTEM PROMPT ----\nfoo\n---- END SYSTEM PROMPT ----\nbar", + want: "bar", + }, + } + for _, tc := range cases { + got := sanitizeText(tc.input) + if got != tc.want { + t.Errorf("sanitizeText(%q) [%s] = %q; want %q", tc.input, tc.name, got, tc.want) + } + } +} + +func TestClaudeToKiroSanitizesHistoryContent(t *testing.T) { + injectedAssistant := "--- SYSTEM PROMPT ---\nYou are Kiro.\n--- END SYSTEM PROMPT ---\n\nHere is my answer." + req := &ClaudeRequest{ + Model: "claude-sonnet-4.5", + Messages: []ClaudeMessage{ + {Role: "user", Content: "hello"}, + {Role: "assistant", Content: injectedAssistant}, + {Role: "user", Content: "continue"}, + }, + } + + payload := ClaudeToKiro(req, false) + + if len(payload.ConversationState.History) < 2 { + t.Fatalf("expected at least 2 history entries, got %d", len(payload.ConversationState.History)) + } + assistantEntry := payload.ConversationState.History[1].AssistantResponseMessage + if assistantEntry == nil { + t.Fatalf("expected second history entry to be assistant message") + } + if strings.Contains(assistantEntry.Content, "SYSTEM PROMPT") { + t.Errorf("assistant history content still contains injection block: %q", assistantEntry.Content) + } + if !strings.Contains(assistantEntry.Content, "Here is my answer") { + t.Errorf("assistant history content missing expected text: %q", assistantEntry.Content) + } +}