vibe-proxy/backend/internal/util/claude_tool_id.go
2026-08-24 00:10:41 +02:00

68 lines
2 KiB
Go

package util
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"regexp"
"strings"
"sync/atomic"
"time"
)
const geminiClaudeToolUseIDPrefix = "cpa_gemini_"
var (
claudeToolUseIDSanitizer = regexp.MustCompile(`[^a-zA-Z0-9_-]`)
claudeToolUseIDCounter uint64
)
// SanitizeClaudeToolID ensures the given id conforms to Claude's
// tool_use.id regex ^[a-zA-Z0-9_-]+$. Non-conforming characters are
// replaced with '_'; an empty result gets a generated fallback.
func SanitizeClaudeToolID(id string) string {
s := claudeToolUseIDSanitizer.ReplaceAllString(id, "_")
if s == "" {
s = fmt.Sprintf("toolu_%d_%d", time.Now().UnixNano(), atomic.AddUint64(&claudeToolUseIDCounter, 1))
}
return s
}
// GeminiClaudeToolUseID returns a stable Claude-facing ID for a provider-native
// Gemini function call. The opaque ID lets the executor recover the exact
// provider call from its replay ledger instead of trusting client-mutated args.
func GeminiClaudeToolUseID(callID, name, argsRaw string) string {
callID = strings.TrimSpace(callID)
name = strings.TrimSpace(name)
if callID == "" || name == "" {
return ""
}
if strings.TrimSpace(argsRaw) != "" {
var value any
if json.Unmarshal([]byte(argsRaw), &value) == nil {
if canonical, errMarshal := json.Marshal(value); errMarshal == nil {
argsRaw = string(canonical)
}
} else {
argsRaw = strings.TrimSpace(argsRaw)
}
}
sum := sha256.Sum256([]byte(strings.Join([]string{callID, name, argsRaw}, "\x00")))
return geminiClaudeToolUseIDPrefix + hex.EncodeToString(sum[:16])
}
// IsGeminiClaudeToolUseID reports whether id belongs to the reserved
// Claude-facing Gemini provenance namespace.
func IsGeminiClaudeToolUseID(id string) bool {
id = strings.TrimSpace(id)
if !strings.HasPrefix(id, geminiClaudeToolUseIDPrefix) {
return false
}
digest := strings.TrimPrefix(id, geminiClaudeToolUseIDPrefix)
if len(digest) != 32 {
return false
}
_, errDecode := hex.DecodeString(digest)
return errDecode == nil
}