@bitkyc08/opencodex 2.39.0 → 2.40.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/README.md +49 -21
- package/gui/dist/assets/index-BHe2rl_C.js +112 -0
- package/gui/dist/assets/index-CJSb3HPe.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/cursor/catalog.ts +140 -7
- package/src/adapters/cursor/discovery.ts +79 -87
- package/src/adapters/cursor/effort-map.ts +11 -0
- package/src/adapters/cursor/request-builder.ts +31 -4
- package/src/adapters/cursor.ts +17 -0
- package/src/adapters/openai-chat.ts +95 -9
- package/src/adapters/openai-responses.ts +73 -9
- package/src/bridge.ts +58 -13
- package/src/claude/auth-mode.ts +6 -9
- package/src/claude/gateway-cache.ts +23 -3
- package/src/claude/model-info.ts +13 -2
- package/src/cli/access.ts +30 -0
- package/src/cli/account-extended.ts +19 -10
- package/src/cli/capabilities.ts +40 -1
- package/src/cli/claude-agent-startup-sync.ts +3 -0
- package/src/cli/claude.ts +134 -36
- package/src/cli/connect.ts +226 -0
- package/src/cli/dispatch.ts +126 -21
- package/src/cli/gui-pair-client.ts +170 -0
- package/src/cli/gui.ts +87 -0
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +76 -22
- package/src/cli/models-runtime-subcommands.ts +34 -0
- package/src/cli/models-runtime.ts +4 -0
- package/src/cli/models.ts +2 -1
- package/src/cli/opencode.ts +2 -1
- package/src/cli/provider-runtime.ts +31 -1
- package/src/cli/registry.ts +32 -2
- package/src/cli/runtime-api.ts +10 -1
- package/src/cli/status.ts +26 -0
- package/src/cli/system-command.ts +9 -3
- package/src/client/connect.ts +650 -0
- package/src/client/hub-client.ts +457 -0
- package/src/client/hub-relay.ts +288 -0
- package/src/client/machine-api.ts +139 -0
- package/src/client/machine-auth.ts +54 -0
- package/src/client/machine-listener.ts +143 -0
- package/src/client/runtime.ts +93 -0
- package/src/client/state.ts +175 -0
- package/src/clients/config-export.ts +16 -5
- package/src/codex/auth-api.ts +169 -15
- package/src/codex/auth-context.ts +61 -6
- package/src/codex/catalog/aggregation.ts +17 -13
- package/src/codex/catalog/metadata.ts +28 -0
- package/src/codex/catalog/provider-fetch.ts +38 -5
- package/src/codex/catalog.ts +1 -1
- package/src/codex/desired-state.ts +13 -3
- package/src/codex/inject.ts +212 -31
- package/src/codex/journal.ts +54 -6
- package/src/codex/main-account.ts +105 -33
- package/src/codex/native-main-claim.ts +23 -1
- package/src/codex/native-residue.ts +7 -0
- package/src/codex/reset-credit-auto-redeem.ts +237 -0
- package/src/combos/request.ts +15 -3
- package/src/combos/types.ts +12 -0
- package/src/config/paths.ts +15 -1
- package/src/config/provider-validation.ts +40 -0
- package/src/config.ts +447 -3
- package/src/generated/compatibility-version.json +224 -108
- package/src/images/artifacts.ts +73 -37
- package/src/images/fulfill.ts +6 -1
- package/src/images/index.ts +1 -1
- package/src/images/plan.ts +16 -2
- package/src/images/synthetic-tool.ts +5 -0
- package/src/images/xai-client.ts +34 -1
- package/src/integrations/cursor-detect.ts +133 -0
- package/src/integrations/cursor-seen.ts +31 -0
- package/src/lib/errors.ts +18 -0
- package/src/lib/gui-pair-capability.ts +104 -0
- package/src/lib/lab-live-route-production.ts +2 -1
- package/src/lib/process-control.ts +4 -3
- package/src/lib/service-secrets.ts +161 -1
- package/src/lib/windows-system-proxy.ts +115 -0
- package/src/oauth/index.ts +3 -2
- package/src/oauth/pool-settings-capability.ts +55 -0
- package/src/providers/api-keys.ts +3 -1
- package/src/providers/codex-capacity.ts +34 -4
- package/src/providers/derive.ts +10 -0
- package/src/providers/fastwire.ts +10 -2
- package/src/providers/key-store.ts +197 -0
- package/src/providers/model-rename-migration.ts +3 -0
- package/src/providers/openai-sidecar.ts +3 -3
- package/src/providers/quota.ts +91 -42
- package/src/providers/registry.ts +33 -3
- package/src/reasoning-effort.ts +32 -0
- package/src/remote/protocol.ts +109 -0
- package/src/responses/citation-markers.ts +101 -0
- package/src/responses/parser.ts +43 -3
- package/src/responses/spill-store.ts +9 -4
- package/src/router.ts +72 -8
- package/src/server/auth-cors.ts +37 -1
- package/src/server/gui-session.ts +449 -0
- package/src/server/gui-static.ts +24 -7
- package/src/server/images.ts +237 -10
- package/src/server/index.ts +356 -30
- package/src/server/management/agent-settings-routes.ts +7 -1
- package/src/server/management/api-key-rotation.ts +74 -0
- package/src/server/management/combo-routes.ts +18 -6
- package/src/server/management/config-routes.ts +22 -3
- package/src/server/management/context.ts +5 -1
- package/src/server/management/cursor-integration-routes.ts +98 -0
- package/src/server/management/logs-usage-routes.ts +2 -1
- package/src/server/management/model-routes.ts +79 -1
- package/src/server/management/model-rows.ts +23 -1
- package/src/server/management/oauth-account-routes.ts +135 -2
- package/src/server/management/provider-routes.ts +143 -9
- package/src/server/management/route-registry.ts +10 -0
- package/src/server/management/session-routes.ts +13 -0
- package/src/server/management/system-routes.ts +13 -1
- package/src/server/management-api.ts +8 -3
- package/src/server/management-auth.ts +124 -82
- package/src/server/models-capabilities.ts +124 -0
- package/src/server/ports.ts +7 -0
- package/src/server/proxy-liveness.ts +7 -0
- package/src/server/request-log.ts +11 -5
- package/src/server/responses/agent-task-recovery.ts +1 -0
- package/src/server/responses/codex-auth-error.ts +3 -1
- package/src/server/responses/collaboration.ts +26 -1
- package/src/server/responses/combo-stream-preflight.ts +25 -6
- package/src/server/responses/compact.ts +32 -12
- package/src/server/responses/context-overflow.ts +49 -0
- package/src/server/responses/core.ts +196 -12
- package/src/server/responses/fetch-helpers.ts +2 -1
- package/src/server/responses/outbound-body-guard.ts +110 -0
- package/src/server/responses/ws-upstream.ts +87 -6
- package/src/server/responses-self-named-namespace-scrub.ts +181 -0
- package/src/server/system-env.ts +78 -25
- package/src/service.ts +218 -42
- package/src/types/config.ts +134 -0
- package/src/types/provider.ts +54 -1
- package/src/types/request.ts +2 -0
- package/src/types.ts +5 -0
- package/src/update/job.ts +33 -5
- package/src/usage/cost.ts +32 -1
- package/src/usage/expected-prices.ts +16 -0
- package/src/usage/log.ts +6 -2
- package/src/usage/summary.ts +48 -6
- package/gui/dist/assets/index-D-lchsPw.js +0 -112
- package/gui/dist/assets/index-uvENYLin.css +0 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
1
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
2
2
|
import {
|
|
3
3
|
chmodSync,
|
|
4
4
|
closeSync,
|
|
@@ -39,35 +39,41 @@ import {
|
|
|
39
39
|
parseExpectedLocalProviderReloadPid,
|
|
40
40
|
verifyLocalProviderReloadCapability,
|
|
41
41
|
} from "../lib/local-provider-reload-contract";
|
|
42
|
+
import {
|
|
43
|
+
GUI_PAIR_BROWSER_ORIGIN_HEADER,
|
|
44
|
+
GUI_PAIR_CAPABILITY_HEADER,
|
|
45
|
+
GUI_PAIR_EXPECTED_PID_HEADER,
|
|
46
|
+
GUI_PAIR_EXPIRES_AT_HEADER,
|
|
47
|
+
GUI_PAIR_NONCE_HEADER,
|
|
48
|
+
GUI_PAIR_PATH,
|
|
49
|
+
parseExpectedGuiPairPid,
|
|
50
|
+
verifyGuiPairCapability,
|
|
51
|
+
} from "../lib/gui-pair-capability";
|
|
42
52
|
import { forgetEphemeralSecretPath, forgetHardenedSecretPath, hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
|
|
43
53
|
import type { OcxConfig } from "../types";
|
|
44
54
|
import {
|
|
45
|
-
isAllowedManagementOrigin,
|
|
46
|
-
isApiAuthRequired,
|
|
47
55
|
isDataPlaneAdmissionSecret,
|
|
48
|
-
isLoopbackHostname,
|
|
49
|
-
managementRequestOrigin,
|
|
50
|
-
parseHttpHost,
|
|
51
56
|
} from "./auth-cors";
|
|
57
|
+
import {
|
|
58
|
+
authorizeGuiSessionRequest,
|
|
59
|
+
issueGuiSession as issueGuiSessionFromState,
|
|
60
|
+
type GuiPairingGrantRecord,
|
|
61
|
+
type GuiSessionBootstrap,
|
|
62
|
+
type GuiSessionRecord,
|
|
63
|
+
type GuiSessionRequestContext,
|
|
64
|
+
} from "./gui-session";
|
|
65
|
+
export type { GuiSessionBootstrap, GuiSessionRequestContext } from "./gui-session";
|
|
52
66
|
|
|
53
|
-
const GUI_SESSION_TTL_MS = 5 * 60_000;
|
|
54
|
-
const GUI_SESSION_LIMIT = 128;
|
|
55
67
|
const LOCAL_READ_REPLAY_LIMIT = 256;
|
|
56
68
|
const consumedLocalReadCapabilities = new Map<string, number>();
|
|
57
69
|
const admittedLocalReadRequests = new WeakSet<Request>();
|
|
58
70
|
const LOCAL_PROVIDER_RELOAD_REPLAY_LIMIT = 256;
|
|
59
71
|
const consumedLocalProviderReloadCapabilities = new Map<string, number>();
|
|
60
72
|
const admittedLocalProviderReloadRequests = new WeakSet<Request>();
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
expiresAt: number;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export interface GuiSessionBootstrap extends GuiSessionRecord {
|
|
69
|
-
token: string;
|
|
70
|
-
}
|
|
73
|
+
const GUI_PAIR_REPLAY_LIMIT = 256;
|
|
74
|
+
const consumedGuiPairCapabilities = new Map<string, number>();
|
|
75
|
+
const admittedGuiPairRequests = new WeakSet<Request>();
|
|
76
|
+
const admittedManagementRequests = new WeakMap<Request, ManagementPrincipal>();
|
|
71
77
|
|
|
72
78
|
export type ManagementAuthState =
|
|
73
79
|
| {
|
|
@@ -75,6 +81,7 @@ export type ManagementAuthState =
|
|
|
75
81
|
token: string;
|
|
76
82
|
source: "environment" | "file";
|
|
77
83
|
sessions: Map<string, GuiSessionRecord>;
|
|
84
|
+
pairingGrants: Map<string, GuiPairingGrantRecord>;
|
|
78
85
|
}
|
|
79
86
|
| { available: false; reason: string };
|
|
80
87
|
|
|
@@ -201,7 +208,7 @@ function ready(token: string, source: "environment" | "file", config: OcxConfig)
|
|
|
201
208
|
if (isDataPlaneAdmissionSecret(token, config)) {
|
|
202
209
|
return fail("management credential conflicts with a data-plane credential");
|
|
203
210
|
}
|
|
204
|
-
return { available: true, token, source, sessions: new Map() };
|
|
211
|
+
return { available: true, token, source, sessions: new Map(), pairingGrants: new Map() };
|
|
205
212
|
}
|
|
206
213
|
|
|
207
214
|
export function initializeManagementAuthState(config: OcxConfig): ManagementAuthState {
|
|
@@ -232,41 +239,32 @@ function equalSecret(actual: string, expected: string): boolean {
|
|
|
232
239
|
return left.length === right.length && timingSafeEqual(left, right);
|
|
233
240
|
}
|
|
234
241
|
|
|
235
|
-
function removeExpiredSessions(state: Extract<ManagementAuthState, { available: true }>, now = Date.now()): void {
|
|
236
|
-
for (const [token, session] of state.sessions) {
|
|
237
|
-
if (session.expiresAt <= now) state.sessions.delete(token);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function randomSessionSecret(prefix: "ocx_session_"): string {
|
|
242
|
-
return `${prefix}${randomBytes(32).toString("base64url")}`;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
242
|
export function issueGuiSession(
|
|
246
243
|
req: Request,
|
|
247
244
|
config: OcxConfig,
|
|
248
245
|
state: ManagementAuthState,
|
|
246
|
+
context?: GuiSessionRequestContext,
|
|
249
247
|
): GuiSessionBootstrap | null {
|
|
250
|
-
if (
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
248
|
+
if (!state.available) return null;
|
|
249
|
+
return issueGuiSessionFromState(req, config, state, context);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export interface ManagementSessionControl {
|
|
253
|
+
revokeCurrent(req: Request): boolean;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function createManagementSessionControl(state: ManagementAuthState): ManagementSessionControl {
|
|
257
|
+
return {
|
|
258
|
+
revokeCurrent(req: Request): boolean {
|
|
259
|
+
if (!state.available) return false;
|
|
260
|
+
const credential = requestManagementCredential(req);
|
|
261
|
+
if (!credential) return false;
|
|
262
|
+
for (const token of state.sessions.keys()) {
|
|
263
|
+
if (equalSecret(credential, token)) return state.sessions.delete(token);
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
},
|
|
267
267
|
};
|
|
268
|
-
state.sessions.set(token, session);
|
|
269
|
-
return { token, ...session };
|
|
270
268
|
}
|
|
271
269
|
|
|
272
270
|
/**
|
|
@@ -284,6 +282,7 @@ export function issueGuiSession(
|
|
|
284
282
|
export type ManagementPrincipal =
|
|
285
283
|
| "admin-token"
|
|
286
284
|
| "gui-session"
|
|
285
|
+
| "gui-pair-capability"
|
|
287
286
|
| "local-read-capability"
|
|
288
287
|
| "local-provider-reload-capability"
|
|
289
288
|
| "system-restart-capability";
|
|
@@ -416,6 +415,81 @@ function hasLocalProviderReloadCapability(
|
|
|
416
415
|
return true;
|
|
417
416
|
}
|
|
418
417
|
|
|
418
|
+
function hasGuiPairCapability(
|
|
419
|
+
req: Request,
|
|
420
|
+
local: LocalManagementAuthContext | undefined,
|
|
421
|
+
): boolean {
|
|
422
|
+
if (admittedGuiPairRequests.has(req)) return true;
|
|
423
|
+
if (!local || req.method !== "POST") return false;
|
|
424
|
+
let url: URL;
|
|
425
|
+
try {
|
|
426
|
+
url = new URL(req.url);
|
|
427
|
+
} catch {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
if (url.pathname !== GUI_PAIR_PATH || url.search !== "") return false;
|
|
431
|
+
const contentLength = req.headers.get("content-length");
|
|
432
|
+
if (contentLength !== "0" || req.headers.has("transfer-encoding")) return false;
|
|
433
|
+
const expectedPid = parseExpectedGuiPairPid(req.headers.get(GUI_PAIR_EXPECTED_PID_HEADER));
|
|
434
|
+
if (expectedPid.kind !== "present" || expectedPid.pid !== local.pid) return false;
|
|
435
|
+
const expiresAtRaw = req.headers.get(GUI_PAIR_EXPIRES_AT_HEADER);
|
|
436
|
+
if (!expiresAtRaw || !/^[1-9]\d*$/.test(expiresAtRaw)) return false;
|
|
437
|
+
const expiresAt = Number(expiresAtRaw);
|
|
438
|
+
if (!Number.isSafeInteger(expiresAt)) return false;
|
|
439
|
+
const capability = req.headers.get(GUI_PAIR_CAPABILITY_HEADER);
|
|
440
|
+
const now = Date.now();
|
|
441
|
+
if (!verifyGuiPairCapability(
|
|
442
|
+
local.attestationSecret,
|
|
443
|
+
req.headers.get(GUI_PAIR_NONCE_HEADER),
|
|
444
|
+
req.method,
|
|
445
|
+
url.pathname,
|
|
446
|
+
req.headers.get(GUI_PAIR_BROWSER_ORIGIN_HEADER),
|
|
447
|
+
local.pid,
|
|
448
|
+
local.port,
|
|
449
|
+
expiresAt,
|
|
450
|
+
capability,
|
|
451
|
+
now,
|
|
452
|
+
)) return false;
|
|
453
|
+
for (const [consumed, retainedUntil] of consumedGuiPairCapabilities) {
|
|
454
|
+
if (retainedUntil <= now) consumedGuiPairCapabilities.delete(consumed);
|
|
455
|
+
}
|
|
456
|
+
if (!capability) return false;
|
|
457
|
+
const capabilityDigest = createHash("sha256").update(capability).digest("base64url");
|
|
458
|
+
if (consumedGuiPairCapabilities.has(capabilityDigest)) return false;
|
|
459
|
+
if (consumedGuiPairCapabilities.size >= GUI_PAIR_REPLAY_LIMIT) return false;
|
|
460
|
+
consumedGuiPairCapabilities.set(capabilityDigest, expiresAt);
|
|
461
|
+
admittedGuiPairRequests.add(req);
|
|
462
|
+
return true;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function requestManagementCredential(req: Request): string | null {
|
|
466
|
+
return req.headers.get("x-opencodex-api-key")?.trim()
|
|
467
|
+
|| req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim()
|
|
468
|
+
|| null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function resolveManagementAdmission(
|
|
472
|
+
req: Request,
|
|
473
|
+
state: ManagementAuthState,
|
|
474
|
+
config?: OcxConfig,
|
|
475
|
+
local?: LocalManagementAuthContext,
|
|
476
|
+
): ManagementPrincipal | null {
|
|
477
|
+
const cached = admittedManagementRequests.get(req);
|
|
478
|
+
if (cached) return cached;
|
|
479
|
+
let principal: ManagementPrincipal | null = null;
|
|
480
|
+
if (hasSystemRestartCapability(req, local)) principal = "system-restart-capability";
|
|
481
|
+
else if (hasLocalProviderReloadCapability(req, local)) principal = "local-provider-reload-capability";
|
|
482
|
+
else if (hasLocalReadCapability(req, local)) principal = "local-read-capability";
|
|
483
|
+
else if (hasGuiPairCapability(req, local)) principal = "gui-pair-capability";
|
|
484
|
+
else if (state.available) {
|
|
485
|
+
const actual = requestManagementCredential(req);
|
|
486
|
+
if (actual && equalSecret(actual, state.token)) principal = "admin-token";
|
|
487
|
+
else if (config && authorizeGuiSessionRequest(req, config, state).ok) principal = "gui-session";
|
|
488
|
+
}
|
|
489
|
+
if (principal) admittedManagementRequests.set(req, principal);
|
|
490
|
+
return principal;
|
|
491
|
+
}
|
|
492
|
+
|
|
419
493
|
/**
|
|
420
494
|
* The principal for a request that already passed `requireManagementAuth`. Kept as a
|
|
421
495
|
* separate resolution (rather than a changed return type) so every existing caller
|
|
@@ -429,17 +503,7 @@ export function managementPrincipal(
|
|
|
429
503
|
config?: OcxConfig,
|
|
430
504
|
local?: LocalManagementAuthContext,
|
|
431
505
|
): ManagementPrincipal | null {
|
|
432
|
-
|
|
433
|
-
if (hasLocalProviderReloadCapability(req, local)) return "local-provider-reload-capability";
|
|
434
|
-
if (hasLocalReadCapability(req, local)) return "local-read-capability";
|
|
435
|
-
if (!state.available) return null;
|
|
436
|
-
const actual = req.headers.get("x-opencodex-api-key")?.trim()
|
|
437
|
-
|| req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
438
|
-
if (!actual) return null;
|
|
439
|
-
if (equalSecret(actual, state.token)) return "admin-token";
|
|
440
|
-
if (!config) return null;
|
|
441
|
-
removeExpiredSessions(state);
|
|
442
|
-
return state.sessions.has(actual) ? "gui-session" : null;
|
|
506
|
+
return resolveManagementAdmission(req, state, config, local);
|
|
443
507
|
}
|
|
444
508
|
|
|
445
509
|
export function requireManagementAuth(
|
|
@@ -448,9 +512,7 @@ export function requireManagementAuth(
|
|
|
448
512
|
config?: OcxConfig,
|
|
449
513
|
local?: LocalManagementAuthContext,
|
|
450
514
|
): Response | null {
|
|
451
|
-
if (
|
|
452
|
-
if (hasLocalProviderReloadCapability(req, local)) return null;
|
|
453
|
-
if (hasLocalReadCapability(req, local)) return null;
|
|
515
|
+
if (resolveManagementAdmission(req, state, config, local)) return null;
|
|
454
516
|
if (!state.available) {
|
|
455
517
|
return Response.json({
|
|
456
518
|
error: "management API unavailable",
|
|
@@ -458,25 +520,5 @@ export function requireManagementAuth(
|
|
|
458
520
|
hint: "Set OPENCODEX_ADMIN_AUTH_TOKEN to bypass file-backed admin token ACL hardening",
|
|
459
521
|
}, { status: 503 });
|
|
460
522
|
}
|
|
461
|
-
const actual = req.headers.get("x-opencodex-api-key")?.trim()
|
|
462
|
-
|| req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim();
|
|
463
|
-
if (actual && equalSecret(actual, state.token)) return null;
|
|
464
|
-
if (actual && config) {
|
|
465
|
-
removeExpiredSessions(state);
|
|
466
|
-
const session = state.sessions.get(actual);
|
|
467
|
-
if (session) {
|
|
468
|
-
const requestOrigin = managementRequestOrigin(req, config);
|
|
469
|
-
const claimedOrigin = req.headers.get("x-opencodex-gui-origin");
|
|
470
|
-
const browserOrigin = req.headers.get("Origin");
|
|
471
|
-
const sameOrigin = requestOrigin === session.origin
|
|
472
|
-
&& claimedOrigin === session.origin
|
|
473
|
-
&& (!browserOrigin || browserOrigin === session.origin);
|
|
474
|
-
const safeMethod = req.method === "GET" || req.method === "HEAD";
|
|
475
|
-
const csrf = req.headers.get("x-opencodex-csrf-token")?.trim();
|
|
476
|
-
if (sameOrigin && (safeMethod || (browserOrigin === session.origin && !!csrf && equalSecret(csrf, session.csrfToken)))) {
|
|
477
|
-
return null;
|
|
478
|
-
}
|
|
479
|
-
}
|
|
480
|
-
}
|
|
481
523
|
return Response.json({ error: "opencodex admin token required" }, { status: 401 });
|
|
482
524
|
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extended capability advertisement for the OpenAI-shape `GET /v1/models` list.
|
|
3
|
+
*
|
|
4
|
+
* Cursor's local-agent runtime (the "Private Inference" build, `localMode=true`) enables its
|
|
5
|
+
* reasoning-effort control only when at least one row in `data[]` carries `api_types` naming an
|
|
6
|
+
* API family it can speak, optionally with a `capabilities` object. Plain OpenAI clients, Grok
|
|
7
|
+
* Build, and the Codex catalog branch ignore both keys. Every OpenCodex route serves Chat
|
|
8
|
+
* Completions, Responses and Anthropic Messages, streams, and accepts tool calls, so those are
|
|
9
|
+
* constants; context length and vision come from catalog data when known and are omitted
|
|
10
|
+
* otherwise, matching Cursor's optional-field schema.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Membership is load-bearing for Cursor: its wire selector picks the Anthropic Messages path only
|
|
15
|
+
* when NO OpenAI-family type (`chat_completions`/`responses`/`openai_chat`/`openai_responses`)
|
|
16
|
+
* is present. Keep at least one OpenAI-family entry; a unit test guards this.
|
|
17
|
+
*/
|
|
18
|
+
export const OPENCODEX_MODEL_API_TYPES: readonly string[] = Object.freeze(["chat_completions", "responses", "anthropic_messages"]);
|
|
19
|
+
|
|
20
|
+
export const OPENAI_FAMILY_API_TYPES: ReadonlySet<string> = new Set(["chat_completions", "responses", "openai_chat", "openai_responses"]);
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The reasoning-effort ladder Cursor's local-agent runtime attaches to a model, keyed by the
|
|
24
|
+
* model id after its last `/`. Cursor decides this from its own table rather than from the
|
|
25
|
+
* gateway's `reasoning_effort` list, so the dashboard can only PREDICT it; the values here
|
|
26
|
+
* mirror that table (read from the 3.18.25 bundle) and carry no Cursor behavior of their own.
|
|
27
|
+
* Null means Cursor shows no Reasoning control for the id. Distinct from
|
|
28
|
+
* `src/adapters/cursor/effort-map.ts`, which maps opencodex efforts onto Cursor's *backend*
|
|
29
|
+
* tiers for the outbound provider; this is what Cursor's *local* picker renders.
|
|
30
|
+
*/
|
|
31
|
+
const CURSOR_EFFORT_FAMILIES: ReadonlyArray<{ test: RegExp; ladder: readonly string[] }> = [
|
|
32
|
+
{ test: /^gpt-5[.-]6-(?:luna|sol|terra)$/u, ladder: ["low", "medium", "high", "xhigh"] },
|
|
33
|
+
{ test: /^gpt-5(?:\.\d+)?$/u, ladder: ["low", "medium", "high", "xhigh"] },
|
|
34
|
+
{ test: /^claude-opus-5$/u, ladder: ["low", "medium", "high", "xhigh", "max"] },
|
|
35
|
+
{ test: /^claude-opus-4[-.](?:7|8)$/u, ladder: ["low", "medium", "high", "xhigh", "max"] },
|
|
36
|
+
{ test: /^claude-sonnet-5$/u, ladder: ["low", "medium", "high", "xhigh", "max"] },
|
|
37
|
+
{ test: /^claude-opus-4[-.](?:5|6)$/u, ladder: ["low", "medium", "high", "max"] },
|
|
38
|
+
{ test: /^claude-sonnet-4[-.]6$/u, ladder: ["low", "medium", "high", "max"] },
|
|
39
|
+
{ test: /^grok-4[.-](?:3|5|6)(?:-(?:batch|build|nocomp))?$/u, ladder: ["minimal", "low", "medium", "high", "xhigh"] },
|
|
40
|
+
{ test: /^grok-build-latest$/u, ladder: ["minimal", "low", "medium", "high", "xhigh"] },
|
|
41
|
+
{ test: /^gemini-3\.[1-9].*flash-lite/u, ladder: [] },
|
|
42
|
+
{ test: /^gemini-/u, ladder: ["minimal", "low", "medium", "high"] },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
export function cursorEffortFamily(modelId: string): string[] | null {
|
|
46
|
+
let id = modelId.trim().toLowerCase();
|
|
47
|
+
const slash = id.lastIndexOf("/");
|
|
48
|
+
if (slash !== -1) id = id.slice(slash + 1);
|
|
49
|
+
const at = id.indexOf("@");
|
|
50
|
+
if (at !== -1) id = id.slice(0, at);
|
|
51
|
+
for (const family of CURSOR_EFFORT_FAMILIES) {
|
|
52
|
+
if (family.test.test(id)) return family.ladder.length > 0 ? [...family.ladder] : null;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ModelCapabilityInput {
|
|
58
|
+
reasoningEfforts?: readonly string[];
|
|
59
|
+
contextWindow?: number;
|
|
60
|
+
/**
|
|
61
|
+
* Larger opt-in window (Cursor "Max Mode"). When it exceeds contextWindow, the row advertises
|
|
62
|
+
* the long window as context_length and the default window as the long-context threshold,
|
|
63
|
+
* which makes Cursor's local runtime show a Context selector (default vs long, long marked
|
|
64
|
+
* as costing more).
|
|
65
|
+
*/
|
|
66
|
+
longContextWindow?: number;
|
|
67
|
+
inputModalities?: readonly string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ModelCapabilityFields {
|
|
71
|
+
api_types: readonly string[];
|
|
72
|
+
capabilities: {
|
|
73
|
+
context_length?: number;
|
|
74
|
+
/** Cursor's extended-row filter REQUIRES this to contain "text"; every route emits text. */
|
|
75
|
+
output_modalities: string[];
|
|
76
|
+
input_modalities?: string[];
|
|
77
|
+
supports_tool_use: true;
|
|
78
|
+
supports_streaming: true;
|
|
79
|
+
supports_reasoning: boolean;
|
|
80
|
+
supports_vision?: boolean;
|
|
81
|
+
reasoning_effort?: string[];
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Cursor reads the long-context threshold from `pricing.overrides[].min_prompt_tokens`. That
|
|
85
|
+
* key sits outside its validated capability schema, so it is the one place a threshold can
|
|
86
|
+
* be carried without failing row validation (`cost.long_context` is rejected by that schema).
|
|
87
|
+
*/
|
|
88
|
+
pricing?: { overrides: Array<{ min_prompt_tokens: number }> };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function positiveInt(value: unknown): number | undefined {
|
|
92
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return undefined;
|
|
93
|
+
const floored = Math.floor(value);
|
|
94
|
+
return floored > 0 ? floored : undefined;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function modelCapabilityFields(input: ModelCapabilityInput): ModelCapabilityFields {
|
|
98
|
+
const efforts = (input.reasoningEfforts ?? []).filter(effort => typeof effort === "string" && effort.length > 0);
|
|
99
|
+
const contextLength = positiveInt(input.contextWindow);
|
|
100
|
+
const longContextLength = positiveInt(input.longContextWindow);
|
|
101
|
+
const hasLongTier = contextLength !== undefined && longContextLength !== undefined && longContextLength > contextLength;
|
|
102
|
+
const modalities = Array.isArray(input.inputModalities)
|
|
103
|
+
? input.inputModalities.filter(modality => typeof modality === "string" && modality.length > 0)
|
|
104
|
+
: undefined;
|
|
105
|
+
const supportsVision = modalities !== undefined ? modalities.includes("image") : undefined;
|
|
106
|
+
return {
|
|
107
|
+
api_types: [...OPENCODEX_MODEL_API_TYPES],
|
|
108
|
+
capabilities: {
|
|
109
|
+
...(hasLongTier
|
|
110
|
+
? { context_length: longContextLength }
|
|
111
|
+
: contextLength !== undefined ? { context_length: contextLength } : {}),
|
|
112
|
+
// Once a gateway advertises api_types, Cursor keeps only rows whose output_modalities
|
|
113
|
+
// include "text"; omitting the key drops the row from the extended catalog.
|
|
114
|
+
output_modalities: ["text"],
|
|
115
|
+
...(modalities !== undefined && modalities.length > 0 ? { input_modalities: [...modalities] } : {}),
|
|
116
|
+
supports_tool_use: true,
|
|
117
|
+
supports_streaming: true,
|
|
118
|
+
supports_reasoning: efforts.length > 0,
|
|
119
|
+
...(supportsVision !== undefined ? { supports_vision: supportsVision } : {}),
|
|
120
|
+
...(efforts.length > 0 ? { reasoning_effort: [...efforts] } : {}),
|
|
121
|
+
},
|
|
122
|
+
...(hasLongTier ? { pricing: { overrides: [{ min_prompt_tokens: contextLength }] } } : {}),
|
|
123
|
+
};
|
|
124
|
+
}
|
package/src/server/ports.ts
CHANGED
|
@@ -151,6 +151,13 @@ export function shouldPersistSelectedPort(
|
|
|
151
151
|
configPort: number | undefined,
|
|
152
152
|
selectedPort: number,
|
|
153
153
|
preferredPort: number,
|
|
154
|
+
options: { sibling?: boolean } = {},
|
|
154
155
|
): boolean {
|
|
156
|
+
// A sibling start (`--port X` beside a live proxy on the configured port) is a
|
|
157
|
+
// second instance, not a new home for this config. Persisting its port rewrote
|
|
158
|
+
// config.port under the still-running configured-port proxy, and the next
|
|
159
|
+
// `ocx service` install then baked the sibling's port into the service and
|
|
160
|
+
// re-pointed every client at a listener that no longer existed.
|
|
161
|
+
if (options.sibling) return false;
|
|
155
162
|
return selectedPort === preferredPort && configPort !== selectedPort;
|
|
156
163
|
}
|
|
@@ -22,6 +22,7 @@ export interface HealthzIdentity {
|
|
|
22
22
|
port?: unknown;
|
|
23
23
|
restartCapability?: unknown;
|
|
24
24
|
providerReloadCapability?: unknown;
|
|
25
|
+
guiPairCapability?: unknown;
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
export interface LivenessIo {
|
|
@@ -269,6 +270,12 @@ interface ReadyzBody {
|
|
|
269
270
|
pid?: unknown;
|
|
270
271
|
port?: unknown;
|
|
271
272
|
status?: unknown;
|
|
273
|
+
// Remote protocol metadata is intentionally additive here. Ordinary
|
|
274
|
+
// readiness remains compatible with legacy standalone servers; `ocx connect`
|
|
275
|
+
// validates these fields separately in src/remote/protocol.ts.
|
|
276
|
+
protocol?: unknown;
|
|
277
|
+
minimumClientProtocol?: unknown;
|
|
278
|
+
managementUrl?: unknown;
|
|
272
279
|
}
|
|
273
280
|
|
|
274
281
|
export interface ReadinessProbeResult {
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
isClientClosedMessage,
|
|
9
9
|
isCyberPolicyCode,
|
|
10
10
|
isCyberPolicyMessage,
|
|
11
|
+
upstreamErrorMessageFromPayload,
|
|
11
12
|
} from "../lib/errors";
|
|
12
13
|
import { CODEX_CONFIG_PATH, readRootTomlString } from "../codex/paths";
|
|
13
14
|
import { readCodexCatalogPath } from "../codex/catalog";
|
|
@@ -124,6 +125,12 @@ export interface RequestLogContext {
|
|
|
124
125
|
terminalHttpStatus?: number;
|
|
125
126
|
/** Recognized structured terminal code whose exact identity must survive status mapping. */
|
|
126
127
|
terminalErrorCode?: typeof CYBER_POLICY_ERROR_CODE;
|
|
128
|
+
/**
|
|
129
|
+
* Proxy-owned error code for a request OpenCodex terminated locally, before or instead of an
|
|
130
|
+
* upstream send. Status-derived classification cannot name these: there is no upstream
|
|
131
|
+
* message to classify, and the status alone would read as a provider failure.
|
|
132
|
+
*/
|
|
133
|
+
errorCode?: string;
|
|
127
134
|
/** Structured reason from `response.incomplete`; internal-only input to log classification. */
|
|
128
135
|
terminalIncompleteReason?: string;
|
|
129
136
|
affinity?: "reused" | "new_bind" | "rebound" | "cleared";
|
|
@@ -794,10 +801,7 @@ function captureUpstreamErrorParsed(
|
|
|
794
801
|
logCtx.terminalIncompleteReason = reason.trim();
|
|
795
802
|
}
|
|
796
803
|
if (logCtx.upstreamError) return;
|
|
797
|
-
const message =
|
|
798
|
-
?? json?.last_error?.message
|
|
799
|
-
?? json?.response?.error?.message
|
|
800
|
-
?? json?.response?.incomplete_details?.message;
|
|
804
|
+
const message = upstreamErrorMessageFromPayload(parsed);
|
|
801
805
|
if (typeof message === "string" && message.trim()) {
|
|
802
806
|
logCtx.upstreamError = redactSecretString(message).slice(0, 500);
|
|
803
807
|
return;
|
|
@@ -927,7 +931,9 @@ export function addFinalRequestLog(
|
|
|
927
931
|
const effectiveStatus = status >= 500 && logCtx.upstreamError && isClientClosedMessage(logCtx.upstreamError)
|
|
928
932
|
? 499
|
|
929
933
|
: status;
|
|
930
|
-
|
|
934
|
+
// A locally assigned code wins: it names a refusal this proxy made itself, which no
|
|
935
|
+
// status-plus-upstream-message classification can reconstruct.
|
|
936
|
+
const errorCode = logCtx.errorCode ?? requestLogErrorCode(
|
|
931
937
|
effectiveStatus,
|
|
932
938
|
logCtx.upstreamError,
|
|
933
939
|
logCtx.terminalErrorCode,
|
|
@@ -23,6 +23,7 @@ const CODEX_ORIGINATORS = new Set([
|
|
|
23
23
|
"Codex Desktop",
|
|
24
24
|
"codex_app",
|
|
25
25
|
"codex_work_desktop",
|
|
26
|
+
"codexless_agent",
|
|
26
27
|
]);
|
|
27
28
|
const CODEX_OAUTH_CLIENT_ID = "app_EMoamEEZ73f0CkXaXp7hrann";
|
|
28
29
|
const OPENAI_TOKEN_ISSUERS = new Set(["https://auth.openai.com", "https://auth.openai.com/"]);
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
MainAccountTokenRefreshError,
|
|
16
16
|
MainAuthJsonChangedDuringRefreshError,
|
|
17
17
|
} from "../../codex/main-account";
|
|
18
|
+
import { NativeProfileError } from "../../codex/native-profile-types";
|
|
18
19
|
|
|
19
20
|
export interface CodexAuthContextErrorResponseOptions {
|
|
20
21
|
accountSelector?: string;
|
|
@@ -26,7 +27,8 @@ export function nativeMainRefreshFailureResponse(error: unknown): Response {
|
|
|
26
27
|
return formatErrorResponse(401, "authentication_error", "Codex main account needs reauthentication");
|
|
27
28
|
}
|
|
28
29
|
if (error instanceof MainAccountTokenRefreshError
|
|
29
|
-
|| error instanceof MainAuthJsonChangedDuringRefreshError
|
|
30
|
+
|| error instanceof MainAuthJsonChangedDuringRefreshError
|
|
31
|
+
|| (error instanceof NativeProfileError && error.retryable)) {
|
|
30
32
|
const response = formatErrorResponse(
|
|
31
33
|
503,
|
|
32
34
|
"server_busy",
|
|
@@ -108,12 +108,16 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato
|
|
|
108
108
|
/** Declared parameter schema per request-visible tool name (#1611 integer repair). */
|
|
109
109
|
toolParameterSchemas: Map<string, Record<string, unknown>>;
|
|
110
110
|
freeformToolNames: Set<string>;
|
|
111
|
+
bareCustomToolNames: Set<string>;
|
|
112
|
+
bareFunctionToolNames: Set<string>;
|
|
111
113
|
toolSearchToolNames: Set<string>;
|
|
112
114
|
} {
|
|
113
115
|
const toolNsMap = new Map<string, { namespace: string; name: string; freeform?: true }>();
|
|
114
116
|
const declaredToolNames = new Set<string>();
|
|
115
117
|
const toolParameterSchemas = new Map<string, Record<string, unknown>>();
|
|
116
118
|
const freeformToolNames = new Set<string>();
|
|
119
|
+
const bareCustomToolNames = new Set<string>();
|
|
120
|
+
const bareFunctionToolNames = new Set<string>();
|
|
117
121
|
const toolSearchToolNames = new Set<string>();
|
|
118
122
|
const requestedTools = parsed.context.tools ?? [];
|
|
119
123
|
const toolAllowed = toolChoiceToolPredicate(parsed.options.toolChoice, requestedTools);
|
|
@@ -133,6 +137,19 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato
|
|
|
133
137
|
if (t.freeform) {
|
|
134
138
|
budget?.chargeRetained(new TextEncoder().encode(t.name).byteLength, { kind: "retained_collectors" });
|
|
135
139
|
freeformToolNames.add(t.name);
|
|
140
|
+
if (!t.namespace || t.namespace === "functions") {
|
|
141
|
+
budget?.chargeRetained(new TextEncoder().encode(t.name).byteLength, { kind: "retained_collectors" });
|
|
142
|
+
bareCustomToolNames.add(t.name);
|
|
143
|
+
}
|
|
144
|
+
} else if (
|
|
145
|
+
!t.toolSearch
|
|
146
|
+
&& !t.webSearch
|
|
147
|
+
&& !t.imageGeneration
|
|
148
|
+
&& !t.videoGeneration
|
|
149
|
+
&& (!t.namespace || t.namespace === "functions")
|
|
150
|
+
) {
|
|
151
|
+
budget?.chargeRetained(new TextEncoder().encode(t.name).byteLength, { kind: "retained_collectors" });
|
|
152
|
+
bareFunctionToolNames.add(t.name);
|
|
136
153
|
}
|
|
137
154
|
if (t.toolSearch) {
|
|
138
155
|
budget?.chargeRetained(new TextEncoder().encode(t.name).byteLength, { kind: "retained_collectors" });
|
|
@@ -162,7 +179,15 @@ export function buildToolBridgeMaps(parsed: OcxParsedRequest, budget?: Translato
|
|
|
162
179
|
toolParameterSchemas.set(t.name, t.parameters);
|
|
163
180
|
}
|
|
164
181
|
}
|
|
165
|
-
return {
|
|
182
|
+
return {
|
|
183
|
+
toolNsMap,
|
|
184
|
+
declaredToolNames,
|
|
185
|
+
toolParameterSchemas,
|
|
186
|
+
freeformToolNames,
|
|
187
|
+
bareCustomToolNames,
|
|
188
|
+
bareFunctionToolNames,
|
|
189
|
+
toolSearchToolNames,
|
|
190
|
+
};
|
|
166
191
|
}
|
|
167
192
|
|
|
168
193
|
|
|
@@ -24,6 +24,24 @@ const TERMINAL_EVENTS = new Set([
|
|
|
24
24
|
"response.incomplete",
|
|
25
25
|
]);
|
|
26
26
|
|
|
27
|
+
const RETRYABLE_ZERO_OUTPUT_INCOMPLETE_REASONS = new Set([
|
|
28
|
+
"adapter_eof",
|
|
29
|
+
"missing_terminal_event",
|
|
30
|
+
"upstream_stall_timeout",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
function retryableZeroOutputTerminal(payload: unknown): boolean {
|
|
34
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
|
|
35
|
+
const event = payload as {
|
|
36
|
+
type?: unknown;
|
|
37
|
+
response?: { incomplete_details?: { reason?: unknown } };
|
|
38
|
+
};
|
|
39
|
+
if (event.type === "response.failed") return true;
|
|
40
|
+
if (event.type !== "response.incomplete") return false;
|
|
41
|
+
const reason = event.response?.incomplete_details?.reason;
|
|
42
|
+
return typeof reason === "string" && RETRYABLE_ZERO_OUTPUT_INCOMPLETE_REASONS.has(reason);
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
/**
|
|
28
46
|
* Decide when replaying the request on another combo target would risk duplicating
|
|
29
47
|
* client-visible output or a tool-side effect. Unknown event types commit the child
|
|
@@ -128,14 +146,14 @@ export async function preflightComboStreamResponse(
|
|
|
128
146
|
let bufferedBytes = 0;
|
|
129
147
|
let outputCommitted = false;
|
|
130
148
|
let terminalStatus: ResponsesTerminalStatus | undefined;
|
|
131
|
-
let
|
|
149
|
+
let retryableTerminalPayload: Record<string, unknown> | undefined;
|
|
132
150
|
const inspector = createSseInspector({
|
|
133
151
|
logCtx,
|
|
134
152
|
onParsedPayload: payload => {
|
|
135
153
|
if (comboStreamPayloadCommitsOutput(payload)) outputCommitted = true;
|
|
136
154
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return;
|
|
137
|
-
if ((payload
|
|
138
|
-
|
|
155
|
+
if (retryableZeroOutputTerminal(payload)) {
|
|
156
|
+
retryableTerminalPayload = payload as Record<string, unknown>;
|
|
139
157
|
}
|
|
140
158
|
},
|
|
141
159
|
onTerminal: status => { terminalStatus = status; },
|
|
@@ -162,9 +180,10 @@ export async function preflightComboStreamResponse(
|
|
|
162
180
|
inspector.feed(retained);
|
|
163
181
|
}
|
|
164
182
|
|
|
165
|
-
if (terminalStatus === "failed"
|
|
166
|
-
|
|
167
|
-
|
|
183
|
+
if ((terminalStatus === "failed" || terminalStatus === "incomplete")
|
|
184
|
+
&& !outputCommitted && retryableTerminalPayload) {
|
|
185
|
+
await reader.cancel("retrying zero-output combo stream terminal").catch(() => undefined);
|
|
186
|
+
return { kind: "failed", response: failedTerminalResponse(response, retryableTerminalPayload, logCtx) };
|
|
168
187
|
}
|
|
169
188
|
if (next.done || terminalStatus !== undefined || outputCommitted
|
|
170
189
|
|| bufferedBytes >= COMBO_STREAM_PREFLIGHT_MAX_BYTES
|