Skip to content

Add Cursor subscription provider via official sdk-bridge - #24

Draft
weselben wants to merge 17 commits into
mainfrom
feat/cursor-grok-provider
Draft

Add Cursor subscription provider via official sdk-bridge#24
weselben wants to merge 17 commits into
mainfrom
feat/cursor-grok-provider

Conversation

@weselben

Copy link
Copy Markdown
Owner

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 cursor provider: GoModel spawns the official cursor-sdk-bridge subprocess and serves Cursor models through the standard OpenAI-compatible API.

Files to review (27, +4132 / -2):

File Why
internal/providers/cursor/cursor.go (start here) Provider core: six core.Provider methods, lazy bridge start, stateless chat mapping.
internal/providers/cursor/connect_transport.go Hand-rolled Connect-over-HTTP/1.1 client, JSON encoding. No protobuf or connectrpc dependency.
internal/providers/cursor/bridge_manager.go Subprocess lifecycle: spawn, ready-line handshake, scrubbed environment, shutdown.
internal/providers/cursor/chat_stream.go Connect envelope → OpenAI SSE converter.
internal/providers/cursor/cursor_wire.go All sdk.v1 JSON field mappings in one file. Schema drift means a one-line fix.
internal/providers/init.go InitResult.Close() now closes providers that implement io.Closer.
docs/providers/cursor.mdx (new) Configure, models, limits, billing warning, ToS note.
tests/contract/cursor_test.go (new) Four replay cases with fixtures and goldens.

How

  • Transport. The bridge serves Connect-over-HTTP/1.1 with JSON framing. The client is ~300 lines on llmclient.DoRaw/DoStream. Cursor's own curl-only smoke test proves JSON framing works. No buf, no generated code.
  • Bridge lifecycle. One managed bridge per provider, started on first request. The child environment contains only PATH, HOME, TMPDIR, USER, LANG, and CURSOR_API_KEY. Provider keys in the gateway environment cannot leak into the bridge. Shutdown uses the control RPC, then SIGTERM, then SIGKILL.
  • Chat semantics. Each request creates a fresh bridge agent. The full message history flattens into one user message. The agent closes when the run ends. Server-side conversation reuse is a possible follow-up.
  • Usage. The converter emits a final SSE chunk with top-level usage when the bridge run result carries token counts. The existing StreamUsageObserver records it unchanged. No usage data means a graceful omission, not an error.
  • Registration. The type name cursor drives the env convention: CURSOR_API_KEY, CURSOR_BASE_URL, CURSOR_MODELS.

Reviewer notes

  • cfg.BaseURL is ignored in managed mode. The bridge picks its own ephemeral port. The registration comment says so. The test seam (NewWithHTTPClient) uses attach mode.
  • Pre-existing leak, now reachable. CredentialsService.install unregisters providers without Close(). 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.
  • Live validation stopped at the plan gate. The full path works: gateway → provider → bridge spawn → Connect RPC → Cursor backend. The test key belongs to a free-tier account, and Cursor answered plan_required: Cloud Agent is not available for free users. A chat completion with a Pro key is the last open check.
  • Focus area: bridge_manager.go spawn 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.
  • Live smoke against the real Cursor backend: verified to the plan gate (see Reviewer notes).

Follow-up

  • Server-side conversation reuse across requests.
  • Post-hoc usage lookup through the dashboard RPC when stream results omit token counts.
  • CredentialsService.install subprocess leak (see Reviewer notes).
  • Grok slug confirmation with a Pro-tier key.

Links


This PR description was generated with AI assistance.

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.
@weselben weselben linked an issue Aug 20, 2026 that may be closed by this pull request
@coderabbitai

coderabbitai Bot commented Aug 20, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4cf981a9-15f9-47f8-8f26-2789f6b7a7b4

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@weselben weselben left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — PR #24 (cursor provider, 9 commits)

No 🔴 bugs. Three 🟡 risks worth addressing before merge:

  1. cursor.go:160 — managed-mode SetBaseURL corrupts cached state. After a successful Start, p.startDone==true. SetBaseURL unconditionally resets p.tr=nil, p.curURL=url, p.curToken="" but never touches p.startDone in the managed branch. Next transport() skips re-handshake (startDone), skips startErr, sees p.tr==nil, rebuilds Transport with 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 calls SetBaseURL on managed cursor today (latent), but it is a real state bug.
  2. cursor.go:359closeAgent errors are silently swallowed in the agentCloser closure (_ = p.closeAgent(context.Background(), tr, agentID)). Same pattern in StreamChatCompletion's stream-error path (~line 360). On an unresponsive bridge the agent leaks server-side and accumulates across requests with no log/counter. Suggest slog.Warn on error.
  3. chat_stream.go:114 — unbounded recursion in streamConverter.Read. Frames producing zero bytes (env.Done, non-assistant sdkMessage types, assistant messages with no text blocks) fall through to return c.Read(p). A bridge streaming endless no-op frames stack-overflows the reader goroutine. Replace with a for loop 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

