@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,1353 @@
|
|
|
1
|
+
import http from "node:http";
|
|
2
|
+
import http2 from "node:http2";
|
|
3
|
+
import https from "node:https";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { pathToFileURL } from "node:url";
|
|
6
|
+
import zlib from "node:zlib";
|
|
7
|
+
import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
|
|
8
|
+
import { agentExecClientMessageFields, cursorOpenAITools, executeCursorToolCall, } from "./agentTools/registry.js";
|
|
9
|
+
import { loadTlsMaterial } from "./certs.js";
|
|
10
|
+
import { decodeEnvelopes, encodeEndStream, encodeEnvelope, firstMessageEnvelope, firstMessagePayload, isCompressedEnvelope, isEndStreamEnvelope, parseEnvelopes, } from "./connectEnvelope.js";
|
|
11
|
+
import { createExtensionManager, } from "./extensions/registry.js";
|
|
12
|
+
import { ModelRegistry, registerConfiguredModels } from "./models/registry.js";
|
|
13
|
+
import { OpenAICompatibleProvider } from "./providers/openai.js";
|
|
14
|
+
import { loadCursorProto } from "./proto.js";
|
|
15
|
+
import { attachRouteInventoryLogger, } from "./routeInventory.js";
|
|
16
|
+
import { AGENT_RUN_PATH, AGENT_RUN_SSE_PATH, AUTH_FULL_STRIPE_PROFILE_PATH, AUTH_STRIPE_PROFILE_PATH, classifyRoute, AVAILABLE_MODELS_PATH, BIDI_APPEND_PATH, GET_DEFAULT_MODEL_FOR_CLI_PATH, GET_DEFAULT_MODEL_PATH, GET_SERVER_CONFIG_PATH, GET_USABLE_MODELS_PATH, NAME_AGENT_PATH, STREAM_CHAT_WITH_TOOLS_PATH, UPLOAD_ISSUE_TRACE_PATH, } from "./routes.js";
|
|
17
|
+
import { buildLocalNameAgentResponse } from "./services/agent.js";
|
|
18
|
+
import { getLocalChatDecision, writeLocalChatResponse, } from "./services/chat.js";
|
|
19
|
+
import { describeAgentRunPayload, getLocalAgentRunDecision, getLocalAgentRunDecisionFromClientMessage, withNativeRequestContext, writeLocalAgentRunResponse, } from "./services/agentRun.js";
|
|
20
|
+
import { emitTrace, newTraceId } from "./trace.js";
|
|
21
|
+
import { AgentClientMessageSchema, AgentServerMessageSchema, ExecServerMessageSchema, InteractionUpdateSchema, RequestContextArgsSchema, TextDeltaUpdateSchema, TurnEndedUpdateSchema, } from "./gen/agent/v1/agent_pb.js";
|
|
22
|
+
import { BidiAppendRequestSchema, BidiAppendResponseSchema, BidiRequestIdSchema, UploadIssueTraceRequestSchema, } from "./gen/aiserver/v1/aiserver_pb.js";
|
|
23
|
+
import { mergeDefaultModelForCli, mergeDefaultModel, mergeAvailableModels, mergeUsableModels, writeAvailableModelsResponse, writeModelResponse, } from "./services/models.js";
|
|
24
|
+
import { buildLocalServerConfig, rewriteServerConfigAgentUrls, } from "./services/serverConfig.js";
|
|
25
|
+
import { fetchUpstreamBuffer, proxyBufferedRequest, proxyRequest, RequestBodyTooLargeError, readRequestBody, UpstreamRequestTimeoutError, } from "./upstream.js";
|
|
26
|
+
/**
|
|
27
|
+
* A single-consumer queue of ExecClientMessage payload buffers (each a binary
|
|
28
|
+
* AgentClientMessage) delivered out-of-band via BidiAppend. Exposes an
|
|
29
|
+
* AsyncIterator so the shared Cursor tool loop can await tool results exactly as
|
|
30
|
+
* it does on the inline duplex stream.
|
|
31
|
+
*/
|
|
32
|
+
export class ToolResultMailbox {
|
|
33
|
+
#queue = [];
|
|
34
|
+
#waiters = [];
|
|
35
|
+
#closed = false;
|
|
36
|
+
push(payload) {
|
|
37
|
+
const waiter = this.#waiters.shift();
|
|
38
|
+
if (waiter !== undefined) {
|
|
39
|
+
waiter({ done: false, value: payload });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this.#queue.push(payload);
|
|
43
|
+
}
|
|
44
|
+
close() {
|
|
45
|
+
this.#closed = true;
|
|
46
|
+
const waiters = this.#waiters.splice(0);
|
|
47
|
+
for (const waiter of waiters) {
|
|
48
|
+
waiter({ done: true, value: undefined });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
iterator() {
|
|
52
|
+
return {
|
|
53
|
+
next: () => {
|
|
54
|
+
const queued = this.#queue.shift();
|
|
55
|
+
if (queued !== undefined) {
|
|
56
|
+
return Promise.resolve({ done: false, value: queued });
|
|
57
|
+
}
|
|
58
|
+
if (this.#closed) {
|
|
59
|
+
return Promise.resolve({ done: true, value: undefined });
|
|
60
|
+
}
|
|
61
|
+
return new Promise((resolve) => this.#waiters.push(resolve));
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const MAX_AGENT_STREAM_BYTES = 256 * 1024 * 1024;
|
|
67
|
+
const DEFAULT_AGENT_RUN_SSE_WAIT_TIMEOUT_MS = 5_000;
|
|
68
|
+
const DEFAULT_AGENT_CONTEXT_TIMEOUT_MS = 5 * 60_000;
|
|
69
|
+
const DEFAULT_EXTENSION_SETUP_TIMEOUT_MS = 10_000;
|
|
70
|
+
const MAX_PENDING_AGENT_RUNS = 100;
|
|
71
|
+
const MAX_PENDING_AGENT_CONTEXT_RUNS = 100;
|
|
72
|
+
export async function createBridgeRuntime(config, logger) {
|
|
73
|
+
const proto = await loadCursorProto();
|
|
74
|
+
const models = new ModelRegistry();
|
|
75
|
+
registerConfiguredModels(models, config.models, (modelConfig) => new OpenAICompatibleProvider(modelConfig, logger, {
|
|
76
|
+
logFullPayload: config.modelPayloadLogging === "full",
|
|
77
|
+
}));
|
|
78
|
+
const extensions = createExtensionManager(models, logger);
|
|
79
|
+
if (config.pluginPath !== undefined) {
|
|
80
|
+
try {
|
|
81
|
+
await withTimeout(extensions.load(await loadExtension(config.pluginPath)), config.extensionSetupTimeoutMs ?? DEFAULT_EXTENSION_SETUP_TIMEOUT_MS, `Extension setup timed out for ${config.pluginPath}`);
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
logger.error("extension load failed", {
|
|
85
|
+
pluginPath: config.pluginPath,
|
|
86
|
+
error: error instanceof Error ? error.message : String(error),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
config,
|
|
92
|
+
logger,
|
|
93
|
+
proto,
|
|
94
|
+
models,
|
|
95
|
+
extensions,
|
|
96
|
+
pendingAgentRuns: new Map(),
|
|
97
|
+
pendingAgentContextRuns: new Map(),
|
|
98
|
+
toolResultMailboxes: new Map(),
|
|
99
|
+
nextAgentExecId: 1,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export async function startServer(runtime) {
|
|
103
|
+
const listener = (request, response) => {
|
|
104
|
+
void handleRequest(runtime, request, response);
|
|
105
|
+
};
|
|
106
|
+
const server = runtime.config.useTls
|
|
107
|
+
? runtime.config.desktopMode
|
|
108
|
+
? http2.createSecureServer({ ...(await loadTlsMaterial(runtime.config)), allowHTTP1: true }, listener)
|
|
109
|
+
: https.createServer(await loadTlsMaterial(runtime.config), listener)
|
|
110
|
+
: http.createServer(listener);
|
|
111
|
+
const agentHttpPort = runtime.config.desktopAgentHttpPort;
|
|
112
|
+
const agentHttpServer = agentHttpPort !== undefined ? http.createServer(listener) : undefined;
|
|
113
|
+
await listenWithStartupErrors(server, runtime.config.port, runtime.config.host, "bridge");
|
|
114
|
+
if (agentHttpServer !== undefined && agentHttpPort !== undefined) {
|
|
115
|
+
try {
|
|
116
|
+
await listenWithStartupErrors(agentHttpServer, agentHttpPort, runtime.config.host, "desktop agent http");
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
await closeServer(server);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
server.on("close", () => {
|
|
123
|
+
agentHttpServer.close();
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
runtime.logger.info("bridge listening", {
|
|
127
|
+
host: runtime.config.host,
|
|
128
|
+
port: runtime.config.port,
|
|
129
|
+
agentHttpPort: runtime.config.desktopAgentHttpPort,
|
|
130
|
+
tls: runtime.config.useTls,
|
|
131
|
+
upstreamConfigured: runtime.config.upstreamBaseUrl !== undefined,
|
|
132
|
+
localModels: runtime.models.list().map((model) => model.id),
|
|
133
|
+
});
|
|
134
|
+
return server;
|
|
135
|
+
}
|
|
136
|
+
function listenWithStartupErrors(server, port, host, name) {
|
|
137
|
+
return new Promise((resolve, reject) => {
|
|
138
|
+
const onError = (error) => {
|
|
139
|
+
server.off("listening", onListening);
|
|
140
|
+
reject(new Error(`${name} listener failed to start: ${error.message}`));
|
|
141
|
+
};
|
|
142
|
+
const onListening = () => {
|
|
143
|
+
server.off("error", onError);
|
|
144
|
+
resolve();
|
|
145
|
+
};
|
|
146
|
+
server.once("error", onError);
|
|
147
|
+
server.once("listening", onListening);
|
|
148
|
+
server.listen(port, host);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
function closeServer(server) {
|
|
152
|
+
return new Promise((resolve) => {
|
|
153
|
+
server.close(() => resolve());
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
async function handleRequest(runtime, request, response) {
|
|
157
|
+
if (request.url === "/healthz") {
|
|
158
|
+
response.writeHead(200, { "content-type": "application/json" });
|
|
159
|
+
response.end(JSON.stringify({ ok: true }));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (!authorizeRequest(runtime, request, response)) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const decision = classifyRoute(request);
|
|
166
|
+
let routeOutcome = decision.policy === "intercept" ? "intercept" : "pass-through";
|
|
167
|
+
attachRouteInventoryLogger(runtime.config, runtime.logger, request, response, decision, () => routeOutcome);
|
|
168
|
+
await runMetadataOnlyRequestMiddleware(runtime, request);
|
|
169
|
+
const handledByPlugin = await handlePluginRoute(runtime, request, response, decision.path);
|
|
170
|
+
if (handledByPlugin) {
|
|
171
|
+
routeOutcome = "plugin";
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
runtime.logger.debug("route decision", { ...decision });
|
|
175
|
+
if (decision.policy !== "intercept") {
|
|
176
|
+
routeOutcome = "pass-through";
|
|
177
|
+
proxyRequest(request, response, runtime.config, runtime.logger);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
try {
|
|
181
|
+
routeOutcome = "intercept";
|
|
182
|
+
if (decision.path === AVAILABLE_MODELS_PATH) {
|
|
183
|
+
await handleAvailableModels(runtime, request, response);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (decision.path === GET_USABLE_MODELS_PATH) {
|
|
187
|
+
await handleGetUsableModels(runtime, request, response);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (decision.path === GET_DEFAULT_MODEL_FOR_CLI_PATH) {
|
|
191
|
+
await handleGetDefaultModelForCli(runtime, request, response);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
if (decision.path === GET_DEFAULT_MODEL_PATH) {
|
|
195
|
+
await handleGetDefaultModel(runtime, request, response);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (decision.path === NAME_AGENT_PATH) {
|
|
199
|
+
await handleNameAgent(runtime, request, response);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (decision.path === GET_SERVER_CONFIG_PATH) {
|
|
203
|
+
await handleGetServerConfig(runtime, request, response);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (decision.path === AUTH_FULL_STRIPE_PROFILE_PATH ||
|
|
207
|
+
decision.path === AUTH_STRIPE_PROFILE_PATH) {
|
|
208
|
+
await handleAuthFullStripeProfile(runtime, request, response);
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (decision.path === AGENT_RUN_PATH ||
|
|
212
|
+
decision.path === AGENT_RUN_SSE_PATH) {
|
|
213
|
+
await handleAgentRun(runtime, request, response, decision.path);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (decision.path === BIDI_APPEND_PATH) {
|
|
217
|
+
await handleBidiAppend(runtime, request, response);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
if (decision.path === STREAM_CHAT_WITH_TOOLS_PATH) {
|
|
221
|
+
await handleChat(runtime, request, response);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
if (decision.path === UPLOAD_ISSUE_TRACE_PATH) {
|
|
225
|
+
await handleUploadIssueTrace(runtime, request, response);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
const handled = handleKnownRequestError(runtime, response, decision.path, error);
|
|
231
|
+
if (handled) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
runtime.logger.error("intercept failed", {
|
|
235
|
+
path: decision.path,
|
|
236
|
+
error: error instanceof Error ? error.message : String(error),
|
|
237
|
+
});
|
|
238
|
+
if (response.headersSent) {
|
|
239
|
+
response.end(encodeEndStream({ error: "intercept failed" }));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (runtime.config.failOpen) {
|
|
243
|
+
response.writeHead(502, { "content-type": "application/json" });
|
|
244
|
+
response.end(JSON.stringify({
|
|
245
|
+
error: "intercept failed after request body was consumed",
|
|
246
|
+
}));
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
response.writeHead(500, { "content-type": "application/json" });
|
|
250
|
+
response.end(JSON.stringify({ error: "intercept failed" }));
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
proxyRequest(request, response, runtime.config, runtime.logger);
|
|
254
|
+
}
|
|
255
|
+
function authorizeRequest(runtime, request, response) {
|
|
256
|
+
const token = runtime.config.authToken;
|
|
257
|
+
if (token === undefined) {
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
const authorization = headerValue(request.headers.authorization);
|
|
261
|
+
const bridgeToken = headerValue(request.headers["x-cursor-rpc-auth"]);
|
|
262
|
+
const authorized = authorization === `Bearer ${token}` || bridgeToken === token;
|
|
263
|
+
if (authorized) {
|
|
264
|
+
return true;
|
|
265
|
+
}
|
|
266
|
+
response.writeHead(401, {
|
|
267
|
+
"content-type": "application/json",
|
|
268
|
+
"www-authenticate": "Bearer",
|
|
269
|
+
});
|
|
270
|
+
response.end(JSON.stringify({ error: "bridge authentication required" }));
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
function handleKnownRequestError(runtime, response, pathName, error) {
|
|
274
|
+
if (error instanceof RequestBodyTooLargeError) {
|
|
275
|
+
runtime.logger.warn("request body too large", {
|
|
276
|
+
path: pathName,
|
|
277
|
+
maxBytes: error.maxBytes,
|
|
278
|
+
});
|
|
279
|
+
writeErrorResponse(response, 413, "request body too large");
|
|
280
|
+
return true;
|
|
281
|
+
}
|
|
282
|
+
if (error instanceof UpstreamRequestTimeoutError) {
|
|
283
|
+
runtime.logger.warn("upstream request timed out", {
|
|
284
|
+
path: pathName,
|
|
285
|
+
timeoutMs: error.timeoutMs,
|
|
286
|
+
});
|
|
287
|
+
writeErrorResponse(response, 504, "upstream request timed out");
|
|
288
|
+
return true;
|
|
289
|
+
}
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
function writeErrorResponse(response, statusCode, message) {
|
|
293
|
+
if (response.headersSent) {
|
|
294
|
+
response.end(encodeEndStream({ error: message }));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
response.writeHead(statusCode, { "content-type": "application/json" });
|
|
298
|
+
response.end(JSON.stringify({ error: message }));
|
|
299
|
+
}
|
|
300
|
+
function headerValue(value) {
|
|
301
|
+
if (value === undefined) {
|
|
302
|
+
return "";
|
|
303
|
+
}
|
|
304
|
+
return Array.isArray(value) ? (value[0] ?? "") : value;
|
|
305
|
+
}
|
|
306
|
+
function requestAbortSignal(request, response) {
|
|
307
|
+
const controller = new AbortController();
|
|
308
|
+
const abort = () => {
|
|
309
|
+
if (!controller.signal.aborted) {
|
|
310
|
+
controller.abort();
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
request.once("aborted", abort);
|
|
314
|
+
request.once("error", abort);
|
|
315
|
+
response.once("close", () => {
|
|
316
|
+
if (!response.writableEnded) {
|
|
317
|
+
abort();
|
|
318
|
+
}
|
|
319
|
+
});
|
|
320
|
+
return controller.signal;
|
|
321
|
+
}
|
|
322
|
+
async function handlePluginRoute(runtime, request, response, pathName) {
|
|
323
|
+
for (const route of runtime.extensions.context.routes.list()) {
|
|
324
|
+
if (route.path !== pathName) {
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
if (route.bodyAccess !== "consume") {
|
|
328
|
+
runtime.logger.warn("plugin route declined because bodyAccess is not consume", { path: route.path });
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
return await route.handle(request, response);
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
runtime.logger.error("plugin route failed", {
|
|
336
|
+
path: route.path,
|
|
337
|
+
error: error instanceof Error ? error.message : String(error),
|
|
338
|
+
});
|
|
339
|
+
if (!response.headersSent) {
|
|
340
|
+
response.writeHead(502, { "content-type": "application/json" });
|
|
341
|
+
response.end(JSON.stringify({ error: "plugin route failed" }));
|
|
342
|
+
}
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return false;
|
|
347
|
+
}
|
|
348
|
+
async function runMetadataOnlyRequestMiddleware(runtime, request) {
|
|
349
|
+
for (const middleware of runtime.extensions.context.middleware.list()) {
|
|
350
|
+
if (middleware.kind !== "request") {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const requestMiddleware = middleware;
|
|
354
|
+
if (requestMiddleware.bodyAccess === "metadata-only") {
|
|
355
|
+
try {
|
|
356
|
+
await requestMiddleware.run(request);
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
runtime.logger.error("extension request middleware failed", {
|
|
360
|
+
error: error instanceof Error ? error.message : String(error),
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
async function loadExtension(pluginPath) {
|
|
367
|
+
const resolvedPath = path.isAbsolute(pluginPath)
|
|
368
|
+
? pluginPath
|
|
369
|
+
: path.resolve(process.cwd(), pluginPath);
|
|
370
|
+
const imported = (await import(pathToFileURL(resolvedPath).href));
|
|
371
|
+
if (imported.default === undefined) {
|
|
372
|
+
throw new Error(`Plugin ${pluginPath} must export a default CursorExtension`);
|
|
373
|
+
}
|
|
374
|
+
return imported.default;
|
|
375
|
+
}
|
|
376
|
+
async function withTimeout(promise, timeoutMs, message) {
|
|
377
|
+
let timer;
|
|
378
|
+
try {
|
|
379
|
+
return await Promise.race([
|
|
380
|
+
promise,
|
|
381
|
+
new Promise((_resolve, reject) => {
|
|
382
|
+
timer = setTimeout(() => reject(new Error(message)), timeoutMs);
|
|
383
|
+
}),
|
|
384
|
+
]);
|
|
385
|
+
}
|
|
386
|
+
finally {
|
|
387
|
+
if (timer !== undefined) {
|
|
388
|
+
clearTimeout(timer);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
async function handleAvailableModels(runtime, request, response) {
|
|
393
|
+
const body = await readRequestBody(request, runtime.config.maxInterceptBodyBytes);
|
|
394
|
+
const format = await validatedRequestFormatOrPassThrough(runtime, request, response, body);
|
|
395
|
+
if (format === undefined) {
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
const upstreamBody = await fetchUpstreamBuffer(request, body, runtime.config).catch((error) => {
|
|
399
|
+
runtime.logger.warn("available models upstream unavailable", {
|
|
400
|
+
error: error instanceof Error ? error.message : String(error),
|
|
401
|
+
});
|
|
402
|
+
return undefined;
|
|
403
|
+
});
|
|
404
|
+
const merged = mergeAvailableModelsWithFallback(runtime, upstreamMessagePayload(upstreamBody, format));
|
|
405
|
+
writeAvailableModelsResponse(response, merged, runtime.logger, format);
|
|
406
|
+
}
|
|
407
|
+
async function handleGetUsableModels(runtime, request, response) {
|
|
408
|
+
const body = await readRequestBody(request, runtime.config.maxInterceptBodyBytes);
|
|
409
|
+
const format = await validatedRequestFormatOrPassThrough(runtime, request, response, body);
|
|
410
|
+
if (format === undefined) {
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const upstreamBody = await fetchUpstreamBuffer(request, body, runtime.config).catch((error) => {
|
|
414
|
+
runtime.logger.warn("usable models upstream unavailable", {
|
|
415
|
+
error: error instanceof Error ? error.message : String(error),
|
|
416
|
+
});
|
|
417
|
+
return undefined;
|
|
418
|
+
});
|
|
419
|
+
const merged = mergeUsableModelsWithFallback(runtime, upstreamMessagePayload(upstreamBody, format));
|
|
420
|
+
writeModelResponse(response, merged, runtime.logger, "served usable models", format);
|
|
421
|
+
}
|
|
422
|
+
async function handleGetDefaultModelForCli(runtime, request, response) {
|
|
423
|
+
const body = await readRequestBody(request, runtime.config.maxInterceptBodyBytes);
|
|
424
|
+
const format = await validatedRequestFormatOrPassThrough(runtime, request, response, body);
|
|
425
|
+
if (format === undefined) {
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const upstreamBody = await fetchUpstreamBuffer(request, body, runtime.config).catch((error) => {
|
|
429
|
+
runtime.logger.warn("default CLI model upstream unavailable", {
|
|
430
|
+
error: error instanceof Error ? error.message : String(error),
|
|
431
|
+
});
|
|
432
|
+
return undefined;
|
|
433
|
+
});
|
|
434
|
+
const merged = mergeDefaultModelForCliWithFallback(runtime, upstreamMessagePayload(upstreamBody, format));
|
|
435
|
+
writeModelResponse(response, merged, runtime.logger, "served default CLI model", format);
|
|
436
|
+
}
|
|
437
|
+
async function handleGetDefaultModel(runtime, request, response) {
|
|
438
|
+
const body = await readRequestBody(request, runtime.config.maxInterceptBodyBytes);
|
|
439
|
+
const format = await validatedRequestFormatOrPassThrough(runtime, request, response, body);
|
|
440
|
+
if (format === undefined) {
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const upstreamBody = await fetchUpstreamBuffer(request, body, runtime.config).catch((error) => {
|
|
444
|
+
runtime.logger.warn("default model upstream unavailable", {
|
|
445
|
+
error: error instanceof Error ? error.message : String(error),
|
|
446
|
+
});
|
|
447
|
+
return undefined;
|
|
448
|
+
});
|
|
449
|
+
const payload = mergeDefaultModel(upstreamMessagePayload(upstreamBody, format), runtime.models);
|
|
450
|
+
writeModelResponse(response, payload, runtime.logger, "served default model", format);
|
|
451
|
+
}
|
|
452
|
+
async function handleNameAgent(runtime, request, response) {
|
|
453
|
+
const body = await readRequestBody(request, runtime.config.maxInterceptBodyBytes);
|
|
454
|
+
const format = await validatedRequestFormatOrPassThrough(runtime, request, response, body);
|
|
455
|
+
if (format === undefined) {
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
writeModelResponse(response, buildLocalNameAgentResponse(), runtime.logger, "served local name agent", format);
|
|
459
|
+
}
|
|
460
|
+
async function handleUploadIssueTrace(runtime, request, response) {
|
|
461
|
+
const body = await readRequestBody(request, runtime.config.maxInterceptBodyBytes);
|
|
462
|
+
const format = modelResponseFormatForRequest(request);
|
|
463
|
+
const payload = requestPayloadForFormat(body, format);
|
|
464
|
+
if (payload !== undefined) {
|
|
465
|
+
try {
|
|
466
|
+
const trace = fromBinary(UploadIssueTraceRequestSchema, payload);
|
|
467
|
+
runtime.logger.warn("desktop issue trace uploaded", {
|
|
468
|
+
payloadHash: trace.payloadHash,
|
|
469
|
+
payloadChars: trace.payload.length,
|
|
470
|
+
payloadPreview: summarizeIssueTracePayload(trace.payload),
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
catch (error) {
|
|
474
|
+
runtime.logger.warn("desktop issue trace decode failed", {
|
|
475
|
+
error: error instanceof Error ? error.message : String(error),
|
|
476
|
+
bytes: payload.length,
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
await proxyBufferedRequest(request, response, body, runtime.config, runtime.logger);
|
|
481
|
+
}
|
|
482
|
+
async function handleGetServerConfig(runtime, request, response) {
|
|
483
|
+
const body = await readRequestBody(request, runtime.config.maxInterceptBodyBytes);
|
|
484
|
+
const format = await validatedRequestFormatOrPassThrough(runtime, request, response, body);
|
|
485
|
+
if (format === undefined) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
const upstreamBody = await fetchUpstreamBuffer(request, body, runtime.config).catch((error) => {
|
|
489
|
+
runtime.logger.warn("server config upstream unavailable", {
|
|
490
|
+
error: error instanceof Error ? error.message : String(error),
|
|
491
|
+
});
|
|
492
|
+
return undefined;
|
|
493
|
+
});
|
|
494
|
+
const bridgeOrigin = agentRequestOrigin(request, runtime.config);
|
|
495
|
+
const upstreamPayload = upstreamMessagePayload(upstreamBody, format);
|
|
496
|
+
const payload = rewriteServerConfigWithFallback(runtime, upstreamPayload, bridgeOrigin);
|
|
497
|
+
writeModelResponse(response, payload, runtime.logger, "served server config", format);
|
|
498
|
+
}
|
|
499
|
+
async function handleAuthFullStripeProfile(runtime, request, response) {
|
|
500
|
+
if (request.method === "OPTIONS") {
|
|
501
|
+
response.writeHead(204, authProfileCorsHeaders(request));
|
|
502
|
+
response.end();
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
const body = await readRequestBody(request, runtime.config.maxInterceptBodyBytes);
|
|
506
|
+
const upstreamBody = await fetchUpstreamBuffer(request, body, runtime.config).catch((error) => {
|
|
507
|
+
runtime.logger.warn("auth profile upstream unavailable", {
|
|
508
|
+
error: error instanceof Error ? error.message : String(error),
|
|
509
|
+
});
|
|
510
|
+
return undefined;
|
|
511
|
+
});
|
|
512
|
+
if (upstreamBody === undefined) {
|
|
513
|
+
response.writeHead(502, { "content-type": "application/json" });
|
|
514
|
+
response.end(JSON.stringify({ error: "auth profile upstream unavailable" }));
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (upstreamBody.byteLength === 0) {
|
|
518
|
+
response.writeHead(200, authProfileCorsHeaders(request));
|
|
519
|
+
response.end(upstreamBody);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
let profile;
|
|
523
|
+
try {
|
|
524
|
+
profile = JSON.parse(upstreamBody.toString("utf8"));
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
runtime.logger.warn("auth profile JSON rewrite skipped", {
|
|
528
|
+
error: error instanceof Error ? error.message : String(error),
|
|
529
|
+
bytes: upstreamBody.byteLength,
|
|
530
|
+
});
|
|
531
|
+
response.writeHead(200, {
|
|
532
|
+
...authProfileCorsHeaders(request),
|
|
533
|
+
"content-type": "application/octet-stream",
|
|
534
|
+
"content-length": String(upstreamBody.byteLength),
|
|
535
|
+
});
|
|
536
|
+
response.end(upstreamBody);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const bridgeOrigin = agentRequestOrigin(request, runtime.config);
|
|
540
|
+
const rewrites = rewriteAgentBackendUrlsInJson(profile, bridgeOrigin);
|
|
541
|
+
const payload = Buffer.from(JSON.stringify(profile));
|
|
542
|
+
response.writeHead(200, {
|
|
543
|
+
...authProfileCorsHeaders(request),
|
|
544
|
+
"content-type": "application/json",
|
|
545
|
+
"content-length": String(payload.byteLength),
|
|
546
|
+
});
|
|
547
|
+
response.end(payload);
|
|
548
|
+
runtime.logger.info("served auth profile", { rewrites });
|
|
549
|
+
}
|
|
550
|
+
function rewriteAgentBackendUrlsInJson(value, agentOrigin) {
|
|
551
|
+
if (!isRecord(value)) {
|
|
552
|
+
return 0;
|
|
553
|
+
}
|
|
554
|
+
let rewrites = 0;
|
|
555
|
+
for (const [key, child] of Object.entries(value)) {
|
|
556
|
+
if (typeof child === "string" && isCursorAgentBackendUrl(child)) {
|
|
557
|
+
value[key] = agentOrigin;
|
|
558
|
+
rewrites += 1;
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
rewrites += rewriteAgentBackendUrlsInJson(child, agentOrigin);
|
|
562
|
+
}
|
|
563
|
+
if ("agentBackendUrlPrivacy" in value ||
|
|
564
|
+
"agentBackendUrlNonPrivacy" in value) {
|
|
565
|
+
value.agentBackendUrlPrivacy = { default: agentOrigin };
|
|
566
|
+
value.agentBackendUrlNonPrivacy = { default: agentOrigin };
|
|
567
|
+
rewrites += 1;
|
|
568
|
+
}
|
|
569
|
+
return rewrites;
|
|
570
|
+
}
|
|
571
|
+
function isCursorAgentBackendUrl(value) {
|
|
572
|
+
return /^https:\/\/agent[a-z0-9.-]*\.cursor\.sh(?::\d+)?$/i.test(value);
|
|
573
|
+
}
|
|
574
|
+
function authProfileCorsHeaders(request) {
|
|
575
|
+
return {
|
|
576
|
+
"access-control-allow-origin": request.headers.origin ?? "*",
|
|
577
|
+
"access-control-allow-methods": "GET,POST,OPTIONS",
|
|
578
|
+
"access-control-allow-headers": request.headers["access-control-request-headers"] ??
|
|
579
|
+
"authorization,content-type,x-cursor-checksum,x-cursor-client-version,x-cursor-timezone",
|
|
580
|
+
"access-control-allow-credentials": "true",
|
|
581
|
+
vary: "origin, access-control-request-headers",
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
function isRecord(value) {
|
|
585
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
586
|
+
}
|
|
587
|
+
function rewriteServerConfigWithFallback(runtime, upstreamPayload, bridgeOrigin) {
|
|
588
|
+
if (upstreamPayload === undefined) {
|
|
589
|
+
return buildLocalServerConfig(bridgeOrigin);
|
|
590
|
+
}
|
|
591
|
+
try {
|
|
592
|
+
return rewriteServerConfigAgentUrls(upstreamPayload, bridgeOrigin);
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
runtime.logger.warn("server config upstream decode failed", {
|
|
596
|
+
error: error instanceof Error ? error.message : String(error),
|
|
597
|
+
});
|
|
598
|
+
return buildLocalServerConfig(bridgeOrigin);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function requestOrigin(request, config) {
|
|
602
|
+
if (config.publicOrigin !== undefined) {
|
|
603
|
+
return config.publicOrigin;
|
|
604
|
+
}
|
|
605
|
+
const host = request.headers.host ?? "127.0.0.1";
|
|
606
|
+
return `${config.useTls ? "https" : "http"}://${host}`;
|
|
607
|
+
}
|
|
608
|
+
function agentRequestOrigin(request, config) {
|
|
609
|
+
return config.agentPublicOrigin ?? requestOrigin(request, config);
|
|
610
|
+
}
|
|
611
|
+
function summarizeIssueTracePayload(payload) {
|
|
612
|
+
const normalized = payload.replace(/\s+/g, " ").trim();
|
|
613
|
+
if (normalized.length === 0) {
|
|
614
|
+
return "";
|
|
615
|
+
}
|
|
616
|
+
try {
|
|
617
|
+
const parsed = JSON.parse(payload);
|
|
618
|
+
return JSON.stringify(summarizeIssueTraceJson(parsed)).slice(0, 4_000);
|
|
619
|
+
}
|
|
620
|
+
catch {
|
|
621
|
+
return normalized.slice(0, 4_000);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
function summarizeIssueTraceJson(value) {
|
|
625
|
+
if (Array.isArray(value)) {
|
|
626
|
+
return value.slice(0, 20).map((item) => summarizeIssueTraceJson(item));
|
|
627
|
+
}
|
|
628
|
+
if (typeof value !== "object" || value === null) {
|
|
629
|
+
return typeof value === "string" ? value.slice(0, 1_000) : value;
|
|
630
|
+
}
|
|
631
|
+
const result = {};
|
|
632
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
633
|
+
if (/token|key|authorization|cookie|secret/i.test(key)) {
|
|
634
|
+
result[key] = "<redacted>";
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
result[key] = summarizeIssueTraceJson(entry);
|
|
638
|
+
}
|
|
639
|
+
return result;
|
|
640
|
+
}
|
|
641
|
+
function modelResponseFormatForRequest(request) {
|
|
642
|
+
const contentType = request.headers["content-type"];
|
|
643
|
+
const value = Array.isArray(contentType) ? contentType[0] : contentType;
|
|
644
|
+
return value?.includes("application/connect+proto") ? "connect" : "proto";
|
|
645
|
+
}
|
|
646
|
+
async function validatedRequestFormatOrPassThrough(runtime, request, response, body) {
|
|
647
|
+
const format = modelResponseFormatForRequest(request);
|
|
648
|
+
if (requestPayloadForFormat(body, format) !== undefined) {
|
|
649
|
+
return format;
|
|
650
|
+
}
|
|
651
|
+
await proxyBufferedRequest(request, response, body, runtime.config, runtime.logger);
|
|
652
|
+
return undefined;
|
|
653
|
+
}
|
|
654
|
+
function upstreamMessagePayload(upstreamBody, format) {
|
|
655
|
+
if (upstreamBody === undefined) {
|
|
656
|
+
return undefined;
|
|
657
|
+
}
|
|
658
|
+
return format === "connect"
|
|
659
|
+
? firstMessagePayload(upstreamBody)
|
|
660
|
+
: upstreamBody;
|
|
661
|
+
}
|
|
662
|
+
function mergeAvailableModelsWithFallback(runtime, upstreamPayload) {
|
|
663
|
+
try {
|
|
664
|
+
return mergeAvailableModels(upstreamPayload, runtime.models);
|
|
665
|
+
}
|
|
666
|
+
catch (error) {
|
|
667
|
+
runtime.logger.warn("available models upstream decode failed", {
|
|
668
|
+
error: error instanceof Error ? error.message : String(error),
|
|
669
|
+
});
|
|
670
|
+
return mergeAvailableModels(undefined, runtime.models);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
function mergeUsableModelsWithFallback(runtime, upstreamPayload) {
|
|
674
|
+
try {
|
|
675
|
+
return mergeUsableModels(upstreamPayload, runtime.models);
|
|
676
|
+
}
|
|
677
|
+
catch (error) {
|
|
678
|
+
runtime.logger.warn("usable models upstream decode failed", {
|
|
679
|
+
error: error instanceof Error ? error.message : String(error),
|
|
680
|
+
});
|
|
681
|
+
return mergeUsableModels(undefined, runtime.models);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
function mergeDefaultModelForCliWithFallback(runtime, upstreamPayload) {
|
|
685
|
+
try {
|
|
686
|
+
return mergeDefaultModelForCli(upstreamPayload, runtime.models);
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
runtime.logger.warn("default CLI model upstream decode failed", {
|
|
690
|
+
error: error instanceof Error ? error.message : String(error),
|
|
691
|
+
});
|
|
692
|
+
return mergeDefaultModelForCli(undefined, runtime.models);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
async function handleChat(runtime, request, response) {
|
|
696
|
+
const body = await readRequestBody(request, runtime.config.maxInterceptBodyBytes);
|
|
697
|
+
const format = modelResponseFormatForRequest(request);
|
|
698
|
+
const payload = requestPayloadForFormat(body, format);
|
|
699
|
+
if (payload === undefined) {
|
|
700
|
+
await proxyBufferedRequest(request, response, body, runtime.config, runtime.logger);
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
const decision = getLocalChatDecision(payload, runtime.models);
|
|
704
|
+
if (decision === undefined) {
|
|
705
|
+
await proxyBufferedRequest(request, response, body, runtime.config, runtime.logger);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
await writeLocalChatResponse(response, decision, runtime.logger, {
|
|
709
|
+
signal: requestAbortSignal(request, response),
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
async function handleAgentRun(runtime, request, response, path) {
|
|
713
|
+
const format = modelResponseFormatForRequest(request);
|
|
714
|
+
if (path === AGENT_RUN_PATH &&
|
|
715
|
+
format === "connect" &&
|
|
716
|
+
(request.httpVersionMajor >= 2 ||
|
|
717
|
+
request.headers["x-cursor-rpc-native-agent"] === "true")) {
|
|
718
|
+
await handleStreamingConnectAgentRun(runtime, request, response);
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
const { body, payloads } = format === "connect"
|
|
722
|
+
? await readStreamingConnectPayloadCandidates(request, runtime.config.maxInterceptBodyBytes)
|
|
723
|
+
: {
|
|
724
|
+
body: await readRequestBody(request, runtime.config.maxInterceptBodyBytes),
|
|
725
|
+
payloads: [],
|
|
726
|
+
};
|
|
727
|
+
const candidates = format === "connect"
|
|
728
|
+
? payloads
|
|
729
|
+
: requestPayloadCandidatesForFormat(body, format);
|
|
730
|
+
if (candidates.length === 0) {
|
|
731
|
+
await proxyBufferedRequest(request, response, body, runtime.config, runtime.logger);
|
|
732
|
+
return;
|
|
733
|
+
}
|
|
734
|
+
const requestId = path === AGENT_RUN_SSE_PATH
|
|
735
|
+
? getBidiRequestIdFromPayloads(candidates)
|
|
736
|
+
: undefined;
|
|
737
|
+
const decision = requestId === undefined
|
|
738
|
+
? getLocalAgentRunDecisionFromPayloads(candidates, runtime)
|
|
739
|
+
: await waitForPendingAgentRun(runtime, requestId, requestAbortSignal(request, response));
|
|
740
|
+
if (decision === undefined) {
|
|
741
|
+
runtime.logger.warn("agent run did not match local model", {
|
|
742
|
+
path,
|
|
743
|
+
payloads: candidates.length,
|
|
744
|
+
descriptions: candidates.flatMap((payload) => describeAgentRunPayload(payload)),
|
|
745
|
+
});
|
|
746
|
+
if (format === "connect") {
|
|
747
|
+
response.writeHead(502, { "content-type": "application/json" });
|
|
748
|
+
response.end(JSON.stringify({ error: "agent run did not match local model" }));
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
await proxyBufferedRequest(request, response, body, runtime.config, runtime.logger);
|
|
752
|
+
return;
|
|
753
|
+
}
|
|
754
|
+
const traceId = newTraceId();
|
|
755
|
+
emitTrace({
|
|
756
|
+
event_type: "cursor.route",
|
|
757
|
+
traceId,
|
|
758
|
+
modelId: decision.model.id,
|
|
759
|
+
payload: { message: "served local agent run", path, format },
|
|
760
|
+
});
|
|
761
|
+
const signal = requestAbortSignal(request, response);
|
|
762
|
+
// Over the SSE + BidiAppend transport (the real cursor-agent CLI), drive the
|
|
763
|
+
// same Cursor tool loop as the inline duplex path: ExecServerMessage frames go
|
|
764
|
+
// out on this SSE response, and tool results arrive on later BidiAppend POSTs
|
|
765
|
+
// routed into a per-request mailbox.
|
|
766
|
+
const tools = cursorOpenAITools(runtime.config.agentToolPolicy);
|
|
767
|
+
if (requestId !== undefined &&
|
|
768
|
+
requestId.length > 0 &&
|
|
769
|
+
tools.length > 0 &&
|
|
770
|
+
decision.model.provider.streamCompletionEvents !== undefined) {
|
|
771
|
+
const mailbox = new ToolResultMailbox();
|
|
772
|
+
runtime.toolResultMailboxes.set(requestId, mailbox);
|
|
773
|
+
response.statusCode = 200;
|
|
774
|
+
if (!response.headersSent) {
|
|
775
|
+
response.setHeader("content-type", "application/connect+proto");
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
await writeLocalAgentRunResponseWithCursorTools(response, decision, runtime, mailbox.iterator(), signal);
|
|
779
|
+
}
|
|
780
|
+
finally {
|
|
781
|
+
mailbox.close();
|
|
782
|
+
runtime.toolResultMailboxes.delete(requestId);
|
|
783
|
+
}
|
|
784
|
+
return;
|
|
785
|
+
}
|
|
786
|
+
await writeLocalAgentRunResponse(response, decision, runtime.logger, {
|
|
787
|
+
signal,
|
|
788
|
+
traceId,
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
async function handleStreamingConnectAgentRun(runtime, request, response) {
|
|
792
|
+
response.statusCode = 200;
|
|
793
|
+
response.setHeader("content-type", "application/connect+proto");
|
|
794
|
+
let pending;
|
|
795
|
+
const payloads = connectPayloadStream(request, Math.max(runtime.config.maxInterceptBodyBytes, MAX_AGENT_STREAM_BYTES))[Symbol.asyncIterator]();
|
|
796
|
+
while (true) {
|
|
797
|
+
const next = await payloads.next();
|
|
798
|
+
if (next.done === true) {
|
|
799
|
+
break;
|
|
800
|
+
}
|
|
801
|
+
const payload = next.value;
|
|
802
|
+
if (pending === undefined) {
|
|
803
|
+
const decision = getLocalAgentRunDecisionFromPayloads([payload], runtime);
|
|
804
|
+
if (decision === undefined) {
|
|
805
|
+
continue;
|
|
806
|
+
}
|
|
807
|
+
if (!runtime.config.agentNativeContextEnabled) {
|
|
808
|
+
runtime.logger.info("skipped native agent context", {
|
|
809
|
+
model: decision.model.id,
|
|
810
|
+
});
|
|
811
|
+
await writeLocalAgentRunResponseWithCursorTools(response, decision, runtime, payloads, requestAbortSignal(request, response));
|
|
812
|
+
return;
|
|
813
|
+
}
|
|
814
|
+
pending = storePendingAgentContextRun(runtime, decision);
|
|
815
|
+
response.write(createAgentRequestContextEnvelope(pending));
|
|
816
|
+
continue;
|
|
817
|
+
}
|
|
818
|
+
const contextDecision = takePendingAgentContextRunFromPayloads(runtime, [
|
|
819
|
+
payload,
|
|
820
|
+
]);
|
|
821
|
+
if (contextDecision !== undefined) {
|
|
822
|
+
await writeLocalAgentRunResponseWithCursorTools(response, contextDecision, runtime, payloads, requestAbortSignal(request, response));
|
|
823
|
+
return;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
if (pending !== undefined && !response.writableEnded) {
|
|
827
|
+
runtime.pendingAgentContextRuns.delete(agentContextKey(pending));
|
|
828
|
+
}
|
|
829
|
+
if (!response.writableEnded) {
|
|
830
|
+
response.writeHead(502, { "content-type": "application/json" });
|
|
831
|
+
response.end(JSON.stringify({ error: "agent run stream ended early" }));
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
async function writeLocalAgentRunResponseWithCursorTools(response, decision, runtime, payloads, signal) {
|
|
835
|
+
const traceId = newTraceId();
|
|
836
|
+
emitTrace({
|
|
837
|
+
event_type: "cursor.route",
|
|
838
|
+
traceId,
|
|
839
|
+
modelId: decision.model.id,
|
|
840
|
+
payload: { message: "served local agent run (native context)" },
|
|
841
|
+
});
|
|
842
|
+
const streamEvents = decision.model.provider.streamCompletionEvents;
|
|
843
|
+
if (streamEvents === undefined) {
|
|
844
|
+
await writeLocalAgentRunResponse(response, decision, runtime.logger, {
|
|
845
|
+
signal,
|
|
846
|
+
traceId,
|
|
847
|
+
});
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
const messages = [...decision.messages];
|
|
851
|
+
const tools = cursorOpenAITools(runtime.config.agentToolPolicy);
|
|
852
|
+
const maxIterations = runtime.config.agentToolMaxIterations;
|
|
853
|
+
let outputCharacters = 0;
|
|
854
|
+
let toolIterations = 0;
|
|
855
|
+
while (toolIterations < maxIterations) {
|
|
856
|
+
let assistantContent = "";
|
|
857
|
+
let requestedToolCalls = [];
|
|
858
|
+
for await (const event of streamEvents.call(decision.model.provider, messages, tools, { signal, traceId })) {
|
|
859
|
+
if (event.type === "text") {
|
|
860
|
+
outputCharacters += event.text.length;
|
|
861
|
+
assistantContent += event.text;
|
|
862
|
+
response.write(agentTextDeltaEnvelope(event.text));
|
|
863
|
+
}
|
|
864
|
+
else {
|
|
865
|
+
requestedToolCalls = event.toolCalls;
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
if (requestedToolCalls.length === 0) {
|
|
869
|
+
response.write(agentTurnEndedEnvelope(outputCharacters));
|
|
870
|
+
response.end(encodeEndStream());
|
|
871
|
+
runtime.logger.info("served local agent run", {
|
|
872
|
+
model: decision.model.id,
|
|
873
|
+
});
|
|
874
|
+
runtime.logger.info("local agent run diagnostics", {
|
|
875
|
+
...decision.diagnostics,
|
|
876
|
+
});
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
toolIterations += 1;
|
|
880
|
+
messages.push({
|
|
881
|
+
role: "assistant",
|
|
882
|
+
content: assistantContent,
|
|
883
|
+
tool_calls: requestedToolCalls,
|
|
884
|
+
});
|
|
885
|
+
for (const toolCall of requestedToolCalls) {
|
|
886
|
+
const toolResult = await executeCursorToolCall(response, runtime, payloads, toolCall, { signal });
|
|
887
|
+
messages.push({
|
|
888
|
+
role: "tool",
|
|
889
|
+
tool_call_id: toolCall.id,
|
|
890
|
+
content: toolResult,
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
response.write(agentTextDeltaEnvelope("\n\nTool iteration limit reached."));
|
|
895
|
+
response.write(agentTurnEndedEnvelope(outputCharacters));
|
|
896
|
+
response.end(encodeEndStream());
|
|
897
|
+
}
|
|
898
|
+
function agentTextDeltaEnvelope(text) {
|
|
899
|
+
return encodeEnvelope(toBinary(AgentServerMessageSchema, create(AgentServerMessageSchema, {
|
|
900
|
+
interactionUpdate: create(InteractionUpdateSchema, {
|
|
901
|
+
textDelta: create(TextDeltaUpdateSchema, { text }),
|
|
902
|
+
}),
|
|
903
|
+
})));
|
|
904
|
+
}
|
|
905
|
+
function agentTurnEndedEnvelope(outputCharacters) {
|
|
906
|
+
return encodeEnvelope(toBinary(AgentServerMessageSchema, create(AgentServerMessageSchema, {
|
|
907
|
+
interactionUpdate: create(InteractionUpdateSchema, {
|
|
908
|
+
turnEnded: create(TurnEndedUpdateSchema, {
|
|
909
|
+
outputTokens: BigInt(Math.ceil(outputCharacters / 4)),
|
|
910
|
+
}),
|
|
911
|
+
}),
|
|
912
|
+
})));
|
|
913
|
+
}
|
|
914
|
+
function storePendingAgentContextRun(runtime, decision) {
|
|
915
|
+
pruneStalePendingAgentContextRuns(runtime);
|
|
916
|
+
const id = runtime.nextAgentExecId;
|
|
917
|
+
runtime.nextAgentExecId += 1;
|
|
918
|
+
const pending = {
|
|
919
|
+
id,
|
|
920
|
+
execId: `cursor-rpc-context-${String(id)}`,
|
|
921
|
+
decision,
|
|
922
|
+
createdAt: Date.now(),
|
|
923
|
+
};
|
|
924
|
+
runtime.pendingAgentContextRuns.set(agentContextKey(pending), pending);
|
|
925
|
+
prunePendingMapToLimit(runtime.pendingAgentContextRuns, MAX_PENDING_AGENT_CONTEXT_RUNS);
|
|
926
|
+
runtime.logger.info("requested native agent context", {
|
|
927
|
+
id: pending.id,
|
|
928
|
+
execId: pending.execId,
|
|
929
|
+
model: decision.model.id,
|
|
930
|
+
});
|
|
931
|
+
return pending;
|
|
932
|
+
}
|
|
933
|
+
function createAgentRequestContextEnvelope(pending) {
|
|
934
|
+
const payload = Buffer.from(toBinary(AgentServerMessageSchema, create(AgentServerMessageSchema, {
|
|
935
|
+
execServerMessage: create(ExecServerMessageSchema, {
|
|
936
|
+
id: pending.id,
|
|
937
|
+
execId: pending.execId,
|
|
938
|
+
requestContextArgs: create(RequestContextArgsSchema),
|
|
939
|
+
}),
|
|
940
|
+
})));
|
|
941
|
+
return encodeEnvelope(payload);
|
|
942
|
+
}
|
|
943
|
+
function takePendingAgentContextRunFromPayloads(runtime, payloads) {
|
|
944
|
+
for (const payload of payloads) {
|
|
945
|
+
let message;
|
|
946
|
+
try {
|
|
947
|
+
message = fromBinary(AgentClientMessageSchema, payload);
|
|
948
|
+
}
|
|
949
|
+
catch {
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
const execMessage = message.execClientMessage;
|
|
953
|
+
if (execMessage?.requestContextResult === undefined) {
|
|
954
|
+
if (message.execClientMessage !== undefined) {
|
|
955
|
+
runtime.logger.debug("agent exec client message ignored", {
|
|
956
|
+
id: message.execClientMessage.id,
|
|
957
|
+
execId: message.execClientMessage.execId,
|
|
958
|
+
fields: agentExecClientMessageFields(message.execClientMessage),
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
const keyed = agentContextKey({
|
|
964
|
+
id: execMessage.id,
|
|
965
|
+
execId: execMessage.execId,
|
|
966
|
+
});
|
|
967
|
+
const pending = runtime.pendingAgentContextRuns.get(keyed) ??
|
|
968
|
+
firstPendingAgentContextRun(runtime);
|
|
969
|
+
if (pending === undefined) {
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
runtime.pendingAgentContextRuns.delete(agentContextKey(pending));
|
|
973
|
+
const requestContext = execMessage.requestContextResult.success?.requestContext;
|
|
974
|
+
if (requestContext === undefined) {
|
|
975
|
+
runtime.logger.warn("native agent context unavailable", {
|
|
976
|
+
id: execMessage.id,
|
|
977
|
+
execId: execMessage.execId,
|
|
978
|
+
error: execMessage.requestContextResult.error?.error,
|
|
979
|
+
rejected: execMessage.requestContextResult.rejected?.reason,
|
|
980
|
+
});
|
|
981
|
+
return pending.decision;
|
|
982
|
+
}
|
|
983
|
+
runtime.logger.info("received native agent context", {
|
|
984
|
+
id: execMessage.id,
|
|
985
|
+
execId: execMessage.execId,
|
|
986
|
+
rules: requestContext.rules.length + requestContext.nonFileRules.length,
|
|
987
|
+
tools: requestContext.tools.length,
|
|
988
|
+
fileContents: Object.keys(requestContext.fileContents).length,
|
|
989
|
+
gitRepos: requestContext.gitRepos.length,
|
|
990
|
+
projectLayouts: requestContext.projectLayouts.length,
|
|
991
|
+
});
|
|
992
|
+
return withNativeRequestContext(pending.decision, requestContext);
|
|
993
|
+
}
|
|
994
|
+
pruneStalePendingAgentContextRuns(runtime);
|
|
995
|
+
return undefined;
|
|
996
|
+
}
|
|
997
|
+
function firstPendingAgentContextRun(runtime) {
|
|
998
|
+
return runtime.pendingAgentContextRuns.values().next().value;
|
|
999
|
+
}
|
|
1000
|
+
function agentContextKey(value) {
|
|
1001
|
+
return `${value.execId}:${String(value.id)}`;
|
|
1002
|
+
}
|
|
1003
|
+
function pruneStalePendingAgentContextRuns(runtime) {
|
|
1004
|
+
const expiresBefore = Date.now() -
|
|
1005
|
+
(runtime.config.agentContextTimeoutMs ?? DEFAULT_AGENT_CONTEXT_TIMEOUT_MS);
|
|
1006
|
+
for (const [key, pending] of runtime.pendingAgentContextRuns) {
|
|
1007
|
+
if (pending.createdAt < expiresBefore) {
|
|
1008
|
+
runtime.pendingAgentContextRuns.delete(key);
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
function prunePendingMapToLimit(pendingMap, limit) {
|
|
1013
|
+
while (pendingMap.size > limit) {
|
|
1014
|
+
let oldestKey;
|
|
1015
|
+
let oldestCreatedAt = Number.POSITIVE_INFINITY;
|
|
1016
|
+
for (const [key, pending] of pendingMap) {
|
|
1017
|
+
if (pending.createdAt < oldestCreatedAt) {
|
|
1018
|
+
oldestKey = key;
|
|
1019
|
+
oldestCreatedAt = pending.createdAt;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
if (oldestKey === undefined) {
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
pendingMap.delete(oldestKey);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
function getLocalAgentRunDecisionFromPayloads(payloads, runtime) {
|
|
1029
|
+
for (const payload of payloads) {
|
|
1030
|
+
try {
|
|
1031
|
+
const decision = getLocalAgentRunDecision(payload, runtime.models);
|
|
1032
|
+
if (decision !== undefined) {
|
|
1033
|
+
return decision;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
catch {
|
|
1037
|
+
// Try the client-message wrapper below.
|
|
1038
|
+
}
|
|
1039
|
+
try {
|
|
1040
|
+
const decision = getLocalAgentRunDecisionFromClientMessage(payload, runtime.models);
|
|
1041
|
+
if (decision !== undefined) {
|
|
1042
|
+
return decision;
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
catch {
|
|
1046
|
+
// Keep scanning later frames.
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return undefined;
|
|
1050
|
+
}
|
|
1051
|
+
async function handleBidiAppend(runtime, request, response) {
|
|
1052
|
+
const format = modelResponseFormatForRequest(request);
|
|
1053
|
+
const body = await readRequestBody(request, runtime.config.maxInterceptBodyBytes);
|
|
1054
|
+
const payload = requestPayloadForFormat(body, format);
|
|
1055
|
+
if (payload === undefined) {
|
|
1056
|
+
await proxyBufferedRequest(request, response, body, runtime.config, runtime.logger);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const append = fromBidiAppendPayload(payload);
|
|
1060
|
+
const requestId = append?.requestId?.requestId;
|
|
1061
|
+
const decision = decodeLocalAgentRunDecisionFromAppend(append, runtime);
|
|
1062
|
+
// Tool-result append for an in-flight SSE agent run: deliver the
|
|
1063
|
+
// ExecClientMessage to the waiting tool loop instead of proxying upstream.
|
|
1064
|
+
if (decision === undefined &&
|
|
1065
|
+
requestId !== undefined &&
|
|
1066
|
+
requestId.length > 0 &&
|
|
1067
|
+
append !== undefined) {
|
|
1068
|
+
const mailbox = runtime.toolResultMailboxes.get(requestId);
|
|
1069
|
+
if (mailbox !== undefined) {
|
|
1070
|
+
const toolResult = extractExecClientPayload(append);
|
|
1071
|
+
if (toolResult !== undefined) {
|
|
1072
|
+
mailbox.push(toolResult);
|
|
1073
|
+
writeModelResponse(response, Buffer.from(toBinary(BidiAppendResponseSchema, create(BidiAppendResponseSchema))), runtime.logger, "served cursor tool result append", format);
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
if (requestId === undefined ||
|
|
1079
|
+
requestId.length === 0 ||
|
|
1080
|
+
decision === undefined) {
|
|
1081
|
+
runtime.logger.debug("bidi append did not match local agent run", {
|
|
1082
|
+
requestId,
|
|
1083
|
+
dataBytes: append?.dataBinary.length,
|
|
1084
|
+
dataChars: append?.data.length,
|
|
1085
|
+
candidates: append
|
|
1086
|
+
? bidiAppendClientPayloadCandidates(append).map((candidate) => ({
|
|
1087
|
+
bytes: candidate.length,
|
|
1088
|
+
prefixHex: Buffer.from(candidate.subarray(0, 8)).toString("hex"),
|
|
1089
|
+
descriptions: describeAgentRunPayload(candidate),
|
|
1090
|
+
}))
|
|
1091
|
+
: [],
|
|
1092
|
+
});
|
|
1093
|
+
await proxyBufferedRequest(request, response, body, runtime.config, runtime.logger);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
storePendingAgentRun(runtime, requestId, decision);
|
|
1097
|
+
writeModelResponse(response, Buffer.from(toBinary(BidiAppendResponseSchema, create(BidiAppendResponseSchema))), runtime.logger, "served local bidi append", format);
|
|
1098
|
+
}
|
|
1099
|
+
function getBidiRequestId(payload) {
|
|
1100
|
+
try {
|
|
1101
|
+
const requestId = fromBinary(BidiRequestIdSchema, payload);
|
|
1102
|
+
return requestId.requestId || undefined;
|
|
1103
|
+
}
|
|
1104
|
+
catch {
|
|
1105
|
+
return undefined;
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
function getBidiRequestIdFromPayloads(payloads) {
|
|
1109
|
+
for (const payload of payloads) {
|
|
1110
|
+
const requestId = getBidiRequestId(payload);
|
|
1111
|
+
if (requestId !== undefined) {
|
|
1112
|
+
return requestId;
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return undefined;
|
|
1116
|
+
}
|
|
1117
|
+
function requestPayloadForFormat(body, format) {
|
|
1118
|
+
if (format === "proto") {
|
|
1119
|
+
return body;
|
|
1120
|
+
}
|
|
1121
|
+
try {
|
|
1122
|
+
const envelope = firstMessageEnvelope(body);
|
|
1123
|
+
if (envelope === undefined || isCompressedEnvelope(envelope)) {
|
|
1124
|
+
return undefined;
|
|
1125
|
+
}
|
|
1126
|
+
return envelope.payload;
|
|
1127
|
+
}
|
|
1128
|
+
catch {
|
|
1129
|
+
return undefined;
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
function requestPayloadCandidatesForFormat(body, format) {
|
|
1133
|
+
if (format === "proto") {
|
|
1134
|
+
return [body];
|
|
1135
|
+
}
|
|
1136
|
+
try {
|
|
1137
|
+
const framedPayloads = decodeEnvelopes(body)
|
|
1138
|
+
.filter((envelope) => !isEndStreamEnvelope(envelope))
|
|
1139
|
+
.flatMap((envelope) => connectEnvelopePayloadCandidates(envelope.payload, isCompressedEnvelope(envelope)));
|
|
1140
|
+
return [body, ...framedPayloads];
|
|
1141
|
+
}
|
|
1142
|
+
catch {
|
|
1143
|
+
const payload = requestPayloadForFormat(body, format);
|
|
1144
|
+
return payload === undefined ? [body] : [body, payload];
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
function connectEnvelopePayloadCandidates(payload, compressed) {
|
|
1148
|
+
if (!compressed) {
|
|
1149
|
+
return [payload];
|
|
1150
|
+
}
|
|
1151
|
+
const candidates = [];
|
|
1152
|
+
for (const decode of [
|
|
1153
|
+
zlib.gunzipSync,
|
|
1154
|
+
zlib.inflateSync,
|
|
1155
|
+
zlib.brotliDecompressSync,
|
|
1156
|
+
]) {
|
|
1157
|
+
try {
|
|
1158
|
+
candidates.push(decode(payload));
|
|
1159
|
+
}
|
|
1160
|
+
catch {
|
|
1161
|
+
// Try the next compression format.
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return candidates;
|
|
1165
|
+
}
|
|
1166
|
+
async function readStreamingConnectPayloadCandidates(request, maxBytes) {
|
|
1167
|
+
const chunks = [];
|
|
1168
|
+
let totalBytes = 0;
|
|
1169
|
+
let resolved = false;
|
|
1170
|
+
return await new Promise((resolve, reject) => {
|
|
1171
|
+
let idleTimer;
|
|
1172
|
+
const cleanup = () => {
|
|
1173
|
+
if (idleTimer !== undefined) {
|
|
1174
|
+
clearTimeout(idleTimer);
|
|
1175
|
+
}
|
|
1176
|
+
request.off("data", onData);
|
|
1177
|
+
request.off("end", onEnd);
|
|
1178
|
+
request.off("error", onError);
|
|
1179
|
+
};
|
|
1180
|
+
const finish = () => {
|
|
1181
|
+
if (resolved) {
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
resolved = true;
|
|
1185
|
+
cleanup();
|
|
1186
|
+
const body = Buffer.concat(chunks, totalBytes);
|
|
1187
|
+
resolve({
|
|
1188
|
+
body,
|
|
1189
|
+
payloads: requestPayloadCandidatesForFormat(body, "connect"),
|
|
1190
|
+
});
|
|
1191
|
+
};
|
|
1192
|
+
const scheduleFinish = () => {
|
|
1193
|
+
if (idleTimer !== undefined) {
|
|
1194
|
+
clearTimeout(idleTimer);
|
|
1195
|
+
}
|
|
1196
|
+
idleTimer = setTimeout(finish, 100);
|
|
1197
|
+
};
|
|
1198
|
+
const onData = (chunk) => {
|
|
1199
|
+
chunks.push(chunk);
|
|
1200
|
+
totalBytes += chunk.byteLength;
|
|
1201
|
+
if (totalBytes > maxBytes) {
|
|
1202
|
+
cleanup();
|
|
1203
|
+
reject(new RequestBodyTooLargeError(maxBytes));
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
const parsed = parseEnvelopes(Buffer.concat(chunks, totalBytes));
|
|
1207
|
+
if (parsed.envelopes.some((envelope) => !isEndStreamEnvelope(envelope))) {
|
|
1208
|
+
scheduleFinish();
|
|
1209
|
+
}
|
|
1210
|
+
};
|
|
1211
|
+
const onEnd = () => finish();
|
|
1212
|
+
const onError = (error) => {
|
|
1213
|
+
cleanup();
|
|
1214
|
+
reject(error);
|
|
1215
|
+
};
|
|
1216
|
+
request.on("data", onData);
|
|
1217
|
+
request.on("end", onEnd);
|
|
1218
|
+
request.on("error", onError);
|
|
1219
|
+
});
|
|
1220
|
+
}
|
|
1221
|
+
async function* connectPayloadStream(request, maxBytes) {
|
|
1222
|
+
let buffered = Buffer.alloc(0);
|
|
1223
|
+
let totalBytes = 0;
|
|
1224
|
+
for await (const chunk of request) {
|
|
1225
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
1226
|
+
totalBytes += buffer.byteLength;
|
|
1227
|
+
if (totalBytes > maxBytes) {
|
|
1228
|
+
throw new RequestBodyTooLargeError(maxBytes);
|
|
1229
|
+
}
|
|
1230
|
+
buffered = Buffer.concat([buffered, buffer], buffered.length + buffer.length);
|
|
1231
|
+
const parsed = parseEnvelopes(buffered);
|
|
1232
|
+
buffered = Buffer.from(parsed.remainder);
|
|
1233
|
+
for (const envelope of parsed.envelopes) {
|
|
1234
|
+
if (isEndStreamEnvelope(envelope)) {
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
yield* connectEnvelopePayloadCandidates(envelope.payload, isCompressedEnvelope(envelope));
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
function fromBidiAppendPayload(payload) {
|
|
1242
|
+
try {
|
|
1243
|
+
return fromBinary(BidiAppendRequestSchema, payload);
|
|
1244
|
+
}
|
|
1245
|
+
catch {
|
|
1246
|
+
return undefined;
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
function decodeLocalAgentRunDecisionFromAppend(append, runtime) {
|
|
1250
|
+
if (append === undefined) {
|
|
1251
|
+
return undefined;
|
|
1252
|
+
}
|
|
1253
|
+
for (const candidate of bidiAppendClientPayloadCandidates(append)) {
|
|
1254
|
+
try {
|
|
1255
|
+
const decision = getLocalAgentRunDecisionFromClientMessage(candidate, runtime.models);
|
|
1256
|
+
if (decision !== undefined) {
|
|
1257
|
+
return decision;
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
catch {
|
|
1261
|
+
// Try the raw run-request shape below.
|
|
1262
|
+
}
|
|
1263
|
+
try {
|
|
1264
|
+
const decision = getLocalAgentRunDecision(Buffer.from(candidate), runtime.models);
|
|
1265
|
+
if (decision !== undefined) {
|
|
1266
|
+
return decision;
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
catch {
|
|
1270
|
+
continue;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
return undefined;
|
|
1274
|
+
}
|
|
1275
|
+
function extractExecClientPayload(append) {
|
|
1276
|
+
for (const candidate of bidiAppendClientPayloadCandidates(append)) {
|
|
1277
|
+
try {
|
|
1278
|
+
const message = fromBinary(AgentClientMessageSchema, candidate);
|
|
1279
|
+
if (message.execClientMessage !== undefined) {
|
|
1280
|
+
return Buffer.from(candidate);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
catch {
|
|
1284
|
+
continue;
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
1287
|
+
return undefined;
|
|
1288
|
+
}
|
|
1289
|
+
function bidiAppendClientPayloadCandidates(append) {
|
|
1290
|
+
const candidates = [];
|
|
1291
|
+
if (append.dataBinary.length > 0) {
|
|
1292
|
+
candidates.push(append.dataBinary);
|
|
1293
|
+
try {
|
|
1294
|
+
candidates.push(firstMessagePayload(Buffer.from(append.dataBinary)));
|
|
1295
|
+
}
|
|
1296
|
+
catch {
|
|
1297
|
+
// Not a nested Connect frame.
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (append.data.length > 0) {
|
|
1301
|
+
const hex = decodeHexPayload(append.data);
|
|
1302
|
+
if (hex !== undefined) {
|
|
1303
|
+
candidates.push(hex);
|
|
1304
|
+
}
|
|
1305
|
+
candidates.push(Buffer.from(append.data, "base64"));
|
|
1306
|
+
candidates.push(Buffer.from(append.data, "utf8"));
|
|
1307
|
+
candidates.push(Buffer.from(append.data, "latin1"));
|
|
1308
|
+
}
|
|
1309
|
+
return candidates;
|
|
1310
|
+
}
|
|
1311
|
+
function decodeHexPayload(data) {
|
|
1312
|
+
if (data.length % 2 !== 0 || !/^[0-9a-fA-F]+$/.test(data)) {
|
|
1313
|
+
return undefined;
|
|
1314
|
+
}
|
|
1315
|
+
return Buffer.from(data, "hex");
|
|
1316
|
+
}
|
|
1317
|
+
async function waitForPendingAgentRun(runtime, requestId, signal) {
|
|
1318
|
+
const deadline = Date.now() +
|
|
1319
|
+
(runtime.config.agentRunSseWaitTimeoutMs ??
|
|
1320
|
+
DEFAULT_AGENT_RUN_SSE_WAIT_TIMEOUT_MS);
|
|
1321
|
+
while (Date.now() < deadline) {
|
|
1322
|
+
if (signal.aborted) {
|
|
1323
|
+
return undefined;
|
|
1324
|
+
}
|
|
1325
|
+
pruneStalePendingAgentRuns(runtime);
|
|
1326
|
+
const pending = runtime.pendingAgentRuns.get(requestId);
|
|
1327
|
+
if (pending !== undefined) {
|
|
1328
|
+
runtime.pendingAgentRuns.delete(requestId);
|
|
1329
|
+
return pending.decision;
|
|
1330
|
+
}
|
|
1331
|
+
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
1332
|
+
}
|
|
1333
|
+
pruneStalePendingAgentRuns(runtime);
|
|
1334
|
+
return undefined;
|
|
1335
|
+
}
|
|
1336
|
+
function storePendingAgentRun(runtime, requestId, decision) {
|
|
1337
|
+
pruneStalePendingAgentRuns(runtime);
|
|
1338
|
+
runtime.pendingAgentRuns.set(requestId, {
|
|
1339
|
+
decision,
|
|
1340
|
+
createdAt: Date.now(),
|
|
1341
|
+
});
|
|
1342
|
+
prunePendingMapToLimit(runtime.pendingAgentRuns, MAX_PENDING_AGENT_RUNS);
|
|
1343
|
+
}
|
|
1344
|
+
function pruneStalePendingAgentRuns(runtime) {
|
|
1345
|
+
const expiresBefore = Date.now() -
|
|
1346
|
+
(runtime.config.agentRunSseWaitTimeoutMs ??
|
|
1347
|
+
DEFAULT_AGENT_RUN_SSE_WAIT_TIMEOUT_MS);
|
|
1348
|
+
for (const [key, pending] of runtime.pendingAgentRuns) {
|
|
1349
|
+
if (pending.createdAt < expiresBefore) {
|
|
1350
|
+
runtime.pendingAgentRuns.delete(key);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
}
|