@bitkyc08/opencodex 2.70.0 → 2.71.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{App-_MrbTrBe.js → App-BKML41Kr.js} +4 -4
- package/gui/dist/assets/{Tray-5PuUalET.js → Tray-DFOclkLw.js} +1 -1
- package/gui/dist/assets/index-BKsHGDTE.css +1 -0
- package/gui/dist/assets/{index-DKrO2eF3.js → index-DSLWDj_0.js} +11 -11
- package/gui/dist/assets/{quota-summary-BCLivc0Q.js → quota-summary-Cit0cOn7.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-model-contract.ts +51 -8
- package/src/adapters/anthropic.ts +6 -2
- package/src/adapters/openai-chat/summary-budget.ts +11 -1
- package/src/claude/desktop-picker.ts +6 -2
- package/src/claude/intercept/local-ca.ts +25 -1
- package/src/claude/intercept/picker-ca.ts +132 -0
- package/src/cli/capabilities.ts +2 -1
- package/src/cli/claude-desktop.ts +2 -1
- package/src/cli/cross-home-owner.ts +221 -22
- package/src/cli/index.ts +3 -1
- package/src/codex/catalog-auto-refresh.ts +8 -1
- package/src/config/owner-registry.ts +154 -0
- package/src/config/process-state.ts +8 -0
- package/src/generated/compatibility-version.json +32 -20
- package/src/integrations/cursor-detect.ts +3 -2
- package/src/integrations/cursor-local-installer.ts +150 -0
- package/src/server/management/cursor-integration-routes.ts +20 -0
- package/src/server/management/route-registry.ts +1 -0
- package/src/server/port-reclaim.ts +57 -7
- package/src/server/proxy-liveness.ts +46 -19
- package/src/service/windows-process-priority.ts +39 -0
- package/src/vision/anthropic-describe.ts +1 -1
- package/src/web-search/anthropic-executor.ts +3 -2
- package/gui/dist/assets/index-BF9QnKO_.css +0 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
12
|
"path": "package.json",
|
|
13
|
-
"sha256": "
|
|
13
|
+
"sha256": "94fb6ebefca26ab1c618600b5c9e836bf562f88e442cad127562d862d024cada"
|
|
14
14
|
},
|
|
15
15
|
{
|
|
16
16
|
"path": "scripts/model-metadata.source.json",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
},
|
|
39
39
|
{
|
|
40
40
|
"path": "src/adapters/anthropic-model-contract.ts",
|
|
41
|
-
"sha256": "
|
|
41
|
+
"sha256": "6cca852818694d3a1676c5fe89d9fb69c08a32bc857f56b9dda94bec468f3b81"
|
|
42
42
|
},
|
|
43
43
|
{
|
|
44
44
|
"path": "src/adapters/anthropic-output-schema.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
},
|
|
47
47
|
{
|
|
48
48
|
"path": "src/adapters/anthropic.ts",
|
|
49
|
-
"sha256": "
|
|
49
|
+
"sha256": "9a01a71b8e5e97b1ae76bc26e284665e629de94ee9fa0efa087c2d09c7fd96b8"
|
|
50
50
|
},
|
|
51
51
|
{
|
|
52
52
|
"path": "src/adapters/anthropic/beta-allowlist.ts",
|
|
@@ -578,7 +578,7 @@
|
|
|
578
578
|
},
|
|
579
579
|
{
|
|
580
580
|
"path": "src/adapters/openai-chat/summary-budget.ts",
|
|
581
|
-
"sha256": "
|
|
581
|
+
"sha256": "1c96b651a6683a2b3df5089ac26e2df9e1e02612ccd09e666562345c8bfc6733"
|
|
582
582
|
},
|
|
583
583
|
{
|
|
584
584
|
"path": "src/adapters/openai-chat/tool-call-id-remint.ts",
|
|
@@ -830,7 +830,7 @@
|
|
|
830
830
|
},
|
|
831
831
|
{
|
|
832
832
|
"path": "src/claude/desktop-picker.ts",
|
|
833
|
-
"sha256": "
|
|
833
|
+
"sha256": "d10d6826b572f12f8f6b27facaa80fac7c0d98812ebb4e658f8978d10f12a687"
|
|
834
834
|
},
|
|
835
835
|
{
|
|
836
836
|
"path": "src/claude/desktop-policy.ts",
|
|
@@ -910,7 +910,7 @@
|
|
|
910
910
|
},
|
|
911
911
|
{
|
|
912
912
|
"path": "src/claude/intercept/local-ca.ts",
|
|
913
|
-
"sha256": "
|
|
913
|
+
"sha256": "5344c5e90053515dfd8f6efe04180f0f96f66fadd1d4d20e96568a0592070919"
|
|
914
914
|
},
|
|
915
915
|
{
|
|
916
916
|
"path": "src/claude/intercept/model-bindings.ts",
|
|
@@ -926,7 +926,7 @@
|
|
|
926
926
|
},
|
|
927
927
|
{
|
|
928
928
|
"path": "src/claude/intercept/picker-ca.ts",
|
|
929
|
-
"sha256": "
|
|
929
|
+
"sha256": "4afa56ccad7adf07082dbf49ac0c46b8f75a678b50f28bcbb5e5237a812c6c93"
|
|
930
930
|
},
|
|
931
931
|
{
|
|
932
932
|
"path": "src/claude/intercept/picker-listener.ts",
|
|
@@ -1038,7 +1038,7 @@
|
|
|
1038
1038
|
},
|
|
1039
1039
|
{
|
|
1040
1040
|
"path": "src/cli/capabilities.ts",
|
|
1041
|
-
"sha256": "
|
|
1041
|
+
"sha256": "f475daacc68ae84f891d6a1642d5accd572e797a771d176a2a54272e05abbe9b"
|
|
1042
1042
|
},
|
|
1043
1043
|
{
|
|
1044
1044
|
"path": "src/cli/catalog-prewarm.ts",
|
|
@@ -1054,7 +1054,7 @@
|
|
|
1054
1054
|
},
|
|
1055
1055
|
{
|
|
1056
1056
|
"path": "src/cli/claude-desktop.ts",
|
|
1057
|
-
"sha256": "
|
|
1057
|
+
"sha256": "2a66d4d53877026f33185340dc2086345e1ac893575d55d57d51f72c01663efd"
|
|
1058
1058
|
},
|
|
1059
1059
|
{
|
|
1060
1060
|
"path": "src/cli/claude.ts",
|
|
@@ -1098,7 +1098,7 @@
|
|
|
1098
1098
|
},
|
|
1099
1099
|
{
|
|
1100
1100
|
"path": "src/cli/cross-home-owner.ts",
|
|
1101
|
-
"sha256": "
|
|
1101
|
+
"sha256": "706c4eb6706c67ad05e1c6a4c1ef4e79850ac073b492cca4202f29687bc2e006"
|
|
1102
1102
|
},
|
|
1103
1103
|
{
|
|
1104
1104
|
"path": "src/cli/debug.ts",
|
|
@@ -1142,7 +1142,7 @@
|
|
|
1142
1142
|
},
|
|
1143
1143
|
{
|
|
1144
1144
|
"path": "src/cli/index.ts",
|
|
1145
|
-
"sha256": "
|
|
1145
|
+
"sha256": "34af92d38d0ad171884ab83954043ff4d1752c0aa22c54cfed81cd72c4cb74d4"
|
|
1146
1146
|
},
|
|
1147
1147
|
{
|
|
1148
1148
|
"path": "src/cli/init.ts",
|
|
@@ -1590,7 +1590,7 @@
|
|
|
1590
1590
|
},
|
|
1591
1591
|
{
|
|
1592
1592
|
"path": "src/codex/catalog-auto-refresh.ts",
|
|
1593
|
-
"sha256": "
|
|
1593
|
+
"sha256": "6c700d85818802a8d87b7b718d74a2e0da9e7bbe3e7b50eccf8a3558d7256f2c"
|
|
1594
1594
|
},
|
|
1595
1595
|
{
|
|
1596
1596
|
"path": "src/codex/catalog-refresh-status.ts",
|
|
@@ -2488,6 +2488,10 @@
|
|
|
2488
2488
|
"path": "src/config/openai-tier-backup.ts",
|
|
2489
2489
|
"sha256": "dcd1ecb3020c3ff30b661121a515df27d09f750bebdd3369aa5bc0e8c95bc4e6"
|
|
2490
2490
|
},
|
|
2491
|
+
{
|
|
2492
|
+
"path": "src/config/owner-registry.ts",
|
|
2493
|
+
"sha256": "0d0f97d81db4cbe2ac029a79938e83119be4c76340abd8dd3c823332bad3419b"
|
|
2494
|
+
},
|
|
2491
2495
|
{
|
|
2492
2496
|
"path": "src/config/paths.ts",
|
|
2493
2497
|
"sha256": "37f6b7f0c9e078d7fe79e2aaf54ba2961aa4cbeb281d84b3d950e89846bfaf3f"
|
|
@@ -2514,7 +2518,7 @@
|
|
|
2514
2518
|
},
|
|
2515
2519
|
{
|
|
2516
2520
|
"path": "src/config/process-state.ts",
|
|
2517
|
-
"sha256": "
|
|
2521
|
+
"sha256": "a0be3e7c31894b1cc4a23c430bc22fa300ccf95a2880364dcb7280bdb7ba16a5"
|
|
2518
2522
|
},
|
|
2519
2523
|
{
|
|
2520
2524
|
"path": "src/config/provider-name.ts",
|
|
@@ -2702,12 +2706,16 @@
|
|
|
2702
2706
|
},
|
|
2703
2707
|
{
|
|
2704
2708
|
"path": "src/integrations/cursor-detect.ts",
|
|
2705
|
-
"sha256": "
|
|
2709
|
+
"sha256": "5218146125d92d9d5fa7f51743b85618ab84ecdbb8710d0e85d58baf62c41821"
|
|
2706
2710
|
},
|
|
2707
2711
|
{
|
|
2708
2712
|
"path": "src/integrations/cursor-effort-table.ts",
|
|
2709
2713
|
"sha256": "34b27afb34f2169c042354998e4af91616075f27bd6b086457ae50be8d6194d4"
|
|
2710
2714
|
},
|
|
2715
|
+
{
|
|
2716
|
+
"path": "src/integrations/cursor-local-installer.ts",
|
|
2717
|
+
"sha256": "a771e3ace879f3b8dae8f9b9fa58046e43845e2539edafa31c521dc6fe2894ab"
|
|
2718
|
+
},
|
|
2711
2719
|
{
|
|
2712
2720
|
"path": "src/integrations/cursor-seen.ts",
|
|
2713
2721
|
"sha256": "ed100257d59585f2f42c46157f844a44f0f53ec66f82cbd267675baef798e231"
|
|
@@ -5142,7 +5150,7 @@
|
|
|
5142
5150
|
},
|
|
5143
5151
|
{
|
|
5144
5152
|
"path": "src/server/management/cursor-integration-routes.ts",
|
|
5145
|
-
"sha256": "
|
|
5153
|
+
"sha256": "767a0c404c3c3c30cc4610749a21008570abfcdda8304a158a2b3267e97527bf"
|
|
5146
5154
|
},
|
|
5147
5155
|
{
|
|
5148
5156
|
"path": "src/server/management/grok-coupon-routes.ts",
|
|
@@ -5230,7 +5238,7 @@
|
|
|
5230
5238
|
},
|
|
5231
5239
|
{
|
|
5232
5240
|
"path": "src/server/management/route-registry.ts",
|
|
5233
|
-
"sha256": "
|
|
5241
|
+
"sha256": "0ab475d125edf34c9f0617980289f992db73fba000e3a3080818730151d2d2e9"
|
|
5234
5242
|
},
|
|
5235
5243
|
{
|
|
5236
5244
|
"path": "src/server/management/routing-analytics-routes.ts",
|
|
@@ -5326,7 +5334,7 @@
|
|
|
5326
5334
|
},
|
|
5327
5335
|
{
|
|
5328
5336
|
"path": "src/server/port-reclaim.ts",
|
|
5329
|
-
"sha256": "
|
|
5337
|
+
"sha256": "e020d6243d94e77b97977ab5db7b5601123d8d9e1382dbbcc2bb4f88d5ba694b"
|
|
5330
5338
|
},
|
|
5331
5339
|
{
|
|
5332
5340
|
"path": "src/server/ports.ts",
|
|
@@ -5334,7 +5342,7 @@
|
|
|
5334
5342
|
},
|
|
5335
5343
|
{
|
|
5336
5344
|
"path": "src/server/proxy-liveness.ts",
|
|
5337
|
-
"sha256": "
|
|
5345
|
+
"sha256": "33f225eb0132bf2b5e2f6f3415613e034c149a7ada4dd578925c5dddeb427390"
|
|
5338
5346
|
},
|
|
5339
5347
|
{
|
|
5340
5348
|
"path": "src/server/readiness.ts",
|
|
@@ -5916,6 +5924,10 @@
|
|
|
5916
5924
|
"path": "src/service/windows-ops.ts",
|
|
5917
5925
|
"sha256": "fd1343ec88bc57fbec24a90e7662567cbb5e8883133826038caca643198d3e50"
|
|
5918
5926
|
},
|
|
5927
|
+
{
|
|
5928
|
+
"path": "src/service/windows-process-priority.ts",
|
|
5929
|
+
"sha256": "59d3f6685f884fc11e6d2e1a0f3edd729c4c01355aa01a26d202b7ea89520e05"
|
|
5930
|
+
},
|
|
5919
5931
|
{
|
|
5920
5932
|
"path": "src/service/windows-scheduler.ts",
|
|
5921
5933
|
"sha256": "bc47a79ea9427d27eb6b127f63b7be80efc05d355bac2fb7762250c9d222256f"
|
|
@@ -6350,7 +6362,7 @@
|
|
|
6350
6362
|
},
|
|
6351
6363
|
{
|
|
6352
6364
|
"path": "src/vision/anthropic-describe.ts",
|
|
6353
|
-
"sha256": "
|
|
6365
|
+
"sha256": "b696ff4e1deaf7b00a03487902776dc7da9a237f61590ff70fbcc8dd10500fed"
|
|
6354
6366
|
},
|
|
6355
6367
|
{
|
|
6356
6368
|
"path": "src/vision/backends.ts",
|
|
@@ -6394,7 +6406,7 @@
|
|
|
6394
6406
|
},
|
|
6395
6407
|
{
|
|
6396
6408
|
"path": "src/web-search/anthropic-executor.ts",
|
|
6397
|
-
"sha256": "
|
|
6409
|
+
"sha256": "de002f4ba40b28f94e014a3cf39e37dcaf920d9f612addd6c449d24a4fef51a3"
|
|
6398
6410
|
},
|
|
6399
6411
|
{
|
|
6400
6412
|
"path": "src/web-search/backends.ts",
|
|
@@ -91,8 +91,9 @@ export function cursorProductJsonCandidates(deps: CursorDetectDeps): Array<{ roo
|
|
|
91
91
|
return out;
|
|
92
92
|
}
|
|
93
93
|
// Linux: AppImages carry product.json only once extracted, so this covers the tarball /
|
|
94
|
-
// package layouts and stays best-effort.
|
|
95
|
-
|
|
94
|
+
// package layouts and stays best-effort. /usr/share covers distro-style installs of the
|
|
95
|
+
// tarball (e.g. /usr/share/cursor), which is where the Private Inference .deb lands.
|
|
96
|
+
for (const dir of ["/opt", "/usr/share", join(deps.homedir, ".local", "share")]) {
|
|
96
97
|
for (const entry of deps.listDir(dir)) {
|
|
97
98
|
if (!/^cursor/i.test(entry)) continue;
|
|
98
99
|
const root = join(dir, entry);
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `cursor-local` update channel is how Cursor now distributes Private Inference: a
|
|
3
|
+
* same-version installer advertised through the regular install's own update endpoint
|
|
4
|
+
* (Cursor 3.21.18+). Regular Cursor users were previously told "not found" with no path
|
|
5
|
+
* forward; the channel manifest is the path forward, surfaced read-only.
|
|
6
|
+
*
|
|
7
|
+
* This module never downloads, launches, or installs anything: it formats one URL the
|
|
8
|
+
* dashboard shows the user. The manifest request is a plain GET of a public endpoint and
|
|
9
|
+
* its failure degrades to `available: false` with the reason recorded — the update
|
|
10
|
+
* endpoint is undocumented and can change shape or vanish without notice.
|
|
11
|
+
*
|
|
12
|
+
* The lookup runs only on an explicit dashboard action, never from the polled status route. The
|
|
13
|
+
* answer is cached per update host and platform (successes for 30 minutes, failures for 5) and
|
|
14
|
+
* concurrent lookups share one request: repeated presses against a slow or unreachable channel
|
|
15
|
+
* cost at most one bounded wait per failure window.
|
|
16
|
+
*/
|
|
17
|
+
const DEFAULT_UPDATE_HOST = "https://api2.cursor.sh";
|
|
18
|
+
|
|
19
|
+
const UPDATE_MANIFEST_TIMEOUT_MS = 4_000;
|
|
20
|
+
const CACHE_TTL_OK_MS = 30 * 60_000;
|
|
21
|
+
const CACHE_TTL_FAILED_MS = 5 * 60_000;
|
|
22
|
+
|
|
23
|
+
/** Channel segments for the builds Cursor ships; Windows uses the per-user installer. */
|
|
24
|
+
const PLATFORMS = ["win32-x64-user", "win32-arm64-user", "darwin-x64", "darwin-arm64", "linux-x64", "linux-arm64"] as const;
|
|
25
|
+
|
|
26
|
+
export type CursorLocalPlatform = (typeof PLATFORMS)[number];
|
|
27
|
+
|
|
28
|
+
export interface CursorLocalInstallerHint {
|
|
29
|
+
/** Whether the channel advertised an installer the dashboard can name. */
|
|
30
|
+
available: boolean;
|
|
31
|
+
/** The download URL from the manifest, when one was advertised. */
|
|
32
|
+
url: string | null;
|
|
33
|
+
/** The advertised installer version, when the manifest carried one. */
|
|
34
|
+
version: string | null;
|
|
35
|
+
/**
|
|
36
|
+
* Why nothing was advertised: no regular install, a host Cursor ships no build for,
|
|
37
|
+
* an unreachable manifest, or an unusable answer.
|
|
38
|
+
*/
|
|
39
|
+
reason: "no-regular-install" | "unsupported-platform" | "unreachable" | "unusable-response" | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
interface CursorLocalManifest {
|
|
43
|
+
version?: unknown;
|
|
44
|
+
url?: unknown;
|
|
45
|
+
productVersion?: unknown;
|
|
46
|
+
/** The macOS manifests carry the version only here. */
|
|
47
|
+
name?: unknown;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface CursorLocalHintDeps {
|
|
51
|
+
platform: string;
|
|
52
|
+
/** `process.arch` values; the installer is architecture-specific. */
|
|
53
|
+
arch: string;
|
|
54
|
+
fetchJson(url: string, timeoutMs: number): Promise<unknown>;
|
|
55
|
+
/** Clock for the cache; defaults to `Date.now`. */
|
|
56
|
+
now?(): number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function realCursorLocalHintDeps(): CursorLocalHintDeps {
|
|
60
|
+
return {
|
|
61
|
+
platform: process.platform,
|
|
62
|
+
arch: process.arch,
|
|
63
|
+
fetchJson: async (url, timeoutMs) => {
|
|
64
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
65
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
66
|
+
return response.json();
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The channel segment for this host, or null when Cursor ships no build for it. The installer
|
|
73
|
+
* is architecture-specific, so only `x64` and `arm64` map; any other CPU (ia32, arm, riscv64…)
|
|
74
|
+
* or OS gets no link rather than another machine's build.
|
|
75
|
+
*/
|
|
76
|
+
export function platformForHost(os: string, arch: string): CursorLocalPlatform | null {
|
|
77
|
+
if (arch !== "x64" && arch !== "arm64") return null;
|
|
78
|
+
switch (os) {
|
|
79
|
+
case "win32": return arch === "arm64" ? "win32-arm64-user" : "win32-x64-user";
|
|
80
|
+
case "darwin": return arch === "arm64" ? "darwin-arm64" : "darwin-x64";
|
|
81
|
+
case "linux": return arch === "arm64" ? "linux-arm64" : "linux-x64";
|
|
82
|
+
default: return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function parseManifest(raw: unknown): { version: string; url: string } | null {
|
|
87
|
+
if (!raw || typeof raw !== "object") return null;
|
|
88
|
+
const record = raw as CursorLocalManifest;
|
|
89
|
+
if (typeof record.url !== "string" || !/^https:\/\/downloads\.cursor\.com\/local-mode\//.test(record.url)) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
// Windows and Linux manifests carry `version`/`productVersion`; the Darwin ones return only
|
|
93
|
+
// `{ url, name }`, with the version in `name`.
|
|
94
|
+
const candidate = typeof record.version === "string" ? record.version
|
|
95
|
+
: typeof record.productVersion === "string" ? record.productVersion
|
|
96
|
+
: typeof record.name === "string" ? record.name : "";
|
|
97
|
+
const version = candidate.trim();
|
|
98
|
+
if (version === "") return null;
|
|
99
|
+
// The Linux channel advertises the AppImage's zsync delta metadata (what the in-app updater
|
|
100
|
+
// consumes); the installer a person downloads is the sibling AppImage at the same path.
|
|
101
|
+
return { version, url: record.url.replace(/\.AppImage\.zsync$/, ".AppImage") };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
type ResolvedHint = Omit<CursorLocalInstallerHint, "reason"> & { reason: "unreachable" | "unusable-response" | null };
|
|
105
|
+
|
|
106
|
+
async function resolveInstaller(deps: CursorLocalHintDeps, updateHost: string, platform: CursorLocalPlatform): Promise<ResolvedHint> {
|
|
107
|
+
const manifestUrl = `${updateHost}/updates/api/update/${platform}/cursor-local/0.0.0/manual-check/stable`;
|
|
108
|
+
let raw: unknown;
|
|
109
|
+
try {
|
|
110
|
+
raw = await deps.fetchJson(manifestUrl, UPDATE_MANIFEST_TIMEOUT_MS);
|
|
111
|
+
} catch {
|
|
112
|
+
return { available: false, url: null, version: null, reason: "unreachable" };
|
|
113
|
+
}
|
|
114
|
+
const installer = parseManifest(raw);
|
|
115
|
+
if (!installer) return { available: false, url: null, version: null, reason: "unusable-response" };
|
|
116
|
+
return { available: true, url: installer.url, version: installer.version, reason: null };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const cache = new Map<string, { expiresAt: number; value: Promise<ResolvedHint> }>();
|
|
120
|
+
|
|
121
|
+
export function resetCursorLocalInstallerCacheForTests(): void {
|
|
122
|
+
cache.clear();
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Build the installer hint for the dashboard's Cursor card. The hint is only resolved
|
|
127
|
+
* when a regular install exists but Private Inference does not — the exact "not found"
|
|
128
|
+
* state the card could previously do nothing about.
|
|
129
|
+
*/
|
|
130
|
+
export async function buildCursorLocalInstallerHint(
|
|
131
|
+
installs: { regularInstalled: boolean; privateInferenceInstalled: boolean },
|
|
132
|
+
deps: CursorLocalHintDeps = realCursorLocalHintDeps(),
|
|
133
|
+
updateHost: string = DEFAULT_UPDATE_HOST,
|
|
134
|
+
): Promise<CursorLocalInstallerHint> {
|
|
135
|
+
if (installs.privateInferenceInstalled || !installs.regularInstalled) {
|
|
136
|
+
return { available: false, url: null, version: null, reason: installs.privateInferenceInstalled ? null : "no-regular-install" };
|
|
137
|
+
}
|
|
138
|
+
const platform = platformForHost(deps.platform, deps.arch);
|
|
139
|
+
if (!platform) return { available: false, url: null, version: null, reason: "unsupported-platform" };
|
|
140
|
+
const now = deps.now ?? Date.now;
|
|
141
|
+
const key = `${updateHost} ${platform}`;
|
|
142
|
+
const cached = cache.get(key);
|
|
143
|
+
if (cached && cached.expiresAt > now()) return cached.value;
|
|
144
|
+
const value = resolveInstaller(deps, updateHost, platform);
|
|
145
|
+
// The in-flight promise is shared immediately; its lifetime is settled once it answers.
|
|
146
|
+
cache.set(key, { expiresAt: Number.POSITIVE_INFINITY, value });
|
|
147
|
+
const settled = await value;
|
|
148
|
+
cache.set(key, { expiresAt: now() + (settled.available ? CACHE_TTL_OK_MS : CACHE_TTL_FAILED_MS), value });
|
|
149
|
+
return settled;
|
|
150
|
+
}
|
|
@@ -12,6 +12,7 @@ import { readRuntimePort } from "../../config/process-state";
|
|
|
12
12
|
import { filterCatalogVisibleModels, nativeContextLimits, nativeOpenAiContextTier, nativeReasoningEfforts, uniqueCatalogModelsForRawPublicList, visibleNativeSlugs } from "../../codex/catalog";
|
|
13
13
|
import { cursorLastSeen, type CursorSeen } from "../../integrations/cursor-seen";
|
|
14
14
|
import { detectCursorInstalls, type CursorInstall } from "../../integrations/cursor-detect";
|
|
15
|
+
import { buildCursorLocalInstallerHint, type CursorLocalInstallerHint } from "../../integrations/cursor-local-installer";
|
|
15
16
|
import { loadCursorEffortTable } from "../../integrations/cursor-effort-table";
|
|
16
17
|
import { configuredApiAuthToken, isApiAuthRequired, jsonResponse } from "../auth-cors";
|
|
17
18
|
import { localInferenceDestination } from "../../lib/local-destinations";
|
|
@@ -127,10 +128,29 @@ export async function buildCursorIntegrationStatus(
|
|
|
127
128
|
};
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
/**
|
|
132
|
+
* The cursor-local installer the update channel advertises (#5679), resolved only on an explicit
|
|
133
|
+
* user action. The status route above is polled while the Cursor tab is open and must stay
|
|
134
|
+
* local, so the remote channel lookup lives behind its own route the dashboard calls from a
|
|
135
|
+
* button, never on page load.
|
|
136
|
+
*/
|
|
137
|
+
export function resolveCursorLocalInstaller(
|
|
138
|
+
installs: CursorInstall[] = detectCursorInstalls(),
|
|
139
|
+
deps?: Parameters<typeof buildCursorLocalInstallerHint>[1],
|
|
140
|
+
): Promise<CursorLocalInstallerHint> {
|
|
141
|
+
return buildCursorLocalInstallerHint({
|
|
142
|
+
regularInstalled: pick(installs, "regular") !== undefined,
|
|
143
|
+
privateInferenceInstalled: pick(installs, "private-inference") !== undefined,
|
|
144
|
+
}, deps);
|
|
145
|
+
}
|
|
146
|
+
|
|
130
147
|
export async function handleCursorIntegrationRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
131
148
|
const { req, url } = ctx;
|
|
132
149
|
if (url.pathname === "/api/native-integrations/cursor" && req.method === "GET") {
|
|
133
150
|
return jsonResponse(await buildCursorIntegrationStatus(ctx));
|
|
134
151
|
}
|
|
152
|
+
if (url.pathname === "/api/native-integrations/cursor/local-installer" && req.method === "GET") {
|
|
153
|
+
return jsonResponse(await resolveCursorLocalInstaller());
|
|
154
|
+
}
|
|
135
155
|
return null;
|
|
136
156
|
}
|
|
@@ -289,6 +289,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
289
289
|
{ method: "PUT", path: "/api/native-integrations/grok", module: "server/management/native-integration-routes", mutates: true },
|
|
290
290
|
// server/management/cursor-integration-routes
|
|
291
291
|
{ method: "GET", path: "/api/native-integrations/cursor", module: "server/management/cursor-integration-routes", mutates: false },
|
|
292
|
+
{ method: "GET", path: "/api/native-integrations/cursor/local-installer", module: "server/management/cursor-integration-routes", mutates: false },
|
|
292
293
|
// server/management/oauth-account-routes
|
|
293
294
|
{ method: "DELETE", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
|
|
294
295
|
{ method: "DELETE", path: "/api/keys/rotate", module: "server/management/oauth-account-routes", mutates: true },
|
|
@@ -148,10 +148,58 @@ export function parseListenPidsFromNetstat(output: string, port: number): number
|
|
|
148
148
|
return [...new Set(parseListenEntriesFromNetstat(output, port).map(entry => entry.pid))];
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* Field names `ss -p` is known to emit inside a `users:` tuple. comm names are printed
|
|
153
|
+
* unescaped and are attacker-controlled, but bounded to 15 bytes (TASK_COMM_LEN - 1):
|
|
154
|
+
* a forged complete tuple needs `",pid=N,fd=N),("` — closing one tuple and opening the
|
|
155
|
+
* next leaves no room for a nonempty name — and a forged in-tuple field needs a key
|
|
156
|
+
* outside this list to stay under the bound, so it trips the grammar check instead.
|
|
157
|
+
*/
|
|
158
|
+
const SS_OWNER_FIELD_KEYS = new Set(["fd", "ino", "sk", "v6only"]);
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Strictly parse a `users:(("name",pid=N,fd=N)[,("name2",...)])` column, returning every
|
|
162
|
+
* attributed PID, or null when the column deviates from the grammar anywhere — a row that
|
|
163
|
+
* cannot be trusted must not attribute an owner at all. Every accepted tuple must carry
|
|
164
|
+
* its own `fd=`: a forged tuple fragment emitted inside a comm (a 15-byte comm has room
|
|
165
|
+
* for `a",pid=N),("b` but never for a full tuple plus `fd=`) supplies only `pid=`, so
|
|
166
|
+
* its PID must never reach the owner list.
|
|
167
|
+
*/
|
|
168
|
+
function parseSsOwnerPids(field: string): number[] | null {
|
|
169
|
+
if (!field.startsWith("users:(")) return null;
|
|
170
|
+
const pids: number[] = [];
|
|
171
|
+
let at = "users:(".length;
|
|
172
|
+
while (field.startsWith("(", at)) {
|
|
173
|
+
at += 1;
|
|
174
|
+
// ss prints comm raw between quotes with no escaping; a quote inside the name
|
|
175
|
+
// therefore ends it early and the rest of the name lands in field position.
|
|
176
|
+
const name = /^"[^"\n]*"/.exec(field.slice(at));
|
|
177
|
+
if (name === null || name[0] === `""`) return null;
|
|
178
|
+
at += name[0].length;
|
|
179
|
+
const pid = /^,pid=(\d+)/.exec(field.slice(at));
|
|
180
|
+
if (pid === null) return null;
|
|
181
|
+
at += pid[0].length;
|
|
182
|
+
let hasFd = false;
|
|
183
|
+
for (;;) {
|
|
184
|
+
const kv = /^,([a-z_]+)=([^,"()\s]+)/.exec(field.slice(at));
|
|
185
|
+
if (kv === null) break;
|
|
186
|
+
if (!SS_OWNER_FIELD_KEYS.has(kv[1]!)) return null;
|
|
187
|
+
if (kv[1] === "fd") hasFd = true;
|
|
188
|
+
at += kv[0].length;
|
|
189
|
+
}
|
|
190
|
+
if (field[at] !== ")" || !hasFd) return null;
|
|
191
|
+
pids.push(Number(pid[1]));
|
|
192
|
+
at += 1;
|
|
193
|
+
if (field.startsWith(",(", at)) at += 1;
|
|
194
|
+
}
|
|
195
|
+
return field[at] === ")" && field.slice(at + 1).trim() === "" ? pids : null;
|
|
196
|
+
}
|
|
197
|
+
|
|
151
198
|
/**
|
|
152
199
|
* Parse `ss -Hltnp` rows for a port, keeping each distinct PID/address pair. A row
|
|
153
|
-
* without a `pid=` attribution (another user's socket)
|
|
154
|
-
* reported unverifiable. Exported
|
|
200
|
+
* without a `pid=` attribution (another user's socket), or whose `users:` column
|
|
201
|
+
* does not parse cleanly, is dropped rather than reported unverifiable. Exported
|
|
202
|
+
* for unit tests.
|
|
155
203
|
*/
|
|
156
204
|
export function parseListenEntriesFromSs(output: string, port: number): ListenEntry[] {
|
|
157
205
|
const entries = new Map<string, ListenEntry>();
|
|
@@ -163,11 +211,13 @@ export function parseListenEntriesFromSs(output: string, port: number): ListenEn
|
|
|
163
211
|
// LISTEN <recv-q> <send-q> <local-addr:port> <peer-addr:port> users:(...)
|
|
164
212
|
const localIdx = parts.findIndex(part => part.endsWith(portSuffix) || part.endsWith(`]:${port}`));
|
|
165
213
|
if (localIdx < 0) continue;
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
214
|
+
const usersIdx = line.indexOf("users:(");
|
|
215
|
+
if (usersIdx < 0) continue;
|
|
216
|
+
const ownerPids = parseSsOwnerPids(line.slice(usersIdx));
|
|
217
|
+
if (ownerPids === null) continue;
|
|
218
|
+
const address = normalizeListenAddress(parts[localIdx]);
|
|
219
|
+
for (const pid of ownerPids) {
|
|
220
|
+
if (Number.isSafeInteger(pid) && pid > 0) entries.set(`${pid}|${address}`, { pid, address });
|
|
171
221
|
}
|
|
172
222
|
}
|
|
173
223
|
return [...entries.values()];
|
|
@@ -185,9 +185,17 @@ export interface LiveProxy {
|
|
|
185
185
|
/**
|
|
186
186
|
* A /healthz identity proves only that a proxy holds the port. Before a destructive orphan stop,
|
|
187
187
|
* require that listener to prove possession of this home's runtime-record secret as well.
|
|
188
|
+
*
|
|
189
|
+
* The verdict is three-valued on purpose. "proven" is the only answer that authorizes the
|
|
190
|
+
* caller's action; "refuted" means the answer was definitive (no record, a pid/port
|
|
191
|
+
* mismatch, or a failed proof); "indeterminate" means transport, deadline, or an
|
|
192
|
+
* unattestable record left the question open, which a shared-write decision must treat
|
|
193
|
+
* the same as a live owner it simply could not verify (#6198).
|
|
188
194
|
*/
|
|
189
|
-
export
|
|
190
|
-
|
|
195
|
+
export type HomeOwnershipProof = "proven" | "refuted" | "indeterminate";
|
|
196
|
+
|
|
197
|
+
export async function proveLiveProxyOwnedByHome(live: LiveProxy, io: LivenessIo = {}): Promise<HomeOwnershipProof> {
|
|
198
|
+
if (live.pid === null) return "indeterminate";
|
|
191
199
|
return attestFencedIdentity(
|
|
192
200
|
`http://${probeHostname(live.hostname)}:${live.port}/healthz`,
|
|
193
201
|
live.port,
|
|
@@ -258,31 +266,50 @@ async function attestFencedIdentity(
|
|
|
258
266
|
io: LivenessIo,
|
|
259
267
|
fetchFn: LivenessFetch,
|
|
260
268
|
timeoutMs: number,
|
|
261
|
-
): Promise<
|
|
269
|
+
): Promise<HomeOwnershipProof> {
|
|
262
270
|
const readRuntimeFn = io.readRuntimeFn ?? readRuntimePort;
|
|
263
271
|
let record: ReturnType<NonNullable<LivenessIo["readRuntimeFn"]>>;
|
|
264
272
|
try {
|
|
265
273
|
record = readRuntimeFn(pid);
|
|
266
274
|
} catch {
|
|
267
|
-
return
|
|
275
|
+
return "indeterminate";
|
|
268
276
|
}
|
|
269
277
|
// The typed seam omits the secret; the production record (readRuntimePort) carries it.
|
|
270
278
|
const secret: unknown = record ? Reflect.get(record, "attestationSecret") : undefined;
|
|
271
|
-
if (!record || record.pid !== pid || record.port !== port
|
|
279
|
+
if (!record || record.pid !== pid || record.port !== port) return "refuted";
|
|
280
|
+
if (typeof secret !== "string" || secret.length === 0) return "indeterminate";
|
|
272
281
|
const challenge = (io.createChallengeFn ?? createLocalAttestationChallenge)();
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
282
|
+
// One proof failure is definitive and never retried; a transport failure only means
|
|
283
|
+
// the listener did not answer yet, so it gets the same bounded retry the identity
|
|
284
|
+
// probe uses — "did not answer" is not "not ours" (#6198). The challenge is minted
|
|
285
|
+
// once: a retried attempt proves the same fresh nonce, not a replayed proof.
|
|
286
|
+
const sleepFn = io.sleepFn ?? ((ms: number) => new Promise<void>(r => setTimeout(r, ms)));
|
|
287
|
+
const nowFn = io.nowFn ?? Date.now;
|
|
288
|
+
const requestedAttempts = Math.trunc(io.attempts ?? 1);
|
|
289
|
+
const attempts = Number.isNaN(requestedAttempts)
|
|
290
|
+
? 1
|
|
291
|
+
: Math.max(1, Math.min(requestedAttempts, 5));
|
|
292
|
+
for (let attempt = 1; attempt <= attempts; attempt++) {
|
|
293
|
+
const remainingMs = io.deadlineAt === undefined ? timeoutMs : Math.min(timeoutMs, io.deadlineAt - nowFn());
|
|
294
|
+
if (remainingMs <= 0) return "indeterminate";
|
|
295
|
+
try {
|
|
296
|
+
const res = await fetchFn(url, {
|
|
297
|
+
headers: { [LOCAL_ATTESTATION_CHALLENGE_HEADER]: challenge },
|
|
298
|
+
signal: AbortSignal.timeout(remainingMs),
|
|
299
|
+
});
|
|
300
|
+
const body = (await res.json().catch(() => null)) as HealthzIdentity | null;
|
|
301
|
+
// The second answer must still be the same fenced (or by now healthy) process.
|
|
302
|
+
if (!isOpencodexHealthz(body) && !isPackageTreeFencedHealthz(body)) return "refuted";
|
|
303
|
+
if (body?.pid !== pid) return "refuted";
|
|
304
|
+
return verifyLocalAttestationProof(secret, challenge, pid, port, res.headers.get(LOCAL_ATTESTATION_PROOF_HEADER))
|
|
305
|
+
? "proven"
|
|
306
|
+
: "refuted";
|
|
307
|
+
} catch {
|
|
308
|
+
if (attempt >= attempts) return "indeterminate";
|
|
309
|
+
await sleepFn(100);
|
|
310
|
+
}
|
|
285
311
|
}
|
|
312
|
+
return "indeterminate";
|
|
286
313
|
}
|
|
287
314
|
|
|
288
315
|
/** A bounded version string safe to carry beyond the untrusted health response. */
|
|
@@ -317,7 +344,7 @@ export function isConnectionRefused(error: unknown): boolean {
|
|
|
317
344
|
return visit(error, 0);
|
|
318
345
|
}
|
|
319
346
|
|
|
320
|
-
async function classifyHealthz(
|
|
347
|
+
export async function classifyHealthz(
|
|
321
348
|
url: string,
|
|
322
349
|
fetchFn: LivenessFetch,
|
|
323
350
|
timeoutMs: number,
|
|
@@ -391,7 +418,7 @@ export async function proxyIdentityAt(
|
|
|
391
418
|
if (opts.expectedPid !== undefined && fencedPid !== opts.expectedPid) return null;
|
|
392
419
|
const attestMs = io.deadlineAt === undefined ? baseTimeoutMs : Math.min(baseTimeoutMs, io.deadlineAt - nowFn());
|
|
393
420
|
if (attestMs <= 0) return null;
|
|
394
|
-
if (
|
|
421
|
+
if ((await attestFencedIdentity(url, port, fencedPid, io, fetchFn, attestMs)) !== "proven") return null;
|
|
395
422
|
const fencedVersion = isHealthzVersion(fenced?.version) ? fenced!.version as string : undefined;
|
|
396
423
|
return {
|
|
397
424
|
pid: fencedPid,
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { constants as osConstants, setPriority } from "node:os";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Windows proxy scheduling priority.
|
|
5
|
+
*
|
|
6
|
+
* The Task Scheduler definition runs the proxy at NORMAL (task XML `<Priority>4</Priority>`,
|
|
7
|
+
* #3682). On a host saturated by unrelated NORMAL-priority work (Defender scans, encoders,
|
|
8
|
+
* emulators), the proxy's main thread can sit runnable-but-unscheduled for seconds, so
|
|
9
|
+
* `/healthz` misses the 750 ms CLI liveness ceiling and every verb reports "Proxy not
|
|
10
|
+
* reachable" while the listener is up. ABOVE_NORMAL lets the Windows scheduler pick the
|
|
11
|
+
* mostly idle proxy ahead of that work without HIGH's ability to starve the desktop.
|
|
12
|
+
*
|
|
13
|
+
* Best-effort and never fatal: a refused priority change leaves the process at NORMAL.
|
|
14
|
+
* Processes the proxy spawns do not inherit ABOVE_NORMAL; Windows only propagates IDLE and
|
|
15
|
+
* BELOW_NORMAL classes to children. `OCX_DISABLE_PRIORITY_BOOST=1` opts out.
|
|
16
|
+
*/
|
|
17
|
+
export interface ProcessPriorityDeps {
|
|
18
|
+
platform: NodeJS.Platform;
|
|
19
|
+
disabled: () => boolean;
|
|
20
|
+
setPriority: (pid: number, priority: number) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const defaults: ProcessPriorityDeps = {
|
|
24
|
+
platform: process.platform,
|
|
25
|
+
disabled: () => process.env.OCX_DISABLE_PRIORITY_BOOST === "1",
|
|
26
|
+
setPriority,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type ProcessPriorityResult = "raised" | "skipped" | "failed";
|
|
30
|
+
|
|
31
|
+
export function raiseWindowsProxyPriority(deps: ProcessPriorityDeps = defaults): ProcessPriorityResult {
|
|
32
|
+
if (deps.platform !== "win32" || deps.disabled()) return "skipped";
|
|
33
|
+
try {
|
|
34
|
+
deps.setPriority(0, osConstants.priority.PRIORITY_ABOVE_NORMAL);
|
|
35
|
+
return "raised";
|
|
36
|
+
} catch {
|
|
37
|
+
return "failed";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -182,7 +182,7 @@ export async function describeImageAnthropic(
|
|
|
182
182
|
const body = {
|
|
183
183
|
model: settings.model,
|
|
184
184
|
max_tokens: ANTHROPIC_VISION_MAX_TOKENS,
|
|
185
|
-
|
|
185
|
+
...sidecarThinkingOff(settings.model),
|
|
186
186
|
system: [
|
|
187
187
|
{ type: "text", text: CLAUDE_CODE_SYSTEM_INSTRUCTION },
|
|
188
188
|
{ type: "text", text: DESCRIBE_INSTRUCTION },
|