mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 11:33:18 +08:00
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.
This commit is contained in:
@@ -205,43 +205,54 @@ func (v *ClaudeCodeValidator) hasClaudeCodeSystemPrompt(body map[string]any) boo
|
||||
return false
|
||||
}
|
||||
|
||||
// claudeCodeSecurityMonitorMarkers 与固定前缀、长度下限共同构成分类器提示词的
|
||||
// 判别条件,须全部命中。
|
||||
var claudeCodeSecurityMonitorMarkers = []string{
|
||||
"## Threat Model",
|
||||
"- `<transcript>`:",
|
||||
"## HARD BLOCK",
|
||||
"## SOFT BLOCK",
|
||||
"## Classification Process",
|
||||
"## Output Format",
|
||||
"<block>yes</block>",
|
||||
"<block>no</block>",
|
||||
}
|
||||
|
||||
// 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",
|
||||
"- `<transcript>`:",
|
||||
"## HARD BLOCK",
|
||||
"## SOFT BLOCK",
|
||||
"## Classification Process",
|
||||
"## Output Format",
|
||||
"<block>yes</block>",
|
||||
"<block>no</block>",
|
||||
}
|
||||
for _, marker := range markers {
|
||||
func hasAllClaudeCodeSecurityMonitorMarkers(text string) bool {
|
||||
for _, marker := range claudeCodeSecurityMonitorMarkers {
|
||||
if !strings.Contains(text, marker) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}(),
|
||||
|
||||
Reference in New Issue
Block a user