@velum-labs/cursorkit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (142) hide show
  1. package/DISCLAIMER.md +12 -0
  2. package/README.md +157 -0
  3. package/dist/src/agentTools/diff.d.ts +11 -0
  4. package/dist/src/agentTools/diff.js +88 -0
  5. package/dist/src/agentTools/policy.d.ts +3 -0
  6. package/dist/src/agentTools/policy.js +12 -0
  7. package/dist/src/agentTools/registry.d.ts +114 -0
  8. package/dist/src/agentTools/registry.js +663 -0
  9. package/dist/src/agentTools/results.d.ts +14 -0
  10. package/dist/src/agentTools/results.js +117 -0
  11. package/dist/src/agentTools/schemas.d.ts +3 -0
  12. package/dist/src/agentTools/schemas.js +89 -0
  13. package/dist/src/agentTools/surface.d.ts +11 -0
  14. package/dist/src/agentTools/surface.js +251 -0
  15. package/dist/src/certs.d.ts +8 -0
  16. package/dist/src/certs.js +34 -0
  17. package/dist/src/ck.d.ts +2 -0
  18. package/dist/src/ck.js +6 -0
  19. package/dist/src/ckLauncher.d.ts +150 -0
  20. package/dist/src/ckLauncher.js +1496 -0
  21. package/dist/src/cli.d.ts +2 -0
  22. package/dist/src/cli.js +265 -0
  23. package/dist/src/config.d.ts +52 -0
  24. package/dist/src/config.js +210 -0
  25. package/dist/src/connectEnvelope.d.ts +16 -0
  26. package/dist/src/connectEnvelope.js +70 -0
  27. package/dist/src/desktop.d.ts +19 -0
  28. package/dist/src/desktop.js +167 -0
  29. package/dist/src/desktopConnectProxy.d.ts +26 -0
  30. package/dist/src/desktopConnectProxy.js +175 -0
  31. package/dist/src/extensions/index.d.ts +2 -0
  32. package/dist/src/extensions/index.js +1 -0
  33. package/dist/src/extensions/registry.d.ts +8 -0
  34. package/dist/src/extensions/registry.js +52 -0
  35. package/dist/src/extensions/types.d.ts +42 -0
  36. package/dist/src/extensions/types.js +1 -0
  37. package/dist/src/fixtures/modelFusion.d.ts +103 -0
  38. package/dist/src/fixtures/modelFusion.js +404 -0
  39. package/dist/src/fixtures/replay.d.ts +9 -0
  40. package/dist/src/fixtures/replay.js +41 -0
  41. package/dist/src/fixtures/sanitizer.d.ts +9 -0
  42. package/dist/src/fixtures/sanitizer.js +43 -0
  43. package/dist/src/fixtures/schema.d.ts +38 -0
  44. package/dist/src/fixtures/schema.js +33 -0
  45. package/dist/src/gen/agent/v1/agent_pb.d.ts +21577 -0
  46. package/dist/src/gen/agent/v1/agent_pb.js +5325 -0
  47. package/dist/src/gen/aiserver/v1/aiserver_pb.d.ts +135242 -0
  48. package/dist/src/gen/aiserver/v1/aiserver_pb.js +34430 -0
  49. package/dist/src/gen/anyrun/v1/anyrun_pb.d.ts +1163 -0
  50. package/dist/src/gen/anyrun/v1/anyrun_pb.js +374 -0
  51. package/dist/src/gen/google/protobuf/google_pb.d.ts +142 -0
  52. package/dist/src/gen/google/protobuf/google_pb.js +54 -0
  53. package/dist/src/gen/internapi/v1/internapi_pb.d.ts +121 -0
  54. package/dist/src/gen/internapi/v1/internapi_pb.js +79 -0
  55. package/dist/src/logger.d.ts +8 -0
  56. package/dist/src/logger.js +37 -0
  57. package/dist/src/modelFusion/cursorHarness.d.ts +146 -0
  58. package/dist/src/modelFusion/cursorHarness.js +647 -0
  59. package/dist/src/modelFusion/index.d.ts +4 -0
  60. package/dist/src/modelFusion/index.js +2 -0
  61. package/dist/src/models/registry.d.ts +22 -0
  62. package/dist/src/models/registry.js +30 -0
  63. package/dist/src/proto.d.ts +13 -0
  64. package/dist/src/proto.js +61 -0
  65. package/dist/src/providers/openai.d.ts +64 -0
  66. package/dist/src/providers/openai.js +355 -0
  67. package/dist/src/redaction.d.ts +4 -0
  68. package/dist/src/redaction.js +65 -0
  69. package/dist/src/routeInventory.d.ts +16 -0
  70. package/dist/src/routeInventory.js +39 -0
  71. package/dist/src/routes.d.ts +37 -0
  72. package/dist/src/routes.js +227 -0
  73. package/dist/src/server.d.ts +50 -0
  74. package/dist/src/server.js +1353 -0
  75. package/dist/src/services/agent.d.ts +1 -0
  76. package/dist/src/services/agent.js +7 -0
  77. package/dist/src/services/agentRun.d.ts +60 -0
  78. package/dist/src/services/agentRun.js +391 -0
  79. package/dist/src/services/chat.d.ts +11 -0
  80. package/dist/src/services/chat.js +47 -0
  81. package/dist/src/services/models.d.ts +10 -0
  82. package/dist/src/services/models.js +216 -0
  83. package/dist/src/services/serverConfig.d.ts +2 -0
  84. package/dist/src/services/serverConfig.js +19 -0
  85. package/dist/src/testing/artifacts.d.ts +14 -0
  86. package/dist/src/testing/artifacts.js +92 -0
  87. package/dist/src/testing/cli.d.ts +4 -0
  88. package/dist/src/testing/cli.js +192 -0
  89. package/dist/src/testing/localBackend.d.ts +24 -0
  90. package/dist/src/testing/localBackend.js +310 -0
  91. package/dist/src/testing/processRunner.d.ts +7 -0
  92. package/dist/src/testing/processRunner.js +74 -0
  93. package/dist/src/testing/runner.d.ts +9 -0
  94. package/dist/src/testing/runner.js +85 -0
  95. package/dist/src/testing/scenarios.d.ts +3 -0
  96. package/dist/src/testing/scenarios.js +2535 -0
  97. package/dist/src/testing/types.d.ts +66 -0
  98. package/dist/src/testing/types.js +1 -0
  99. package/dist/src/tools/baselineInventory.d.ts +12 -0
  100. package/dist/src/tools/baselineInventory.js +680 -0
  101. package/dist/src/tools/checkModelFusionProtocol.d.ts +1 -0
  102. package/dist/src/tools/checkModelFusionProtocol.js +274 -0
  103. package/dist/src/tools/checkReleasePublishConfig.d.ts +1 -0
  104. package/dist/src/tools/checkReleasePublishConfig.js +99 -0
  105. package/dist/src/tools/generateProtoInventory.d.ts +1 -0
  106. package/dist/src/tools/generateProtoInventory.js +89 -0
  107. package/dist/src/tools/normalizeGeneratedCode.d.ts +1 -0
  108. package/dist/src/tools/normalizeGeneratedCode.js +18 -0
  109. package/dist/src/tools/releaseCheck.d.ts +26 -0
  110. package/dist/src/tools/releaseCheck.js +367 -0
  111. package/dist/src/trace.d.ts +39 -0
  112. package/dist/src/trace.js +106 -0
  113. package/dist/src/translation.d.ts +6 -0
  114. package/dist/src/translation.js +22 -0
  115. package/dist/src/upstream.d.ts +20 -0
  116. package/dist/src/upstream.js +270 -0
  117. package/docs/configuration.md +55 -0
  118. package/docs/cursor-app.md +263 -0
  119. package/docs/implementation-inventory.json +609 -0
  120. package/docs/learnings.md +363 -0
  121. package/docs/model-fusion-protocol-origin.json +126 -0
  122. package/docs/model-fusion-protocol.md +110 -0
  123. package/docs/plugin-authoring.md +24 -0
  124. package/docs/proto-inventory.md +1477 -0
  125. package/docs/protocol-surface-audit.md +92 -0
  126. package/docs/protocol.md +52 -0
  127. package/docs/refreshing-protos.md +78 -0
  128. package/docs/release-gates.md +110 -0
  129. package/docs/release-summary.json +86 -0
  130. package/docs/route-contract-manifest.json +288 -0
  131. package/docs/route-policy.json +133 -0
  132. package/docs/service-manifest.json +9490 -0
  133. package/docs/test-manifest.json +155 -0
  134. package/docs/testing-harness.md +204 -0
  135. package/docs/troubleshooting.md +36 -0
  136. package/docs/type-manifest-summary.json +28927 -0
  137. package/package.json +93 -0
  138. package/proto/agent/v1/agent.proto +5371 -0
  139. package/proto/aiserver/v1/aiserver.proto +32944 -0
  140. package/proto/anyrun/v1/anyrun.proto +294 -0
  141. package/proto/google/protobuf/google.proto +37 -0
  142. package/proto/internapi/v1/internapi.proto +32 -0
