feat: sanitize injection blocks from conversation history before forwarding upstream
Some checks failed
Build Docker Image / build (push) Has been cancelled

When the proxy's own --- SYSTEM PROMPT --- wrapper or Claude Code's
<system-reminder> 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 <noreply@anthropic.com>
This commit is contained in:
2026-05-12 10:58:19 +08:00
parent 2b29616723
commit c87517c0bf
2 changed files with 89 additions and 1 deletions

View File

@@ -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 <system-reminder> blocks injected by Claude Code into conversation
// context; these are internal metadata and must not be forwarded to Kiro.
var reSystemReminder = regexp.MustCompile(`(?s)<system-reminder>.*?</system-reminder>\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 {

View File

@@ -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: "<system-reminder>\nsome metadata\n</system-reminder>\n\nActual question",
want: "Actual question",
},
{
name: "strips both blocks",
input: "--- SYSTEM PROMPT ---\nidentity\n--- END SYSTEM PROMPT ---\n<system-reminder>ctx</system-reminder>\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)
}
}