@bitkyc08/opencodex 2.14.1 → 2.14.2

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 (44) hide show
  1. package/gui/dist/assets/{index-DWhX3yMp.css → index-DUCH59lJ.css} +1 -1
  2. package/gui/dist/assets/index-DUyQeU1j.js +76 -0
  3. package/gui/dist/index.html +2 -2
  4. package/package.json +1 -1
  5. package/src/adapters/command-code.ts +15 -4
  6. package/src/adapters/cursor/request-builder.ts +54 -10
  7. package/src/adapters/cursor/tool-definitions.ts +24 -0
  8. package/src/adapters/kiro.ts +10 -1
  9. package/src/adapters/openai-chat.ts +5 -3
  10. package/src/adapters/openai-responses.ts +109 -0
  11. package/src/adapters/tool-catalog-nudge.ts +26 -4
  12. package/src/bridge.ts +50 -3
  13. package/src/cli/init.ts +4 -17
  14. package/src/codex/catalog/effort.ts +2 -1
  15. package/src/codex/catalog/metadata.ts +62 -12
  16. package/src/codex/catalog/native-models.ts +27 -0
  17. package/src/codex/catalog/parsing.ts +17 -2
  18. package/src/codex/catalog/provider-fetch.ts +47 -5
  19. package/src/codex/catalog/sync.ts +21 -7
  20. package/src/codex/catalog.ts +1 -1
  21. package/src/config.ts +79 -4
  22. package/src/generated/compatibility-version.json +48 -36
  23. package/src/lib/app-owned-memory-stores.ts +22 -0
  24. package/src/lib/tool-argument-integers.ts +158 -0
  25. package/src/oauth/nous.ts +58 -9
  26. package/src/providers/base-url-choices.ts +10 -0
  27. package/src/providers/command-code-efforts.ts +18 -0
  28. package/src/providers/model-rename-migration.ts +202 -0
  29. package/src/providers/model-rename-startup.ts +28 -0
  30. package/src/providers/openai-tier-startup.ts +31 -2
  31. package/src/providers/quota.ts +9 -2
  32. package/src/providers/registry.ts +12 -5
  33. package/src/responses/spill-store.ts +5 -1
  34. package/src/responses/state.ts +50 -2
  35. package/src/server/index.ts +2 -1
  36. package/src/server/management/api-key-usage.ts +31 -5
  37. package/src/server/management/logs-usage-routes.ts +48 -10
  38. package/src/server/management/provider-routes.ts +2 -1
  39. package/src/server/management/usage-summary-cache.ts +7 -1
  40. package/src/server/responses/collaboration.ts +12 -2
  41. package/src/server/responses/core.ts +33 -16
  42. package/src/server/startup-health-cache.ts +12 -0
  43. package/src/usage/log.ts +430 -12
  44. package/gui/dist/assets/index-DuaUVm_d.js +0 -76
@@ -10,7 +10,7 @@
10
10
  },
11
11
  {
12
12
  "path": "package.json",
13
- "sha256": "c2501a020b2b8a7c01f0199dc7a1d7763d82181fe7a329bd2b265fd7474ec994"
13
+ "sha256": "e9ef4aa5a62aa64f795882518ae1821b3b49b5ce002310f3b1b5e5b512abdc05"
14
14
  },
15
15
  {
16
16
  "path": "scripts/model-metadata.source.json",
@@ -50,7 +50,7 @@
50
50
  },
51
51
  {
52
52
  "path": "src/adapters/command-code.ts",
53
- "sha256": "7b969b84c92664588d5b427676743b05169ccc9b26b140e244279186b6c08b4a"
53
+ "sha256": "e92d27e7988deaad93169778a57ca0afbf2f94b1ea14ba382618ea7617f14e3f"
54
54
  },
55
55
  {
56
56
  "path": "src/adapters/cursor.ts",
@@ -158,7 +158,7 @@
158
158
  },
159
159
  {
160
160
  "path": "src/adapters/cursor/request-builder.ts",
161
- "sha256": "d2a9a7fe975c4c858df2bcb5b2b2c5ba43a931b1a959a18dcc504613fabc1eb1"
161
+ "sha256": "b2c76bb40171e70827c0119c28f246f41332dbc6965035abe37054220795ea30"
162
162
  },
163
163
  {
164
164
  "path": "src/adapters/cursor/thread-continuity.ts",
@@ -166,7 +166,7 @@
166
166
  },
167
167
  {
168
168
  "path": "src/adapters/cursor/tool-definitions.ts",
169
- "sha256": "66f6c5c6b6f25c0d58cd63f6a23b8e3bf4192ac8f3608b1cfe37c9bec7d5901c"
169
+ "sha256": "be643963a3f36fe2c68bafb3f3a82ba5e9b59114d689f79cf83383c5852ff34f"
170
170
  },
171
171
  {
172
172
  "path": "src/adapters/cursor/transport-retry.ts",
@@ -262,7 +262,7 @@
262
262
  },
