feat(antigravity): configurable signature cache with bypass-mode validation

Antigravity 的 Claude thinking signature 处理新增 cache/bypass 双模式,
并为 bypass 模式实现按 SIGNATURE-CHANNEL-SPEC.md 的签名校验。

新增 antigravity-signature-cache-enabled 配置项(默认 true):
- cache mode(true):使用服务端缓存的签名,行为与原有逻辑完全一致
- bypass mode(false):直接使用客户端提供的签名,经过校验和归一化

支持配置热重载,运行时可切换模式。

校验流程:
1. 剥离历史 cache-mode 的 'modelGroup#' 前缀(如 claude#Exxxx → Exxxx)
2. 首字符必须为 'E'(单层编码)或 'R'(双层编码),否则拒绝
3. R 开头:base64 解码 → 内层必须以 'E' 开头 → 继续单层校验
4. E 开头:base64 解码 → 首字节必须为 0x12(Claude protobuf 标识)
5. 所有合法签名归一化为 R 形式(双层 base64)发往 Antigravity 后端

非法签名处理策略:
- 非严格模式(默认):translator 静默丢弃无签名的 thinking block
- 严格模式(antigravity-signature-bypass-strict: true):
  executor 层在请求发往上游前直接返回 HTTP 400

按 SIGNATURE-CHANNEL-SPEC.md 解析 Claude 签名的完整 protobuf 结构:
- Top-level Field 2(容器)→ Field 1(渠道块)
- 渠道块提取:channel_id (Field 1)、infrastructure (Field 2)、
  model_text (Field 6)、field7 (Field 7)
- 计算 routing_class、infrastructure_class、schema_features
- 使用 google.golang.org/protobuf/encoding/protowire 解析

- resolveThinkingSignature 拆分为 resolveCacheModeSignature / resolveBypassModeSignature
- hasResolvedThinkingSignature:mode-aware 签名有效性判断
  (cache: len>=50 via HasValidSignature,bypass: non-empty)
- validateAntigravityRequestSignatures:executor 预检,
  仅在 bypass + strict 模式下拦截非法签名返回 400
- 响应侧签名缓存逻辑与 cache mode 集成
- Cache mode 行为完全保留:无 '#' 前缀的原生签名静默丢弃
This commit is contained in:
sususu98
2026-03-31 14:15:06 +08:00
parent 1dba2d0f81
commit cf249586a9
11 changed files with 1494 additions and 62 deletions
@@ -9,6 +9,7 @@ package claude
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"strings"
"sync/atomic"
@@ -23,6 +24,33 @@ import (
"github.com/tidwall/sjson"
)
// decodeSignature decodes R... (2-layer Base64) to E... (1-layer Base64, Anthropic format).
// Returns empty string if decoding fails (skip invalid signatures).
func decodeSignature(signature string) string {
if signature == "" {
return signature
}
if strings.HasPrefix(signature, "R") {
decoded, err := base64.StdEncoding.DecodeString(signature)
if err != nil {
log.Warnf("antigravity claude response: failed to decode signature, skipping")
return ""
}
return string(decoded)
}
return signature
}
func formatClaudeSignatureValue(modelName, signature string) string {
if cache.SignatureCacheEnabled() {
return fmt.Sprintf("%s#%s", cache.GetModelGroup(modelName), signature)
}
if cache.GetModelGroup(modelName) == "claude" {
return decodeSignature(signature)
}
return signature
}
// Params holds parameters for response conversion and maintains state across streaming chunks.
// This structure tracks the current state of the response translation process to ensure
// proper sequencing of SSE events and transitions between different content types.
@@ -144,13 +172,30 @@ func ConvertAntigravityResponseToClaude(_ context.Context, _ string, originalReq
if thoughtSignature := partResult.Get("thoughtSignature"); thoughtSignature.Exists() && thoughtSignature.String() != "" {
// log.Debug("Branch: signature_delta")
// Flush co-located text before emitting the signature
if partText := partTextResult.String(); partText != "" {
if params.ResponseType != 2 {
if params.ResponseType != 0 {
appendEvent("content_block_stop", fmt.Sprintf(`{"type":"content_block_stop","index":%d}`, params.ResponseIndex))
params.ResponseIndex++
}
appendEvent("content_block_start", fmt.Sprintf(`{"type":"content_block_start","index":%d,"content_block":{"type":"thinking","thinking":""}}`, params.ResponseIndex))
params.ResponseType = 2
params.CurrentThinkingText.Reset()
}
params.CurrentThinkingText.WriteString(partText)
data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"thinking_delta","thinking":""}}`, params.ResponseIndex)), "delta.thinking", partText)
appendEvent("content_block_delta", string(data))
}
if params.CurrentThinkingText.Len() > 0 {
cache.CacheSignature(modelName, params.CurrentThinkingText.String(), thoughtSignature.String())
// log.Debugf("Cached signature for thinking block (textLen=%d)", params.CurrentThinkingText.Len())
params.CurrentThinkingText.Reset()
}
data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":""}}`, params.ResponseIndex)), "delta.signature", fmt.Sprintf("%s#%s", cache.GetModelGroup(modelName), thoughtSignature.String()))
sigValue := formatClaudeSignatureValue(modelName, thoughtSignature.String())
data, _ := sjson.SetBytes([]byte(fmt.Sprintf(`{"type":"content_block_delta","index":%d,"delta":{"type":"signature_delta","signature":""}}`, params.ResponseIndex)), "delta.signature", sigValue)
appendEvent("content_block_delta", string(data))
params.HasContent = true
} else if params.ResponseType == 2 { // Continue existing thinking block if already in thinking state
@@ -419,7 +464,8 @@ func ConvertAntigravityResponseToClaudeNonStream(_ context.Context, _ string, or
block := []byte(`{"type":"thinking","thinking":""}`)
block, _ = sjson.SetBytes(block, "thinking", thinkingBuilder.String())
if thinkingSignature != "" {
block, _ = sjson.SetBytes(block, "signature", fmt.Sprintf("%s#%s", cache.GetModelGroup(modelName), thinkingSignature))
sigValue := formatClaudeSignatureValue(modelName, thinkingSignature)
block, _ = sjson.SetBytes(block, "signature", sigValue)
}
responseJSON, _ = sjson.SetRawBytes(responseJSON, "content.-1", block)
thinkingBuilder.Reset()