feat: sanitize injection blocks from conversation history before forwarding upstream
Some checks failed
Build Docker Image / build (push) Has been cancelled
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:
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user