263
263
  {
264
264
  "path": "src/adapters/kiro.ts",
265
- "sha256": "a134e30227b79c9ee6c64124f40f86150b06b5f1175dde565f31f14679a499e8"
265
+ "sha256": "c396bcf9810a1381f71957f95ed8399a13e240f5dc6be878c8836dfa7ca66f52"
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": "7700ec687a4cb7516c9cc975ac6726ab1ad858fe4b39a342aea155bd2be291c4"
277
+ "sha256": "1fcd9a2da1e675026fa2ac03b396cefa7cd62e3c1baf27844bbe7fd589c70275"
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": "78013f261ce7f7b5f06ad8fa02f49c8d5c58ab4a2718d8571d41812cfe71b605"
285
+ "sha256": "9c8d4483c0dacf3bf0562f08fe61b43d3c3107f1a978d66fc916c3da5dfe1d17"
286
286
  },
287
287
  {
288
288
  "path": "src/adapters/run-turn-queue.ts",
@@ -290,7 +290,7 @@
290
290
  },
291
291
  {
292
292
  "path": "src/adapters/tool-catalog-nudge.ts",
293
- "sha256": "b20b726a97b85e4199ad2bcf2dfebf4040b392224e9f3b9671b00abb2c54ca81"
293
+ "sha256": "bf2bc3f1860ef7264df4a2f438df55cc46326c31e1a16e37f1d92b5446b5937b"
294
294
  },
295
295
  {
296
296
  "path": "src/adapters/upstream-http-error.ts",
@@ -298,7 +298,7 @@
298
298
  },
299
299
  {
300
300
  "path": "src/bridge.ts",
301
- "sha256": "20c2c4b1f40828b01a223e64c415dd34c98acb0fcd78ece6dab7bfee333a5b22"
301
+ "sha256": "137c6d4b5cc3e7c932fed242b5200469171ae786b4835bec7f667f15a1d79a75"
302
302
  },
303
303
  {
304
304
  "path": "src/chat/inbound.ts",
@@ -466,7 +466,7 @@
466
466
  },
467
467
  {
468
468
  "path": "src/cli/init.ts",
469
- "sha256": "211045465b1ecf46b0653234b8339425d42ee2fbcbdf216525ec6ed57ab20b3e"
469
+ "sha256": "753945880735c59dd35f3e2143a9b032945769f3ef9d8f5859c64b4493db39d0"
470
470
  },
471
471
  {
472
472
  "path": "src/cli/integrations.ts",
@@ -638,7 +638,7 @@
638
638
  },
639
639
  {
640
640
  "path": "src/codex/catalog.ts",
641
- "sha256": "b3a69c6d7093c4acd570f02e2704d8e3c804c14114e461444d208e9d69db1670"
641
+ "sha256": "ae99f51a899f5fdd753b1bd605b47d8f658885648cf26e5d1d001b5f75bb8134"
642
642
  },
643
643
  {
644
644
  "path": "src/codex/catalog/account-models.ts",
@@ -654,7 +654,7 @@
654
654
  },
655
655
  {
656
656
  "path": "src/codex/catalog/effort.ts",
657
- "sha256": "f76a68fa34639b16c044990af869b4be5e6dd326741b7c0e3f0f9655f22f4da3"
657
+ "sha256": "c859382789a4edf333b0e269dac54ff3a29cb205de9d07e36979a20c93fbef02"
658
658
  },
659
659
  {
660
660
  "path": "src/codex/catalog/filesystem-evidence.ts",
@@ -666,23 +666,23 @@
666
666
  },
667
667
  {
668
668
  "path": "src/codex/catalog/metadata.ts",
669
- "sha256": "49066e273177f772fa0adc6ee9ecb8258e3b9591d244d01fc3f6fdf0d2e695a6"
669
+ "sha256": "da2aff0057e1b98d62e481043b2f16fb9679b4df4226e9754963b7262fde2691"
670
670
  },
671
671
  {
672
672
  "path": "src/codex/catalog/native-models.ts",
673
- "sha256": "86d7789553db1b2d70f7183c39fab59921dd704403370a0a8a7909adeff78e02"
673
+ "sha256": "3d8a70d56fee1cf503da01c62608b338c3ec293114d1825c0bd1f96f817cda99"
674
674
  },
675
675
  {
676
676
  "path": "src/codex/catalog/parsing.ts",
677
- "sha256": "b73918da5145d0422484d29bcaf05d00cd044ef1866187ff36168cfcb1aa16d9"
677
+ "sha256": "2c3e16a4615e98d0bea0acee6994661eddeb7df80488a44d4bec253bb7ca1e79"
678
678
  },
679
679
  {
680
680
  "path": "src/codex/catalog/provider-fetch.ts",
681
- "sha256": "9f2bb70bcc8efe942b13c7b94cb06387948a2db8baba9042114787f3a5de8931"
681
+ "sha256": "61c35c9a09ada9c79f2b2979612e6ef618d735afa0b33d5f5915b5ab2e39ccde"
682
682
  },
