@bitkyc08/opencodex 2.8.0 → 2.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/README.md +24 -0
  2. package/bin/ocx.mjs +32 -4
  3. package/gui/dist/assets/index-CHwf3tTD.css +1 -0
  4. package/gui/dist/assets/index-u5eFOv2y.js +67 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/anthropic-image-normalize.ts +114 -20
  8. package/src/adapters/anthropic.ts +126 -10
  9. package/src/adapters/azure.ts +3 -3
  10. package/src/adapters/base.ts +7 -3
  11. package/src/adapters/cursor/discovery.ts +14 -4
  12. package/src/adapters/cursor/effort-map.ts +18 -6
  13. package/src/adapters/cursor/framing.ts +102 -27
  14. package/src/adapters/cursor/kv-store.ts +30 -3
  15. package/src/adapters/cursor/live-models.ts +22 -2
  16. package/src/adapters/cursor/live-transport.ts +245 -49
  17. package/src/adapters/cursor/mcp-manager.ts +105 -8
  18. package/src/adapters/cursor/native-exec-mcp.ts +5 -3
  19. package/src/adapters/cursor/native-exec-shell.ts +296 -14
  20. package/src/adapters/cursor/native-exec.ts +381 -33
  21. package/src/adapters/cursor/protobuf-events.ts +28 -1
  22. package/src/adapters/cursor/protobuf-request.ts +71 -39
  23. package/src/adapters/cursor/request-builder.ts +2 -2
  24. package/src/adapters/cursor/transport.ts +2 -0
  25. package/src/adapters/cursor.ts +13 -2
  26. package/src/adapters/google-antigravity-replay.ts +184 -17
  27. package/src/adapters/google.ts +58 -8
  28. package/src/adapters/kiro-thinking.ts +23 -9
  29. package/src/adapters/kiro-tools.ts +49 -18
  30. package/src/adapters/kiro.ts +377 -133
  31. package/src/adapters/mimo-free.ts +36 -4
  32. package/src/adapters/openai-chat.ts +143 -17
  33. package/src/adapters/openai-responses.ts +130 -14
  34. package/src/adapters/run-turn-queue.ts +7 -1
  35. package/src/bridge.ts +466 -69
  36. package/src/chat/outbound.ts +144 -38
  37. package/src/claude/inbound-debug.ts +53 -8
  38. package/src/claude/outbound.ts +224 -38
  39. package/src/cli/agent-driven.ts +34 -1
  40. package/src/cli/catalog-prewarm.ts +5 -2
  41. package/src/cli/claude-desktop.ts +2 -2
  42. package/src/cli/doctor.ts +12 -0
  43. package/src/cli/export-command.ts +187 -0
  44. package/src/cli/help.ts +11 -0
  45. package/src/cli/index.ts +13 -3
  46. package/src/cli/init.ts +129 -102
  47. package/src/cli/opencode.ts +36 -151
  48. package/src/cli/star-prompt.ts +13 -4
  49. package/src/cli/status-oauth.ts +12 -2
  50. package/src/clients/config-export.ts +377 -0
  51. package/src/codex/account-runtime-state.ts +19 -1
  52. package/src/codex/account-store.ts +162 -82
  53. package/src/codex/auth-api.ts +467 -159
  54. package/src/codex/auth-context.ts +15 -2
  55. package/src/codex/catalog/aggregation.ts +15 -0
  56. package/src/codex/catalog/effort.ts +16 -6
  57. package/src/codex/catalog/metadata.ts +6 -0
  58. package/src/codex/catalog/parsing.ts +3 -1
  59. package/src/codex/catalog/provider-fetch.ts +29 -0
  60. package/src/codex/catalog/sync.ts +64 -7
  61. package/src/codex/catalog.ts +2 -2
  62. package/src/codex/inject.ts +5 -5
  63. package/src/codex/main-account-cache.ts +8 -1
  64. package/src/codex/model-cache.ts +81 -2
  65. package/src/codex/pool-rotation.ts +39 -0
  66. package/src/codex/project-config-warnings.ts +12 -1
  67. package/src/codex/quota.ts +35 -3
  68. package/src/codex/routing.ts +46 -1
  69. package/src/codex/shim.ts +10 -4
  70. package/src/codex/subagent-model-fallback.ts +12 -0
  71. package/src/codex/websocket-registry.ts +27 -0
  72. package/src/combos/failover.ts +31 -1
  73. package/src/combos/request.ts +9 -0
  74. package/src/combos/resolve.ts +60 -4
  75. package/src/combos/types.ts +12 -0
  76. package/src/config.ts +510 -55
  77. package/src/github/star-state.ts +13 -1
  78. package/src/images/fulfill.ts +39 -1
  79. package/src/images/loop.ts +52 -12
  80. package/src/lib/admission.ts +83 -0
  81. package/src/lib/app-owned-memory-stores.ts +173 -0
  82. package/src/lib/app-owned-memory.ts +265 -0
  83. package/src/lib/bun-stream-caps.ts +31 -7
  84. package/src/lib/config-ownership.ts +33 -0
  85. package/src/lib/crash-guard.ts +65 -5
  86. package/src/lib/debug-log-buffer.ts +47 -6
  87. package/src/lib/destination-policy.ts +12 -1
  88. package/src/lib/errors.ts +3 -0
  89. package/src/lib/gcp-adc.ts +40 -2
  90. package/src/lib/injection-debug-log.ts +26 -2
  91. package/src/lib/provider-outbound.ts +3 -0
  92. package/src/lib/sidecar-tracker.ts +5 -2
  93. package/src/lib/sse-decoder.ts +257 -37
  94. package/src/lib/state-store-registrations.ts +109 -0
  95. package/src/lib/state-store-sweeper.ts +184 -0
  96. package/src/lib/translator-budget.ts +356 -0
  97. package/src/lib/windows-secret-acl.ts +33 -12
  98. package/src/lib/winsw.ts +14 -1
  99. package/src/oauth/anthropic-routing.ts +31 -7
  100. package/src/oauth/google-antigravity.ts +2 -1
  101. package/src/oauth/health.ts +30 -12
  102. package/src/oauth/index.ts +127 -23
  103. package/src/oauth/kiro-credentials.ts +72 -1
  104. package/src/oauth/kiro.ts +23 -4
  105. package/src/oauth/store.ts +165 -18
  106. package/src/oauth/token-guardian.ts +43 -4
  107. package/src/oauth/types.ts +2 -1
  108. package/src/providers/base-url-choices.ts +10 -0
  109. package/src/providers/derive.ts +12 -0
  110. package/src/providers/free-directory.ts +4 -1
  111. package/src/providers/key-failover.ts +12 -0
  112. package/src/providers/openai-sidecar.ts +4 -1
  113. package/src/providers/quota.ts +68 -7
  114. package/src/providers/registry.ts +279 -3
  115. package/src/responses/parser.ts +5 -1
  116. package/src/responses/spill-store.ts +394 -0
  117. package/src/responses/state.ts +520 -102
  118. package/src/router.ts +18 -1
  119. package/src/server/adapter-resolve.ts +20 -3
  120. package/src/server/auth-cors.ts +121 -28
  121. package/src/server/chat-completions.ts +57 -12
  122. package/src/server/claude-messages.ts +85 -13
  123. package/src/server/index.ts +242 -100
  124. package/src/server/lifecycle.ts +155 -25
  125. package/src/server/management/agent-settings-routes.ts +79 -36
  126. package/src/server/management/api-key-usage.ts +167 -0
  127. package/src/server/management/body.ts +35 -0
  128. package/src/server/management/combo-routes.ts +5 -1
  129. package/src/server/management/config-routes.ts +42 -12
  130. package/src/server/management/logs-usage-routes.ts +41 -21
  131. package/src/server/management/model-routes.ts +188 -54
  132. package/src/server/management/oauth-account-routes.ts +115 -26
  133. package/src/server/management/provider-routes.ts +56 -6
  134. package/src/server/management/shared.ts +16 -3
  135. package/src/server/management/sidebar-routes.ts +50 -1
  136. package/src/server/management/system-restart.ts +13 -6
  137. package/src/server/management/system-routes.ts +15 -3
  138. package/src/server/management/usage-summary-cache.ts +86 -0
  139. package/src/server/management-api.ts +39 -5
  140. package/src/server/management-auth.ts +65 -14
  141. package/src/server/port-reclaim.ts +58 -12
  142. package/src/server/ports.ts +2 -0
  143. package/src/server/proxy-liveness.ts +60 -14
  144. package/src/server/relay-eager.ts +20 -4
  145. package/src/server/relay.ts +548 -154
  146. package/src/server/request-decompress.ts +51 -4
  147. package/src/server/request-log.ts +134 -15
  148. package/src/server/responses/collaboration.ts +15 -4
  149. package/src/server/responses/compact.ts +3 -0
  150. package/src/server/responses/core.ts +241 -66
  151. package/src/server/responses-image-gen-repair.ts +19 -5
  152. package/src/server/responses-item-id-repair.ts +23 -5
  153. package/src/server/sse-payload-rewrite.ts +71 -12
  154. package/src/server/startup-health-cache.ts +14 -1
  155. package/src/server/system-env.ts +8 -1
  156. package/src/server/windows-tcp-drop.ts +15 -5
  157. package/src/server/ws-bridge.ts +25 -0
  158. package/src/service.ts +179 -13
  159. package/src/storage/policy-job.ts +93 -23
  160. package/src/storage/policy-worker.ts +6 -0
  161. package/src/storage/restore-job.ts +62 -16
  162. package/src/storage/restore-worker.ts +6 -0
  163. package/src/storage/storage-mutation-coordinator.ts +36 -6
  164. package/src/storage/worker-lifecycle.ts +181 -47
  165. package/src/tray/windows.ts +97 -25
  166. package/src/types.ts +39 -6
  167. package/src/update/index.ts +24 -5
  168. package/src/update/job.ts +598 -73
  169. package/src/usage/log.ts +115 -17
  170. package/src/usage/summary.ts +67 -2
  171. package/src/vision/index.ts +112 -22
  172. package/src/web-search/loop.ts +38 -6
  173. package/src/web-search/progress-stream.ts +14 -3
  174. package/gui/dist/assets/index-BDjpkcRN.js +0 -67
  175. package/gui/dist/assets/index-BHsKRFh9.css +0 -1
