diff --git a/backend/internal/service/claude_code_validator.go b/backend/internal/service/claude_code_validator.go index 03b0a8281f..923f30b7cc 100644 --- a/backend/internal/service/claude_code_validator.go +++ b/backend/internal/service/claude_code_validator.go @@ -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 创建验证器实例 @@ -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 } diff --git a/backend/internal/service/claude_code_validator_test.go b/backend/internal/service/claude_code_validator_test.go index f34348720f..0f02aca769 100644 --- a/backend/internal/service/claude_code_validator_test.go +++ b/backend/internal/service/claude_code_validator_test.go @@ -135,7 +135,44 @@ 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)") @@ -143,14 +180,14 @@ func TestClaudeCodeValidator_BillingBlockNonCLIEntrypointFallsThrough(t *testing 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",