683
683
  {
684
684
  "path": "src/codex/catalog/sync.ts",
685
- "sha256": "c285b373e030af62e33976ccb7e8b0f6ec2741783e24853404fdf1dfc4c19283"
685
+ "sha256": "6f2846d1b03ffb21b73381ba833703f14b303532c0ef028945cd463ecc639beb"
686
686
  },
687
687
  {
688
688
  "path": "src/codex/codex-write-lock.ts",
@@ -966,7 +966,7 @@
966
966
  },
967
967
  {
968
968
  "path": "src/config.ts",
969
- "sha256": "dbbabcdc1f62870ed69ecc6da675b7f82705d3f97179ad4841ad4ca368cd5932"
969
+ "sha256": "d98507303396605a2f698448295572038d6f30f3593af58910d04ecec918e9b8"
970
970
  },
971
971
  {
972
972
  "path": "src/generated/model-metadata.ts",
@@ -1474,7 +1474,7 @@
1474
1474
  },
1475
1475
  {
1476
1476
  "path": "src/lib/app-owned-memory-stores.ts",
1477
- "sha256": "d4852d93145f8a765bf0374bd37e734970e2eb50bb559ea8a42f3921aa05e8c9"
1477
+ "sha256": "947c1cd5bdff6e15de778a76eb4d2d4cf103bf482354d2736003807b2e8d409c"
1478
1478
  },
1479
1479
  {
1480
1480
  "path": "src/lib/app-owned-memory.ts",
@@ -1652,6 +1652,10 @@
1652
1652
  "path": "src/lib/token-estimate.ts",
1653
1653
  "sha256": "2c3cc4b8193166ca100197ca22202b06ac98c02547ffe32f3e0f8720190b9897"
1654
1654
  },
1655
+ {
1656
+ "path": "src/lib/tool-argument-integers.ts",
1657
+ "sha256": "f0a2d8dea66735881d7aebf98ebe3b2657fb6422f540289e0aba1721d17ec990"
1658
+ },
1655
1659
  {
1656
1660
  "path": "src/lib/translator-budget.ts",
1657
1661
  "sha256": "872d5d35eb0d2ee099fc4ab4bf3b9331c6207d4282f61f2c1b11a5a36c00d981"
@@ -1782,7 +1786,7 @@
1782
1786
  },
1783
1787
  {
1784
1788
  "path": "src/oauth/nous.ts",
1785
- "sha256": "76013229a77a53b3eab34d14f73e84ecef693f4e7939e37414570cb5ff751549"
1789
+ "sha256": "49fb089b01a8a118e575069d2308912b7fd661ef882913193b0e9f4d8df28201"
1786
1790
  },
1787
1791
  {
1788
1792
  "path": "src/oauth/pkce.ts",
@@ -1826,7 +1830,7 @@
1826
1830
  },
1827
1831
  {
1828
1832
  "path": "src/providers/base-url-choices.ts",
1829
- "sha256": "ec5f3f084616f79dde7149d1bb5dc88bb9b3c406cd08698e499863c15c375c7f"
1833
+ "sha256": "f75c050dd851d38fd90335def782699aea43f89930dfff95060fa88d9724a9a6"
1830
1834
  },
1831
1835
  {
1832
1836
  "path": "src/providers/codex-capacity.ts",
@@ -1834,7 +1838,7 @@
1834
1838
  },
1835
1839
  {
1836
1840
  "path": "src/providers/command-code-efforts.ts",
1837
- "sha256": "437845ad99e09307450889f149dd7770a69770dbcd4d024506f560e739804292"
1841
+ "sha256": "279cf80e7d2a3e2019f23bb8761f4cb7a6a58d0ce754b974e1b3d759c76bbff1"
1838
1842
  },
1839
1843
  {
1840
1844
  "path": "src/providers/context-cap.ts",
@@ -1876,13 +1880,21 @@
1876
1880
  "path": "src/providers/model-discovery.ts",
1877
1881
  "sha256": "298d6f169883bfd5de3b6059fb3ae71c07328d0cec85cf4e188b80e78a50edc6"
1878
1882
  },
1883
+ {
1884
+ "path": "src/providers/model-rename-migration.ts",
1885
+ "sha256": "cc3bd468dba3387eff13aedaa1fc48e68ea328fb5bc77c02d17bd84dce5e6393"
1886
+ },
1887
+ {
1888
+ "path": "src/providers/model-rename-startup.ts",
1889
+ "sha256": "8a143284d736759bf63c8cb296c19e61dec9596dfc781d8dd87038778153f55f"
1890
+ },
1879
1891
  {
1880
1892
  "path": "src/providers/openai-sidecar.ts",
1881
1893
  "sha256": "d07ec55e8600563fa9cbdb9e535cfecb8163b8235aa44547c9359f6ca8ce62d1"
1882
1894
  },
1883
1895
  {
1884
1896
  "path": "src/providers/openai-tier-startup.ts",
1885
- "sha256": "5dc89dd548d71a5f5e25b85a5f924125f4faeb74c23a327f74294eb40ba414d0"
1897
+ "sha256": "41256c1ef4b2271168a52dcfb3ab986077cfa8edc0386230a01f5b196155ca42"
1886
1898
  },