@@ -0,0 +1,270 @@
1
+ import http from "node:http";
2
+ import https from "node:https";
3
+ import { pipeline } from "node:stream";
4
+ import { promisify } from "node:util";
5
+ import { brotliDecompress, gunzip, inflate } from "node:zlib";
6
+ import { DESKTOP_HOSTNAMES } from "./desktop.js";
7
+ import { redactHeaders } from "./redaction.js";
8
+ const HOP_BY_HOP_HEADERS = new Set([
9
+ "connection",
10
+ "keep-alive",
11
+ "proxy-authenticate",
12
+ "proxy-authorization",
13
+ "te",
14
+ "trailer",
15
+ "transfer-encoding",
16
+ "upgrade",
17
+ ]);
18
+ const gunzipAsync = promisify(gunzip);
19
+ const inflateAsync = promisify(inflate);
20
+ const brotliDecompressAsync = promisify(brotliDecompress);
21
+ const DEFAULT_UPSTREAM_REQUEST_TIMEOUT_MS = 120_000;
22
+ export class RequestBodyTooLargeError extends Error {
23
+ maxBytes;
24
+ constructor(maxBytes) {
25
+ super(`Request body exceeds limit of ${maxBytes} bytes`);
26
+ this.maxBytes = maxBytes;
27
+ this.name = "RequestBodyTooLargeError";
28
+ }
29
+ }
30
+ export class UpstreamRequestTimeoutError extends Error {
31
+ timeoutMs;
32
+ constructor(timeoutMs) {
33
+ super(`Upstream request timed out after ${timeoutMs}ms`);
34
+ this.timeoutMs = timeoutMs;
35
+ this.name = "UpstreamRequestTimeoutError";
36
+ }
37
+ }
38
+ export async function readRequestBody(request, maxBytes) {
39
+ const chunks = [];
40
+ let total = 0;
41
+ for await (const chunk of request) {
42
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
43
+ total += buffer.length;
44
+ if (total > maxBytes) {
45
+ throw new RequestBodyTooLargeError(maxBytes);
46
+ }
47
+ chunks.push(buffer);
48
+ }
49
+ return Buffer.concat(chunks);
50
+ }
51
+ export function proxyRequest(request, response, config, logger) {
52
+ const upstreamUrl = upstreamRequestUrl(request, config);
53
+ const options = upstreamRequestOptions(request, config, upstreamUrl);
54
+ const client = upstreamUrl.protocol === "https:" ? https : http;
55
+ const timeoutMs = config.upstreamRequestTimeoutMs ?? DEFAULT_UPSTREAM_REQUEST_TIMEOUT_MS;
56
+ let settled = false;
57
+ const upstreamRequest = client.request({
58
+ ...options,
59
+ method: request.method,
60
+ }, (upstreamResponse) => {
61
+ response.writeHead(upstreamResponse.statusCode ?? 502, responseHeaders(upstreamResponse.headers));
62
+ pipeline(upstreamResponse, response, (error) => {
63
+ if (error != null && !response.destroyed) {
64
+ logger.warn("upstream response pipeline failed", {
65
+ error: error.message,
66
+ });
67
+ }
68
+ });
69
+ });
70
+ upstreamRequest.on("error", (error) => {
71
+ if (settled) {
72
+ return;
73
+ }
74
+ settled = true;
75
+ const timedOut = error instanceof UpstreamRequestTimeoutError;
76
+ logger.error("upstream proxy request failed", {
77
+ url: upstreamUrl.toString(),
78
+ error: error.message,
79
+ code: timedOut ? "upstream_timeout" : "upstream_request_failed",
80
+ requestHeaders: redactHeaders(request.headers),
81
+ });
82
+ if (!response.headersSent && !response.destroyed) {
83
+ response.writeHead(timedOut ? 504 : 502, {
84
+ "content-type": "application/json",
85
+ });
86
+ }
87
+ if (!response.destroyed) {
88
+ response.end(JSON.stringify({
89
+ error: timedOut
90
+ ? "upstream request timed out"
91
+ : "upstream request failed",
92
+ }));
93
+ }
94
+ });
95
+ upstreamRequest.setTimeout(timeoutMs, () => {
96
+ upstreamRequest.destroy(new UpstreamRequestTimeoutError(timeoutMs));
97
+ });
98
+ request.on("aborted", () => {
99
+ upstreamRequest.destroy(new Error("client aborted request"));
100
+ });
101
+ request.on("error", (error) => {
102
+ upstreamRequest.destroy(error);
103
+ });
104
+ response.on("close", () => {
105
+ if (!response.writableEnded) {
106
+ upstreamRequest.destroy(new Error("downstream response closed"));
107
+ }
108
+ });
109
+ request.pipe(upstreamRequest);
110
+ }
111
+ export async function proxyBufferedRequest(request, response, body, config, logger) {
112
+ const upstreamResponse = await requestUpstreamBuffer(request, body, config);
113
+ response.statusCode = upstreamResponse.statusCode;
114
+ for (const [key, value] of Object.entries(upstreamResponse.headers)) {
115
+ if (value !== undefined && !HOP_BY_HOP_HEADERS.has(key.toLowerCase())) {
116
+ response.setHeader(key, value);
117
+ }
118
+ }
119
+ response.end(upstreamResponse.body);
120
+ }
121
+ export async function fetchUpstreamBuffer(request, body, config) {
122
+ if (config.upstreamBaseUrl === undefined) {
123
+ return undefined;
124
+ }
125
+ const response = await requestUpstreamBuffer(request, body, config);
126
+ if (response.statusCode < 200 || response.statusCode >= 300) {
127
+ throw new Error(`Upstream returned ${response.statusCode}`);
128
+ }
129
+ return decodeResponseBody(response.body, response.headers);
130
+ }
131
+ export function upstreamRequestUrl(request, config) {
132
+ if (config.upstreamBaseUrl === undefined) {
133
+ throw new Error("CURSOR_UPSTREAM_BASE_URL is required for pass-through traffic");
134
+ }
135
+ const requestHost = requestHostWithoutPort(request);
136
+ const upstreamBaseUrl = config.desktopMode &&
137
+ requestHost !== undefined &&
138
+ DESKTOP_HOSTNAMES.includes(requestHost)
139
+ ? new URL(`https://${requestHost}`)
140
+ : new URL(config.upstreamBaseUrl);
141
+ return new URL(request.url ?? "/", upstreamBaseUrl);
142
+ }
143
+ function requestHostWithoutPort(request) {
144
+ const host = request.headers.host;
145
+ if (host === undefined) {
146
+ return undefined;
147
+ }
148
+ const first = Array.isArray(host) ? host[0] : host;
149
+ if (first === undefined || first.length === 0) {
150
+ return undefined;
151
+ }
152
+ return first.split(":")[0];
153
+ }
154
+ export function upstreamRequestOptions(request, config, upstreamUrl = upstreamRequestUrl(request, config)) {
155
+ const isHttps = upstreamUrl.protocol === "https:";
156
+ return {
157
+ protocol: upstreamUrl.protocol,
158
+ hostname: config.upstreamConnectHost ?? upstreamUrl.hostname,
159
+ port: config.upstreamConnectPort ??
160
+ (upstreamUrl.port.length > 0 ? Number(upstreamUrl.port) : undefined),
161
+ path: `${upstreamUrl.pathname}${upstreamUrl.search}`,
162
+ headers: upstreamHeaders(request.headers, upstreamUrl),
163
+ servername: isHttps ? upstreamUrl.hostname : undefined,
164
+ };
165
+ }
166
+ async function requestUpstreamBuffer(request, body, config) {
167
+ const upstreamUrl = upstreamRequestUrl(request, config);
168
+ const options = upstreamRequestOptions(request, config, upstreamUrl);
169
+ const client = upstreamUrl.protocol === "https:" ? https : http;
170
+ return new Promise((resolve, reject) => {
171
+ const timeoutMs = config.upstreamRequestTimeoutMs ?? DEFAULT_UPSTREAM_REQUEST_TIMEOUT_MS;
172
+ let settled = false;
173
+ const canObserveRequest = typeof request.on === "function" && typeof request.off === "function";
174
+ const cleanup = () => {
175
+ if (canObserveRequest) {
176
+ request.off("aborted", onRequestAborted);
177
+ request.off("error", onRequestError);
178
+ }
179
+ };
180
+ const finish = (callback) => {
181
+ if (settled) {
182
+ return;
183
+ }
184
+ settled = true;
185
+ cleanup();
186
+ callback();
187
+ };
188
+ let upstreamRequest;
189
+ const onRequestAborted = () => {
190
+ upstreamRequest.destroy(new Error("client aborted request"));
191
+ };
192
+ const onRequestError = (error) => {
193
+ upstreamRequest.destroy(error);
194
+ };
195
+ upstreamRequest = client.request({
196
+ ...options,
197
+ method: request.method,
198
+ }, (upstreamResponse) => {
199
+ const chunks = [];
200
+ upstreamResponse.on("data", (chunk) => chunks.push(chunk));
201
+ upstreamResponse.on("end", () => {
202
+ finish(() => {
203
+ resolve({
204
+ statusCode: upstreamResponse.statusCode ?? 502,
205
+ headers: responseHeaders(upstreamResponse.headers),
206
+ body: Buffer.concat(chunks),
207
+ });
208
+ });
209
+ });
210
+ });
211
+ upstreamRequest.on("error", (error) => {
212
+ finish(() => reject(error));
213
+ });
214
+ upstreamRequest.setTimeout(timeoutMs, () => {
215
+ upstreamRequest.destroy(new UpstreamRequestTimeoutError(timeoutMs));
216
+ });
217
+ if (canObserveRequest) {
218
+ request.on("aborted", onRequestAborted);
219
+ request.on("error", onRequestError);
220
+ }
221
+ upstreamRequest.end(body);
222
+ });
223
+ }
224
+ function upstreamHeaders(headers, upstreamUrl) {
225
+ const next = {};
226
+ for (const [key, value] of Object.entries(headers)) {
227
+ if (value === undefined ||
228
+ key.startsWith(":") ||
229
+ HOP_BY_HOP_HEADERS.has(key.toLowerCase()) ||
230
+ key.toLowerCase() === "host") {
231
+ continue;
232
+ }
233
+ next[key] = value;
234
+ }
235
+ next.host = upstreamUrl.host;
236
+ return next;
237
+ }
238
+ function responseHeaders(headers) {
239
+ const next = {};
240
+ for (const [key, value] of Object.entries(headers)) {
241
+ if (value === undefined ||
242
+ key.startsWith(":") ||
243
+ HOP_BY_HOP_HEADERS.has(key.toLowerCase())) {
244
+ continue;
245
+ }
246
+ next[key] = value;
247
+ }
248
+ return next;
249
+ }
250
+ async function decodeResponseBody(body, headers) {
251
+ const encodingHeader = headers["content-encoding"];
252
+ const encoding = Array.isArray(encodingHeader)
253
+ ? encodingHeader[0]
254
+ : encodingHeader;
255
+ switch (encoding?.toLowerCase()) {
256
+ case undefined:
257
+ case "":
258
+ case "identity":
259
+ return body;
260
+ case "gzip":
261
+ case "x-gzip":
262
+ return gunzipAsync(body);
263
+ case "deflate":
264
+ return inflateAsync(body);
265
+ case "br":
266
+ return brotliDecompressAsync(body);
267
+ default:
268
+ return body;
269
+ }
270
+ }
@@ -0,0 +1,55 @@
1
+ # Configuration
2
+
3
+ Configuration is loaded from environment variables. CLI flags are intentionally minimal for now so startup behavior is easy to audit.
4
+
5
+ `docs/implementation-inventory.json` contains the generated config inventory
6
+ from `src/config.ts`. Run `pnpm baseline:check` before changing config docs or
7
+ route policy.
8
+
9
+ ## Core
10
+
11
+ - `BRIDGE_HOST`: bind host. Defaults to `127.0.0.1`.
12
+ - `BRIDGE_PORT`: bind port. Defaults to `9443`.
13
+ - `BRIDGE_UNSAFE_ALLOW_NON_LOCALHOST`: required before binding to anything other than `127.0.0.1`, `::1`, or `localhost`.
14
+ - `BRIDGE_USE_TLS`: set to `true` to run HTTPS.
15
+ - `BRIDGE_CERT_PATH` and `BRIDGE_KEY_PATH`: custom TLS material. Both must be set together.
16
+ - `BRIDGE_TLS_HOSTNAMES`: comma-separated hostnames/IPs for generated TLS certificate SANs. Desktop mode defaults to `api2.cursor.sh,api3.cursor.sh,agent.api5.cursor.sh,agentn.api5.cursor.sh,agentn.global.api5.cursor.sh,localhost,127.0.0.1,::1`.
17
+ - `BRIDGE_PUBLIC_ORIGIN`: public origin advertised in rewritten server config responses. Desktop mode defaults to `https://api2.cursor.sh`.
18
+ - `BRIDGE_AGENT_PUBLIC_ORIGIN`: optional agent-facing origin override for desktop agent compatibility experiments.
19
+ - `CURSOR_UPSTREAM_BASE_URL`: upstream Cursor backend base URL for pass-through traffic.
20
+ - `CURSOR_UPSTREAM_CONNECT_HOST`: optional physical upstream host/IP to connect to while preserving `CURSOR_UPSTREAM_BASE_URL` for Host and TLS SNI. Use this in desktop proxy mode after redirecting `api2.cursor.sh` to localhost.
21
+ - `CURSOR_UPSTREAM_CONNECT_PORT`: optional physical upstream port paired with `CURSOR_UPSTREAM_CONNECT_HOST`.
22
+
23
+ ## Desktop Proxy
24
+
25
+ - `BRIDGE_DESKTOP_MODE`: enables desktop proxy defaults when set to `true`.
26
+ - `BRIDGE_ROUTE_INVENTORY`: logs redacted route metadata for observing Cursor desktop traffic. Desktop mode enables this automatically.
27
+ - `BRIDGE_DESKTOP_AGENT_HTTP_PORT`: optional secondary HTTP listener port for desktop agent compatibility experiments.
28
+
29
+ The `desktop-proxy` CLI command sets desktop defaults without changing the
30
+ normal `serve` command. See `docs/cursor-app.md` for certificate, cutover, and
31
+ rollback instructions.
32
+
33
+ ## Models
34
+
35
+ - `MODEL_BASE_URL`: OpenAI-compatible local model endpoint. Defaults to `http://localhost:8080/v1`.
36
+ - `MODEL_API_KEY`: optional local model API key.
37
+ - `MODEL_NAME`: local model ID exposed to Cursor. Defaults to `local-model`.
38
+ - `MODEL_PROVIDER_MODEL`: provider-facing model ID sent to the local backend when different from `MODEL_NAME`.
39
+ - `MODEL_CONTEXT_TOKEN_LIMIT`: advertised local context window. Defaults to `128000`.
40
+ - `MODEL_REQUEST_TIMEOUT_MS`: optional request deadline for OpenAI-compatible local backend calls.
41
+ - `BRIDGE_HARDCODED_RESPONSE`: optional fixed local model response for deterministic experiments.
42
+ - `BRIDGE_MODELS_JSON`: JSON array for multiple local models. Each item supports `id`, `displayName`, `providerModel`, `baseUrl`, `apiKey`, `contextTokenLimit`, `requestTimeoutMs`, and `hardcodedResponse`.
43
+
44
+ ## Safety And Diagnostics
45
+
46
+ - `BRIDGE_FAIL_OPEN`: defaults to `true`. Unknown routes pass through; typed intercept failures return an explicit error after the body has been consumed.
47
+ - `BRIDGE_CAPTURE_ENABLED`: defaults to `false`.
48
+ - `BRIDGE_CAPTURE_DIR`: defaults to `fixtures/captures`.
49
+ - `BRIDGE_LOG_LEVEL`: `debug`, `info`, `warn`, or `error`.
50
+ - `BRIDGE_LOG_MODEL_PAYLOADS`: defaults to `summary`; set to `full` only for explicit local debugging.
51
+ - `BRIDGE_AGENT_TOOL_POLICY`: defaults to `safe`; set to `all` only to advertise the approved extended local tool set.
52
+ - `BRIDGE_PLUGIN_PATH`: future local plugin module path.
53
+ - `BRIDGE_MAX_INTERCEPT_BODY_BYTES`: request body limit for typed routes. Defaults to 50 MiB.
54
+
55
+ Run `cursorkit doctor` after changing config.
@@ -0,0 +1,263 @@
1
+ # Cursor Desktop App Proxy
2
+
3
+ The Cursor desktop app does not expose the same `--endpoint` flag that
4
+ `cursor-agent` supports. Desktop proxy mode makes `cursorkit` look like the
5
+ Cursor backend host locally, then passes unknown routes through to the real
6
+ backend while logging route metadata and serving registered local models on
7
+ typed, allowlisted routes.
8
+
9
+ This is macOS-first and intentionally operator-controlled. The CLI does not
10
+ edit `/etc/hosts`, install certificates, or change `pf` rules for you.
11
+
12
+ Before changing desktop proxy behavior, read `docs/learnings.md`; it records the
13
+ route, framing, DNS, TLS, and upstream-loop lessons learned from the CLI and
14
+ desktop proxy work.
15
+
16
+ For a unified runner that includes desktop route-inventory smoke tests, local
17
+ backend probes, and cursor-agent e2e, see `docs/testing-harness.md`.
18
+
19
+ ## Recommended: Launch With `ck`
20
+
21
+ Use `ck` for the non-privileged desktop test path:
22
+
23
+ ```bash
24
+ pnpm ck
25
+ ```
26
+
27
+ If the isolated window asks you to log in and the browser confirmation does not
28
+ complete inside that window, use your already-authenticated Cursor profile for
29
+ the test:
30
+
31
+ ```bash
32
+ pnpm ck --use-default-profile
33
+ ```
34
+
35
+ That mode is less isolated because it reuses your normal Cursor auth state, but
36
+ it is still non-privileged and keeps `ck` from editing system routing or trust
37
+ settings.
38
+
39
+ `ck` does the safe parts automatically:
40
+
41
+ - generates `.cursor-rpc/certs/api2.cursor.sh.crt` and `.key` if needed
42
+ - starts `cursorkit desktop-proxy` with desktop defaults and debug logging
43
+ - starts a local HTTP CONNECT proxy and launches isolated Cursor with
44
+ `--proxy-server`, so renderer and extension/plugin helper traffic can be
45
+ routed without `/etc/hosts` or `pf`
46
+ - opens a separate Cursor instance with isolated `--user-data-dir` and
47
+ `--extensions-dir` by default
48
+ - routes `api2.cursor.sh`, `api3.cursor.sh`, `agent.api5.cursor.sh`, and
49
+ `agentn.global.api5.cursor.sh` CONNECT tunnels into the bridge while allowing
50
+ non-Cursor hosts to pass through normally
51
+ - watches for `desktop route inventory` logs
52
+ - for isolated profiles, seeds the configured local models into Cursor's model
53
+ settings so they are enabled additively rather than replacing built-in models
54
+
55
+ It does not run `sudo`, install certificate trust, edit `/etc/hosts`, modify
56
+ `pf`, bind privileged ports, or kill your normal Cursor instance.
57
+
58
+ Useful commands:
59
+
60
+ ```bash
61
+ pnpm ck test --use-default-profile
62
+ pnpm ck --print
63
+ pnpm ck --use-default-profile --print
64
+ pnpm ck --debug-port 9333 --instance-id ui-test --seed-auth-from-default
65
+ pnpm ck doctor
66
+ pnpm ck cert
67
+ pnpm ck route
68
+ pnpm ck route status
69
+ pnpm ck route rollback
70
+ pnpm ck stop
71
+ ```
72
+
73
+ If no route inventory arrives, inspect the `CONNECT proxy log` printed by
74
+ `pnpm ck --print` or written under `.cursor-rpc/ck/<instance>/`. The isolated
75
+ path should now show `desktop connect proxy` events before any manual routing is
76
+ needed. `ck route` remains a non-mutating fallback for operator-reviewed system
77
+ routing.
78
+
79
+ For app-level automation evidence, run:
80
+
81
+ ```bash
82
+ pnpm test:harness -- \
83
+ --suite desktop-ui-experimental \
84
+ --include-experimental \
85
+ --base-url http://127.0.0.1:8080/v1 \
86
+ --model local-qwen \
87
+ --provider-model mlx-community/Qwen3.5-4B-8bit \
88
+ --display-name local-qwen \
89
+ --api-key local
90
+ ```
91
+
92
+ This launches an isolated Cursor instance with `--remote-debugging-port`, seeds
93
+ `cursorAuth/*` auth rows from the logged-in default profile, waits for Cursor to
94
+ initialize its settings state, enables the configured local model in the isolated
95
+ profile, reloads the workbench, opens the current repo, attaches to the Electron
96
+ renderer through CDP, dismisses safe onboarding prompts, opens a new Agent
97
+ composer, clicks the active model picker trigger, selects the local model in the
98
+ active composer, submits a test prompt, and asserts both:
99
+
100
+ - `local-qwen` appears in the real model picker.
101
+ - Existing Cursor models such as `Auto`, `Composer`, `GPT`, or `Sonnet` still
102
+ appear, proving the local model was added rather than replacing the catalog.
103
+
104
+ Use `--model` for the Cursor-facing id and `--provider-model` for the real
105
+ OpenAI-compatible backend model id when they differ.
106
+
107
+ Important: isolated `ck` now uses a first-class CONNECT proxy instead of relying
108
+ only on Chromium `--host-resolver-rules`. In validation, this routes desktop
109
+ startup, auth, telemetry, model-list, default-model, dashboard, MCP, and
110
+ pass-through traffic through the bridge without privileged system changes. The
111
+ remaining `desktop-ui-experimental` gap is reliable CDP submission into Cursor's
112
+ Monaco-backed Agent composer; the harness deliberately fails rather than
113
+ claiming a local-model response when the prompt was not actually submitted.
114
+
115
+ ## Automated Desktop Smoke Test
116
+
117
+ For the local MLX server:
118
+
119
+ ```bash
120
+ BRIDGE_MODELS_JSON='[{"id":"local-qwen","displayName":"local-qwen","providerModel":"mlx-community/Qwen3.5-4B-8bit","baseUrl":"http://127.0.0.1:8080/v1","apiKey":"local","contextTokenLimit":128000}]' \
121
+ pnpm ck test --use-default-profile --timeout-ms 30000
122
+ ```
123
+
124
+ `ck test` starts the bridge, launches Cursor, watches route inventory for the
125
+ timeout, prints a report, writes `.cursor-rpc/ck/bridge.log`, updates
126
+ `.cursor-rpc/ck/state.json`, and stops only the bridge process it started.
127
+
128
+ Interpret the report this way:
129
+
130
+ - `route inventory: no` means Cursor desktop traffic did not reach the bridge.
131
+ The likely next step is manual routing below.
132
+ - `route inventory: yes` but `model routes seen: none` means desktop reached the
133
+ bridge, but not through the known CLI-derived model-list RPCs.
134
+ - `model routes seen` includes `AvailableModels` or `GetUsableModels` but
135
+ `local-qwen` is missing in the UI means the bridge is on the path and the next
136
+ task is app-specific route/metadata discovery, not DNS or login debugging.
137
+ - `failed routes` greater than zero means inspect `.cursor-rpc/ck/bridge.log`
138
+ for HTTP errors.
139
+ - `pass-through routes` greater than zero is expected for non-intercepted
140
+ backend calls and helps discover which routes Cursor is using.
141
+
142
+ ## Generate And Trust A Local Certificate
143
+
144
+ ```bash
145
+ pnpm exec tsx src/cli.ts desktop-cert
146
+ ```
147
+
148
+ The command writes a local certificate and key under `.cursor-rpc/certs/` with
149
+ SANs for `api2.cursor.sh`, `api3.cursor.sh`, `localhost`, `127.0.0.1`, and
150
+ `::1`. It also prints the exact `security add-trusted-cert` command to run
151
+ manually.
152
+
153
+ After trusting the certificate, start the proxy with the generated paths:
154
+
155
+ ```bash
156
+ BRIDGE_CERT_PATH=.cursor-rpc/certs/api2.cursor.sh.crt \
157
+ BRIDGE_KEY_PATH=.cursor-rpc/certs/api2.cursor.sh.key \
158
+ BRIDGE_MODELS_JSON='[{"id":"local-qwen","displayName":"local-qwen","providerModel":"mlx-community/Qwen3.5-4B-8bit","baseUrl":"http://127.0.0.1:8080/v1","apiKey":"local","contextTokenLimit":128000}]' \
159
+ pnpm exec tsx src/cli.ts desktop-proxy
160
+ ```
161
+
162
+ `desktop-proxy` defaults to:
163
+
164
+ - `BRIDGE_DESKTOP_MODE=true`
165
+ - `BRIDGE_USE_TLS=true`
166
+ - `CURSOR_UPSTREAM_BASE_URL=https://api2.cursor.sh`
167
+ - `BRIDGE_PUBLIC_ORIGIN=https://api2.cursor.sh`
168
+ - `BRIDGE_ROUTE_INVENTORY=true`
169
+
170
+ ## Check The Setup
171
+
172
+ ```bash
173
+ BRIDGE_CERT_PATH=.cursor-rpc/certs/api2.cursor.sh.crt \
174
+ BRIDGE_KEY_PATH=.cursor-rpc/certs/api2.cursor.sh.key \
175
+ pnpm exec tsx src/cli.ts desktop-doctor
176
+ ```
177
+
178
+ `desktop-doctor` prints TLS hostname coverage, current DNS resolution for
179
+ `api2.cursor.sh`, upstream config, route-inventory status, upstream
180
+ reachability, and local model backend reachability.
181
+
182
+ Important: after you redirect `api2.cursor.sh` to localhost, the bridge must
183
+ still be able to reach the real Cursor backend. Set
184
+ `CURSOR_UPSTREAM_CONNECT_HOST` to a real upstream address captured before
185
+ cutover, or use your own split-DNS setup. The bridge preserves Host and TLS SNI
186
+ as `api2.cursor.sh` while connecting to that physical address.
187
+
188
+ ## Route Cursor To The Proxy
189
+
190
+ Pick one local cutover method.
191
+
192
+ The preferred starting point is:
193
+
194
+ ```bash
195
+ pnpm ck route status
196
+ pnpm ck route
197
+ ```
198
+
199
+ Run `pnpm ck route` before changing `/etc/hosts` so it can capture the current
200
+ real `api2.cursor.sh` address for `CURSOR_UPSTREAM_CONNECT_HOST`. After cutover,
201
+ use:
202
+
203
+ ```bash
204
+ pnpm ck route rollback
205
+ ```
206
+
207
+ to print the rollback commands.
208
+
209
+ ### Option A: Bind Directly To Port 443
210
+
211
+ Run the proxy on `127.0.0.1:443` and point `api2.cursor.sh` at localhost:
212
+
213
+ ```bash
214
+ sudo sh -c 'printf "\n127.0.0.1 api2.cursor.sh\n" >> /etc/hosts'
215
+
216
+ BRIDGE_PORT=443 \
217
+ CURSOR_UPSTREAM_CONNECT_HOST=<real-api2-address> \
218
+ BRIDGE_CERT_PATH=.cursor-rpc/certs/api2.cursor.sh.crt \
219
+ BRIDGE_KEY_PATH=.cursor-rpc/certs/api2.cursor.sh.key \
220
+ pnpm exec tsx src/cli.ts desktop-proxy
221
+ ```
222
+
223
+ Binding port `443` may require privilege. Prefer using the minimum shell scope
224
+ needed for your machine.
225
+
226
+ ### Option B: Use A Temporary pf Redirect
227
+
228
+ Keep the bridge on its unprivileged default port and redirect local port `443`
229
+ traffic to it with your own temporary `pf` rule.
230
+
231
+ ```bash
232
+ BRIDGE_PORT=9443 \
233
+ CURSOR_UPSTREAM_CONNECT_HOST=<real-api2-address> \
234
+ BRIDGE_CERT_PATH=.cursor-rpc/certs/api2.cursor.sh.crt \
235
+ BRIDGE_KEY_PATH=.cursor-rpc/certs/api2.cursor.sh.key \
236
+ pnpm exec tsx src/cli.ts desktop-proxy
237
+ ```
238
+
239
+ Then install a temporary local redirect from `127.0.0.1:443` to
240
+ `127.0.0.1:9443` using your preferred `pf` workflow.
241
+
242
+ ## Verify Desktop App Traffic
243
+
244
+ 1. Start the proxy with `BRIDGE_LOG_LEVEL=debug`.
245
+ 2. Quit and reopen Cursor.
246
+ 3. Confirm logs include `desktop route inventory` entries.
247
+ 4. Open the model picker and look for the local display name, for example
248
+ `local-qwen`.
249
+ 5. Send a small prompt through the local model.
250
+
251
+ The first pass is observe-first. If Cursor desktop uses model or chat RPCs not
252
+ already allowlisted in `src/routes.ts`, add typed interceptors only after the
253
+ route appears in the inventory and is decoded against the generated proto.
254
+
255
+ ## Rollback
256
+
257
+ Before experimenting, know your rollback path:
258
+
259
+ 1. Quit Cursor.
260
+ 2. Stop `cursorkit`.
261
+ 3. Remove the `api2.cursor.sh` line from `/etc/hosts`.
262
+ 4. Disable any temporary `pf` redirect.
263
+ 5. Reopen Cursor and confirm it reaches the real backend normally.