From 2ef124629526439d71fb7410951ed21795d030d9 Mon Sep 17 00:00:00 2001 From: shaw Date: Sat, 1 Aug 2026 10:32:43 +0800 Subject: [PATCH] fix(anthropic): recognize classifier requests with extra system entries Real claude-cli/2.1.220 auto-mode classifier requests carry two system entries: the security-monitor prompt plus an appended session-context block. The previous len(systemEntries) != 1 guard rejected them before any content check ran, so claude_code_only groups kept refusing the classifier (#5152, follow-up to #5041/#5048). Scan every entry for the monitor prompt instead of requiring exactly one. Discrimination is unchanged: the matching entry still needs the 10k-char minimum, the fixed prefix, and all eight markers. --- .../internal/service/claude_code_validator.go | 69 +++++++++++-------- .../service/claude_code_validator_test.go | 49 ++++++++++++- 2 files changed, 87 insertions(+), 31 deletions(-) diff --git a/backend/internal/service/claude_code_validator.go b/backend/internal/service/claude_code_validator.go index 52f351f1b3..c37d444370 100644 --- a/backend/internal/service/claude_code_validator.go +++ b/backend/internal/service/claude_code_validator.go @@ -205,43 +205,54 @@ func (v *ClaudeCodeValidator) hasClaudeCodeSystemPrompt(body map[string]any) boo return false } +// claudeCodeSecurityMonitorMarkers 与固定前缀、长度下限共同构成分类器提示词的 +// 判别条件,须全部命中。 +var claudeCodeSecurityMonitorMarkers = []string{ + "## Threat Model", + "- ``:", + "## HARD BLOCK", + "## SOFT BLOCK", + "## Classification Process", + "## Output Format", + "yes", + "no", +} + +// isClaudeCodeSecurityMonitorPrompt 识别 Claude Code auto 模式安全监视器分类器请求。 +// 真实 CLI(实测 2.1.220)会在监视器提示词之外追加独立的会话上下文 system 块, +// entry 数量不受服务端控制,故逐 entry 查找匹配项而非限定恰好一个 entry。 func isClaudeCodeSecurityMonitorPrompt(systemEntries []any) bool { - if len(systemEntries) != 1 { - return false + for _, raw := range systemEntries { + entry, ok := raw.(map[string]any) + if !ok { + continue + } + + entryType, ok := entry["type"].(string) + if !ok || entryType != "text" { + continue + } + + text, ok := entry["text"].(string) + if !ok || len(text) < claudeCodeSecurityMonitorPromptMinLen || + !strings.HasPrefix(text, claudeCodeSecurityMonitorPromptPrefix) { + continue + } + + if hasAllClaudeCodeSecurityMonitorMarkers(text) { + return true + } } - entry, ok := systemEntries[0].(map[string]any) - if !ok { - return false - } + return false +} - entryType, ok := entry["type"].(string) - if !ok || entryType != "text" { - return false - } - - text, ok := entry["text"].(string) - if !ok || len(text) < claudeCodeSecurityMonitorPromptMinLen || - !strings.HasPrefix(text, claudeCodeSecurityMonitorPromptPrefix) { - return false - } - - markers := []string{ - "## Threat Model", - "- ``:", - "## HARD BLOCK", - "## SOFT BLOCK", - "## Classification Process", - "## Output Format", - "yes", - "no", - } - for _, marker := range markers { +func hasAllClaudeCodeSecurityMonitorMarkers(text string) bool { + for _, marker := range claudeCodeSecurityMonitorMarkers { if !strings.Contains(text, marker) { return false } } - return true } diff --git a/backend/internal/service/claude_code_validator_test.go b/backend/internal/service/claude_code_validator_test.go index 8fd0f2126c..bf0e127e82 100644 --- a/backend/internal/service/claude_code_validator_test.go +++ b/backend/internal/service/claude_code_validator_test.go @@ -156,6 +156,11 @@ func TestClaudeCodeValidator_SecurityMonitorWithoutBillingBlock(t *testing.T) { } } + // 真实 CLI(2.1.220)在监视器提示词之后追加的独立会话上下文块(脱敏), + // 随会话/环境变化,服务端不可控(见 issue #5152 抓包)。 + sessionContext := "\n\n## Session Context\n\n- **User identity**: testuser\n" + + "- **Working directory**: /home/testuser/project\n- **Platform**: linux" + tests := []struct { name string headers map[string]string @@ -253,7 +258,9 @@ func TestClaudeCodeValidator_SecurityMonitorWithoutBillingBlock(t *testing.T) { wantAccept: false, }, { - name: "multiple system entries without billing block", + // 回归 issue #5152:真实分类器请求携带 2 个 system entry + //(监视器提示词 + 追加的会话上下文块),不得因 entry 数量拒识。 + name: "classifier with trailing session context entry", headers: validHeaders, body: func() map[string]any { body := validBody(string(monitorPrompt)) @@ -261,7 +268,45 @@ func TestClaudeCodeValidator_SecurityMonitorWithoutBillingBlock(t *testing.T) { require.True(t, ok) body["system"] = append(system, map[string]any{ "type": "text", - "text": "Additional unrelated system content.", + "text": sessionContext, + }) + return body + }(), + wantAccept: true, + }, + { + name: "classifier with leading session context entry", + headers: validHeaders, + body: func() map[string]any { + body := validBody(string(monitorPrompt)) + system, ok := body["system"].([]any) + require.True(t, ok) + body["system"] = append([]any{map[string]any{ + "type": "text", + "text": sessionContext, + }}, system...) + return body + }(), + wantAccept: true, + }, + { + name: "session context entry alone", + headers: validHeaders, + body: validBody(sessionContext), + wantAccept: false, + }, + { + // 篡改后的长提示词(marker 缺失)即便带上会话上下文块也不得放行。 + name: "tampered classifier with session context entry", + headers: validHeaders, + body: func() map[string]any { + body := validBody(strings.ReplaceAll( + string(monitorPrompt), "## HARD BLOCK", "## ALTERED BLOCK")) + system, ok := body["system"].([]any) + require.True(t, ok) + body["system"] = append(system, map[string]any{ + "type": "text", + "text": sessionContext, }) return body }(),