1887
1899
  {
1888
1900
  "path": "src/providers/openai-tiers.ts",
@@ -1906,11 +1918,11 @@
1906
1918
  },
1907
1919
  {
1908
1920
  "path": "src/providers/quota.ts",
1909
- "sha256": "d2d0f30c3e8dad900c8dce0ba0278a975f809b5e39830def8d59fe8663aefa99"
1921
+ "sha256": "fa2d207d569be34fd50fd72b90389e3e4147c5101cce4318629f3f21172a5e9a"
1910
1922
  },
1911
1923
  {
1912
1924
  "path": "src/providers/registry.ts",
1913
- "sha256": "f2a1b6f538bc7939bb98d9d0187c3350c6c46d9c844a3019a9f56582aa816c12"
1925
+ "sha256": "cfa71c40b3f93b2e21e62838ddbfb42ff93577b077f6aadbe34158089aedb307"
1914
1926
  },
1915
1927
  {
1916
1928
  "path": "src/providers/slug-codec.ts",
@@ -1954,11 +1966,11 @@
1954
1966
  },
1955
1967
  {
1956
1968
  "path": "src/responses/spill-store.ts",
1957
- "sha256": "f16559b63a214861cbbc47908653b831ae7ade2aa88f1cfd3c0907373d668f99"
1969
+ "sha256": "c0cd3343d0803eb6d14203f19d6ee38f33110042d8aa896cc9f60bd422ca8f21"
1958
1970
  },
1959
1971
  {
1960
1972
  "path": "src/responses/state.ts",
1961
- "sha256": "7d5586e7b48ced98e9ca38ee077fc828a53ab2425651c258688b43fb608542e7"
1973
+ "sha256": "59952393060143aae734770fbf78761a8cc69e5b6335c6452bdf5ac7da686bb4"
1962
1974
  },
1963
1975
  {
1964
1976
  "path": "src/responses/tool-groups.ts",
@@ -2102,7 +2114,7 @@
2102
2114
  },
2103
2115
  {
2104
2116
  "path": "src/server/index.ts",
2105
- "sha256": "cdf8ee40eeed9a69cfcb7ab95730b131ba57dad547349f74d585ffca97f171b9"
2117
+ "sha256": "cff5571eb50f6ec315fa2f3ddd835d3bdd25b59c94a5b991771f066ad3e3ffc2"
2106
2118
  },
2107
2119
  {
2108
2120
  "path": "src/server/lifecycle.ts",
@@ -2138,7 +2150,7 @@
2138
2150
  },
2139
2151
  {
2140
2152
  "path": "src/server/management/api-key-usage.ts",
2141
- "sha256": "37ef0b22cb1b7b2a03177c39cf609ebe40183aeadeab11462a17e67cc2e6848f"
2153
+ "sha256": "86a9fbebb6986d115b46e32da123b706d9ee773881bfa5e13b9d87554d89001f"
2142
2154
  },
2143
2155
  {
2144
2156
  "path": "src/server/management/body.ts",
@@ -2170,7 +2182,7 @@
2170
2182
  },
2171
2183
  {
2172
2184
  "path": "src/server/management/logs-usage-routes.ts",
2173
- "sha256": "bcd6c00783c78a3dd1a5b71a482f15595fefcf07610506729e309bd312b3e2f2"
2185
+ "sha256": "645c8902e0d3b589a5b20a7d607fb9b40197f34a4c73945a4bf9fd91336bf15e"
2174
2186
  },
2175
2187
  {
2176
2188
  "path": "src/server/management/model-routes.ts",
@@ -2190,7 +2202,7 @@
2190
2202
  },
2191
2203
  {
2192
2204
  "path": "src/server/management/provider-routes.ts",
2193
- "sha256": "d5a5a4980f1a5cf56a994a8d2b454c86c376ec9161b3e67903b1fc768bf59cf2"
2205
+ "sha256": "af98e4f45ae93adcc5e66e2b51f0dc08c0a3406924b5a63b97698dcdc1eeeb1d"
2194
2206
  },
2195
2207
  {
2196
2208
  "path": "src/server/management/request-history-routes.ts",
@@ -2226,7 +2238,7 @@
2226
2238
  },
2227
2239
  {
2228
2240
  "path": "src/server/management/usage-summary-cache.ts",
2229
- "sha256": "fb1b889f645afb2efb02f49d78f3390c504d073867af627ace9d29f97318fc9f"
2241
+ "sha256": "a8a386202dac11802e51c3bef85ce868363f80ed05aef9db3775109e40a2aba2"
2230
2242
  },
2231
2243
  {
2232
2244
  "path": "src/server/management/vision-sidecar-options.ts",
@@ -2314,7 +2326,7 @@
2314
2326
  },
2315
2327
  {
2316
2328
  "path": "src/server/responses/collaboration.ts",
2317
- "sha256": "e3bcb01890c2d5e3b2f5032de223a537455c21bf06a134e4f5c828c6b56dc243"
2329
+ "sha256": "bfeef4d9bf5538d850446d5805769779518b62f8ef33693ecfd1c48f8545093e"
2318
2330
  },
