@bitkyc08/opencodex 2.15.1 → 2.16.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 (33) hide show
  1. package/gui/dist/assets/{index-CMCDkQ7U.js → index-CZwbOse7.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/base.ts +2 -0
  5. package/src/adapters/google-antigravity-replay.ts +9 -1
  6. package/src/adapters/kiro-thinking.ts +8 -0
  7. package/src/adapters/kiro.ts +45 -42
  8. package/src/adapters/openai-chat.ts +5 -2
  9. package/src/adapters/openai-responses.ts +5 -1
  10. package/src/cli/dispatch.ts +6 -3
  11. package/src/cli/index.ts +1 -0
  12. package/src/generated/compatibility-version.json +48 -24
  13. package/src/integrations/config-io.ts +119 -1
  14. package/src/integrations/omp-yaml-source.ts +6 -1
  15. package/src/integrations/serialize.ts +80 -1
  16. package/src/integrations/state.ts +37 -6
  17. package/src/integrations/writer.ts +11 -3
  18. package/src/lab/automation/orchestrator.ts +19 -0
  19. package/src/lib/lab-activation.ts +161 -0
  20. package/src/lib/lab-passive-linker-registration.ts +26 -0
  21. package/src/lib/optional-shutdown-hooks.ts +57 -0
  22. package/src/lib/translator-budget.ts +34 -0
  23. package/src/providers/antigravity-models.ts +65 -10
  24. package/src/routing/compatibility/assemble.ts +21 -107
  25. package/src/routing/compatibility/lab-evidence-provider.ts +130 -0
  26. package/src/routing/compatibility/provider-slot.ts +56 -0
  27. package/src/server/index.ts +8 -17
  28. package/src/server/lifecycle.ts +5 -3
  29. package/src/server/management/routing-profile-routes.ts +9 -1
  30. package/src/server/management-api.ts +37 -6
  31. package/src/server/passive-route-linker.ts +66 -0
  32. package/src/server/responses/core.ts +20 -20
  33. package/src/types.ts +10 -0
@@ -19,6 +19,8 @@ import { createKiroToolNameRegistry, fallbackToolUseId, fingerprint, invocationI
19
19
  import { namespacedToolName } from "../types";
20
20
  import {
21
21
  isTranslatorBudgetExceededError,
22
+ releaseTranslatedEvent,
23
+ retainTranslatedEvent,
22
24
  type TranslatorBudget,
23
25
  } from "../lib/translator-budget";
24
26
  import type {
@@ -636,8 +638,8 @@ export function buildKiroPayload(
636
638
 
637
639
  // Stream parsing (shared by parseStream + parseResponse)
638
640
  // CodeWhisperer GenerateAssistantResponse ALWAYS returns an AWS eventstream body (there is no
639
- // non-streaming mode), so both the streaming bridge and the non-streaming web-search sidecar loop
640
- // decode the same way — parseResponse just collects what parseStream yields.
641
+ // non-streaming wire mode), so the streaming bridge and non-streaming Responses path decode the
642
+ // same way — parseResponse just collects what parseStream yields.
641
643
  interface KiroAttemptParseResult {
642
644
  terminal?: AdapterEvent;
643
645
  needsFallback?: boolean;
@@ -859,16 +861,12 @@ async function* parseKiroAttempt(
859
861
  );
860
862
  let handedOff = false;
861
863
  try {
862
- let next = await attempt.next();
863
- while (!next.done) {
864
- yield next.value;
865
- next = await attempt.next();
866
- }
864
+ const result = yield* attempt;
867
865
  for (const event of deferred.splice(0)) {
868
866
  try { yield event; } finally { retention.releaseEvent(event); }
869
867
  }
870
868
  handedOff = true;
871
- return { ...next.value, releaseRetained: () => retention.releaseAll() };
869
+ return { ...result, releaseRetained: () => retention.releaseAll() };
872
870
  } finally {
873
871
  if (!handedOff) retention.releaseAll();
874
872
  }
@@ -897,6 +895,12 @@ async function* parseKiroAttemptEvents(
897
895
  }
898
896
 
899
897
  let open: { id: string; name: string; chunks: string[]; completion: boolean } | null = null;
898
+ let openCallId: string | undefined;
899
+ const closeOpenCall = () => {
900
+ if (!openCallId) return;
901
+ budget.closeCall(openCallId);
902
+ openCallId = undefined;
903
+ };
900
904
  let outputChars = "";
901
905
  let outputCharsBytes = 0;
902
906
  let contextUsagePercentage: number | undefined;
@@ -1109,7 +1113,7 @@ async function* parseKiroAttemptEvents(
1109
1113
  if (!open) return { events: [] };
1110
1114
  const tool = open;
1111
1115
  open = null;
1112
- budget.closeCall(tool.id);
1116
+ closeOpenCall();
1113
1117
  const input = tool.chunks.join("");
1114
1118
  if (!isCompleteKiroToolInput(input)) {
1115
1119
  return { events: [], terminal: protocolTerminal(kiroTruncationErrorMessage("incomplete tool input JSON"), tool.completion) };
@@ -1221,11 +1225,12 @@ async function* parseKiroAttemptEvents(
1221
1225
  if (started.terminal) return { assistantText, sawReasoning, terminal: started.terminal };
1222
1226
  open = started.tool!;
1223
1227
  budget.openCall(open.id);
1228
+ openCallId = open.id;
1224
1229
  } else if (
1225
1230
  (ev.toolUseId && ev.toolUseId !== open.id)
1226
1231
  || (ev.name && open.name !== "unknown" && ev.name !== open.name)
1227
1232
  ) {
1228
- budget.closeCall(open.id);
1233
+ closeOpenCall();
1229
1234
  open = null;
1230
1235
  return { assistantText, sawReasoning, terminal: protocolTerminal(kiroTruncationErrorMessage("tool input changed identity before stop")) };
1231
1236
  }
@@ -1489,7 +1494,7 @@ async function* parseKiroAttemptEvents(
1489
1494
  };
1490
1495
  } catch (err) {
1491
1496
  if (isTranslatorBudgetExceededError(err)) {
1492
- if (open) budget.closeCall(open.id);
1497
+ closeOpenCall();
1493
1498
  return {
1494
1499
  assistantText,
1495
1500
  sawReasoning,
@@ -1531,6 +1536,9 @@ async function* parseKiroAttemptEvents(
1531
1536
  usage: usage(),
1532
1537
  },
1533
1538
  };
1539
+ } finally {
1540
+ thinking.dispose();
1541
+ closeOpenCall();
1534
1542
  }
1535
1543
  }
1536
1544
 
@@ -1547,7 +1555,7 @@ export async function* parseKiroStream(
1547
1555
  contextInputEstimate?: number,
1548
1556
  ): AsyncGenerator<AdapterEvent> {
1549
1557
  const contextWindowState: KiroContextWindowState = { value: contextWindow };
1550
- const first = parseKiroAttempt(
1558
+ const firstResult = yield* parseKiroAttempt(
1551
1559
  response,
1552
1560
  budget,
1553
1561
  completionMode,
@@ -1559,12 +1567,6 @@ export async function* parseKiroStream(
1559
1567
  contextInputEstimate,
1560
1568
  false,
1561
1569
  );
1562
- let firstNext = await first.next();
1563
- while (!firstNext.done) {
1564
- yield firstNext.value;
1565
- firstNext = await first.next();
1566
- }
1567
- const firstResult = firstNext.value;
1568
1570
  try {
1569
1571
  if (!firstResult.needsFallback) {
1570
1572
  if (firstResult.terminal) yield firstResult.terminal;
@@ -1642,7 +1644,7 @@ export async function* parseKiroStream(
1642
1644
  return;
1643
1645
  }
1644
1646
 
1645
- const second = parseKiroAttempt(
1647
+ const secondResult = yield* parseKiroAttempt(
1646
1648
  fallback.response,
1647
1649
  budget,
1648
1650
  "text_fallback",
@@ -1656,12 +1658,6 @@ export async function* parseKiroStream(
1656
1658
  // A zero-output transport failure here must stay non-retryable to avoid duplicating that text.
1657
1659
  priorEmittedOutput,
1658
1660
  );
1659
- let secondNext = await second.next();
1660
- while (!secondNext.done) {
1661
- yield secondNext.value;
1662
- secondNext = await second.next();
1663
- }
1664
- const secondResult = secondNext.value;
1665
1661
  try {
1666
1662
  if (!secondResult.terminal) {
1667
1663
  yield retryableKiroIncomplete(
@@ -1909,25 +1905,32 @@ export function createKiroAdapter(provider: OcxProviderConfig): ProviderAdapter
1909
1905
  return safeKiroHttpErrorMessage(status, headers, payloadText);
1910
1906
  },
1911
1907
 
1912
- // Non-streaming path used by the web-search sidecar loop (loop.ts runs each iteration
1913
- // non-streamed so it can inspect tool calls). CW only ever event-streams, so we drain the
1914
- // same decoder into an array. Without this, any Codex request that includes the web_search
1915
- // tool failed with "web-search sidecar requires a non-streaming adapter" (kiro-only).
1908
+ // Kiro always returns an event stream, including for non-streaming Responses requests. Drain
1909
+ // the decoder into a budget-owned batch so an upstream stream cannot grow this array without
1910
+ // bound while the caller waits for the complete JSON response.
1916
1911
  async parseResponse(response: Response, budget: TranslatorBudget): Promise<AdapterEvent[]> {
1917
1912
  const events: AdapterEvent[] = [];
1918
- for await (const e of parseKiroStream(
1919
- response,
1920
- budget,
1921
- modelId,
1922
- inputTokens,
1923
- contextWindow,
1924
- toolNameMap,
1925
- conversationId,
1926
- completionMode,
1927
- completionMode === "required" ? fallbackFactory : undefined,
1928
- contextInputEstimate,
1929
- )) events.push(e);
1930
- return events;
1913
+ try {
1914
+ for await (const e of parseKiroStream(
1915
+ response,
1916
+ budget,
1917
+ modelId,
1918
+ inputTokens,
1919
+ contextWindow,
1920
+ toolNameMap,
1921
+ conversationId,
1922
+ completionMode,
1923
+ completionMode === "required" ? fallbackFactory : undefined,
1924
+ contextInputEstimate,
1925
+ )) {
1926
+ retainTranslatedEvent(e, budget, events.at(-1));
1927
+ events.push(e);
1928
+ }
1929
+ return events;
1930
+ } catch (error) {
1931
+ for (const event of events) releaseTranslatedEvent(event, budget);
1932
+ throw error;
1933
+ }
1931
1934
  },
1932
1935
  };
1933
1936
  }
@@ -968,8 +968,11 @@ export function createOpenAIChatAdapter(provider: OcxProviderConfig): ProviderAd
968
968
  if (provider.parallelToolCalls === false) {
969
969
  // NIM documents the Boolean defaulting to false and kimi rejects true; pin the
970
970
  // wire bit so Codex cannot opt in via request.options. Other opted-out providers
971
- // omit the field so strict OpenAI-compatible hosts never see an unsupported knob.
972
- if (provider.baseUrl === "https://integrate.api.nvidia.com/v1") {
971
+ // omit the field by default so strict OpenAI-compatible hosts never see an
972
+ // unsupported knob, but a self-hosted gateway that DOES honor the field and keeps
973
+ // emitting parallel calls without it can opt in via pinParallelToolCallsFalse.
974
+ if (provider.baseUrl === "https://integrate.api.nvidia.com/v1"
975
+ || provider.pinParallelToolCallsFalse === true) {
973
976
  body.parallel_tool_calls = false;
974
977
  }
975
978
  } else if (provider.parallelToolCalls === true) {
@@ -1361,6 +1361,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1361
1361
  }
1362
1362
 
1363
1363
  const forward = provider.authMode === "forward";
1364
+ let convertedRoutedCustomToolNames: Set<string> | undefined;
1364
1365
  const unexpandedMiss = !!parsed.previousResponseId && parsed._previousResponseInputExpanded !== true;
1365
1366
  let outBody = stripPreviousResponseId(
1366
1367
  parsed._rawBody,
@@ -1408,7 +1409,9 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1408
1409
  outBody = promoteClientLoadedTools(outBody);
1409
1410
  }
1410
1411
  if (provider.authMode !== "forward") {
1411
- outBody = rewriteRoutedCustomToolsForUpstream(outBody).body;
1412
+ const rewritten = rewriteRoutedCustomToolsForUpstream(outBody);
1413
+ outBody = rewritten.body;
1414
+ convertedRoutedCustomToolNames = rewritten.names;
1412
1415
  }
1413
1416
  const sanitizedBody = normalizeToolSchemas(stripSparkCompatibility(stripUnsupportedReasoningParams(stripItemIdsWhenUnstored(stripInvalidItemIds(stripUnsupportedHostedTools(sanitizeReasoningInputContent(scrubOcxCompactionItems(outBody), { preserveRawReasoningContent: provider.preserveResponsesReasoningContent === true })))))));
1414
1417
  const body = JSON.stringify(stripDisabledReasoningSummaries(
@@ -1426,6 +1429,7 @@ export function createResponsesPassthroughAdapter(provider: OcxProviderConfig):
1426
1429
  headers,
1427
1430
  body,
1428
1431
  releaseBodyObservation,
1432
+ ...(convertedRoutedCustomToolNames ? { convertedRoutedCustomToolNames } : {}),
1429
1433
  };
1430
1434
  },
1431
1435
 
@@ -21,7 +21,6 @@ import { restoreNativeCodexAsync } from "../codex/inject";
21
21
  import { stripGrokConfig } from "../grok/inject";
22
22
  import { afterCatalogWriteHandleAppServers } from "../codex/app-server-processes";
23
23
  import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
24
- import { serviceCommand } from "../service";
25
24
 
26
25
  export interface CliDispatchDeps {
27
26
  args: string[];
@@ -45,6 +44,7 @@ export interface CliDispatchDeps {
45
44
  handleStatus: () => Promise<void>;
46
45
  handleRecoverHistory: () => Promise<void>;
47
46
  handleReady: (args: ReadyArgs) => Promise<number>;
47
+ serviceCommand: (...args: string[]) => Promise<void>;
48
48
  }
49
49
 
50
50
  type CommandRunner = (deps: CliDispatchDeps) => Promise<number>;
@@ -261,8 +261,11 @@ const commandRunners: Record<string, CommandRunner> = {
261
261
  return 0;
262
262
  },
263
263
  service: async deps => {
264
- await serviceCommand(...deps.args.slice(1));
265
- return 0;
264
+ process.exitCode = 0;
265
+ await deps.serviceCommand(...deps.args.slice(1));
266
+ // serviceCommand uses process.exitCode for recoverable install/stop failures
267
+ // that must finish cleanup before the single top-level process.exit runs.
268
+ return Number(process.exitCode ?? 0);
266
269
  },
267
270
  tray: async deps => {
268
271
  const { windowsTrayCommand } = await import("../tray/windows");
package/src/cli/index.ts CHANGED
@@ -941,4 +941,5 @@ process.exit(await dispatchCommand(head, {
941
941
  handleStatus,
942
942
  handleRecoverHistory,
943
943
  handleReady,
944
+ serviceCommand,
944
945
  }));
@@ -10,7 +10,7 @@
10
10
  },
11
11
  {
12
12
  "path": "package.json",
13
- "sha256": "9db72337e10283bebce7752061492ab1be0a14fe85b2c9dbd32bcf1395cf5f2f"
13
+ "sha256": "5c2d417ce3cb466651385bca4d8fee522e0bdf1c6d8c2fc1f84b4cf54574aee1"
14
14
  },
15
15
  {
16
16
  "path": "scripts/model-metadata.source.json",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  {
44
44
  "path": "src/adapters/base.ts",
45
- "sha256": "67f50a3e0e45514f55f08d19726acfcab6fe4a9bd5e88bb7455b5af236a10663"
45
+ "sha256": "f20fdca89cd4bac4ec049ab46e200f2aa6435dae548c5994b7edf5b6d73f1bd5"
46
46
  },
47
47
  {
48
48
  "path": "src/adapters/client-fingerprint.ts",
@@ -182,7 +182,7 @@
182
182
  },
183
183
  {
184
184
  "path": "src/adapters/google-antigravity-replay.ts",
185
- "sha256": "230da5cd5c6a94c2f520eeea2905294df6320e95e6f6e42569a4bfe04724193d"
185
+ "sha256": "52704bcd65eab346025e6faffe70d0c1d07e39651e4139f34bc67759fbaa4f08"
186
186
  },
187
187
  {
188
188
  "path": "src/adapters/google-antigravity-wire.ts",
@@ -242,7 +242,7 @@
242
242
  },
243
243
  {
244
244
  "path": "src/adapters/kiro-thinking.ts",
245
- "sha256": "d4016e1314c8b91bada92fff3ec4b94d0e1d88c4e51db49c29a482584844dfe0"
245
+ "sha256": "c7093dd20dc16156aee4d6a37de0b71f6e646c09f2ba99be8f3c6a388703a981"
246
246
  },
247
247
  {
248
248
  "path": "src/adapters/kiro-tool-fallback.ts",
@@ -262,7 +262,7 @@
262
262
  },
263
263
  {
264
264
  "path": "src/adapters/kiro.ts",
265
- "sha256": "c396bcf9810a1381f71957f95ed8399a13e240f5dc6be878c8836dfa7ca66f52"
265
+ "sha256": "fe61e9e4f8d7eada8baab90e09e9b2b03641c114eabe7846f2947cb0667996f1"
266
266
  },
267
267
  {
268
268
  "path": "src/adapters/mimo-free.ts",
@@ -274,7 +274,7 @@
274
274
  },
275
275
  {
276
276
  "path": "src/adapters/openai-chat.ts",
277
- "sha256": "1fcd9a2da1e675026fa2ac03b396cefa7cd62e3c1baf27844bbe7fd589c70275"
277
+ "sha256": "c87e922e32ccf8c224891e9a3a42e86139eb7db8d97b9c46613f01a132afc6fa"
278
278
  },
279
279
  {
280
280
  "path": "src/adapters/openai-responses-url.ts",
@@ -282,7 +282,7 @@
282
282
  },
283
283
  {
284
284
  "path": "src/adapters/openai-responses.ts",
285
- "sha256": "9c8d4483c0dacf3bf0562f08fe61b43d3c3107f1a978d66fc916c3da5dfe1d17"
285
+ "sha256": "8d4809ac4228beea1fa08716cd43c9ab7a87fdeaed5f30f39336ee808375b98d"
286
286
  },
287
287
  {
288
288
  "path": "src/adapters/run-turn-queue.ts",
@@ -446,7 +446,7 @@
446
446
  },
447
447
  {
448
448
  "path": "src/cli/dispatch.ts",
449
- "sha256": "2cba2f25e64bef2b08f1299a8b94757517bdf8bcd56e9ce2c7d46e50be3cce47"
449
+ "sha256": "478bd9539c26322ec5365e80f44674d507c708ccff6d09605a6e7804abedee79"
450
450
  },
451
451
  {
452
452
  "path": "src/cli/doctor.ts",
@@ -462,7 +462,7 @@
462
462
  },
463
463
  {
464
464
  "path": "src/cli/index.ts",
465
- "sha256": "8e3e096dac1115ae5a1a8c12d740b72766f4a1a1ece1e96c7e05d11b83080adb"
465
+ "sha256": "63e8747afb606295340bd30598d7112b75efb2a89353234a874df42c5fc7e1a7"
466
466
  },
467
467
  {
468
468
  "path": "src/cli/init.ts",
@@ -1038,7 +1038,7 @@
1038
1038
  },
1039
1039
  {
1040
1040
  "path": "src/integrations/config-io.ts",
1041
- "sha256": "5193f6718cab71d9b7117faec5feb3a413bc1ccfc1b1f5649451cba909b5bf15"
1041
+ "sha256": "5271736a6499bc4bcb68bde2ccefd8ef8ce7953bddf13e764fb1b9ee66433fe3"
1042
1042
  },
1043
1043
  {
1044
1044
  "path": "src/integrations/journal.ts",
@@ -1054,7 +1054,7 @@
1054
1054
  },
1055
1055
  {
1056
1056
  "path": "src/integrations/omp-yaml-source.ts",
1057
- "sha256": "b9e4355893d74cfff01892c2b2a0e164420d07beb5222818e14cddaff0c3d718"
1057
+ "sha256": "58a8769edc6c1f409f2851955e52dba3cdb5e30938d6b7dc19e59b900731d219"
1058
1058
  },
1059
1059
  {
1060
1060
  "path": "src/integrations/ownership.ts",
@@ -1066,11 +1066,11 @@
1066
1066
  },
1067
1067
  {
1068
1068
  "path": "src/integrations/serialize.ts",
1069
- "sha256": "e7a23ead6a5a6445ddc7803af9aa8cfebc745e2d6ab325828b95689a50c9b29d"
1069
+ "sha256": "0560601276b25d82eaf6e5460c815e0a67ecb4cf1cf6e378952c60dedfb333e6"
1070
1070
  },
1071
1071
  {
1072
1072
  "path": "src/integrations/state.ts",
1073
- "sha256": "65dd695356bce8ac9593e4cbec64ab85ca9d54784a0e7c8ae7294b9e9a81fba0"
1073
+ "sha256": "e2446f333ab3b57eebc120bf674f37aff6210cde7cf45628e7ff0ca77ca4d4f0"
1074
1074
  },
1075
1075
  {
1076
1076
  "path": "src/integrations/store.ts",
@@ -1078,7 +1078,7 @@
1078
1078
  },
1079
1079
  {
1080
1080
  "path": "src/integrations/writer.ts",
1081
- "sha256": "e2857bcc0351f1b6de8ba7d2fb6da27eff621e35ff69d40138e789ac24b116d6"
1081
+ "sha256": "92cc7c85a6bffa520f0fce57890f45758c712f69a128451be60368444d5ff725"
1082
1082
  },
1083
1083
  {
1084
1084
  "path": "src/lab/artifacts/sanitize.ts",
@@ -1118,7 +1118,7 @@
1118
1118
  },
1119
1119
  {
1120
1120
  "path": "src/lab/automation/orchestrator.ts",
1121
- "sha256": "ea12b23eceb13af13e6139a90ecd6b62ddd9485be118ef15027aff8c75621b6b"
1121
+ "sha256": "5f10d45593ea6464f6ecd1db6ee922bc7627fc6578fe6f3f93b7a35c1b479df8"
1122
1122
  },
1123
1123
  {
1124
1124
  "path": "src/lab/automation/persistence.ts",
@@ -1548,6 +1548,10 @@
1548
1548
  "path": "src/lib/injection-debug-log.ts",
1549
1549
  "sha256": "ab3b8e53c546c0ff43dc129d29c0b11c2d258f86e9d12475d87bf55510eb02c9"
1550
1550
  },
1551
+ {
1552
+ "path": "src/lib/lab-activation.ts",
1553
+ "sha256": "7cdfb758543a789a75354ebfba730c516d264a4d6a10648456f57e2654034dec"
1554
+ },
1551
1555
  {
1552
1556
  "path": "src/lib/lab-live-execution-authority.ts",
1553
1557
  "sha256": "7a4c488521b861ad70a87f560d85cb5c9f552e16493c5d44e19734947b4ac714"
@@ -1564,6 +1568,10 @@
1564
1568
  "path": "src/lib/lab-live-route-production.ts",
1565
1569
  "sha256": "fb890cceb6390cd2d846c86c97e0d490ea8b58783cd7410d032b6d8573a07f49"
1566
1570
  },
1571
+ {
1572
+ "path": "src/lib/lab-passive-linker-registration.ts",
1573
+ "sha256": "5f88d3428f87f1773f944e43d812356b96e881dcb8bd13d34baf504ce509e54c"
1574
+ },
1567
1575
  {
1568
1576
  "path": "src/lib/local-management-attestation.ts",
1569
1577
  "sha256": "1bccf8f85ecd89cb017224024ca3d199d97c8ce88f5ac936dbc1419845a77f6e"
@@ -1580,6 +1588,10 @@
1580
1588
  "path": "src/lib/open-url.ts",
1581
1589
  "sha256": "a6db7ec7181ccb4e95411b2c7499fdd00f3dd48bebcdaca099da086c0dae2e59"
1582
1590
  },
1591
+ {
1592
+ "path": "src/lib/optional-shutdown-hooks.ts",
1593
+ "sha256": "ba1fb3f46ddd1ad7d1cf90e22ae8df5dd2969dbb99449beaf8fac3100425a908"
1594
+ },
1583
1595
  {
1584
1596
  "path": "src/lib/pinned-http.ts",
1585
1597
  "sha256": "27a69c4eac63db31ef754c84d7f5f2e2949221b5fe27e066420f91b40fcc51d7"
@@ -1658,7 +1670,7 @@
1658
1670
  },
1659
1671
  {
1660
1672
  "path": "src/lib/translator-budget.ts",
1661
- "sha256": "872d5d35eb0d2ee099fc4ab4bf3b9331c6207d4282f61f2c1b11a5a36c00d981"
1673
+ "sha256": "4d79fed2d10e23b87af2c67ea0a979ce580eb232ac35e1f6851b5f02ef5a07d7"
1662
1674
  },
1663
1675
  {
1664
1676
  "path": "src/lib/upstream-reachability.ts",
@@ -1822,7 +1834,7 @@
1822
1834
  },
1823
1835
  {
1824
1836
  "path": "src/providers/antigravity-models.ts",
1825
- "sha256": "e7a2b7352997cfdc4268c733def555dae8632ba58a3f889dfc78d812b885d072"
1837
+ "sha256": "cbe65f34a127ad44fd8f6bd2214553b238449b4f10d948d5ed67f4b163964874"
1826
1838
  },
1827
1839
  {
1828
1840
  "path": "src/providers/api-keys.ts",
@@ -1990,7 +2002,7 @@
1990
2002
  },
1991
2003
  {
1992
2004
  "path": "src/routing/compatibility/assemble.ts",
1993
- "sha256": "00f0802e3ef8a2bf79779fbe27b62a73e92a94cff11bf5594784fef35dbda495"
2005
+ "sha256": "186010fe58a02823530819cf7bbf06cefdbb3224850ea8321736d1017f32783f"
1994
2006
  },
1995
2007
  {
1996
2008
  "path": "src/routing/compatibility/behavior.ts",
@@ -2004,10 +2016,18 @@
2004
2016
  "path": "src/routing/compatibility/endpoint.ts",
2005
2017
  "sha256": "38578440e02ce712f8d84fa97ec2d1bc3396f31983be9b6e5b0010ef6edd146b"
2006
2018
  },
2019
+ {
2020
+ "path": "src/routing/compatibility/lab-evidence-provider.ts",
2021
+ "sha256": "352a65b6d95c8e6eaa7e6f81fca49284b49a9ddf7bd1895be192b858784fd861"
2022
+ },
2007
2023
  {
2008
2024
  "path": "src/routing/compatibility/policy.ts",
2009
2025
  "sha256": "d903d561ef2ae16c86d16c7ec157556ee0c78557aaa7e83dad46c1abc0e6530c"
2010
2026
  },
2027
+ {
2028
+ "path": "src/routing/compatibility/provider-slot.ts",
2029
+ "sha256": "809c7b5dbe13b6e90d5abf51cadf33825f8d82b5fd1c7355d9ef25b736371bdc"
2030
+ },
2011
2031
  {
2012
2032
  "path": "src/routing/compatibility/reader.ts",
2013
2033
  "sha256": "c48943c0b660609bbdf4d47896de6338cd46b2c9144f1c916646221fe437a678"
@@ -2114,11 +2134,11 @@
2114
2134
  },
2115
2135
  {
2116
2136
  "path": "src/server/index.ts",
2117
- "sha256": "cff5571eb50f6ec315fa2f3ddd835d3bdd25b59c94a5b991771f066ad3e3ffc2"
2137
+ "sha256": "cc6461e629f1f5e373d81ebee0720a9ba85c7deedded457bbd398ac962e0e404"
2118
2138
  },
2119
2139
  {
2120
2140
  "path": "src/server/lifecycle.ts",
2121
- "sha256": "ee926ab2c5bb9925f91ce908ba8ce707859690bae1d07227afc38b259f40ea73"
2141
+ "sha256": "02d0691de03c510419c971341a7015507491599b9a91bec369fd7144f85b05a3"
2122
2142
  },
2123
2143
  {
2124
2144
  "path": "src/server/live.ts",
@@ -2134,7 +2154,7 @@
2134
2154
  },
2135
2155
  {
2136
2156
  "path": "src/server/management-api.ts",
2137
- "sha256": "ccd670a1618afc1ec6611771ec2519ea6c216d0ef43aa9e1114a3a98a9f0e030"
2157
+ "sha256": "0a513030e2b730393d86c87ad237f1e9f35e64586d8736c91c69fed6aaabb60d"
2138
2158
  },
2139
2159
  {
2140
2160
  "path": "src/server/management-auth.ts",
@@ -2214,7 +2234,7 @@
2214
2234
  },
2215
2235
  {
2216
2236
  "path": "src/server/management/routing-profile-routes.ts",
2217
- "sha256": "1185f47d963c31efa7da224baf9f2a447a15a7afee0705c05e6c110e75b465ff"
2237
+ "sha256": "08484dcc92a3672450122316dad9939707a2669eabf396084bfb93fa9dde35f9"
2218
2238
  },
2219
2239
  {
2220
2240
  "path": "src/server/management/shared.ts",
@@ -2248,6 +2268,10 @@
2248
2268
  "path": "src/server/memory-watchdog.ts",
2249
2269
  "sha256": "d2f7f5cd03316cdfb0bd23abd77b4c28d87986a9a1cb9e0f53e3890695f07f4c"
2250
2270
  },
2271
+ {
2272
+ "path": "src/server/passive-route-linker.ts",
2273
+ "sha256": "6a20d1e864efe80cd9d6c69df9616bc6092a6102cd8d339f0cb376f8821bed0b"
2274
+ },
2251
2275
  {
2252
2276
  "path": "src/server/port-reclaim.ts",
2253
2277
  "sha256": "5b1a9e7e5e3d752ee7dd0dca1ea8737eeaf7945e2c9a4e3a0aed9805cc72d253"
@@ -2334,7 +2358,7 @@
2334
2358
  },
2335
2359
  {
2336
2360
  "path": "src/server/responses/core.ts",
2337
- "sha256": "c67fdae5470a8d0c234e24d7d062bf351ec35c911550363c8d73199f4059fc95"
2361
+ "sha256": "0817eb37ba0ce9425afd9b9794eccab1095b7563d217652562cb281479f1df48"
2338
2362
  },
2339
2363
  {
2340
2364
  "path": "src/server/responses/encrypted-payload.ts",
@@ -2482,7 +2506,7 @@
2482
2506
  },
2483
2507
  {
2484
2508
  "path": "src/types.ts",
2485
- "sha256": "d911cdcf36b5694e58918b7e76b94b42c3bf94962ef4a41b93ae01629be36016"
2509
+ "sha256": "33f4ee11228bb8faca03c57fdd5000512f70153c2fd446f88d78b0a43056d330"
2486
2510
  },
2487
2511
  {
2488
2512
  "path": "src/update/badge.ts",
@@ -9,6 +9,7 @@
9
9
  */
10
10
  import { mkdirSync, readFileSync, rmSync, statSync } from "node:fs";
11
11
  import type { ConfigFormat } from "../clients/config-export";
12
+ import { MAX_JSON_NESTING } from "./serialize";
12
13
  import { atomicWriteFile } from "../config";
13
14
  import type { JournalEntry } from "./journal";
14
15
  import type { OwnershipRecord } from "./ownership";
@@ -23,12 +24,129 @@ import type { IntegrationClientId } from "./registry";
23
24
  */
24
25
  export const PARSE_FAILED = Symbol("parse-failed");
25
26
 
27
+ /**
28
+ * What `JSON.parse` has already discarded by the time we hold the parsed value,
29
+ * and a rewrite would therefore silently change. Both classes are invisible in
30
+ * the parsed object, which is why this scans the RAW text — same reasoning as
31
+ * the TOML inf/nan guard below.
32
+ *
33
+ * Numbers: `1e999` overflows to Infinity (a rewrite bakes in `null` — the merge
34
+ * layer's JSON clone does it even before the serializer could refuse), `1e-9999`
35
+ * underflows to `+0`, an integer literal may have been rounded (a rewrite then
36
+ * hands consumers that read JSON integers exactly — python, jq, BigInt revivers
37
+ * — a different value), and `-0` re-serializes as `0`. Only literals whose value
38
+ * actually changed are refused: `1e21`, `1e-320` or 2^54 round-trip exactly and
39
+ * stay usable.
40
+ *
41
+ * Duplicate members: `{"a":1,"a":2}` parses to a single `a`, so rewriting the
42
+ * document DELETES the earlier member. That is content loss, not the formatting
43
+ * normalization we promise, and this classifier is what makes the rewrite of a
44
+ * user-edited file reachable at all — so it fails closed here.
45
+ *
46
+ * Depth: the same pass counts container nesting against MAX_JSON_NESTING
47
+ * (shared with the serializer, see serialize.ts). JSON.parse handles hundreds
48
+ * of thousands of levels iteratively, but the downstream merge and
49
+ * JSON.stringify recurse — a 100KB file nested 50k deep sailed through parse
50
+ * and guard, then blew up serialization with a raw RangeError after a
51
+ * multi-GB allocation spike. A hand-written scan rather than a JSON.parse
52
+ * source-access reviver on purpose: the reviver walk recurses internally, so
53
+ * its depth limit would be an unpredictable stack size instead of this
54
+ * deterministic ceiling.
55
+ */
56
+ function jsonTextSafeToRewrite(text: string): boolean {
57
+ /**
58
+ * One frame per open container; a Set of member names for objects, null for
59
+ * arrays. Its length is the current nesting depth.
60
+ */
61
+ const containers: Array<Set<string> | null> = [];
62
+ /** The most recent string literal — the member name if a `:` follows. */
63
+ let lastString: string | null = null;
64
+ let inString = false;
65
+ let escaped = false;
66
+ let stringStart = 0;
67
+ for (let i = 0; i < text.length; i += 1) {
68
+ const ch = text[i]!;
69
+ if (inString) {
70
+ if (escaped) escaped = false;
71
+ else if (ch === "\\") escaped = true;
72
+ else if (ch === "\"") {
73
+ inString = false;
74
+ lastString = text.slice(stringStart, i + 1);
75
+ }
76
+ continue;
77
+ }
78
+ if (ch === "\"") { inString = true; stringStart = i; continue; }
79
+ if (ch === "{" || ch === "[") {
80
+ containers.push(ch === "{" ? new Set<string>() : null);
81
+ if (containers.length > MAX_JSON_NESTING) return false;
82
+ lastString = null;
83
+ continue;
84
+ }
85
+ if (ch === "}" || ch === "]") { containers.pop(); lastString = null; continue; }
86
+ if (ch === ":") {
87
+ const members = containers[containers.length - 1];
88
+ if (members && lastString !== null) {
89
+ /*
90
+ * Decoded, not raw: `"a"` and `"a"` are spellings of ONE member,
91
+ * and JSON.parse keeps only the last of them.
92
+ */
93
+ let name: string;
94
+ try { name = JSON.parse(lastString) as string; } catch { return false; }
95
+ if (members.has(name)) return false;
96
+ members.add(name);
97
+ }
98
+ lastString = null;
99
+ continue;
100
+ }
101
+ if (ch !== "-" && (ch < "0" || ch > "9")) continue;
102
+ let end = i + 1;
103
+ while (end < text.length && /[0-9+\-.eE]/.test(text[end]!)) end += 1;
104
+ const literal = text.slice(i, end);
105
+ i = end - 1;
106
+ const value = Number(literal);
107
+ if (!Number.isFinite(value)) return false;
108
+ if (value === 0) {
109
+ /*
110
+ * `-0` (re-serializes as `0`) and underflow: `1e-9999` is a nonzero
111
+ * value the parse already flattened to `+0`. The significand alone
112
+ * decides, so genuine zero spellings (`0`, `0.0`, `0e10`) stay usable.
113
+ */
114
+ if (literal.startsWith("-") || /[1-9]/.test(literal.split(/[eE]/)[0]!)) return false;
115
+ }
116
+ /*
117
+ * Deliberately plain digit runs only. They are the one spelling real
118
+ * consumers read with exact integer semantics (python's json yields an
119
+ * arbitrary-precision int, jq preserves big integer literals), so baking
120
+ * in the rounded double changes what those consumers extract. Decimal or
121
+ * exponent spellings of the same value (`9007199254740993e0`, `…3.0`) are
122
+ * float semantics for every consumer — they round identically before and
123
+ * after a rewrite, and shortest-round-trip stringify preserves what any
124
+ * reader can observe, so refusing them would only manufacture dead ends
125
+ * (`1e308` is not exactly representable either, yet rewrites losslessly
126
+ * for every possible reader).
127
+ */
128
+ const digits = literal[0] === "-" ? literal.slice(1) : literal;
129
+ if (/^[0-9]{16,}$/.test(digits) && BigInt(literal) !== BigInt(value)) return false;
130
+ }
131
+ return true;
132
+ }
133
+
26
134
  /** Parse a client config, tolerating absence. PARSE_FAILED on garbage. */
27
135
  export function parseConfig(text: string | null, format: ConfigFormat): unknown | typeof PARSE_FAILED {
28
136
  if (text === null || text.trim().length === 0) return {};
29
137
  try {
30
138
  switch (format) {
31
- case "json": return JSON.parse(text);
139
+ case "json": {
140
+ /*
141
+ * Scanned BEFORE parsing: the guard is text-only, and refusing first
142
+ * means a hostile document is never materialized — the depth ceiling
143
+ * would otherwise cap the rewrite only after JSON.parse had already
144
+ * built the 50k-deep object graph. The outcome is unchanged: invalid
145
+ * JSON still returns PARSE_FAILED, from the catch below.
146
+ */
147
+ if (!jsonTextSafeToRewrite(text)) return PARSE_FAILED;
148
+ return JSON.parse(text);
149
+ }
32
150
  case "json5": return Bun.JSON5.parse(text);
33
151
  case "yaml": return Bun.YAML.parse(text);
34
152
  case "toml": {