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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user