2319
2331
  {
2320
2332
  "path": "src/server/responses/compact.ts",
@@ -2322,7 +2334,7 @@
2322
2334
  },
2323
2335
  {
2324
2336
  "path": "src/server/responses/core.ts",
2325
- "sha256": "e115d89db4e94c910ea74976c259260b6dd9f6d0238a82ba16a3fe49b12bfa86"
2337
+ "sha256": "c67fdae5470a8d0c234e24d7d062bf351ec35c911550363c8d73199f4059fc95"
2326
2338
  },
2327
2339
  {
2328
2340
  "path": "src/server/responses/encrypted-payload.ts",
@@ -2370,7 +2382,7 @@
2370
2382
  },
2371
2383
  {
2372
2384
  "path": "src/server/startup-health-cache.ts",
2373
- "sha256": "2ef39b33ec79cd6f8d1bf9040ce1510c32142a643d7a9e2ba56690fe23995da5"
2385
+ "sha256": "0193264ad0f951085d774e052342385aa5043fa2b0dbd5fba224a4b790604f1f"
2374
2386
  },
2375
2387
  {
2376
2388
  "path": "src/server/system-env.ts",
@@ -2526,7 +2538,7 @@
2526
2538
  },
2527
2539
  {
2528
2540
  "path": "src/usage/log.ts",
2529
- "sha256": "b96cacb9e6eaac59eeaee8f5124a985cc0d14169364401eec80a10a8d3a24a76"
2541
+ "sha256": "db69314bab2520a05889a3cef8c1de042ecdcdb67792f687399eb23c140e2c75"
2530
2542
  },