Comment thread internal/providers/cursor/cursor.go
Comment thread internal/providers/cursor/cursor.go
Comment thread internal/providers/cursor/chat_stream.go Outdated
Comment thread internal/providers/cursor/cursor.go
Comment thread internal/providers/cursor/bridge_manager.go Outdated
Comment thread internal/providers/cursor/bridge_manager.go
Comment thread internal/providers/cursor/cursor.go
Comment thread internal/providers/cursor/bridge_manager.go
…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.

@weselben weselben left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@weselben weselben left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

smoke test

Comment thread internal/providers/cursor/cursor.go Outdated
if err != nil {
return nil, err
}
defer func() { _ = p.closeAgent(ctx, tr, agentID) }()

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

smoke-test

@weselben weselben left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_MODELS documented 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): cursorRunError builds message from r.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): terminalStatusOK accepts only FINISHED and RUN_LIFECYCLE_STATUS_FINISHED. Whitelist COMPLETED/SUCCEEDED and distinguish CANCELLED from ERROR.
  • L386 (nit): doc says defaultBaseURL is "loopback" but managed bridge uses ephemeral port. Misleading constant.
  • L404-411 (q): createAgent returns BadGateway on missing agentId without calling CloseAgent — server-side agent may already exist. Common enough to warrant cleanup RPC?

bridge_manager.go

  • L122 (nit): NewManagedBridgeManager returns success while b.cmd holds {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 on Close(). Swallows a real exec.Cmd invariant violation.
  • L213-249 (nit): absolute-failure window is 2*shutdownTimeout + 5 s Shutdown RPC budget = 15 s. Worth documenting in Close() doc.
  • (q): Does cursor-sdk-bridge accept SIGTERM cleanly, or trap-and-ignore? SIGTERM escalation is undocumented behaviour.

chat_stream.go

  • L131-135 (nit): c.msgID mutated across reads without synchronization; today driven by single goroutine but io.Reader contract does not preclude concurrent reads. Document // Read is not safe for concurrent use.
  • L147-155 (nit): first assistant chunk combines delta.role=assistant AND delta.content=<first text>. Strict OpenAI streams emit role in chunk 0 with content="" and content starting in chunk 1. Postel alternative works but worth documenting for strict-mode clients.

connect_transport.go

  • L264-285 (nit): parseEndStream silently returns nil on malformed end-frame JSON — a hard protocol violation. Log a slog.Warn with raw bytes (no secrets).
  • L75 (nit): NewTransport headerSetter closes over token. If token contains \r\n, http returns confusing error. Add strings.ContainsAny(token, "\r\n") guard.

cursor_wire.go

  • L131-141 (nit): runStreamEnvelope.Done *struct{} mirrors proto oneof but is never inspected. Cosmetic.
  • L98 (nit): localAgentOptions.CWD []string — bridge may expect a single workspace string in many existing deployments (repeated string per proto). Add a comment explaining the assumption.

cursor_test.go

  • L228-251 (nit): TestChatCompletion_RunError asserts *gw.Code == "model_overloaded" but not status code. Add gw.StatusCode == http.StatusBadGateway.
  • L165-172 (nit): manually composed JSON via string concatenation in resultFrame is fragile. Use json.Marshal of a struct.

connect_transport_test.go

  • (nit): TestUnary_StreamSendsAuthorizationOnEveryRequest is essentially duplicated by TestStream_FramesInOrder which already asserts Authorization.

chat_stream_test.go

  • L328 (nit): TestStreamChatCompletion_CloseReleasesAgent parks on <-releaseCh. If test process panics before close(releaseCh), handler hangs forever. Defer close(releaseCh) immediately after parking.

bridge_manager_test.go

  • L42 (nit): withFakeBridge does os.Chmod(p, 0o755) without checking the path is in testdata/. Worth a defensive guard.

testdata/fake_bridge.sh

  • L42 (nit): cat >&2 <<EOF ... EOF uses ${$:-0} syntax — works in bash but uncertain in pure POSIX sh. Hardcode pid=$$ above the heredoc.
  • L48-51 (nit): exec sleep 3600 after 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 scrubbedBridgeEnv inherits only PATH/HOME/TMPDIR/USER/LANG + CURSOR_API_KEY. Actual code passes a 6th var CURSOR_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/files returns 501 — provider implements Responses/StreamResponses/Embeddings returning 501, not Files. Calls to /v1/files go 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 via ListModels and ignores cfg.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). Add EndpointDiscovery or 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 lifecycle couples InitResult.Close plumbing 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)
  • ListModels with malformed JSON

Comment thread internal/providers/cursor/cursor.go Outdated
if err != nil {
return nil, err
}
defer func() { _ = p.closeAgent(ctx, tr, agentID) }()

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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()

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread internal/providers/cursor/cursor.go Outdated
// 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,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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)).

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in head — see latest commits on this PR for the fix.

return n, nil
}

func TestSpawnReadyParseAndTokenRead(t *testing.T) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in head — see latest commits on this PR for the fix.

Comment thread docs/providers/cursor.mdx

## Models

`ListModels` is served from the bridge's `SdkCursorService.ListModels`, so

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Resolved in head — see latest commits on this PR for the fix.

Comment thread .env.template Outdated
# 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

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 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.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cursor CLI client + Grok subscription inference via GoModel

1 participant