From 680105f84d67ee9a50ee823ced295e2fce3595b6 Mon Sep 17 00:00:00 2001 From: tpob Date: Thu, 19 Mar 2026 13:28:58 +0800 Subject: [PATCH] fix(claude): refresh cached fingerprint after baseline upgrades --- .../runtime/executor/claude_device_profile.go | 17 +++++- .../runtime/executor/claude_executor_test.go | 52 +++++++++++++++++++ 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/internal/runtime/executor/claude_device_profile.go b/internal/runtime/executor/claude_device_profile.go index 68bcd102..aa81eb94 100644 --- a/internal/runtime/executor/claude_device_profile.go +++ b/internal/runtime/executor/claude_device_profile.go @@ -179,6 +179,19 @@ func pinClaudeDeviceProfilePlatform(profile, baseline claudeDeviceProfile) claud return profile } +// normalizeClaudeDeviceProfile keeps stabilized profiles pinned to the current +// baseline platform and enforces the baseline software fingerprint as a floor. +func normalizeClaudeDeviceProfile(profile, baseline claudeDeviceProfile) claudeDeviceProfile { + profile = pinClaudeDeviceProfilePlatform(profile, baseline) + if profile.UserAgent == "" || profile.Version == (claudeCLIVersion{}) || shouldUpgradeClaudeDeviceProfile(baseline, profile) { + profile.UserAgent = baseline.UserAgent + profile.PackageVersion = baseline.PackageVersion + profile.RuntimeVersion = baseline.RuntimeVersion + profile.Version = baseline.Version + } + return profile +} + func extractClaudeDeviceProfile(headers http.Header, cfg *config.Config) (claudeDeviceProfile, bool) { if headers == nil { return claudeDeviceProfile{}, false @@ -277,7 +290,7 @@ func resolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers entry, hasCached = claudeDeviceProfileCache[cacheKey] cachedValid = hasCached && entry.expire.After(now) && entry.profile.UserAgent != "" if cachedValid { - entry.profile = pinClaudeDeviceProfilePlatform(entry.profile, baseline) + entry.profile = normalizeClaudeDeviceProfile(entry.profile, baseline) } if cachedValid && !shouldUpgradeClaudeDeviceProfile(candidate, entry.profile) { entry.expire = now.Add(claudeDeviceProfileTTL) @@ -298,7 +311,7 @@ func resolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers claudeDeviceProfileCacheMu.Lock() entry = claudeDeviceProfileCache[cacheKey] if entry.expire.After(now) && entry.profile.UserAgent != "" { - entry.profile = pinClaudeDeviceProfilePlatform(entry.profile, baseline) + entry.profile = normalizeClaudeDeviceProfile(entry.profile, baseline) entry.expire = now.Add(claudeDeviceProfileTTL) claudeDeviceProfileCache[cacheKey] = entry claudeDeviceProfileCacheMu.Unlock() diff --git a/internal/runtime/executor/claude_executor_test.go b/internal/runtime/executor/claude_executor_test.go index 68d391fa..91242802 100644 --- a/internal/runtime/executor/claude_executor_test.go +++ b/internal/runtime/executor/claude_executor_test.go @@ -208,6 +208,58 @@ func TestApplyClaudeHeaders_DoesNotDowngradeConfiguredBaselineOnFirstClaudeClien assertClaudeFingerprint(t, newerClaudeReq.Header, "claude-cli/2.1.71 (external, cli)", "0.81.0", "v24.6.0", "MacOS", "arm64") } +func TestApplyClaudeHeaders_UpgradesCachedSoftwareFingerprintWhenBaselineAdvances(t *testing.T) { + resetClaudeDeviceProfileCache() + stabilize := true + + oldCfg := &config.Config{ + ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{ + UserAgent: "claude-cli/2.1.70 (external, cli)", + PackageVersion: "0.80.0", + RuntimeVersion: "v24.5.0", + OS: "MacOS", + Arch: "arm64", + StabilizeDeviceProfile: &stabilize, + }, + } + newCfg := &config.Config{ + ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{ + UserAgent: "claude-cli/2.1.77 (external, cli)", + PackageVersion: "0.87.0", + RuntimeVersion: "v24.8.0", + OS: "MacOS", + Arch: "arm64", + StabilizeDeviceProfile: &stabilize, + }, + } + auth := &cliproxyauth.Auth{ + ID: "auth-baseline-reload", + Attributes: map[string]string{ + "api_key": "key-baseline-reload", + }, + } + + officialReq := newClaudeHeaderTestRequest(t, http.Header{ + "User-Agent": []string{"claude-cli/2.1.71 (external, cli)"}, + "X-Stainless-Package-Version": []string{"0.81.0"}, + "X-Stainless-Runtime-Version": []string{"v24.6.0"}, + "X-Stainless-Os": []string{"Linux"}, + "X-Stainless-Arch": []string{"x64"}, + }) + applyClaudeHeaders(officialReq, auth, "key-baseline-reload", false, nil, oldCfg) + assertClaudeFingerprint(t, officialReq.Header, "claude-cli/2.1.71 (external, cli)", "0.81.0", "v24.6.0", "MacOS", "arm64") + + thirdPartyReq := newClaudeHeaderTestRequest(t, http.Header{ + "User-Agent": []string{"curl/8.7.1"}, + "X-Stainless-Package-Version": []string{"0.10.0"}, + "X-Stainless-Runtime-Version": []string{"v18.0.0"}, + "X-Stainless-Os": []string{"Linux"}, + "X-Stainless-Arch": []string{"x64"}, + }) + applyClaudeHeaders(thirdPartyReq, auth, "key-baseline-reload", false, nil, newCfg) + assertClaudeFingerprint(t, thirdPartyReq.Header, "claude-cli/2.1.77 (external, cli)", "0.87.0", "v24.8.0", "MacOS", "arm64") +} + func TestResolveClaudeDeviceProfile_RechecksCacheBeforeStoringCandidate(t *testing.T) { resetClaudeDeviceProfileCache() stabilize := true