2531
2543
  {
2532
2544
  "path": "src/usage/summary.ts",
@@ -34,6 +34,10 @@ import {
34
34
  evictOldestUsageSummaryForBudget,
35
35
  usageSummaryRetainedStoreSnapshot,
36
36
  } from "../server/management/usage-summary-cache";
37
+ import {
38
+ discardRetainedUsageSnapshot,
39
+ retainedUsageSnapshotStats,
40
+ } from "../usage/log";
37
41
  import {
38
42
  cursorBlobRetainedStoreSnapshot,
39
43
  evictOldestCursorBlobForBudget,
@@ -57,6 +61,18 @@ function ringSnapshot(metrics: { entries: number; bytes: number; oldestAt: numbe
57
61
  };
58
62
  }
59
63
 
64
+ /** The retained usage tail is a single all-or-nothing entry: evicting it drops the whole tail. */
65
+ function usageSnapshotRetainedStoreSnapshot(): RetainedStoreSnapshot {
66
+ const stats = retainedUsageSnapshotStats();
67
+ return {
68
+ count: stats.count,
69
+ bytes: stats.bytes,
70
+ evictableBytes: stats.bytes,
71
+ pinnedBytes: 0,
72
+ oldestAt: stats.oldestAt,
73
+ };
74
+ }
75
+
60
76
  function providerDebugSnapshot(): RetainedStoreSnapshot {
61
77
  return ringSnapshot(debugBufferMetrics());
62
78
  }
@@ -134,6 +150,12 @@ export const APP_OWNED_RETAINED_STORE_REGISTRATIONS = [
134
150
  snapshot: usageSummaryRetainedStoreSnapshot,
135
151
  evictOldest: evictOldestUsageSummaryForBudget,
136
152
  },
153
+ {
154
+ id: "usage_snapshot",
155
+ category: "caches",
156
+ snapshot: usageSnapshotRetainedStoreSnapshot,
157
+ evictOldest: discardRetainedUsageSnapshot,
158
+ },
137
159
  {
138
160
  id: "cursor_blobs",
139
161
  category: "blobs",
@@ -0,0 +1,158 @@
1
+ // Schema-aware repair for integer tool arguments that arrive as floats (issue #1611).
2
+ //
3
+ // Grok serializes integer tool-call arguments through a float representation, so
4
+ // `yield_time_ms: 120000` leaves the provider as `120000.0`. Codex declares those
5
+ // fields as Rust integer types, so it rejects the call BEFORE running the tool:
6
+ //
7
+ // failed to parse function arguments: invalid type: floating point `120000.0`, expected u64
8
+ //
9
+ // That is a hard failure, not a degradation — the model gets no result and retries
10
+ // the same float. Nothing on the routed path reconciled argument values against the
11
+ // declared schema, so the `.0` passed straight through.
12
+ //
13
+ // The boundary this file draws is INTENT, not convenience:
14
+ // - an integral float in an integer-typed field is a representation artifact, and
15
+ // `120000.0` has exactly one integer reading, so it is repaired;
16
+ // - `1.5` in an integer field is a genuine disagreement with the schema and is left
17
+ // alone so it still fails, rather than being truncated into a plausible lie.
18
+ // Anything without a declared `integer` type is never touched.
19
+
20
+ /** JSON Schema subset we need; provider tool schemas are untrusted input. */
21
+ type SchemaNode = Record<string, unknown>;
22
+
23
+ function asSchema(value: unknown): SchemaNode | undefined {
24
+ return value && typeof value === "object" && !Array.isArray(value)
25
+ ? value as SchemaNode
26
+ : undefined;
27
+ }
28
+
29
+ /** True when the node declares `integer`, including `["integer","null"]` unions. */
30
+ function declaresInteger(schema: SchemaNode): boolean {
31
+ const type = schema.type;
32
+ if (type === "integer") return true;
33
+ return Array.isArray(type) && type.includes("integer");
34
+ }
35
+
36
+ /**
37
+ * Resolve a local `$ref` (`#/$defs/Foo`, `#/definitions/Foo`).
38
+ *
39
+ * Only same-document refs are followed: a remote ref is not fetchable here, and a
40
+ * schema we cannot resolve must leave its values untouched rather than guessed at.
41
+ */
42
+ function resolveRef(schema: SchemaNode, root: SchemaNode, seen: Set<string>): SchemaNode | undefined {
43
+ const ref = schema.$ref;
44
+ if (typeof ref !== "string" || !ref.startsWith("#/")) return schema;
45
+ if (seen.has(ref)) return undefined; // cyclic $ref: stop rather than recurse forever
46
+ seen.add(ref);
47
+ let node: unknown = root;
48
+ for (const rawSegment of ref.slice(2).split("/")) {
49
+ const segment = rawSegment.replace(/~1/g, "/").replace(/~0/g, "~");
50
+ const current = asSchema(node);
51
+ if (!current) return undefined;
52
+ node = current[segment];
53
+ }
54
+ const resolved = asSchema(node);
55
+ return resolved ? resolveRef(resolved, root, seen) : undefined;
56
+ }
57
+
58
+ /** Composition keywords whose branches may carry the integer declaration. */
59
+ const COMPOSITION_KEYS = ["anyOf", "oneOf", "allOf"] as const;
60
+
61
+ function compositionBranches(schema: SchemaNode): SchemaNode[] {
62
+ const branches: SchemaNode[] = [];
63
+ for (const key of COMPOSITION_KEYS) {
64
+ const value = schema[key];
65
+ if (!Array.isArray(value)) continue;
66
+ for (const branch of value) {
67
+ const node = asSchema(branch);
68
+ if (node) branches.push(node);
69
+ }
70
+ }
71
+ return branches;
72
+ }
73
+
74
+ /**
75
+ * A JS number can only represent integers exactly up to 2^53-1. Beyond that a
76
+ * rewrite would emit a silently different value, so the original text stays.
77
+ */
78
+ function safelyIntegral(value: number): boolean {
79
+ return Number.isInteger(value) && Math.abs(value) <= Number.MAX_SAFE_INTEGER;
80
+ }
81
+
82
+ interface CoerceResult {
83
+ value: unknown;
84
+ changed: boolean;
85
+ }
86
+
87
+ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: SchemaNode, depth: number): CoerceResult {
88
+ // A hostile or deeply nested schema must not blow the stack.
89
+ if (depth > 64) return { value, changed: false };
90
+ const resolved = schema ? resolveRef(schema, root, new Set()) : undefined;
91
+
92
+ if (typeof value === "number") {
93
+ if (!resolved) return { value, changed: false };
94
+ const integerDeclared = declaresInteger(resolved)
95
+ || compositionBranches(resolved).some(declaresInteger);
96
+ // Not an integer field, already an integer, non-integral, or unrepresentable:
97
+ // in every one of those cases the received value is the right thing to keep.
98
+ if (!integerDeclared || !safelyIntegral(value)) return { value, changed: false };
99
+ // `120000.0` and `120000` are the same JS number; the difference is only in the
100
+ // serialized text, which is repaired by re-stringifying the parsed value.
101
+ return { value, changed: true };
102
+ }
103
+
104
+ if (Array.isArray(value)) {
105
+ const itemSchema = resolved ? asSchema(resolved.items) : undefined;
106
+ let changed = false;
107
+ const next = value.map(entry => {
108
+ const result = coerceValue(entry, itemSchema, root, depth + 1);
109
+ if (result.changed) changed = true;
110
+ return result.value;
111
+ });
112
+ return changed ? { value: next, changed } : { value, changed: false };
113
+ }
114
+
115
+ const object = asSchema(value);
116
+ if (!object) return { value, changed: false };
117
+
118
+ const properties = resolved ? asSchema(resolved.properties) : undefined;
119
+ const additional = resolved ? asSchema(resolved.additionalProperties) : undefined;
120
+ let changed = false;
121
+ const next: Record<string, unknown> = {};
122
+ for (const [key, entry] of Object.entries(object)) {
123
+ const childSchema = asSchema(properties?.[key]) ?? additional;
124
+ const result = coerceValue(entry, childSchema, root, depth + 1);
125
+ if (result.changed) changed = true;
126
+ next[key] = result.value;
127
+ }
128
+ return changed ? { value: next, changed } : { value, changed: false };
129
+ }
130
+
131
+ /**
132
+ * Repair integral floats in a tool-call arguments STRING against the tool's declared
133
+ * parameter schema.
134
+ *
135
+ * Returns the original string unchanged when there is no schema, the payload does not
136
+ * parse, or nothing needed repair — so an unaffected call keeps its exact bytes and
137
+ * this stays a no-op for every provider that already emits real integers.
138
+ */
139
+ export function coerceIntegerToolArguments(
140
+ args: string,
141
+ parameters: Record<string, unknown> | undefined,
142
+ ): string {
143
+ if (!parameters || !args) return args;
144
+ // Cheap reject: a payload with no fractional-looking number cannot need repair.
145
+ if (!/\d\.\d/.test(args)) return args;
146
+ let parsed: unknown;
147
+ try {
148
+ parsed = JSON.parse(args);
149
+ } catch {
150
+ // Malformed or still-streaming arguments are not this function's problem; the
151
+ // existing paths already handle them.
152
+ return args;
153
+ }
154
+ const root = parameters as SchemaNode;
155
+ const result = coerceValue(parsed, root, root, 0);
156
+ if (!result.changed) return args;
157
+ return JSON.stringify(result.value);
158
+ }
package/src/oauth/nous.ts CHANGED
@@ -39,6 +39,7 @@ import { join } from "node:path";
39
39
  import type { OAuthController, OAuthCredentials } from "./types";
40
40
  import { getAuthStorePath } from "./store";
41
41
  import { atomicWriteFile, hardenConfigDir, hardenExistingSecret } from "../config";
42
+ import { BOUNDED_BODY_MAX_BYTES, readBoundedResponseBytes } from "../lib/bounded-body";
42
43
 
43
44
  export const NOUS_PORTAL_BASE_URL = "https://portal.nousresearch.com";
44
45
  export const NOUS_INFERENCE_BASE_URL = "https://inference-api.nousresearch.com/v1";
@@ -87,6 +88,44 @@ interface NousJwtPayload {
87
88
  [key: string]: unknown;
88
89
  }
89
90
 
91
+ async function readOAuthBytes(response: Response, signal: AbortSignal): Promise<Uint8Array> {
92
+ const { bytes, oversized } = await readBoundedResponseBytes(response, {
93
+ maxBytes: BOUNDED_BODY_MAX_BYTES,
94
+ signal,
95
+ });
96
+ if (oversized) {
97
+ throw new NousTokenError(
98
+ response.status,
99
+ "response_too_large",
100
+ `Nous Portal OAuth response exceeded the ${BOUNDED_BODY_MAX_BYTES}-byte limit`,
101
+ );
102
+ }
103
+ return bytes;
104
+ }
105
+
106
+ function parseOAuthJson(bytes: Uint8Array): unknown {
107
+ const parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
108
+ return parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)
109
+ ? parsed as Record<string, unknown>
110
+ : {};
111
+ }
112
+
113
+ async function readOAuthJson(response: Response, signal: AbortSignal): Promise<unknown> {
114
+ return parseOAuthJson(await readOAuthBytes(response, signal));
115
+ }
116
+
117
+ async function readOAuthJsonOrEmpty(response: Response, signal: AbortSignal): Promise<unknown> {
118
+ const bytes = await readOAuthBytes(response, signal);
119
+ try {
120
+ return parseOAuthJson(bytes);
121
+ } catch {
122
+ // Preserve the pre-PR behavior for empty/HTML/malformed JSON only. Body
123
+ // read failures, timeouts, caller cancellation, and size-limit errors have
124
+ // already escaped readOAuthBytes and must retain their real identity.
125
+ return {};
126
+ }
127
+ }
128
+
90
129
  // ── Durable refresh-intent (review blocker #2) ──────────────────────────────
91
130
  // A refresh-intent file records that we submitted `refreshToken` to the Portal
92
131
  // and whether we are certain the rotated token was persisted. It lives next to
@@ -495,6 +534,7 @@ async function requestDeviceAuthorization(signal?: AbortSignal): Promise<{
495
534
  expiresInMs: number;
496
535
  intervalMs: number;
497
536
  }> {
537
+ const effectiveSignal = requestSignal(signal);
498
538
  const response = await fetch(`${resolvePortalBaseUrl()}/api/oauth/device/code`, {
499
539
  method: "POST",
500
540
  headers: { Accept: "application/json", "Content-Type": "application/x-www-form-urlencoded" },
@@ -503,14 +543,16 @@ async function requestDeviceAuthorization(signal?: AbortSignal): Promise<{
503
543
  scope: NOUS_OAUTH_SCOPE,
504
544
  }),
505
545
  redirect: "error",
506
- signal: requestSignal(signal),
546
+ signal: effectiveSignal,
507
547
  });
508
- if (!response.ok) throw tokenErrorFromPayload(response.status, await response.json().catch(() => ({})));
548
+ if (!response.ok) {
549
+ throw tokenErrorFromPayload(response.status, await readOAuthJsonOrEmpty(response, effectiveSignal));
550
+ }
509
551
  // A successful HTTP response may still carry an empty/HTML/non-JSON body.
510
552
  // Fall back to an empty object so the required-field check below produces the
511
553
  // clear "missing required fields" validation error instead of leaking a raw
512
554
  // JSON parser exception.
513
- const payload = (await response.json().catch(() => ({}))) as NousDeviceAuthorizationResponse;
555
+ const payload = await readOAuthJsonOrEmpty(response, effectiveSignal) as NousDeviceAuthorizationResponse;
514
556
  const userCode = nonEmptyString(payload.user_code);
515
557
  const deviceCode = nonEmptyString(payload.device_code);
516
558
  const verificationUri = nonEmptyString(payload.verification_uri_complete) ?? nonEmptyString(payload.verification_uri);
@@ -549,6 +591,7 @@ async function pollForToken(
549
591
  while (Date.now() < deadline) {
550
592
  if (signal?.aborted) throw new Error("Login cancelled");
551
593
  let response: Response;
594
+ const effectiveSignal = requestSignal(signal);
552
595
  try {
553
596
  response = await fetch(`${resolvePortalBaseUrl()}/api/oauth/token`, {
554
597
  method: "POST",
@@ -559,7 +602,7 @@ async function pollForToken(
559
602
  grant_type: "urn:ietf:params:oauth:grant-type:device_code",
560
603
  }),
561
604
  redirect: "error",
562
- signal: requestSignal(signal),
605
+ signal: effectiveSignal,
563
606
  });
564
607
  } catch (netErr) {
565
608
  // Genuine cancellation must abort immediately. Any other transport-level
@@ -571,13 +614,15 @@ async function pollForToken(
571
614
  if (await sleepUntilDeadline(waitMs)) continue;
572
615
  break;
573
616
  }
617
+ // Parse under the same deadline that covered the request headers. Keep the
618
+ // read outside the fetch retry catch: a bounded-reader error or caller
619
+ // cancellation is an observed response failure, not a safe poll retry.
620
+ const payload = await readOAuthJsonOrEmpty(response, effectiveSignal) as NousTokenResponse;
574
621
  // Parse once and pass the payload through to the failure path (review #8),
575
622
  // so we never try to re-read a body that has already been consumed.
576
623
  // Normalize a successful-but-non-object body (for example valid JSON
577
624
  // `null`) to an empty object so the required-field validation below
578
625
  // produces a terminal NousTokenError instead of a raw TypeError.
579
- const parsed = (await response.json().catch(() => ({}))) as unknown;
580
- const payload = (parsed && typeof parsed === "object" ? parsed : {}) as NousTokenResponse;
581
626
  if (Date.now() >= deadline) break;
582
627
  if (response.ok) return parseTokenPayload(payload, "");
583
628
  const error = payload.error;
@@ -670,6 +715,7 @@ export async function refreshNousToken(refreshToken: string, signal?: AbortSigna
670
715
  }
671
716
 
672
717
  let response: Response;
718
+ const effectiveSignal = requestSignal(signal);
673
719
  try {
674
720
  response = await fetch(`${baseUrl}/api/oauth/token`, {
675
721
  method: "POST",
@@ -683,7 +729,7 @@ export async function refreshNousToken(refreshToken: string, signal?: AbortSigna
683
729
  client_id: NOUS_OAUTH_CLIENT_ID,
684
730
  }),
685
731
  redirect: "error",
686
- signal: requestSignal(signal),
732
+ signal: effectiveSignal,
687
733
  });
688
734
  } catch (netErr) {
689
735
  // The request may have reached the server and rotated the token even on a
@@ -703,7 +749,6 @@ export async function refreshNousToken(refreshToken: string, signal?: AbortSigna
703
749
 
704
750
  if (!response.ok) {
705
751
  const status = response.status;
706
- const payload = await response.json().catch(() => ({}));
707
752
  // The request reached the Portal's token endpoint. A non-2xx response does
708
753
  // NOT establish that the single-use refresh token was not consumed: 429
709
754
  // rate limits, unknown/custom 4xx, and gateway-generated client-class
@@ -719,6 +764,7 @@ export async function refreshNousToken(refreshToken: string, signal?: AbortSigna
719
764
  // The pre-dispatch "submitted" intent is still on disk, which also
720
765
  // blocks replay; surface the original HTTP error below.
721
766
  }
767
+ const payload = await readOAuthJsonOrEmpty(response, effectiveSignal);
722
768
  throw tokenErrorFromPayload(status, payload);
723
769
  }
724
770
 
@@ -727,7 +773,10 @@ export async function refreshNousToken(refreshToken: string, signal?: AbortSigna
727
773
  // replay it. On success we deliberately LEAVE the intent as "submitted"
728
774
  // (the store clears it once the rotated token is persisted).
729
775
  try {
730
- const creds = parseTokenPayload((await response.json()) as NousTokenResponse, refreshToken);
776
+ const creds = parseTokenPayload(
777
+ (await readOAuthJson(response, effectiveSignal)) as NousTokenResponse,
778
+ refreshToken,
779
+ );
731
780
  return creds;
732
781
  } catch (e) {
733
782
  try {