Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions backend/internal/service/claude_code_validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ const (
// 格式固定、不随提示词改版漂移,是比身份 prose 更稳定的客户端标识。
// 生成见 gateway_billing_block.go;同类识别见 pkg/apicompat/anthropic_to_responses.go。
claudeCodeBillingHeaderPrefix = "x-anthropic-billing-header"
// claudeCodeCLIEntrypointMarker 标识请求来自 Claude Code CLI 入口。
claudeCodeCLIEntrypointMarker = "cc_entrypoint=cli"
// claudeCodeEntrypointMarker 标识计费块携带入口归因字段。不绑定具体入口值
// (cli / claude-vscode / jetbrains / sdk 等都是真实入口):入口值会随新增 IDE 漂移,
// 且伪造者同样可填任意值、不构成防伪边界,故仅要求该字段存在即可。
claudeCodeEntrypointMarker = "cc_entrypoint="
)

// NewClaudeCodeValidator 创建验证器实例
Expand Down Expand Up @@ -180,7 +182,7 @@ func (v *ClaudeCodeValidator) hasClaudeCodeSystemPrompt(body map[string]any) boo
// 计费归因块识别(WHY 见 claudeCodeBillingHeaderPrefix 注释)。先于 Dice 检查,
// 大小写敏感:该块由 gateway_billing_block.go 固定小写生成。
if strings.HasPrefix(text, claudeCodeBillingHeaderPrefix) &&
strings.Contains(text, claudeCodeCLIEntrypointMarker) {
strings.Contains(text, claudeCodeEntrypointMarker) {
return true
}

Expand Down
45 changes: 41 additions & 4 deletions backend/internal/service/claude_code_validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,22 +135,59 @@ func TestClaudeCodeValidator_BillingBlockRecognizedWithoutIdentityPrompt(t *test
require.True(t, ok)
}

func TestClaudeCodeValidator_BillingBlockNonCLIEntrypointFallsThrough(t *testing.T) {
func TestClaudeCodeValidator_BillingBlockVSCodeEntrypointRecognized(t *testing.T) {
// 回归:Claude Code 在 VSCode 扩展内运行时,计费块入口为 cc_entrypoint=claude-vscode
// 而非 cli。其安全监视器子请求同样不携带身份 prose,此前写死 cc_entrypoint=cli 的
// 快速通道无法识别它,导致 claude_code_only 分组误拒。入口值不应作为识别条件。
monitorPrompt, err := os.ReadFile("testdata/security_monitor_system_prompt.txt")
require.NoError(t, err)

validator := NewClaudeCodeValidator()

// 前提:完整监视器正文经 Dice 相似度远低于阈值,放行只可能来自计费归因块识别。
require.Less(t, validator.bestSimilarityScore(string(monitorPrompt)), systemPromptThreshold)

req := httptest.NewRequest(http.MethodPost, "http://example.com/v1/messages", nil)
req.Header.Set("User-Agent", "claude-cli/2.1.181 (external, claude-vscode, agent-sdk/0.3.181)")
req.Header.Set("X-App", "cli")
req.Header.Set("anthropic-beta", "claude-code-20250219")
req.Header.Set("anthropic-version", "2023-06-01")

ok := validator.Validate(req, map[string]any{
"model": "claude-opus-4-8",
"system": []any{
map[string]any{
"type": "text",
"text": "x-anthropic-billing-header: cc_version=2.1.181.f17; cc_entrypoint=claude-vscode;",
},
map[string]any{
"type": "text",
"text": string(monitorPrompt),
},
},
"metadata": map[string]any{
"user_id": claudeCodeMetadataUserIDJSON,
},
})
require.True(t, ok)
}

func TestClaudeCodeValidator_BillingBlockWithoutEntrypointFallsThrough(t *testing.T) {
validator := NewClaudeCodeValidator()
req := httptest.NewRequest(http.MethodPost, "http://example.com/v1/messages", nil)
req.Header.Set("User-Agent", "claude-cli/2.1.162 (external, cli)")
req.Header.Set("X-App", "cli")
req.Header.Set("anthropic-beta", "claude-code-20250219")
req.Header.Set("anthropic-version", "2023-06-01")

// 计费块存在但 entrypoint 不是 cli(如 sdk),且无身份 prose:
// 不应凭前缀放行,应落回 Dice 检查并失败。验证 cc_entrypoint=cli 这一条件是必要的
// 计费块前缀命中但完全没有 cc_entrypoint= 字段,且无身份 prose:
// 不应凭前缀放行,应落回 Dice 检查并失败。验证 cc_entrypoint= 字段的存在仍是必要条件
ok := validator.Validate(req, map[string]any{
"model": "claude-3-5-haiku-20241022",
"system": []any{
map[string]any{
"type": "text",
"text": "x-anthropic-billing-header: cc_version=2.1.162.884; cc_entrypoint=sdk; cch=d8726;",
"text": "x-anthropic-billing-header: cc_version=2.1.162.884; cch=d8726;",
},
map[string]any{
"type": "text",
Expand Down
Loading