History

fix(claude): avoid racy fingerprint downgrades

tpob committed 7 months ago

Commit 8ca4c201 parent ae1d7e92 files · +111 −4Browse files

Unified · Split

2 files
  1. internal/runtime/executor/claude_device_profile.go +18 −4
  2. internal/runtime/executor/claude_executor_test.go +93 −0
internal/runtime/executor/claude_device_profile.go+18−4
@@ -31,6 +31,8 @@ var ( · 30 unmodified lines
31 claudeDeviceProfileCache = make(map[string]claudeDeviceProfileCacheEntry)31 claudeDeviceProfileCache = make(map[string]claudeDeviceProfileCacheEntry)
32 claudeDeviceProfileCacheMu sync.RWMutex32 claudeDeviceProfileCacheMu sync.RWMutex
33 claudeDeviceProfileCacheCleanupOnce sync.Once33 claudeDeviceProfileCacheCleanupOnce sync.Once
34
35 claudeDeviceProfileBeforeCandidateStore func(claudeDeviceProfile)
34)36)
35 37
36type claudeCLIVersion struct {38type claudeCLIVersion struct {
@@ -257,13 +259,25 @@ func resolveClaudeDeviceProfile(auth *cliproxyauth.Auth, apiKey string, headers · 220 unmodified lines
257 cachedValid := hasCached && entry.expire.After(now) && entry.profile.UserAgent != ""259 cachedValid := hasCached && entry.expire.After(now) && entry.profile.UserAgent != ""
258 claudeDeviceProfileCacheMu.RUnlock()260 claudeDeviceProfileCacheMu.RUnlock()
259 261
260 if hasCandidate && (!cachedValid || shouldUpgradeClaudeDeviceProfile(candidate, entry.profile)) {262 if hasCandidate {
261 newEntry := claudeDeviceProfileCacheEntry{263 if claudeDeviceProfileBeforeCandidateStore != nil {
264 claudeDeviceProfileBeforeCandidateStore(candidate)
265 }
266
267 claudeDeviceProfileCacheMu.Lock()
268 entry, hasCached = claudeDeviceProfileCache[cacheKey]
269 cachedValid = hasCached && entry.expire.After(now) && entry.profile.UserAgent != ""
270 if cachedValid && !shouldUpgradeClaudeDeviceProfile(candidate, entry.profile) {
271 entry.expire = now.Add(claudeDeviceProfileTTL)
272 claudeDeviceProfileCache[cacheKey] = entry
273 claudeDeviceProfileCacheMu.Unlock()
274 return entry.profile
275 }
276
277 claudeDeviceProfileCache[cacheKey] = claudeDeviceProfileCacheEntry{
262 profile: candidate,278 profile: candidate,
263 expire: now.Add(claudeDeviceProfileTTL),279 expire: now.Add(claudeDeviceProfileTTL),
264 }280 }
265 claudeDeviceProfileCacheMu.Lock()
266 claudeDeviceProfileCache[cacheKey] = newEntry
267 claudeDeviceProfileCacheMu.Unlock()281 claudeDeviceProfileCacheMu.Unlock()
268 return candidate282 return candidate
269 }283 }
internal/runtime/executor/claude_executor_test.go+93−0
@@ -8,7 +8,9 @@ import ( · 7 unmodified lines
8 "net/http"8 "net/http"
9 "net/http/httptest"9 "net/http/httptest"
10 "strings"10 "strings"
11 "sync"
11 "testing"12 "testing"
13 "time"
12 14
13 "github.com/gin-gonic/gin"15 "github.com/gin-gonic/gin"
14 "github.com/klauspost/compress/zstd"16 "github.com/klauspost/compress/zstd"
@@ -206,6 +208,97 @@ func TestApplyClaudeHeaders_DoesNotDowngradeConfiguredBaselineOnFirstClaudeClien · 191 unmodified lines
206 assertClaudeFingerprint(t, newerClaudeReq.Header, "claude-cli/2.1.71 (external, cli)", "0.81.0", "v24.6.0", "Linux", "x64")208 assertClaudeFingerprint(t, newerClaudeReq.Header, "claude-cli/2.1.71 (external, cli)", "0.81.0", "v24.6.0", "Linux", "x64")
207}209}
208 210
211func TestResolveClaudeDeviceProfile_RechecksCacheBeforeStoringCandidate(t *testing.T) {
212 resetClaudeDeviceProfileCache()
213 stabilize := true
214
215 cfg := &config.Config{
216 ClaudeHeaderDefaults: config.ClaudeHeaderDefaults{
217 UserAgent: "claude-cli/2.1.60 (external, cli)",
218 PackageVersion: "0.70.0",
219 RuntimeVersion: "v22.0.0",
220 OS: "MacOS",
221 Arch: "arm64",
222 StabilizeDeviceProfile: &stabilize,
223 },
224 }
225 auth := &cliproxyauth.Auth{
226 ID: "auth-racy-upgrade",
227 Attributes: map[string]string{
228 "api_key": "key-racy-upgrade",
229 },
230 }
231
232 lowPaused := make(chan struct{})
233 releaseLow := make(chan struct{})
234 var pauseOnce sync.Once
235 var releaseOnce sync.Once
236
237 claudeDeviceProfileBeforeCandidateStore = func(candidate claudeDeviceProfile) {
238 if candidate.UserAgent != "claude-cli/2.1.62 (external, cli)" {
239 return
240 }
241 pauseOnce.Do(func() { close(lowPaused) })
242 <-releaseLow
243 }
244 t.Cleanup(func() {
245 claudeDeviceProfileBeforeCandidateStore = nil
246 releaseOnce.Do(func() { close(releaseLow) })
247 })
248
249 lowResultCh := make(chan claudeDeviceProfile, 1)
250 go func() {
251 lowResultCh <- resolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{
252 "User-Agent": []string{"claude-cli/2.1.62 (external, cli)"},
253 "X-Stainless-Package-Version": []string{"0.74.0"},
254 "X-Stainless-Runtime-Version": []string{"v24.3.0"},
255 "X-Stainless-Os": []string{"Linux"},
256 "X-Stainless-Arch": []string{"x64"},
257 }, cfg)
258 }()
259
260 select {
261 case <-lowPaused:
262 case <-time.After(2 * time.Second):
263 t.Fatal("timed out waiting for lower candidate to pause before storing")
264 }
265
266 highResult := resolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{
267 "User-Agent": []string{"claude-cli/2.1.63 (external, cli)"},
268 "X-Stainless-Package-Version": []string{"0.75.0"},
269 "X-Stainless-Runtime-Version": []string{"v24.4.0"},
270 "X-Stainless-Os": []string{"MacOS"},
271 "X-Stainless-Arch": []string{"arm64"},
272 }, cfg)
273 releaseOnce.Do(func() { close(releaseLow) })
274
275 select {
276 case lowResult := <-lowResultCh:
277 if lowResult.UserAgent != "claude-cli/2.1.63 (external, cli)" {
278 t.Fatalf("lowResult.UserAgent = %q, want %q", lowResult.UserAgent, "claude-cli/2.1.63 (external, cli)")
279 }
280 if lowResult.PackageVersion != "0.75.0" {
281 t.Fatalf("lowResult.PackageVersion = %q, want %q", lowResult.PackageVersion, "0.75.0")
282 }
283 case <-time.After(2 * time.Second):
284 t.Fatal("timed out waiting for lower candidate result")
285 }
286
287 if highResult.UserAgent != "claude-cli/2.1.63 (external, cli)" {
288 t.Fatalf("highResult.UserAgent = %q, want %q", highResult.UserAgent, "claude-cli/2.1.63 (external, cli)")
289 }
290
291 cached := resolveClaudeDeviceProfile(auth, "key-racy-upgrade", http.Header{
292 "User-Agent": []string{"curl/8.7.1"},
293 }, cfg)
294 if cached.UserAgent != "claude-cli/2.1.63 (external, cli)" {
295 t.Fatalf("cached.UserAgent = %q, want %q", cached.UserAgent, "claude-cli/2.1.63 (external, cli)")
296 }
297 if cached.PackageVersion != "0.75.0" {
298 t.Fatalf("cached.PackageVersion = %q, want %q", cached.PackageVersion, "0.75.0")
299 }
300}
301
209func TestApplyClaudeHeaders_DisableDeviceProfileStabilization(t *testing.T) {302func TestApplyClaudeHeaders_DisableDeviceProfileStabilization(t *testing.T) {
210 resetClaudeDeviceProfileCache()303 resetClaudeDeviceProfileCache()
211 304