@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.
- package/DISCLAIMER.md +12 -0
- package/README.md +157 -0
- package/dist/src/agentTools/diff.d.ts +11 -0
- package/dist/src/agentTools/diff.js +88 -0
- package/dist/src/agentTools/policy.d.ts +3 -0
- package/dist/src/agentTools/policy.js +12 -0
- package/dist/src/agentTools/registry.d.ts +114 -0
- package/dist/src/agentTools/registry.js +663 -0
- package/dist/src/agentTools/results.d.ts +14 -0
- package/dist/src/agentTools/results.js +117 -0
- package/dist/src/agentTools/schemas.d.ts +3 -0
- package/dist/src/agentTools/schemas.js +89 -0
- package/dist/src/agentTools/surface.d.ts +11 -0
- package/dist/src/agentTools/surface.js +251 -0
- package/dist/src/certs.d.ts +8 -0
- package/dist/src/certs.js +34 -0
- package/dist/src/ck.d.ts +2 -0
- package/dist/src/ck.js +6 -0
- package/dist/src/ckLauncher.d.ts +150 -0
- package/dist/src/ckLauncher.js +1496 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +265 -0
- package/dist/src/config.d.ts +52 -0
- package/dist/src/config.js +210 -0
- package/dist/src/connectEnvelope.d.ts +16 -0
- package/dist/src/connectEnvelope.js +70 -0
- package/dist/src/desktop.d.ts +19 -0
- package/dist/src/desktop.js +167 -0
- package/dist/src/desktopConnectProxy.d.ts +26 -0
- package/dist/src/desktopConnectProxy.js +175 -0
- package/dist/src/extensions/index.d.ts +2 -0
- package/dist/src/extensions/index.js +1 -0
- package/dist/src/extensions/registry.d.ts +8 -0
- package/dist/src/extensions/registry.js +52 -0
- package/dist/src/extensions/types.d.ts +42 -0
- package/dist/src/extensions/types.js +1 -0
- package/dist/src/fixtures/modelFusion.d.ts +103 -0
- package/dist/src/fixtures/modelFusion.js +404 -0
- package/dist/src/fixtures/replay.d.ts +9 -0
- package/dist/src/fixtures/replay.js +41 -0
- package/dist/src/fixtures/sanitizer.d.ts +9 -0
- package/dist/src/fixtures/sanitizer.js +43 -0
- package/dist/src/fixtures/schema.d.ts +38 -0
- package/dist/src/fixtures/schema.js +33 -0
- package/dist/src/gen/agent/v1/agent_pb.d.ts +21577 -0
- package/dist/src/gen/agent/v1/agent_pb.js +5325 -0
- package/dist/src/gen/aiserver/v1/aiserver_pb.d.ts +135242 -0
- package/dist/src/gen/aiserver/v1/aiserver_pb.js +34430 -0
- package/dist/src/gen/anyrun/v1/anyrun_pb.d.ts +1163 -0
- package/dist/src/gen/anyrun/v1/anyrun_pb.js +374 -0
- package/dist/src/gen/google/protobuf/google_pb.d.ts +142 -0
- package/dist/src/gen/google/protobuf/google_pb.js +54 -0
- package/dist/src/gen/internapi/v1/internapi_pb.d.ts +121 -0
- package/dist/src/gen/internapi/v1/internapi_pb.js +79 -0
- package/dist/src/logger.d.ts +8 -0
- package/dist/src/logger.js +37 -0
- package/dist/src/modelFusion/cursorHarness.d.ts +146 -0
- package/dist/src/modelFusion/cursorHarness.js +647 -0
- package/dist/src/modelFusion/index.d.ts +4 -0
- package/dist/src/modelFusion/index.js +2 -0
- package/dist/src/models/registry.d.ts +22 -0
- package/dist/src/models/registry.js +30 -0
- package/dist/src/proto.d.ts +13 -0
- package/dist/src/proto.js +61 -0
- package/dist/src/providers/openai.d.ts +64 -0
- package/dist/src/providers/openai.js +355 -0
- package/dist/src/redaction.d.ts +4 -0
- package/dist/src/redaction.js +65 -0
- package/dist/src/routeInventory.d.ts +16 -0
- package/dist/src/routeInventory.js +39 -0
- package/dist/src/routes.d.ts +37 -0
- package/dist/src/routes.js +227 -0
- package/dist/src/server.d.ts +50 -0
- package/dist/src/server.js +1353 -0
- package/dist/src/services/agent.d.ts +1 -0
- package/dist/src/services/agent.js +7 -0
- package/dist/src/services/agentRun.d.ts +60 -0
- package/dist/src/services/agentRun.js +391 -0
- package/dist/src/services/chat.d.ts +11 -0
- package/dist/src/services/chat.js +47 -0
- package/dist/src/services/models.d.ts +10 -0
- package/dist/src/services/models.js +216 -0
- package/dist/src/services/serverConfig.d.ts +2 -0
- package/dist/src/services/serverConfig.js +19 -0
- package/dist/src/testing/artifacts.d.ts +14 -0
- package/dist/src/testing/artifacts.js +92 -0
- package/dist/src/testing/cli.d.ts +4 -0
- package/dist/src/testing/cli.js +192 -0
- package/dist/src/testing/localBackend.d.ts +24 -0
- package/dist/src/testing/localBackend.js +310 -0
- package/dist/src/testing/processRunner.d.ts +7 -0
- package/dist/src/testing/processRunner.js +74 -0
- package/dist/src/testing/runner.d.ts +9 -0
- package/dist/src/testing/runner.js +85 -0
- package/dist/src/testing/scenarios.d.ts +3 -0
- package/dist/src/testing/scenarios.js +2535 -0
- package/dist/src/testing/types.d.ts +66 -0
- package/dist/src/testing/types.js +1 -0
- package/dist/src/tools/baselineInventory.d.ts +12 -0
- package/dist/src/tools/baselineInventory.js +680 -0
- package/dist/src/tools/checkModelFusionProtocol.d.ts +1 -0
- package/dist/src/tools/checkModelFusionProtocol.js +274 -0
- package/dist/src/tools/checkReleasePublishConfig.d.ts +1 -0
- package/dist/src/tools/checkReleasePublishConfig.js +99 -0
- package/dist/src/tools/generateProtoInventory.d.ts +1 -0
- package/dist/src/tools/generateProtoInventory.js +89 -0
- package/dist/src/tools/normalizeGeneratedCode.d.ts +1 -0
- package/dist/src/tools/normalizeGeneratedCode.js +18 -0
- package/dist/src/tools/releaseCheck.d.ts +26 -0
- package/dist/src/tools/releaseCheck.js +367 -0
- package/dist/src/trace.d.ts +39 -0
- package/dist/src/trace.js +106 -0
- package/dist/src/translation.d.ts +6 -0
- package/dist/src/translation.js +22 -0
- package/dist/src/upstream.d.ts +20 -0
- package/dist/src/upstream.js +270 -0
- package/docs/configuration.md +55 -0
- package/docs/cursor-app.md +263 -0
- package/docs/implementation-inventory.json +609 -0
- package/docs/learnings.md +363 -0
- package/docs/model-fusion-protocol-origin.json +126 -0
- package/docs/model-fusion-protocol.md +110 -0
- package/docs/plugin-authoring.md +24 -0
- package/docs/proto-inventory.md +1477 -0
- package/docs/protocol-surface-audit.md +92 -0
- package/docs/protocol.md +52 -0
- package/docs/refreshing-protos.md +78 -0
- package/docs/release-gates.md +110 -0
- package/docs/release-summary.json +86 -0
- package/docs/route-contract-manifest.json +288 -0
- package/docs/route-policy.json +133 -0
- package/docs/service-manifest.json +9490 -0
- package/docs/test-manifest.json +155 -0
- package/docs/testing-harness.md +204 -0
- package/docs/troubleshooting.md +36 -0
- package/docs/type-manifest-summary.json +28927 -0
- package/package.json +93 -0
- package/proto/agent/v1/agent.proto +5371 -0
- package/proto/aiserver/v1/aiserver.proto +32944 -0
- package/proto/anyrun/v1/anyrun.proto +294 -0
- package/proto/google/protobuf/google.proto +37 -0
- 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.
|