Add Cursor subscription provider via official sdk-bridge - #24
Conversation
Hand-rolled Connect-over-HTTP/1.1 client with JSON encoding (application/json unary, application/connect+json streaming), built on llmclient.Client.DoRaw/DoStream with RawBody. Bearer on every request; 5-byte envelope framing with end-of-stream parsing. No protobuf/connectrpc/buf deps.
Spawns/attaches to cursor-sdk-bridge (MIT, stable sdk.v1 contract). Scrubbed child env; ready-line handshake on stderr; authTokenFile bearer read; Shutdown RPC → SIGTERM → SIGKILL; io.Closer; attach mode for tests. Mirrors internal/mcpgateway/upstream.go env-scrub pattern.
Non-streaming chat completions, model listing, lazy bridge lifecycle, and 501 stubs for unsupported surfaces. Follows the chatgpt provider pattern; wire structs isolated in cursor_wire.go.
Envelope-to-SSE converter mirroring anthropic/chat_stream.go: assistant deltas → FormatChatChunkSSE chunks; terminal result → final chunk with optional usage; [DONE] on clean end; GatewayError 502 on malformed frame after prior chunks. Agent released exactly once on end/error/Close. Replaces the 501 stub.
Add cursor to factory (run/providers.go, providers test, config fixture, config example). Extend InitResult.Close() to close io.Closer providers with errors.Join aggregation; idempotent via the existing closeOnce guard.
docs/providers/cursor.mdx (configure, models, dialect limits, subscription-billing warning, ToS note), nav in docs.json, overview table row, .env.template CURSOR_API_KEY/CURSOR_MODELS block.
Four contract cases (chat, stream, models, error mapping) with in-memory Connect framing helper mirroring sseFixtureRoute; fixtures + goldens under tests/contract/testdata/cursor/.
…r stream test Registration comment no longer advertises cursor.base_url (managed mode ignores it; bridge picks its own port). streamConverter doc comment now says deltas are incremental. New TestStreamChatCompletion_NonOKTerminalEmitsGatewayError covers the terminal-status error path.
Stub homeDir to t.TempDir() so a host-installed cursor-sdk-bridge at ~/.local/share/gomodel/bin cannot satisfy the fallback path and break the install-hint assertion.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
weselben
left a comment
There was a problem hiding this comment.
Review — PR #24 (cursor provider, 9 commits)
No 🔴 bugs. Three 🟡 risks worth addressing before merge:
cursor.go:160— managed-modeSetBaseURLcorrupts cached state. After a successfulStart,p.startDone==true.SetBaseURLunconditionally resetsp.tr=nil,p.curURL=url,p.curToken=""but never touchesp.startDonein the managed branch. Nexttransport()skips re-handshake (startDone), skips startErr, seesp.tr==nil, rebuildsTransportwith the user-supplied URL (which the spawned bridge is not listening on — bridge chose its own ephemeral port) and an empty bearer ("Bearer "). Every subsequent RPC 401s. Doc comment claims only the cached transport is dropped in managed mode; the code drops more. Nothing callsSetBaseURLon managed cursor today (latent), but it is a real state bug.cursor.go:359—closeAgenterrors are silently swallowed in theagentCloserclosure (_ = p.closeAgent(context.Background(), tr, agentID)). Same pattern inStreamChatCompletion's stream-error path (~line 360). On an unresponsive bridge the agent leaks server-side and accumulates across requests with no log/counter. Suggestslog.Warnon error.chat_stream.go:114— unbounded recursion instreamConverter.Read. Frames producing zero bytes (env.Done, non-assistantsdkMessage types, assistant messages with no text blocks) fall through toreturn c.Read(p). A bridge streaming endless no-op frames stack-overflows the reader goroutine. Replace with aforloop and a depth cap.
🔵 nits (deferred unless cheap to fold in): cursor.go:343 workspaceOrDefault "/" may fail for non-root deployments (use os.TempDir() fallback); bridge_manager.go:387 CRLF leaves \r in the ready-line payload (use strings.TrimRight(line, "\r\n")); bridge_manager.go:101 binary-resolution failure only surfaces on first RPC (log at startup).
❓ questions: cursor.go:30 — production New() ignores BaseURL; the fix-round doc edit acknowledges this for managed mode, but should CURSOR_BASE_URL route to attach mode in production, or is it dead? bridge_manager.go:213 — Shutdown RPC body is {}; if the real endpoint expects fields, every Close() 400s and SIGTERM does the real work (tests only mock a {}-accepting server).
Verified clean: credentials never logged (bearer touches only the Transport header setter and the Shutdown RPC; child env scrub tested); Connect framing matches readFrame byte-for-byte; agent release is single-shot via nil-out on EOF/error/Close; no process/goroutine leaks across the full lifecycle; validate.sh hits are false positives (${CURSOR_API_KEY} env-substitution placeholders, no secret committed).
— posted by weselben via the PR review skill
…gent, bounded stream read) SetBaseURL in managed mode no longer clobbers the bearer; closeAgent failures now slog.Warn; streamConverter.Read no longer tail-recurses (bounded loop with a GatewayError cap); workspaceOrDefault prefers os.TempDir; ready-line scan trims CRLF.
There was a problem hiding this comment.
Resolve round done. 6 resolved (SetBaseURL managed-mode corruption; closeAgent swallowed errors on two comments at the same site; unbounded Read recursion; workspaceOrDefault fallback to os.TempDir; CRLF ready-line scan). 1 deferred (binary-resolution startup logging - needs a logger seam on BridgeManager). 2 answered (CURSOR_BASE_URL is attach-mode-only by design; Shutdown RPC body is empty by contract with SIGTERM/SIGKILL fallback). go test ./...: 82 ok, 0 FAIL. Contract + race suites green. PR head is now 5bbfb5e.
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer func() { _ = p.closeAgent(ctx, tr, agentID) }() |
weselben
left a comment
There was a problem hiding this comment.
Round-2 review (after first review pass)
Scope: validation pass + caveman-format re-review of all 27 changed files.
Head SHA: 5bbfb5e1a35909f1d565c4f3e21dc50bb5ee9021
Hard-rule gate
scripts/validate.sh /tmp/local-pr24.diff produced two false positives on config/config.example.yaml and .env.template for CURSOR_API_KEY="${CURSOR_API_KEY}" — these are env-var templates, not literal secrets. No new findings.
Findings summary
- 🔴 bugs: 4 (closeAgent defer uses request ctx; startFailure always returns 502;
CURSOR_MODELSdocumented but never implemented; appendAssistant silent parse failure is borderline 🟡) - 🟡 risks: ~12 (lock contention on startup, 64 KiB stderr read buffer limit, scrubbed env drops proxy vars, EOF mid-stream, oversized frame >1 MiB, transport ctx swallowed, etc.)
- 🔵 nits: ~26
- ❓ questions: 2
🔴+🟡 inline comments are attached below. The remaining findings (🔵+❓) are listed in this body.
In-body 🔵 nits + ❓ questions
cursor.go
- L531 (nit):
cursorRunErrorbuilds message fromr.ErrorCode/r.Result.Result; if both empty, fallback reads"cursor: run failed with status <status>"— bare enum, not operator-friendly. Include raw run id + non-empty fields. - L518 (nit):
terminalStatusOKaccepts onlyFINISHEDandRUN_LIFECYCLE_STATUS_FINISHED. WhitelistCOMPLETED/SUCCEEDEDand distinguishCANCELLEDfromERROR. - L386 (nit): doc says
defaultBaseURLis "loopback" but managed bridge uses ephemeral port. Misleading constant. - L404-411 (q):
createAgentreturnsBadGatewayon missingagentIdwithout callingCloseAgent— server-side agent may already exist. Common enough to warrant cleanup RPC?
bridge_manager.go
- L122 (nit):
NewManagedBridgeManagerreturns success whileb.cmdholds{workspace}placeholder; never GC'd to a real process if Start is never called. - L194-203 (nit): spawn-timeout path calls
b.cmd.Wait()twice onClose(). Swallows a realexec.Cmdinvariant violation. - L213-249 (nit): absolute-failure window is
2*shutdownTimeout+ 5 s Shutdown RPC budget = 15 s. Worth documenting inClose()doc. - (q): Does
cursor-sdk-bridgeaccept SIGTERM cleanly, or trap-and-ignore? SIGTERM escalation is undocumented behaviour.
chat_stream.go
- L131-135 (nit):
c.msgIDmutated across reads without synchronization; today driven by single goroutine butio.Readercontract does not preclude concurrent reads. Document// Read is not safe for concurrent use. - L147-155 (nit): first assistant chunk combines
delta.role=assistantANDdelta.content=<first text>. Strict OpenAI streams emit role in chunk 0 withcontent=""and content starting in chunk 1. Postel alternative works but worth documenting for strict-mode clients.
connect_transport.go
- L264-285 (nit):
parseEndStreamsilently returns nil on malformed end-frame JSON — a hard protocol violation. Log aslog.Warnwith raw bytes (no secrets). - L75 (nit):
NewTransportheaderSetter closes overtoken. If token contains\r\n,httpreturns confusing error. Addstrings.ContainsAny(token, "\r\n")guard.
cursor_wire.go
- L131-141 (nit):
runStreamEnvelope.Done *struct{}mirrors protooneofbut is never inspected. Cosmetic. - L98 (nit):
localAgentOptions.CWD []string— bridge may expect a single workspace string in many existing deployments (repeated stringper proto). Add a comment explaining the assumption.
cursor_test.go
- L228-251 (nit):
TestChatCompletion_RunErrorasserts*gw.Code == "model_overloaded"but not status code. Addgw.StatusCode == http.StatusBadGateway. - L165-172 (nit): manually composed JSON via string concatenation in
resultFrameis fragile. Usejson.Marshalof a struct.
connect_transport_test.go
- (nit):
TestUnary_StreamSendsAuthorizationOnEveryRequestis essentially duplicated byTestStream_FramesInOrderwhich already assertsAuthorization.
chat_stream_test.go
- L328 (nit):
TestStreamChatCompletion_CloseReleasesAgentparks on<-releaseCh. If test process panics beforeclose(releaseCh), handler hangs forever. Deferclose(releaseCh)immediately after parking.
bridge_manager_test.go
- L42 (nit):
withFakeBridgedoesos.Chmod(p, 0o755)without checking the path is intestdata/. Worth a defensive guard.
testdata/fake_bridge.sh
- L42 (nit):
cat >&2 <<EOF ... EOFuses${$:-0}syntax — works in bash but uncertain in pure POSIXsh. Hardcodepid=$$above the heredoc. - L48-51 (nit):
exec sleep 3600after writing$token_file— if parent killed between printf and exec, token file still exists 0600. Note.
docs/providers/cursor.mdx
- L27-37 (nit): doc claims
scrubbedBridgeEnvinherits only PATH/HOME/TMPDIR/USER/LANG + CURSOR_API_KEY. Actual code passes a 6th varCURSOR_SDK_CLIENT_LANGUAGE=go(bridge_manager.go:357). Document the 6th. - L62-67 (nit): lists specific model slugs without pinned date — doc-rot. Cite bridge docs URL.
- L81-92 (nit):
/v1/filesreturns 501 — provider implements Responses/StreamResponses/Embeddings returning 501, not Files. Calls to/v1/filesgo through router's file handler. Drop the bullet or note it differently. - L96-101 (nit): "Streaming emits OpenAI-conservative SSE" — first chunk combines role+content. Document explicitly.
config/config.example.yaml
- L347-359 (nit):
models:comment says "Override with slugs your plan advertises ... or leave unset to discover at runtime". Implementation always discovers viaListModelsand ignorescfg.Models. Either mark as cosmetic/future-use or implement the filter.
internal/providers/init.go
- L55-67 (nit): sequential provider-close-then-cache-close can extend total shutdown if
cache.Close()is slow. Acceptable trade-off vs parallel close.
config_test.go
- L88-90 (nit):
"cursor": {DefaultBaseURL: "http://127.0.0.1:32123"}is misleading in managed mode (bridge listens on ephemeral port from ready line). AddEndpointDiscoveryor downgrade.
Commit hygiene
5bbfb5e1 fix(cursor): address pr-review findings ...lists three distinct concerns in one commit. Reverting any one requires reverting all three.a26d627e feat(cursor): register provider and wire shutdown lifecyclecouplesInitResult.Closeplumbing with cursor registration. Could be split.
Test coverage gap
Current coverage on internal/providers/cursor/... is 77.9%. To meet the upstream ~99% bar, missing coverage includes:
- Bridge crash mid-stream (success header → unexpected EOF)
p.transport()concurrent calls during startup (lock contention)- Cancelled-ctx defer leaking the agent in
ChatCompletion - Oversized streaming response frame (
readFrame> 1 MiB) - Bridge crash with single stderr line longer than 64 KiB (
BufferFull) ListModelswith malformed JSON
| if err != nil { | ||
| return nil, err | ||
| } | ||
| defer func() { _ = p.closeAgent(ctx, tr, agentID) }() |
There was a problem hiding this comment.
🔴 bug: defer func() { _ = p.closeAgent(ctx, tr, agentID) }() reuses the request context, which may already be cancelled by the time defer runs (user navigated away, client disconnected, etc.). On cancellation the CloseAgent RPC fails instantly with context.Canceled and the bridge agent leaks until the bridge itself shuts down. Fix: call p.closeAgent(context.Background(), tr, agentID) exactly like StreamChatCompletion's agentCloser at lines 343-345 already does.
There was a problem hiding this comment.
Resolved in head — see latest commits on this PR for the fix.
| // handshake on the RPC path. | ||
| func (p *Provider) transport(ctx context.Context) (*Transport, error) { | ||
| p.mu.Lock() | ||
| defer p.mu.Unlock() |
There was a problem hiding this comment.
🟡 risk: p.mu is held for the entire duration of p.manager.Start(ctx), which blocks for up to defaultStartupTimeout (30 s) waiting for the ready line. Every concurrent RPC to this provider queues on the same lock during startup. Fix: wrap Start in singleflight.Group.Do(\"bridge-start\", ...) so only one goroutine spawns; readers retry on lock release.
There was a problem hiding this comment.
Acknowledged, not fixed in this PR — startup lock contention is a low-frequency race (only on first RPC after process start). singleflight hardening tracked for the bridge-pre-warm follow-up.
| // status code surfaces consistently. EOF-heavy environments (the bridge | ||
| // binary missing) land here on the first RPC. | ||
| func (p *Provider) startFailure(err error) error { | ||
| return core.NewProviderError("cursor", http.StatusBadGateway, |
There was a problem hiding this comment.
🟡 risk: startFailure always returns http.StatusBadGateway (502) for bridge-unavailable errors, including "bridge binary not installed". A missing binary is closer to 503 Service Unavailable; 502 is for an upstream that exists and returned a bad response. Fix: pick 502 vs 503 based on whether the bridge is reachable vs. installable.
There was a problem hiding this comment.
Resolved in 4857b54 — startFailure now returns 503 when errors.Is(err, ErrBridgeUnreachable) and 502 otherwise. resolveBridgeBinary wraps missing-binary failures with the sentinel. Covered by new StartFailure unit tests.
| } | ||
| if b.endpoint != "" { | ||
| b.endpt = b.endpoint | ||
| b.tok = os.Getenv(b.tokenEnv) |
There was a problem hiding this comment.
🟡 risk: b.tok = os.Getenv(b.tokenEnv) does not trim whitespace. If the operator ships a .env file with CURSOR_BRIDGE_TOKEN= token (leading space, common editor quirk), the bearer comes back as \" token\" and every Connect RPC responds 401 with no useful clue. Fix: tok = strings.TrimSpace(os.Getenv(b.tokenEnv)).
There was a problem hiding this comment.
Resolved in head — b.tok now uses strings.TrimSpace on the env value. Covered by TestAttachModeTrimsTokenWhitespace.
| // The follow reader is the same bufio.Reader used for scanning, so bytes | ||
| // already buffered past the ready line are handed to the drain intact. | ||
| func scanReadyLine(r io.Reader, out chan<- readyResult) { | ||
| br := bufio.NewReaderSize(r, 64*1024) |
There was a problem hiding this comment.
🟡 risk: bufio.NewReaderSize(r, 64*1024) returns bufio.ErrBufferFull when a single stderr line exceeds 64 KB. scanReadyLine treats that as a non-EOF error and bubbles up as start bridge: <err>: <stderr> — the message reads like the bridge crashed when in reality the line was just long. Some supervisor wrappers print a banner before the ready line. Fix: bump to >=1 MiB or accumulate without delimiter.
There was a problem hiding this comment.
Resolved in head — scanReadyLine buffer raised to 1<<20 (1 MiB). Covered by TestScanReadyLineHandlesLongBanner.
| client *llmclient.Client | ||
| } | ||
|
|
||
| // NewTransport returns a Transport that talks to the bridge at baseURL, |
There was a problem hiding this comment.
🟡 risk: maxConnectBodyBytes = 1 << 20 (1 MiB) caps both unary response body bytes and envelope frame payload length. A streaming payload > 1 MiB (large assistant texts, multi-MB tool output) is rejected with the cryptic envelope frame length N exceeds 1048576 bytes message. Fix: separate maxUnaryBodyBytes and maxFrameBytes constants and tune appropriately.
There was a problem hiding this comment.
Resolved in head — see latest commits on this PR for the fix.
| return n, nil | ||
| } | ||
|
|
||
| func TestSpawnReadyParseAndTokenRead(t *testing.T) { |
There was a problem hiding this comment.
🟡 risk: childPIDs reads /proc/<pid>/stat directly with a hand-rolled parser that locates the closing ) of the comm field. comm can contain spaces, parens (escaped), or Unicode. The current parser works for standard cases but breaks for any process whose comm is modified (e.g. prctl(PR_SET_NAME) with a value containing )). Fix: use gopsutil or just pgrep -P <ppid> shell-out.
There was a problem hiding this comment.
Acknowledged, not fixed here — the /proc//stat parser handles all current Linux kernels we ship on. A future tightening can adopt gopsutil. The existing tests cover the standard case (no spaces in comm).
| if [ -z "$token_file" ]; then | ||
| echo "fake bridge: FAKE_BRIDGE_TOKEN_FILE not set" >&2 | ||
| exit 2 | ||
| fi |
There was a problem hiding this comment.
🟡 risk: the inline cat >&2 <<EOF ... EOF ready line uses \"pid\":${$:-0}. POSIX ${$:-default} syntax works in bash but is uncertain in pure POSIX sh (the shebang is #!/bin/sh). On a minimal Debian dash this can fail. Fix: hardcode the PID via pid=$$ above the heredoc and interpolate $pid.
There was a problem hiding this comment.
Resolved in head — see latest commits on this PR for the fix.
|
|
||
| ## Models | ||
|
|
||
| `ListModels` is served from the bridge's `SdkCursorService.ListModels`, so |
There was a problem hiding this comment.
🔴 bug: CURSOR_MODELS=composer,auto is documented as the way to "Pin a static list ... when you want a fixed surface". The provider never reads this env var (verified — internal/providers/cursor/*.go has zero CURSOR_MODELS references; grep across the whole repo shows only .env.template and this doc mention it). Fix: either remove the documentation and the .env.template line, or implement the static-list override.
There was a problem hiding this comment.
Resolved in head — see latest commits on this PR for the fix.
| # Generate at Cursor Dashboard → API Keys. Draws from the same plan pools as the CLI login. | ||
| # Requires the cursor-sdk-bridge binary: CURSOR_SDK_BRIDGE_BIN, PATH, or ~/.local/share/gomodel/bin/. | ||
| # CURSOR_API_KEY=crsr_... | ||
| # CURSOR_MODELS=composer,auto |
There was a problem hiding this comment.
🔴 bug: CURSOR_MODELS=composer,auto is shown as a comment template but is never read by the codebase (see cursor.mdx finding above). Same fix as the docs issue — remove or implement.
There was a problem hiding this comment.
Resolved in cf0dc6e — docs/providers/cursor.mdx no longer references CURSOR_MODELS (the provider always discovers via ListModels at runtime).
Defer closeAgent reused the request ctx, which is often already cancelled by the time the defer runs (client disconnect, idle timeout). The CloseAgent RPC then fails with context.Canceled and the bridge agent leaks until shutdown. Route the cleanup RPC through context.Background(), matching StreamChatCompletion's agentCloser.
Bridge-start failures always mapped to 502 Bad Gateway, conflating two distinct operator actions: install the binary (503) vs. fix a malformed handshake (502). Add ErrBridgeUnreachable sentinel; wrap resolveBridgeBinary's missing-binary errors with it. startFailure now returns 503 when the error wraps that sentinel and 502 otherwise.
Three bridge_manager hardening fixes: (1) trim whitespace from the attach-mode bearer so editor-injected leading spaces don't silently 401 every RPC; (2) raise the stderr scan buffer from 64 KiB to 1 MiB so supervisor banners no longer surface as misleading 'bridge crashed' errors via bufio.ErrBufferFull; (3) forward HTTP_PROXY/HTTPS_PROXY/NO_PROXY (and lowercase) so operators behind a corporate proxy can still reach the Cursor APIs.
…end-frame Four connect_transport hardening fixes: (1) split the 1 MiB cap into maxUnaryBodyBytes (unary responses) and maxStreamFrameBytes (8 MiB, streaming frames) — multi-MB assistant texts no longer hit the old shared cap; (2) StreamReader.Next now honours ctx via context.AfterFunc that closes the body, so a stalled read unblocks on caller cancel; (3) parseEndStream logs slog.Warn with a scrubbed raw preview when the end-frame JSON is malformed; (4) NewTransport strips CR/LF from the bearer and warns, surfacing the misconfiguration at boot instead of at HTTP write time.
CURSOR_MODELS was documented as a static-list override for cursor provider model discovery, but the code never reads it — the provider always discovers slugs via ListModels at runtime. Remove the misleading reference from .env.template and docs/providers/cursor.mdx; update config/config.example.yaml to note the (currently cosmetic) models field is reserved for a future allow-list filter.
Extends the cursor provider test suite with cases for: - bridge_manager: option chains, drainStderr ctx cancel, attach-mode rejects empty endpoint, attach-mode never touches exec, attach-mode close is no-op, scanReadyLine long-banner, parseReadyLine schema variants, workspace arg replacement - chat_stream: malformed frame 502, non-OK terminal gateway error, close releases agent, send-error closes agent, too-many-empty-frames bound, read-buffer drain, read after close EOF, handleResult non-OK, stream next error, nil close-agent no-op - connect_transport: oversized response, keepalive skipped - cursor: provider option chain, transport race, list-model wire error, missing env fallback, run-error typed error, runError message variants Co-authored-by: weselben <50115212+weselben@users.noreply.github.com>
Expands test surface to cover remaining chat_stream.go inner-loop branches (assistant frame return, malformed frame 502, EOF after skips), connect_transport EOF-on-empty-body and truncated-payload paths, bridge_manager drainStderr-nil and scanReadyLine-truncated branches, plus cursor.go nil-request guards and runSend no-terminal error path. Coverage: 91.1% → 93.2%. Co-authored-by: weselben <50115212+weselben@users.noreply.github.com>
TL;DR
Cursor subscriptions bundle Grok 4.5/4.6 and Composer 2.5 at flat rate, but GoModel had no way to bill inference against them. This PR adds a
cursorprovider: GoModel spawns the officialcursor-sdk-bridgesubprocess and serves Cursor models through the standard OpenAI-compatible API.Files to review (27, +4132 / -2):
internal/providers/cursor/cursor.go(start here)core.Providermethods, lazy bridge start, stateless chat mapping.internal/providers/cursor/connect_transport.gointernal/providers/cursor/bridge_manager.gointernal/providers/cursor/chat_stream.gointernal/providers/cursor/cursor_wire.gosdk.v1JSON field mappings in one file. Schema drift means a one-line fix.internal/providers/init.goInitResult.Close()now closes providers that implementio.Closer.docs/providers/cursor.mdx(new)tests/contract/cursor_test.go(new)How
llmclient.DoRaw/DoStream. Cursor's own curl-only smoke test proves JSON framing works. Nobuf, no generated code.PATH,HOME,TMPDIR,USER,LANG, andCURSOR_API_KEY. Provider keys in the gateway environment cannot leak into the bridge. Shutdown uses the control RPC, then SIGTERM, then SIGKILL.usagewhen the bridge run result carries token counts. The existingStreamUsageObserverrecords it unchanged. No usage data means a graceful omission, not an error.cursordrives the env convention:CURSOR_API_KEY,CURSOR_BASE_URL,CURSOR_MODELS.Reviewer notes
cfg.BaseURLis ignored in managed mode. The bridge picks its own ephemeral port. The registration comment says so. The test seam (NewWithHTTPClient) uses attach mode.CredentialsService.installunregisters providers withoutClose(). Cursor is the first provider that owns a subprocess, so an admin-API credential swap leaks a bridge process until shutdown. This PR does not fix it. Tracked as follow-up.plan_required: Cloud Agent is not available for free users. A chat completion with a Pro key is the last open check.bridge_manager.gospawn and shutdown paths. They own the only subprocess in the codebase.Tests
go test ./...— green (81 packages).go test -race ./internal/providers/cursor/— green. Covers spawn, handshake, env scrub, orphan-free kill, attach mode, streaming, error paths.go test -tags=contract ./tests/contract/— green. Four cursor replay cases.Follow-up
CredentialsService.installsubprocess leak (see Reviewer notes).Links
research/cursor-client-surface,research/cursor-bridges,research/cursor-sdk-bridge-groundingThis PR description was generated with AI assistance.