@@ -2,6 +2,16 @@ import http2 from "node:http2";
2
2
  import { create, fromBinary, toBinary } from "@bufbuild/protobuf";
3
3
  import { namespacedToolName, type OcxProviderConfig, type OcxUsage } from "../../types";
4
4
  import { CONNECT_FLAG_END_STREAM, decodeAvailableConnectFrames, encodeConnectFrame } from "./framing";
5
+ import {
6
+ CURSOR_MAX_EFFECTIVE_CONNECT_PAYLOAD_BYTES,
7
+ CURSOR_MAX_CONNECT_FRAME_BYTES,
8
+ CURSOR_MAX_PENDING_FRAMES,
9
+ CURSOR_PENDING_FRAMES_RESUME,
10
+ CURSOR_TRANSPORT_MAX_BUFFERED_BYTES,
11
+ CURSOR_TRANSPORT_RESUME_BYTES,
12
+ TranslatorBudgetExceededError,
13
+ type TranslatorBudget,
14
+ } from "../../lib/translator-budget";
5
15
  import { activePromptText, prepareCursorRunRequest } from "./protobuf-request";
6
16
  import {
7
17
  createCursorContextUsageTracker,
@@ -41,7 +51,13 @@ import { debugProviderDiagnostic } from "../../lib/debug";
41
51
  import { classifyCursorError, isCursorBenignCancelError, safeCursorErrorMessage } from "./cursor-errors";
42
52
  import { mcpArgsFromToolCall } from "./protobuf-events";
43
53
  import { OCX_RESPONSES_TOOL_PROVIDER } from "./tool-definitions";
44
- import { handleCursorNativeExec, handleCursorNativeKv, type CursorNativeExecContext } from "./native-exec";
54
+ import {
55
+ handleCursorNativeExec,
56
+ handleCursorNativeKv,
57
+ releaseCursorBlobRequestScope,
58
+ type CursorBlobRequestScopeToken,
59
+ type CursorNativeExecContext,
60
+ } from "./native-exec";
45
61
  import { effectiveCursorNativeExecAllow } from "./exec-policy";
46
62
  import { resolveMcpServers } from "./mcp-config";
47
63
  import { CursorMcpManager } from "./mcp-manager";
@@ -58,6 +74,10 @@ import {
58
74
  requestedCursorToolUseCount,
59
75
  } from "./tool-definitions";
60
76
  import type { CursorNativeToolDeps } from "./native-exec-tools";
77
+ import {
78
+ terminateBackgroundShellsForSession,
79
+ type BackgroundShellTerminationReport,
80
+ } from "./native-exec-shell";
61
81
  import type { CursorClientMessage, CursorRunRequest, CursorServerMessage } from "./types";
62
82
  import type { CursorTransport, CursorTransportFactoryInput } from "./transport";
63
83
 
@@ -394,9 +414,15 @@ class LiveCursorTransport implements CursorTransport {
394
414
  private activeClientToolFinalizeGraceMs: number;
395
415
  private readonly token: string;
396
416
  private readonly mcpManager?: CursorMcpManager;
417
+ private readonly translatorBudget: TranslatorBudget;
418
+ private pendingTransportFrames = 0;
419
+ private transportBufferedBytes = 0;
397
420
  private readonly desktopDeps: CursorNativeToolDeps;
398
421
  private execContext: CursorNativeExecContext = {};
399
422
  private mcpPrepared?: Promise<void>;
423
+ private releaseMcpObservation?: () => void;
424
+ private blobRequestScope?: CursorBlobRequestScopeToken;
425
+ private shellCleanup?: Promise<BackgroundShellTerminationReport>;
400
426
  // Per-turn diagnostic counters/timestamps when provider debug is on (`ocx debug provider on`). Stamped in open(), cleared on
401
427
  // close; safe to read after a stream failure because open() owns the only writer before run().
402
428
  private turnStartedAt = 0;
@@ -407,6 +433,7 @@ class LiveCursorTransport implements CursorTransport {
407
433
  private readonly sessionId = crypto.randomUUID();
408
434
 
409
435
  constructor(private readonly input: CursorTransportFactoryInput) {
436
+ this.translatorBudget = input.translatorBudget;
410
437
  this.token = resolveCursorToken(input.provider, input.headers);
411
438
  // Grace window before a drained client-tool turn is finalized. Small enough not to look like a
412
439
  // stall, large enough to catch a sibling tool call announced in the next receive chunk. Injectable
@@ -415,11 +442,18 @@ class LiveCursorTransport implements CursorTransport {
415
442
  this.activeClientToolFinalizeGraceMs = this.clientToolFinalizeGraceMs;
416
443
  // Desktop (computer-use / record-screen) executors are available even with no MCP servers.
417
444
  this.desktopDeps = desktopDepsFromConfig(input.provider.desktopExecutor);
418
- this.execContext = { ...this.desktopDeps, unsafeAllowNativeLocalExec: effectiveCursorNativeExecAllow(input.provider, input.requestDeclaresFullAccess === true) };
445
+ this.execContext = {
446
+ ...this.desktopDeps,
447
+ sessionId: this.sessionId,
448
+ unsafeAllowNativeLocalExec: effectiveCursorNativeExecAllow(input.provider, input.requestDeclaresFullAccess === true),
449
+ };
419
450
  const servers = resolveMcpServers(input.provider);
420
451
  if (servers.length > 0) {
421
452
  this.mcpManager = new CursorMcpManager(servers, {
422
453
  log: message => console.warn(message),
454
+ maxTools: input.provider.mcpMaxTools,
455
+ maxSchemaBytes: input.provider.mcpMaxSchemaBytes,
456
+ maxResultBytes: input.provider.mcpMaxResultBytes,
423
457
  });
424
458
  }
425
459
  }
@@ -436,10 +470,16 @@ class LiveCursorTransport implements CursorTransport {
436
470
  this.mcpPrepared = (async () => {
437
471
  try {
438
472
  const mcpToolDefs = await buildMcpToolDefinitions(this.mcpManager!);
473
+ this.releaseMcpObservation?.();
474
+ this.releaseMcpObservation = this.translatorBudget.observeExternallyCapped(
475
+ "mcp_payload",
476
+ new TextEncoder().encode(JSON.stringify(mcpToolDefs)).byteLength,
477
+ );
439
478
  this.execContext = {
440
479
  ...this.desktopDeps,
441
480
  ...mcpDepsFromManager(this.mcpManager!),
442
481
  mcpToolDefs,
482
+ sessionId: this.sessionId,
443
483
  unsafeAllowNativeLocalExec: effectiveCursorNativeExecAllow(this.input.provider, this.input.requestDeclaresFullAccess === true),
444
484
  };
445
485
  } catch (err) {
@@ -455,11 +495,11 @@ class LiveCursorTransport implements CursorTransport {
455
495
  }
456
496
 
457
497
  async *run(request: CursorRunRequest, signal?: AbortSignal): AsyncIterable<CursorServerMessage> {
458
- const queue: CursorServerMessage[] = [];
498
+ const queue: Array<{ message: CursorServerMessage; bytes: number }> = [];
459
499
  let notify: (() => void) | undefined;
460
500
  let done = false;
461
501
  let failure: Error | undefined;
462
- let state = createCursorProtobufEventState();
502
+ let state = createCursorProtobufEventState({ translatorBudget: this.translatorBudget });
463
503
  let failureLogged = false;
464
504
  // One per-turn summary of the failure path (end-stream error, socket reset, abort) so the
465
505
  // operator can see how far the turn got and how it was classified without re-scanning every
@@ -488,7 +528,9 @@ class LiveCursorTransport implements CursorTransport {
488
528
  };
489
529
 
490
530
  const push = (message: CursorServerMessage) => {
491
- queue.push(message);
531
+ const bytes = new TextEncoder().encode(JSON.stringify(message)).byteLength;
532
+ this.reserveTransportBytes(bytes);
533
+ queue.push({ message, bytes });
492
534
  wake();
493
535
  };
494
536
 
@@ -522,29 +564,40 @@ class LiveCursorTransport implements CursorTransport {
522
564
  const prepared = prepareCursorRunRequest(request, {
523
565
  estimateInputTokens: contextUsage.carryForwardTokens === undefined,
524
566
  });
525
- state = createCursorProtobufEventState({
526
- clientToolNames: clientToolDefs.map(tool => tool.toolName || tool.name),
527
- parallelToolCalls: request.parallelToolCalls,
528
- toolSchemas,
529
- cursorToolNameMap,
530
- contextUsage,
531
- ...(prepared.estimatedInputTokens !== undefined
532
- ? { estimatedInputTokens: prepared.estimatedInputTokens }
533
- : {}),
534
- });
535
-
536
- this.open(prepared.bytes, signal, state, push, err => {
537
- failure = err;
538
- wake();
539
- }, () => {
540
- done = true;
541
- wake();
542
- });
567
+ this.blobRequestScope = prepared.blobRequestScope;
568
+ try {
569
+ state = createCursorProtobufEventState({
570
+ clientToolNames: clientToolDefs.map(tool => tool.toolName || tool.name),
571
+ parallelToolCalls: request.parallelToolCalls,
572
+ toolSchemas,
573
+ cursorToolNameMap,
574
+ translatorBudget: this.translatorBudget,
575
+ contextUsage,
576
+ ...(prepared.estimatedInputTokens !== undefined
577
+ ? { estimatedInputTokens: prepared.estimatedInputTokens }
578
+ : {}),
579
+ });
580
+ this.open(prepared.bytes, signal, state, push, err => {
581
+ this.releaseBlobRequestScope();
582
+ failure = err;
583
+ wake();
584
+ }, () => {
585
+ this.releaseBlobRequestScope();
586
+ done = true;
587
+ wake();
588
+ });
589
+ } catch (error) {
590
+ this.releaseBlobRequestScope();
591
+ throw error;
592
+ }
543
593
 
544
594
  while (!done || queue.length > 0) {
545
595
  while (queue.length > 0) {
546
- const message = queue.shift();
547
- if (message) yield message;
596
+ const queued = queue.shift();
597
+ if (queued) {
598
+ this.releaseTransportBytes(queued.bytes);
599
+ yield queued.message;
600
+ }
548
601
  }
549
602
  if (failure) {
550
603
  // A CANCEL is benign only on the client-tool suspend path (expectedClose); an
@@ -565,6 +618,35 @@ class LiveCursorTransport implements CursorTransport {
565
618
 
566
619
  writeClient(_message: CursorClientMessage): void {}
567
620
 
621
+ private reserveTransportBytes(bytes: number): void {
622
+ if (this.transportBufferedBytes + bytes > CURSOR_TRANSPORT_MAX_BUFFERED_BYTES) {
623
+ throw new TranslatorBudgetExceededError("cursor_transport", CURSOR_TRANSPORT_MAX_BUFFERED_BYTES);
624
+ }
625
+ this.translatorBudget.chargeRetained(bytes, { kind: "cursor_transport" });
626
+ this.transportBufferedBytes += bytes;
627
+ this.updateTransportFlowControl();
628
+ }
629
+
630
+ private releaseTransportBytes(bytes: number): void {
631
+ this.transportBufferedBytes = Math.max(0, this.transportBufferedBytes - bytes);
632
+ this.translatorBudget.releaseRetained(bytes, { kind: "cursor_transport" });
633
+ this.updateTransportFlowControl();
634
+ }
635
+
636
+ private updateTransportFlowControl(): void {
637
+ if (
638
+ this.transportBufferedBytes >= CURSOR_TRANSPORT_MAX_BUFFERED_BYTES
639
+ || this.pendingTransportFrames >= CURSOR_MAX_PENDING_FRAMES
640
+ ) {
641
+ this.stream?.pause();
642
+ return;
643
+ }
644
+ if (
645
+ this.transportBufferedBytes <= CURSOR_TRANSPORT_RESUME_BYTES
646
+ && this.pendingTransportFrames <= CURSOR_PENDING_FRAMES_RESUME
647
+ ) this.stream?.resume();
648
+ }
649
+
568
650
  requestCommitted(): boolean {
569
651
  return this.committed;
570
652
  }
@@ -576,13 +658,21 @@ class LiveCursorTransport implements CursorTransport {
576
658
  }
577
659
  }
578
660
 
579
- close(): void {
661
+ private startShellCleanup(): Promise<BackgroundShellTerminationReport> {
662
+ return this.shellCleanup ??= terminateBackgroundShellsForSession(this.sessionId);
663
+ }
664
+
665
+ async close(): Promise<void> {
580
666
  if (this.heartbeat) clearInterval(this.heartbeat);
581
667
  this.clearPendingFinalize();
582
668
  this.clearFirstFrameTimer();
583
669
  this.stream?.close();
584
670
  this.session?.close();
671
+ this.releaseBlobRequestScope();
672
+ this.releaseMcpObservation?.();
673
+ this.releaseMcpObservation = undefined;
585
674
  void this.mcpManager?.dispose();
675
+ await this.startShellCleanup();
586
676
  }
587
677
 
588
678
  private cancelCursorRun(): void {
@@ -596,7 +686,18 @@ class LiveCursorTransport implements CursorTransport {
596
686
  this.stream?.destroy();
597
687
  }
598
688
  this.session?.close();
689
+ this.releaseBlobRequestScope();
690
+ this.releaseMcpObservation?.();
691
+ this.releaseMcpObservation = undefined;
599
692
  void this.mcpManager?.dispose();
693
+ void this.startShellCleanup().catch(() => { /* close() observes the same cleanup promise */ });
694
+ }
695
+
696
+ private releaseBlobRequestScope(): void {
697
+ const scope = this.blobRequestScope;
698
+ if (!scope) return;
699
+ this.blobRequestScope = undefined;
700
+ releaseCursorBlobRequestScope(scope);
600
701
  }
601
702
 
602
703
  private clearPendingFinalize(): void {
@@ -725,6 +826,71 @@ class LiveCursorTransport implements CursorTransport {
725
826
  }, this.input.firstFrameTimeoutMs ?? CURSOR_FIRST_FRAME_TIMEOUT_MS);
726
827
 
727
828
  let pending: Uint8Array<ArrayBufferLike> = new Uint8Array();
829
+ let frameWork: Promise<void> = Promise.resolve();
830
+ const reservePayloadCopy = (bytes: number) => {
831
+ if (this.transportBufferedBytes + bytes > CURSOR_TRANSPORT_MAX_BUFFERED_BYTES) {
832
+ throw new TranslatorBudgetExceededError("cursor_transport", CURSOR_TRANSPORT_MAX_BUFFERED_BYTES);
833
+ }
834
+ const reservation = this.translatorBudget.reserveTransient(bytes, { kind: "cursor_transport" });
835
+ this.transportBufferedBytes += bytes;
836
+ this.updateTransportFlowControl();
837
+ return {
838
+ commitRetained: () => reservation.commitRetained(),
839
+ release: () => {
840
+ reservation.release();
841
+ this.transportBufferedBytes = Math.max(0, this.transportBufferedBytes - bytes);
842
+ this.updateTransportFlowControl();
843
+ },
844
+ };
845
+ };
846
+ const handleFrame = async (frame: ReturnType<typeof decodeAvailableConnectFrames>["frames"][number]) => {
847
+ this.framesReceived++;
848
+ if ((frame.flags & CONNECT_FLAG_END_STREAM) === CONNECT_FLAG_END_STREAM) {
849
+ const endError = parseConnectEndStreamError(frame.payload);
850
+ debugProviderDiagnostic("cursor", "connect-end-stream", endError ? {
851
+ code: cursorConnectErrorCode(frame.payload),
852
+ message: redactCursorForLog(endError.message),
853
+ classified: classifyCursorError(endError.message),
854
+ framesReceived: this.framesReceived,
855
+ elapsedMs: Date.now() - this.turnStartedAt,
856
+ } : { framesReceived: this.framesReceived, elapsedMs: Date.now() - this.turnStartedAt });
857
+ if (endError) failAndClear(endError);
858
+ return;
859
+ }
860
+ await this.handleServerMessage(fromBinary(AgentServerMessageSchema, frame.payload), state, push);
861
+ };
862
+ const drainPendingFrames = () => {
863
+ const availableSlots = CURSOR_MAX_PENDING_FRAMES - this.pendingTransportFrames;
864
+ if (availableSlots <= 0 || pending.byteLength === 0) {
865
+ this.updateTransportFlowControl();
866
+ return;
867
+ }
868
+ const previousPayloadBytes = connectBufferedPayloadBytes(pending);
869
+ // The decoder materializes payload and residual copies. Keep the aggregate pending owner
870
+ // charged until every replacement has been admitted and committed; a failed admission must
871
+ // leave that predecessor lease intact for deterministic cleanup.
872
+ const decoded = decodeAvailableConnectFrames(
873
+ pending,
874
+ CURSOR_MAX_EFFECTIVE_CONNECT_PAYLOAD_BYTES,
875
+ availableSlots,
876
+ reservePayloadCopy,
877
+ );
878
+ pending = decoded.remainder;
879
+ this.releaseTransportBytes(previousPayloadBytes);
880
+ for (const frame of decoded.frames) {
881
+ this.pendingTransportFrames += 1;
882
+ this.updateTransportFlowControl();
883
+ frameWork = frameWork
884
+ .then(() => handleFrame(frame))
885
+ .catch(err => failAndClear(err instanceof Error ? err : new Error(String(err))))
886
+ .finally(() => {
887
+ this.releaseTransportBytes(frame.payload.byteLength);
888
+ this.pendingTransportFrames = Math.max(0, this.pendingTransportFrames - 1);
889
+ this.updateTransportFlowControl();
890
+ drainPendingFrames();
891
+ });
892
+ }
893
+ };
728
894
  this.stream.on("data", chunk => {
729
895
  this.clearFirstFrameTimer();
730
896
  if (!this.firstFrameLogged) {
@@ -733,30 +899,26 @@ class LiveCursorTransport implements CursorTransport {
733
899
  debugProviderDiagnostic("cursor", "first-frame", { latencyMs: this.firstFrameAt - this.turnStartedAt });
734
900
  }
735
901
  const bytes = typeof chunk === "string" ? new TextEncoder().encode(chunk) : new Uint8Array(chunk.buffer, chunk.byteOffset, chunk.byteLength);
736
- pending = concatBytes(pending, bytes);
902
+ let incomingPayloadBytes = 0;
903
+ let incomingCharged = false;
904
+ let replacement: ReturnType<typeof reservePayloadCopy> | undefined;
737
905
  try {
738
- const decoded = decodeAvailableConnectFrames(pending);
739
- pending = decoded.remainder;
740
- const frames = decoded.frames;
741
- for (const frame of frames) {
742
- this.framesReceived++;
743
- if ((frame.flags & CONNECT_FLAG_END_STREAM) === CONNECT_FLAG_END_STREAM) {
744
- const endError = parseConnectEndStreamError(frame.payload);
745
- debugProviderDiagnostic("cursor", "connect-end-stream", endError ? {
746
- code: cursorConnectErrorCode(frame.payload),
747
- message: redactCursorForLog(endError.message),
748
- classified: classifyCursorError(endError.message),
749
- framesReceived: this.framesReceived,
750
- elapsedMs: Date.now() - this.turnStartedAt,
751
- } : { framesReceived: this.framesReceived, elapsedMs: Date.now() - this.turnStartedAt });
752
- if (endError) failAndClear(endError);
753
- continue;
754
- }
755
- void this.handleServerMessage(fromBinary(AgentServerMessageSchema, frame.payload), state, push).catch(err => {
756
- failAndClear(err instanceof Error ? err : new Error(String(err)));
757
- });
758
- }
906
+ const previousPendingPayloadBytes = connectBufferedPayloadBytes(pending);
907
+ const nextPendingPayloadBytes = connectBufferedPayloadBytesAcross(pending, bytes);
908
+ incomingPayloadBytes = Math.max(0, nextPendingPayloadBytes - previousPendingPayloadBytes);
909
+ this.reserveTransportBytes(incomingPayloadBytes);
910
+ incomingCharged = true;
911
+ replacement = reservePayloadCopy(nextPendingPayloadBytes);
912
+ const nextPending = concatBytes(pending, bytes);
913
+ pending = nextPending;
914
+ replacement.commitRetained();
915
+ replacement = undefined;
916
+ this.releaseTransportBytes(previousPendingPayloadBytes + incomingPayloadBytes);
917
+ incomingCharged = false;
918
+ drainPendingFrames();
759
919
  } catch (err) {
920
+ replacement?.release();
921
+ if (incomingCharged) this.releaseTransportBytes(incomingPayloadBytes);
760
922
  failAndClear(err instanceof Error ? err : new Error(String(err)));
761
923
  }
762
924
  });
@@ -824,7 +986,7 @@ class LiveCursorTransport implements CursorTransport {
824
986
  if (!this.stream) return;
825
987
  debugProviderDiagnostic("cursor", "frame", describeCursorServerFrame(message));
826
988
  if (message.message.case === "kvServerMessage") {
827
- this.stream.write(encodeConnectFrame(handleCursorNativeKv(message.message.value)));
989
+ this.stream.write(encodeConnectFrame(handleCursorNativeKv(message.message.value, this.blobRequestScope)));
828
990
  return;
829
991
  }
830
992
  if (message.message.case === "execServerMessage") {
@@ -985,6 +1147,40 @@ function concatBytes(a: Uint8Array, b: Uint8Array): Uint8Array {
985
1147
  return out;
986
1148
  }
987
1149
 
1150
+ function connectBufferedPayloadBytes(input: Uint8Array): number {
1151
+ let offset = 0;
1152
+ let payloadBytes = 0;
1153
+ while (input.byteLength - offset >= 5) {
1154
+ const length = new DataView(input.buffer, input.byteOffset + offset, input.byteLength - offset).getUint32(1, false);
1155
+ const available = Math.min(length, input.byteLength - offset - 5);
1156
+ payloadBytes += available;
1157
+ if (available < length) break;
1158
+ offset += 5 + length;
1159
+ }
1160
+ return payloadBytes;
1161
+ }
1162
+
1163
+ /** Payload-byte count for a virtual concatenation, without allocating the concatenated buffer. */
1164
+ function connectBufferedPayloadBytesAcross(first: Uint8Array, second: Uint8Array): number {
1165
+ const totalBytes = first.byteLength + second.byteLength;
1166
+ const byteAt = (index: number): number => index < first.byteLength
1167
+ ? first[index]!
1168
+ : second[index - first.byteLength]!;
1169
+ let offset = 0;
1170
+ let payloadBytes = 0;
1171
+ while (totalBytes - offset >= 5) {
1172
+ const length = (byteAt(offset + 1) * 0x1000000)
1173
+ + (byteAt(offset + 2) << 16)
1174
+ + (byteAt(offset + 3) << 8)
1175
+ + byteAt(offset + 4);
1176
+ const available = Math.min(length, totalBytes - offset - 5);
1177
+ payloadBytes += available;
1178
+ if (available < length) break;
1179
+ offset += 5 + length;
1180
+ }
1181
+ return payloadBytes;
1182
+ }
1183
+
988
1184
  /** Host-only label for Cursor transport diagnostics — never leaks path/query/credentials. */
989
1185
  function cursorHostLabel(baseUrl: string): string {
990
1186
  try {
@@ -6,6 +6,34 @@ import type { ResolvedMcpServer } from "./mcp-config";
6
6
 
7
7
  const DEFAULT_CONNECT_TIMEOUT_MS = 15_000;
8
8
  const DEFAULT_CALL_TIMEOUT_MS = 120_000;
9
+ const CURSOR_MCP_MAX_SERVERS = 32;
10
+ const CURSOR_MCP_MAX_TOOLS = 512;
11
+ const CURSOR_MCP_MAX_RESOURCES = 1_024;
12
+ const CURSOR_MCP_MAX_SCHEMA_BYTES = 256 * 1024;
13
+ const CURSOR_MCP_MAX_CATALOG_BYTES = 4 * 1024 * 1024;
14
+ const CURSOR_MCP_MAX_RESULT_BYTES = 8 * 1024 * 1024;
15
+ const utf8 = new TextEncoder();
16
+
17
+ export class McpCatalogLimitError extends Error {
18
+ readonly code = "MCP_CATALOG_LIMIT";
19
+ constructor() { super("Cursor MCP catalog limit exceeded"); this.name = "McpCatalogLimitError"; }
20
+ }
21
+ export class McpPayloadTooLargeError extends Error {
22
+ readonly code = "MCP_PAYLOAD_TOO_LARGE";
23
+ constructor(readonly kind: "result" | "resource") { super(`Cursor MCP ${kind} too large`); this.name = "McpPayloadTooLargeError"; }
24
+ }
25
+ /** Backward-compatible export for existing integrations. */
26
+ export const CursorMcpPayloadTooLargeError = McpPayloadTooLargeError;
27
+
28
+ function stableJson(value: unknown): string {
29
+ return JSON.stringify(value, (_key, nested) => nested && typeof nested === "object" && !Array.isArray(nested)
30
+ ? Object.fromEntries(Object.entries(nested as Record<string, unknown>).sort(([left], [right]) => left.localeCompare(right)))
31
+ : nested);
32
+ }
33
+
34
+ function payloadBytes(value: unknown): number {
35
+ return utf8.encode(stableJson(value)).byteLength;
36
+ }
9
37
 
10
38
  /** A tool discovered on a connected MCP server, with its opencodex-advertised name. */
11
39
  export interface McpToolHandle {
@@ -44,6 +72,9 @@ export interface CursorMcpManagerOptions {
44
72
  /** Test seam: provide a transport factory instead of spawning real processes. */
45
73
  transportFactory?: (server: ResolvedMcpServer) => Transport;
46
74
  log?: (message: string) => void;
75
+ maxTools?: number;
76
+ maxSchemaBytes?: number;
77
+ maxResultBytes?: number;
47
78
  }
48
79
 
49
80
  interface ConnectedServer {
@@ -64,17 +95,25 @@ interface ConnectedServer {
64
95
  export class CursorMcpManager {
65
96
  private readonly connectTimeoutMs: number;
66
97
  private readonly callTimeoutMs: number;
98
+ private readonly maxTools: number;
99
+ private readonly maxSchemaBytes: number;
100
+ private readonly maxResultBytes: number;
67
101
  private connected?: Promise<void>;
68
102
  private readonly servers = new Map<string, ConnectedServer>();
69
103
  /** advertisedName -> { serverName, original toolName } */
70
104
  private readonly toolIndex = new Map<string, { serverName: string; toolName: string; handle: McpToolHandle }>();
105
+ private toolCatalogBytes = 0;
71
106
 
72
107
  constructor(
73
108
  private readonly resolved: ResolvedMcpServer[],
74
109
  private readonly options: CursorMcpManagerOptions = {},
75
110
  ) {
111
+ if (resolved.length > CURSOR_MCP_MAX_SERVERS) throw new McpCatalogLimitError();
76
112
  this.connectTimeoutMs = options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
77
113
  this.callTimeoutMs = options.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT_MS;
114
+ this.maxTools = options.maxTools ?? CURSOR_MCP_MAX_TOOLS;
115
+ this.maxSchemaBytes = options.maxSchemaBytes ?? CURSOR_MCP_MAX_SCHEMA_BYTES;
116
+ this.maxResultBytes = options.maxResultBytes ?? CURSOR_MCP_MAX_RESULT_BYTES;
78
117
  }
79
118
 
80
119
  /** Idempotent, lazy connect + tool discovery across all servers. Never throws. */
@@ -84,20 +123,45 @@ export class CursorMcpManager {
84
123
  }
85
124
 
86
125
  private async connectAll(): Promise<void> {
87
- await Promise.all(this.resolved.map(server => this.connectOne(server)));
126
+ const staged: Array<{ connection: ConnectedServer; tools: Array<[string, { serverName: string; toolName: string; handle: McpToolHandle }]>; bytes: number }> = [];
127
+ try {
128
+ for (const server of this.resolved) {
129
+ const connected = await this.connectOne(server);
130
+ if (!connected) continue;
131
+ staged.push(connected);
132
+ const toolCount = staged.reduce((sum, row) => sum + row.tools.length, 0);
133
+ const catalogBytes = staged.reduce((sum, row) => sum + row.bytes, 0);
134
+ if (toolCount > this.maxTools || catalogBytes > CURSOR_MCP_MAX_CATALOG_BYTES) throw new McpCatalogLimitError();
135
+ }
136
+ for (const row of staged) {
137
+ this.servers.set(row.connection.server.serverName, row.connection);
138
+ for (const [name, entry] of row.tools) this.toolIndex.set(name, entry);
139
+ this.toolCatalogBytes += row.bytes;
140
+ }
141
+ } catch (error) {
142
+ await Promise.all(staged.map(row => row.connection.client.close().catch(() => {})));
143
+ this.servers.clear();
144
+ this.toolIndex.clear();
145
+ this.toolCatalogBytes = 0;
146
+ throw error;
147
+ }
88
148
  }
89
149
 
90
- private async connectOne(server: ResolvedMcpServer): Promise<void> {
150
+ private async connectOne(server: ResolvedMcpServer): Promise<{ connection: ConnectedServer; tools: Array<[string, { serverName: string; toolName: string; handle: McpToolHandle }]>; bytes: number } | null> {
151
+ let client: Client | undefined;
91
152
  try {
92
153
  const transport = this.options.transportFactory
93
154
  ? this.options.transportFactory(server)
94
155
  : this.createTransport(server);
95
- const client = new Client({ name: "opencodex", version: "1.0.0" });
156
+ client = new Client({ name: "opencodex", version: "1.0.0" });
96
157
  await this.withTimeout(client.connect(transport), this.connectTimeoutMs, `connect ${server.serverName}`);
97
- this.servers.set(server.serverName, { server, client });
98
- await this.indexTools(server, client);
158
+ const indexed = await this.indexTools(server, client);
159
+ return { connection: { server, client }, tools: indexed.tools, bytes: indexed.bytes };
99
160
  } catch (err) {
161
+ if (client) await client.close().catch(() => {});
100
162
  this.options.log?.(`[cursor-mcp] server "${server.serverName}" failed to connect: ${errText(err)}`);
163
+ if (err instanceof McpCatalogLimitError || err instanceof McpPayloadTooLargeError) throw err;
164
+ return null;
101
165
  }
102
166
  }
103
167
 
@@ -118,20 +182,31 @@ export class CursorMcpManager {
118
182
  throw new Error(`MCP server "${server.serverName}" has neither command nor url`);
119
183
  }
120
184
 
121
- private async indexTools(server: ResolvedMcpServer, client: Client): Promise<void> {
185
+ private async indexTools(server: ResolvedMcpServer, client: Client): Promise<{ tools: Array<[string, { serverName: string; toolName: string; handle: McpToolHandle }]>; bytes: number }> {
122
186
  const prefix = server.toolPrefix ?? "";
123
187
  const { tools } = await this.withTimeout(client.listTools(), this.connectTimeoutMs, `listTools ${server.serverName}`);
188
+ const additions: Array<[string, { serverName: string; toolName: string; handle: McpToolHandle }]> = [];
189
+ let catalogBytes = 0;
124
190
  for (const tool of tools ?? []) {
191
+ if (additions.length >= this.maxTools) throw new McpCatalogLimitError();
125
192
  const advertisedName = `${prefix}${tool.name}`;
193
+ const schema = tool.inputSchema ?? {};
194
+ const schemaBytes = utf8.encode(stableJson(schema)).byteLength;
195
+ if (schemaBytes > this.maxSchemaBytes) throw new McpCatalogLimitError();
196
+ catalogBytes += utf8.encode(advertisedName).byteLength
197
+ + utf8.encode(tool.description ?? "").byteLength
198
+ + schemaBytes;
199
+ if (catalogBytes > CURSOR_MCP_MAX_CATALOG_BYTES) throw new McpCatalogLimitError();
126
200
  const handle: McpToolHandle = {
127
201
  serverName: server.serverName,
128
202
  toolName: tool.name,
129
203
  advertisedName,
130
204
  description: tool.description ?? "",
131
- inputSchema: tool.inputSchema ?? {},
205
+ inputSchema: schema,
132
206
  };
133
- this.toolIndex.set(advertisedName, { serverName: server.serverName, toolName: tool.name, handle });
207
+ additions.push([advertisedName, { serverName: server.serverName, toolName: tool.name, handle }]);
134
208
  }
209
+ return { tools: additions, bytes: catalogBytes };
135
210
  }
136
211
 
137
212
  async listToolHandles(): Promise<McpToolHandle[]> {
@@ -161,6 +236,7 @@ export class CursorMcpManager {
161
236
  this.callTimeoutMs,
162
237
  `callTool ${advertisedName}`,
163
238
  );
239
+ if (payloadBytes(result) > this.maxResultBytes) throw new McpPayloadTooLargeError("result");
164
240
  return {
165
241
  isError: Boolean((result as { isError?: boolean }).isError),
166
242
  content: normalizeContent((result as { content?: unknown[] }).content),
@@ -171,8 +247,14 @@ export class CursorMcpManager {
171
247
  await this.ensureConnected();
172
248
  const targets = server ? [this.servers.get(server)].filter(Boolean) as ConnectedServer[] : [...this.servers.values()];
173
249
  const out: McpResourceListing[] = [];
250
+ let catalogBytes = 0;
174
251
  for (const conn of targets) {
175
252
  const { resources } = await this.withTimeout(conn.client.listResources(), this.callTimeoutMs, `listResources ${conn.server.serverName}`);
253
+ catalogBytes += payloadBytes(resources ?? []);
254
+ if ((resources?.length ?? 0) + out.length > CURSOR_MCP_MAX_RESOURCES
255
+ || catalogBytes > CURSOR_MCP_MAX_CATALOG_BYTES) {
256
+ throw new McpCatalogLimitError();
257
+ }
176
258
  for (const r of resources ?? []) {
177
259
  out.push({ uri: r.uri, name: r.name, description: r.description, mimeType: r.mimeType, server: conn.server.serverName });
178
260
  }
@@ -185,8 +267,12 @@ export class CursorMcpManager {
185
267
  const conn = this.servers.get(server);
186
268
  if (!conn) throw new Error(`MCP server not connected: ${server}`);
187
269
  const result = await this.withTimeout(conn.client.readResource({ uri }), this.callTimeoutMs, `readResource ${uri}`);
270
+ const rawBytes = payloadBytes(result);
271
+ if (rawBytes > this.maxResultBytes) throw new McpPayloadTooLargeError("resource");
188
272
  const first = (result.contents ?? [])[0] as { uri?: string; mimeType?: string; text?: string; blob?: string } | undefined;
189
273
  if (!first) return { uri, mimeType: undefined, text: "" };
274
+ const decodedBytes = typeof first.blob === "string" ? decodedBase64Length(first.blob) : 0;
275
+ if (rawBytes + decodedBytes > this.maxResultBytes) throw new McpPayloadTooLargeError("resource");
190
276
  return {
191
277
  uri: first.uri ?? uri,
192
278
  mimeType: first.mimeType,
@@ -195,10 +281,15 @@ export class CursorMcpManager {
195
281
  };
196
282
  }
197
283
 
284
+ assertDecodedResultBudget(value: unknown, decodedBytes: number): void {
285
+ if (payloadBytes(value) + decodedBytes > this.maxResultBytes) throw new McpPayloadTooLargeError("result");
286
+ }
287
+
198
288
  async dispose(): Promise<void> {
199
289
  const conns = [...this.servers.values()];
200
290
  this.servers.clear();
201
291
  this.toolIndex.clear();
292
+ this.toolCatalogBytes = 0;
202
293
  await Promise.all(conns.map(async conn => {
203
294
  try {
204
295
  await conn.client.close();
@@ -223,6 +314,12 @@ export class CursorMcpManager {
223
314
  }
224
315
  }
225
316
 
317
+ export function decodedBase64Length(value: string): number {
318
+ if (!value) return 0;
319
+ const padding = value.endsWith("==") ? 2 : value.endsWith("=") ? 1 : 0;
320
+ return Math.max(0, Math.floor(value.length * 3 / 4) - padding);
321
+ }
322
+
226
323
  function normalizeContent(content: unknown[] | undefined): McpCallResult["content"] {
227
324
  if (!Array.isArray(content)) return [];
228
325
  return content.map(item => {