68 lines
2 KiB
Go
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
|
|
}
|