fix: unblock Cursor Composer provider — address review findings + resolve dev conflict#4055
fix: unblock Cursor Composer provider — address review findings + resolve dev conflict#4055bradhallett wants to merge 12 commits into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces support for Cursor Composer API keys, adding configuration management, API endpoints, transport resolution, and a dedicated executor to handle chat completions and streaming via Cursor's Connect-RPC protocol. Feedback on the changes highlights a critical bug in the checksum timestamp calculation, multiple potential nil pointer dereferences in the management handlers when h.cfg is uninitialized, and a recommendation to use a mutex with double-checked locking to prevent cache stampedes during concurrent token exchanges.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
|
||
| func cursorChecksum(identity string) string { | ||
| machineID := sha256Hex("composer-api:cursor-machine:" + identity) | ||
| timestamp := uint64(time.Now().Unix() / 1_000_000) |
There was a problem hiding this comment.
The timestamp calculation is off by a factor of 1,000. time.Now().Unix() returns seconds, but the Cursor checksum algorithm expects milliseconds divided by 1,000,000 (equivalent to seconds divided by 1,000). This will cause the generated checksum to be incorrect and potentially lead to authentication or signature verification failures on the Cursor backend.
| timestamp := uint64(time.Now().Unix() / 1_000_000) | |
| timestamp := uint64(time.Now().UnixMilli() / 1_000_000) |
| h.mu.Lock() | ||
| defer h.mu.Unlock() | ||
| h.cfg.CursorComposerKey = append([]config.CursorComposerKey(nil), arr...) |
There was a problem hiding this comment.
If h.cfg is nil, attempting to access h.cfg.CursorComposerKey will cause a nil pointer dereference panic. A defensive check should be added to ensure the configuration is initialized before accessing it.
h.mu.Lock()
defer h.mu.Unlock()
if h.cfg == nil {
c.JSON(500, gin.H{"error": "configuration not initialized"})
return
}
h.cfg.CursorComposerKey = append([]config.CursorComposerKey(nil), arr...)| h.mu.Lock() | ||
| defer h.mu.Unlock() | ||
| targetIndex := -1 |
There was a problem hiding this comment.
If h.cfg is nil, accessing h.cfg.CursorComposerKey will cause a nil pointer dereference panic. A defensive check should be added to ensure the configuration is initialized before accessing it.
h.mu.Lock()
defer h.mu.Unlock()
if h.cfg == nil {
c.JSON(500, gin.H{"error": "configuration not initialized"})
return
}
targetIndex := -1| h.mu.Lock() | ||
| defer h.mu.Unlock() | ||
| if val := strings.TrimSpace(c.Query("api-key")); val != "" { |
There was a problem hiding this comment.
If h.cfg is nil, accessing h.cfg.CursorComposerKey will cause a nil pointer dereference panic. A defensive check should be added to ensure the configuration is initialized before accessing it.
h.mu.Lock()
defer h.mu.Unlock()
if h.cfg == nil {
c.JSON(500, gin.H{"error": "configuration not initialized"})
return
}
if val := strings.TrimSpace(c.Query("api-key")); val != "" {| type CursorComposerExecutor struct { | ||
| cfg *config.Config | ||
| identityCache sync.Map | ||
| tokenCache sync.Map | ||
| } |
There was a problem hiding this comment.
Add a mutex to prevent concurrent duplicate token exchange requests (thundering herd/cache stampede) when multiple requests arrive simultaneously for the same API key.
| type CursorComposerExecutor struct { | |
| cfg *config.Config | |
| identityCache sync.Map | |
| tokenCache sync.Map | |
| } | |
| type CursorComposerExecutor struct { | |
| cfg *config.Config | |
| identityCache sync.Map | |
| tokenCache sync.Map | |
| exchangeMu sync.Mutex | |
| } |
| func (e *CursorComposerExecutor) exchangeAPIKey(ctx context.Context, auth *cliproxyauth.Auth, apiKey, backendBase string) (string, error) { | ||
| if val, ok := e.tokenCache.Load(apiKey); ok { | ||
| if entry, ok := val.(cursorComposerTokenCacheEntry); ok && entry.token != "" && time.Now().Before(entry.expiresAt) { | ||
| return entry.token, nil | ||
| } | ||
| } |
There was a problem hiding this comment.
Implement double-checked locking using the new exchangeMu mutex to serialize concurrent token exchange requests for the same API key, preventing cache stampedes.
func (e *CursorComposerExecutor) exchangeAPIKey(ctx context.Context, auth *cliproxyauth.Auth, apiKey, backendBase string) (string, error) {
if val, ok := e.tokenCache.Load(apiKey); ok {
if entry, ok := val.(cursorComposerTokenCacheEntry); ok && entry.token != "" && time.Now().Before(entry.expiresAt) {
return entry.token, nil
}
}
e.exchangeMu.Lock()
defer e.exchangeMu.Unlock()
if val, ok := e.tokenCache.Load(apiKey); ok {
if entry, ok := val.(cursorComposerTokenCacheEntry); ok && entry.token != "" && time.Now().Before(entry.expiresAt) {
return entry.token, nil
}
}Add a Cursor Composer API-key provider that adapts OpenAI-compatible chat requests to Cursor Composer's connect-proto flow, based on the originator reference at https://github.com/standardagents/composer-api. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Resolve Cursor Composer model aliases for API-key configs, cache Cursor identity/token lookups, and harden request/protobuf handling. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Propagate Cursor Connect end-stream errors and treat truncated frame headers as upstream response failures. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Do not advertise image input for Cursor Composer models until image content is forwarded upstream. Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Capture VPS + SDK bridge setup, OpenCode client wiring, model registration pitfalls, and common errors from operational work on this branch. Co-authored-by: Cursor <cursoragent@cursor.com>
- Route crsr_ keys through optional sdk-bridge-url (local Cursor SDK bridge) - Add cursor-composer-api-key management routes (alias cursor-api-key) - Merge default Composer model IDs on auth registration to avoid stale sets - Share backend/chat/client-version resolution via internal/cursorcomposer - Document config, bridge mapping, and ops notes in config.example.yaml Co-authored-by: Cursor <cursoragent@cursor.com>
Keep AGENTS.md unchanged on this branch; deployment notes stay out of repo docs. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
3c25d46 to
6527509
Compare
|
All three findings from the Gemini Code Assist review have been addressed in the latest commits: 1. Checksum timestamp (critical) — Fixed. 2. Nil-pointer in management handlers — Investigated. 3. Token cache stampede — Fixed. |
Follow-up to #3651 (Cursor Composer provider) — addresses bot review findings, resolves merge conflict with
dev, and adds regression tests.What changed
Conflict resolution (rebased onto current
dev)sdk/cliproxy/service.go: keeps upstreamNewXAIAutoExecutorforxai, addscursor-composercaseGemini Code Assist findings addressed
cursorChecksumwas dividingUnix()by 1,000,000, collapsing the timestamp to zero. Switched toUnixMilli()with a deterministiccursorChecksumAt()helper. AddedTestCursorChecksumAtUsesTimestamp.exchangeAPIKeynow uses a mutex with double-checked locking so concurrent calls share a single upstream exchange. AddedTestCursorComposerExchangeAPIKeyCachesConcurrentCalls.management.NewHandleralways initializescfg; no nil path exists in the current codebase. Did not add defensive guards that can't be triggered.Original PR #3651 review blockers
continue→returnafter error yield127.0.0.1:8792/sdkfallback so direct Cursor routing works whensdk-bridge-urlis unset (config, synthesizer, and executor all updated)shift >= 64guard inreadProtoVarintbufio.Scannerwithbufio.Reader(64KB buffer limit)bytesTrimSpace: replaced withbytes.TrimSpaceInterface compatibility
ForceMappingfield +GetForceMapping()toCursorComposerModelto satisfy upstreamdev's model alias interfaceTests added
TestCursorSessionIDUsesAccessTokenHash(restored)TestCursorComposerSDKBridgeURLEmptyWhenUnsetTestCursorComposerSDKBridgeURLNormalizesExplicitValueTestReadProtoVarintOverflowTestParseConnectProtoFramesStopsAfterEndStreamTestCursorChecksumAtUsesTimestampTestCursorComposerExchangeAPIKeyCachesConcurrentCallsTestSanitizeCursorComposerKeysLeavesSDKBridgeURLEmptyWhenUnsetTestSanitizeCursorComposerKeysKeepsExplicitSDKBridgeURLTestBytesTrimSpace(darwin)Verification
go test ./...— all passgo build -o test-output ./cmd/server && rm test-output— compiles cleangofmt -w .— formatted