@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
|
@@ -65,6 +65,12 @@ import type { PersistedUsageAttempt } from "../../usage/log";
|
|
|
65
65
|
import { AUTH_MATRIX, isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
|
|
66
66
|
import { applySystemEnvToggle } from "../system-env";
|
|
67
67
|
import { buildApiAccessEndpoints } from "./api-access";
|
|
68
|
+
import {
|
|
69
|
+
abortApiKeyRotation,
|
|
70
|
+
commitApiKeyRotation,
|
|
71
|
+
removeExpiredApiKeyRotations,
|
|
72
|
+
startApiKeyRotation,
|
|
73
|
+
} from "./api-key-rotation";
|
|
68
74
|
|
|
69
75
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
70
76
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
@@ -319,7 +325,16 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
319
325
|
// Opt-in Anthropic OAuth account pool (#294): enable/threshold/strategy + clear cooldown.
|
|
320
326
|
if (url.pathname === "/api/oauth/accounts/pool" && req.method === "GET") {
|
|
321
327
|
const provider = (url.searchParams.get("provider") ?? "").trim().toLowerCase();
|
|
322
|
-
if (provider !== "anthropic")
|
|
328
|
+
if (provider !== "anthropic") {
|
|
329
|
+
// Generic OAuth pool-settings contract (#695 slice 1): persisted per provider, inert until
|
|
330
|
+
// the selector consumes it. Codex keeps /api/codex-auth; api-key providers have no pool.
|
|
331
|
+
const { poolSettingsCapability, genericPoolSettingsDto } = await import("../../oauth/pool-settings-capability");
|
|
332
|
+
const prov = config.providers[provider];
|
|
333
|
+
if (!provider || !prov || poolSettingsCapability(provider, prov) !== "generic") {
|
|
334
|
+
return jsonResponse({ error: "pool config is only supported for anthropic and generic OAuth providers" }, 400);
|
|
335
|
+
}
|
|
336
|
+
return jsonResponse(genericPoolSettingsDto(provider, prov));
|
|
337
|
+
}
|
|
323
338
|
const pool = config.anthropicAccountPool ?? {};
|
|
324
339
|
return jsonResponse({
|
|
325
340
|
provider,
|
|
@@ -345,7 +360,43 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
345
360
|
quotaWindow?: unknown;
|
|
346
361
|
};
|
|
347
362
|
const provider = typeof body.provider === "string" ? body.provider.trim().toLowerCase() : "";
|
|
348
|
-
if (provider !== "anthropic")
|
|
363
|
+
if (provider !== "anthropic") {
|
|
364
|
+
const {
|
|
365
|
+
poolSettingsCapability, genericPoolSettingsDto, parseGenericPoolStrategy, parseGenericAutoSwitchThreshold,
|
|
366
|
+
} = await import("../../oauth/pool-settings-capability");
|
|
367
|
+
const prov = config.providers[provider];
|
|
368
|
+
if (!provider || !prov || poolSettingsCapability(provider, prov) !== "generic") {
|
|
369
|
+
return jsonResponse({ error: "pool config is only supported for anthropic and generic OAuth providers" }, 400);
|
|
370
|
+
}
|
|
371
|
+
if (body.stickyLimit !== undefined || body.quotaWindow !== undefined) {
|
|
372
|
+
return jsonResponse({ error: "stickyLimit and quotaWindow are not part of the generic pool contract yet" }, 400);
|
|
373
|
+
}
|
|
374
|
+
const next = { ...(prov.oauthAccountFailover ?? {}) };
|
|
375
|
+
if (body.enabled !== undefined) {
|
|
376
|
+
if (typeof body.enabled !== "boolean") return jsonResponse({ error: "enabled must be a boolean" }, 400);
|
|
377
|
+
next.enabled = body.enabled;
|
|
378
|
+
}
|
|
379
|
+
if (body.strategy !== undefined) {
|
|
380
|
+
if (body.strategy === null) delete next.strategy;
|
|
381
|
+
else {
|
|
382
|
+
const parsed = parseGenericPoolStrategy(body.strategy);
|
|
383
|
+
if (parsed === null) return jsonResponse({ error: "strategy must be one of: quota, round-robin, fill-first" }, 400);
|
|
384
|
+
next.strategy = parsed;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (body.autoSwitchThreshold !== undefined) {
|
|
388
|
+
if (body.autoSwitchThreshold === null) delete next.autoSwitchThreshold;
|
|
389
|
+
else {
|
|
390
|
+
const parsed = parseGenericAutoSwitchThreshold(body.autoSwitchThreshold);
|
|
391
|
+
if (parsed === null) return jsonResponse({ error: "autoSwitchThreshold must be an integer 0-100" }, 400);
|
|
392
|
+
next.autoSwitchThreshold = parsed;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (Object.keys(next).length > 0) prov.oauthAccountFailover = next;
|
|
396
|
+
else delete prov.oauthAccountFailover;
|
|
397
|
+
saveConfigPreservingClaudeCode(config);
|
|
398
|
+
return jsonResponse({ ok: true, ...genericPoolSettingsDto(provider, prov) });
|
|
399
|
+
}
|
|
349
400
|
let enabled = config.anthropicAccountPool?.enabled === true;
|
|
350
401
|
if (body.enabled !== undefined) {
|
|
351
402
|
if (typeof body.enabled !== "boolean") return jsonResponse({ error: "enabled must be a boolean" }, 400);
|
|
@@ -530,6 +581,35 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
530
581
|
clearKeyCooldowns(name); // manual key management resets 429 cooldown state
|
|
531
582
|
return jsonResponse({ ok: true, id: result.id }, 201);
|
|
532
583
|
}
|
|
584
|
+
// Opt-in OS keychain storage (#1221): move the active key and pool into the OS credential
|
|
585
|
+
// store (config keeps references), or restore plaintext. Store verifies the keychain before
|
|
586
|
+
// touching config so an unavailable store refuses instead of half-migrating.
|
|
587
|
+
if (url.pathname === "/api/providers/keychain" && req.method === "GET") {
|
|
588
|
+
const name = (url.searchParams.get("name") ?? "").trim();
|
|
589
|
+
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
590
|
+
const { probeProviderKeychain, providerKeyStoreKind } = await import("../../providers/key-store");
|
|
591
|
+
const probe = probeProviderKeychain();
|
|
592
|
+
return jsonResponse({
|
|
593
|
+
name,
|
|
594
|
+
store: providerKeyStoreKind(config.providers[name]),
|
|
595
|
+
keychainAvailable: probe.available,
|
|
596
|
+
...(probe.available ? {} : { keychainUnavailableReason: probe.reason }),
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
if (url.pathname === "/api/providers/keychain" && req.method === "POST") {
|
|
600
|
+
const body = await readManagementJsonBodyOr(req, {}) as { name?: unknown; action?: unknown };
|
|
601
|
+
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
602
|
+
if (!name || !isValidProviderName(name) || !hasOwnProvider(config.providers, name)) return jsonResponse({ error: "unknown provider" }, 404);
|
|
603
|
+
if (body.action !== "store" && body.action !== "restore") return jsonResponse({ error: "action must be store or restore" }, 400);
|
|
604
|
+
const { storeProviderKeyInKeychain, restoreProviderKeyFromKeychain, providerKeyStoreKind } = await import("../../providers/key-store");
|
|
605
|
+
const result = body.action === "store"
|
|
606
|
+
? storeProviderKeyInKeychain(config, name)
|
|
607
|
+
: restoreProviderKeyFromKeychain(config, name);
|
|
608
|
+
if (!result.ok) return jsonResponse({ error: result.error }, result.status);
|
|
609
|
+
const { clearProviderQuotaCache } = await import("../../providers/quota");
|
|
610
|
+
clearProviderQuotaCache();
|
|
611
|
+
return jsonResponse({ ...result, name, store: providerKeyStoreKind(config.providers[name]) });
|
|
612
|
+
}
|
|
533
613
|
if (url.pathname === "/api/providers/keys/active" && req.method === "PUT") {
|
|
534
614
|
const body = await readManagementJsonBodyOr(req, {}) as { name?: string; id?: string };
|
|
535
615
|
const name = (body.name ?? "").trim();
|
|
@@ -579,6 +659,10 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
579
659
|
// API Keys management
|
|
580
660
|
// ---------------------------------------------------------------------------
|
|
581
661
|
if (url.pathname === "/api/keys" && req.method === "GET") {
|
|
662
|
+
if (removeExpiredApiKeyRotations(config)) {
|
|
663
|
+
saveConfigPreservingClaudeCode(config);
|
|
664
|
+
reconcileLiveStateStores();
|
|
665
|
+
}
|
|
582
666
|
const keys = config.apiKeys ?? [];
|
|
583
667
|
const endpoints = buildApiAccessEndpoints(config, {
|
|
584
668
|
requestUrl: req.url,
|
|
@@ -596,6 +680,11 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
596
680
|
name: k.name,
|
|
597
681
|
prefix: k.key.slice(0, 17) + "...",
|
|
598
682
|
createdAt: k.createdAt,
|
|
683
|
+
...(k.pendingRotation ? { pendingRotation: {
|
|
684
|
+
id: k.pendingRotation.id,
|
|
685
|
+
createdAt: k.pendingRotation.createdAt,
|
|
686
|
+
expiresAt: k.pendingRotation.expiresAt,
|
|
687
|
+
} } : {}),
|
|
599
688
|
usage: rollup.get(k.id) ?? { requests7d: 0, totalRequests: 0 },
|
|
600
689
|
})),
|
|
601
690
|
// Dataset-level and singular: it describes the usage log, not any one key.
|
|
@@ -606,6 +695,50 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
606
695
|
}, 200, req, config);
|
|
607
696
|
}
|
|
608
697
|
|
|
698
|
+
if (url.pathname === "/api/keys/rotate" && req.method === "POST") {
|
|
699
|
+
const body = await readJsonBody(req);
|
|
700
|
+
if (!body || Object.keys(body).length !== 1 || typeof body.id !== "string" || !body.id) {
|
|
701
|
+
return jsonResponse({ error: "invalid body" }, 400, req, config);
|
|
702
|
+
}
|
|
703
|
+
const result = startApiKeyRotation(config, body.id);
|
|
704
|
+
if ("error" in result) {
|
|
705
|
+
return jsonResponse({ error: result.error === "not-found" ? "key not found" : "rotation already pending" }, result.error === "not-found" ? 404 : 409, req, config);
|
|
706
|
+
}
|
|
707
|
+
saveConfigPreservingClaudeCode(config);
|
|
708
|
+
reconcileLiveStateStores();
|
|
709
|
+
return jsonResponse(result, 201, req, config);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
if (url.pathname === "/api/keys/rotate/commit" && req.method === "POST") {
|
|
713
|
+
const body = await readJsonBody(req);
|
|
714
|
+
if (!body || Object.keys(body).length !== 2 || typeof body.id !== "string" || !body.id
|
|
715
|
+
|| typeof body.rotationId !== "string" || !body.rotationId) {
|
|
716
|
+
return jsonResponse({ error: "invalid body" }, 400, req, config);
|
|
717
|
+
}
|
|
718
|
+
const result = commitApiKeyRotation(config, body.id, body.rotationId);
|
|
719
|
+
if ("error" in result) {
|
|
720
|
+
if (result.error === "expired") saveConfigPreservingClaudeCode(config);
|
|
721
|
+
return jsonResponse({ error: result.error === "not-found" ? "key rotation not found" : `rotation ${result.error}` }, result.error === "not-found" ? 404 : 409, req, config);
|
|
722
|
+
}
|
|
723
|
+
saveConfigPreservingClaudeCode(config);
|
|
724
|
+
reconcileLiveStateStores();
|
|
725
|
+
return jsonResponse({ ok: true }, 200, req, config);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
if (url.pathname === "/api/keys/rotate" && req.method === "DELETE") {
|
|
729
|
+
const body = await readJsonBody(req);
|
|
730
|
+
if (!body || Object.keys(body).length !== 2 || typeof body.id !== "string" || !body.id
|
|
731
|
+
|| typeof body.rotationId !== "string" || !body.rotationId) {
|
|
732
|
+
return jsonResponse({ error: "invalid body" }, 400, req, config);
|
|
733
|
+
}
|
|
734
|
+
if (!abortApiKeyRotation(config, body.id, body.rotationId)) {
|
|
735
|
+
return jsonResponse({ error: "key rotation not found or mismatched" }, 409, req, config);
|
|
736
|
+
}
|
|
737
|
+
saveConfigPreservingClaudeCode(config);
|
|
738
|
+
reconcileLiveStateStores();
|
|
739
|
+
return jsonResponse({ ok: true }, 200, req, config);
|
|
740
|
+
}
|
|
741
|
+
|
|
609
742
|
if (url.pathname === "/api/keys" && req.method === "POST") {
|
|
610
743
|
const body = await readJsonBody(req);
|
|
611
744
|
if (!body) return jsonResponse({ error: "invalid body" }, 400, req, config);
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
codexAutoStartEnabled,
|
|
10
10
|
hasOwnProvider,
|
|
11
11
|
isValidProviderName,
|
|
12
|
+
modelDisplayNamesConfigError,
|
|
12
13
|
multiAgentGuidanceEnabled,
|
|
13
14
|
nonBlankStringArrayConfigError,
|
|
14
15
|
normalizeNonBlankStringArray,
|
|
@@ -105,6 +106,63 @@ type ProviderPatchApplication =
|
|
|
105
106
|
headersTouched: boolean;
|
|
106
107
|
};
|
|
107
108
|
|
|
109
|
+
const PROVIDER_ALIAS_OVERLAY_FIELDS = ["alias", "modelAliases", "defaultAliases"] as const;
|
|
110
|
+
type ProviderAliasOverlayField = typeof PROVIDER_ALIAS_OVERLAY_FIELDS[number];
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Alias overlays are owned by the dedicated alias management routes. A full provider POST
|
|
114
|
+
* may round-trip an already-persisted value, but it must not create, clear, or change one.
|
|
115
|
+
* PATCH is field-masked and rejects these keys outright below.
|
|
116
|
+
*/
|
|
117
|
+
function providerAliasOverlayOwnershipError(
|
|
118
|
+
submitted: Record<string, unknown>,
|
|
119
|
+
existing: OcxProviderConfig | undefined,
|
|
120
|
+
): string | null {
|
|
121
|
+
for (const field of PROVIDER_ALIAS_OVERLAY_FIELDS) {
|
|
122
|
+
if (!Object.hasOwn(submitted, field)) continue;
|
|
123
|
+
if (!existing || !Object.hasOwn(existing, field)) {
|
|
124
|
+
return `${field} is managed by the dedicated alias API`;
|
|
125
|
+
}
|
|
126
|
+
const incoming = submitted[field];
|
|
127
|
+
const persisted = existing[field];
|
|
128
|
+
if (field === "modelAliases") {
|
|
129
|
+
if (!isPlainRecord(incoming) || !isPlainRecord(persisted)) {
|
|
130
|
+
return "modelAliases is managed by the dedicated alias API";
|
|
131
|
+
}
|
|
132
|
+
const incomingEntries = Object.entries(incoming);
|
|
133
|
+
const persistedEntries = Object.entries(persisted);
|
|
134
|
+
if (
|
|
135
|
+
incomingEntries.length !== persistedEntries.length
|
|
136
|
+
|| incomingEntries.some(([model, alias]) => typeof alias !== "string" || persisted[model] !== alias)
|
|
137
|
+
) {
|
|
138
|
+
return "modelAliases is managed by the dedicated alias API";
|
|
139
|
+
}
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (incoming !== persisted) return `${field} is managed by the dedicated alias API`;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Remove only alias overlays whose ownership has already been established by the caller. */
|
|
148
|
+
function providerTransportValidationCandidate(provider: Record<string, unknown>): Record<string, unknown> {
|
|
149
|
+
const candidate = { ...provider };
|
|
150
|
+
for (const field of PROVIDER_ALIAS_OVERLAY_FIELDS) delete candidate[field];
|
|
151
|
+
return candidate;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Preserve the authoritative alias values from the stored provider during a full edit. */
|
|
155
|
+
function restorePersistedAliasOverlays(target: OcxProviderConfig, existing: OcxProviderConfig | undefined): void {
|
|
156
|
+
for (const field of PROVIDER_ALIAS_OVERLAY_FIELDS) {
|
|
157
|
+
delete (target as Record<ProviderAliasOverlayField, unknown>)[field];
|
|
158
|
+
if (!existing || !Object.hasOwn(existing, field)) continue;
|
|
159
|
+
const value = existing[field];
|
|
160
|
+
(target as Record<ProviderAliasOverlayField, unknown>)[field] = field === "modelAliases"
|
|
161
|
+
? structuredClone(value)
|
|
162
|
+
: value;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
108
166
|
/**
|
|
109
167
|
* Apply the recognized PATCH field mask onto a provider copy. The caller runs this once
|
|
110
168
|
* for validation and again inside the config mutation lock against the newest provider,
|
|
@@ -241,6 +299,11 @@ function applyProviderPatchFields(
|
|
|
241
299
|
}
|
|
242
300
|
touched = true;
|
|
243
301
|
}
|
|
302
|
+
if (Object.hasOwn(rawBody, "upstreamWebsocket")) {
|
|
303
|
+
if (typeof rawBody.upstreamWebsocket !== "boolean") return { error: "upstreamWebsocket must be a boolean" };
|
|
304
|
+
next.upstreamWebsocket = rawBody.upstreamWebsocket;
|
|
305
|
+
touched = true;
|
|
306
|
+
}
|
|
244
307
|
// The Models page edits the catalog hints in place; keep them on the existing
|
|
245
308
|
// provider mutation path so validation, cache invalidation, and convergence stay unified (#1073).
|
|
246
309
|
if (Object.hasOwn(rawBody, "contextWindow")) {
|
|
@@ -339,6 +402,32 @@ function applyProviderPatchFields(
|
|
|
339
402
|
}
|
|
340
403
|
touched = true;
|
|
341
404
|
}
|
|
405
|
+
if (Object.hasOwn(rawBody, "retainModels")) {
|
|
406
|
+
const value = rawBody.retainModels;
|
|
407
|
+
if (value === null) {
|
|
408
|
+
delete next.retainModels;
|
|
409
|
+
} else {
|
|
410
|
+
const error = nonBlankStringArrayConfigError(value, "retainModels");
|
|
411
|
+
if (error) return { error };
|
|
412
|
+
const models = normalizeNonBlankStringArray(value as string[]);
|
|
413
|
+
if (models.length > 0) next.retainModels = models;
|
|
414
|
+
else delete next.retainModels;
|
|
415
|
+
}
|
|
416
|
+
touched = true;
|
|
417
|
+
}
|
|
418
|
+
if (Object.hasOwn(rawBody, "omitReasoningEffortWithToolsModels")) {
|
|
419
|
+
const value = rawBody.omitReasoningEffortWithToolsModels;
|
|
420
|
+
if (value === null) {
|
|
421
|
+
delete next.omitReasoningEffortWithToolsModels;
|
|
422
|
+
} else {
|
|
423
|
+
const error = nonBlankStringArrayConfigError(value, "omitReasoningEffortWithToolsModels");
|
|
424
|
+
if (error) return { error };
|
|
425
|
+
const models = normalizeNonBlankStringArray(value as string[]);
|
|
426
|
+
if (models.length > 0) next.omitReasoningEffortWithToolsModels = models;
|
|
427
|
+
else delete next.omitReasoningEffortWithToolsModels;
|
|
428
|
+
}
|
|
429
|
+
touched = true;
|
|
430
|
+
}
|
|
342
431
|
|
|
343
432
|
// headers is the one object-valued field in the mask. PATCH semantics merge it
|
|
344
433
|
// shallowly into the existing block so a single fingerprint header can be added
|
|
@@ -468,7 +557,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
468
557
|
modelAutoCompactTokenLimits: p.modelAutoCompactTokenLimits,
|
|
469
558
|
modelSupportsServiceTier: p.modelSupportsServiceTier,
|
|
470
559
|
noStructuredOutputModels: p.noStructuredOutputModels,
|
|
560
|
+
retainModels: p.retainModels,
|
|
561
|
+
omitReasoningEffortWithToolsModels: p.omitReasoningEffortWithToolsModels,
|
|
471
562
|
upstreamHttpVersion: p.upstreamHttpVersion,
|
|
563
|
+
upstreamWebsocket: p.upstreamWebsocket === true,
|
|
472
564
|
authMode: p.authMode,
|
|
473
565
|
apiKeyTransport: p.apiKeyTransport,
|
|
474
566
|
disabled: p.disabled === true,
|
|
@@ -501,7 +593,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
501
593
|
return jsonResponse({ error: "provider reload target unavailable" }, 404);
|
|
502
594
|
}
|
|
503
595
|
const provider = diskConfig.providers[name]!;
|
|
504
|
-
const providerError = providerManagementConfigError(
|
|
596
|
+
const providerError = providerManagementConfigError(
|
|
597
|
+
name,
|
|
598
|
+
providerTransportValidationCandidate(provider as unknown as Record<string, unknown>),
|
|
599
|
+
)
|
|
505
600
|
?? providerEmptyToolOutputConfigError(name, provider);
|
|
506
601
|
if (providerError) return jsonResponse({ error: "provider reload target invalid" }, 409);
|
|
507
602
|
const namespaceCollision = codexAccountNamespaceProviderCollisionError(
|
|
@@ -561,12 +656,21 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
561
656
|
let body: { name?: unknown; provider?: unknown; setDefault?: boolean };
|
|
562
657
|
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
563
658
|
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
564
|
-
|
|
565
|
-
|
|
659
|
+
if (!isPlainRecord(body.provider)) return jsonResponse({ error: "provider must be a plain object" }, 400);
|
|
660
|
+
const existing = config.providers[name];
|
|
661
|
+
const aliasOwnershipError = providerAliasOverlayOwnershipError(body.provider, existing);
|
|
662
|
+
if (aliasOwnershipError) return jsonResponse({ error: aliasOwnershipError }, 400);
|
|
663
|
+
const transportCandidate = providerTransportValidationCandidate(body.provider);
|
|
664
|
+
const providerError = providerManagementConfigError(name, transportCandidate)
|
|
665
|
+
?? providerEmptyToolOutputConfigError(name, transportCandidate);
|
|
566
666
|
if (providerError) return jsonResponse({ error: providerError }, 400);
|
|
567
|
-
const
|
|
667
|
+
const rawProvider = body.provider as Record<string, unknown>;
|
|
668
|
+
if (rawProvider.upstreamWebsocket !== undefined && typeof rawProvider.upstreamWebsocket !== "boolean") {
|
|
669
|
+
return jsonResponse({ error: "upstreamWebsocket must be a boolean" }, 400);
|
|
670
|
+
}
|
|
671
|
+
const serviceTierError = providerServiceTierConfigError(name, transportCandidate);
|
|
568
672
|
if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
|
|
569
|
-
const prov =
|
|
673
|
+
const prov = stripCodexRuntimeProviderFields(transportCandidate as unknown as OcxProviderConfig);
|
|
570
674
|
// PATCH already clears on null; POST persisted the body as submitted, so a `null` here
|
|
571
675
|
// reached disk and the next loadConfig() refused it. Canonicalize to absent, which is what
|
|
572
676
|
// "clear" means everywhere else.
|
|
@@ -574,6 +678,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
574
678
|
if (!name || !prov?.adapter || !prov?.baseUrl) {
|
|
575
679
|
return jsonResponse({ error: "name, provider.adapter and provider.baseUrl are required" }, 400);
|
|
576
680
|
}
|
|
681
|
+
const displayNamesError = modelDisplayNamesConfigError(prov.modelDisplayNames);
|
|
682
|
+
if (displayNamesError) return jsonResponse({ error: displayNamesError }, 400);
|
|
577
683
|
if (!isValidProviderName(name)) {
|
|
578
684
|
return jsonResponse({ error: "provider name must use letters, numbers, dot, underscore, or hyphen and cannot be a reserved object key" }, 400);
|
|
579
685
|
}
|
|
@@ -603,7 +709,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
603
709
|
const submittedContextWindow = Object.hasOwn(prov, "contextWindow");
|
|
604
710
|
const submittedModelContextWindows = Object.hasOwn(prov, "modelContextWindows");
|
|
605
711
|
const submittedModelAutoCompactTokenLimits = Object.hasOwn(prov, "modelAutoCompactTokenLimits");
|
|
712
|
+
const submittedModelDisplayNames = Object.hasOwn(prov, "modelDisplayNames");
|
|
606
713
|
const submittedRequestPacing = Object.hasOwn(prov, "requestPacing");
|
|
714
|
+
const submittedUpstreamWebsocket = Object.hasOwn(prov, "upstreamWebsocket");
|
|
607
715
|
// Same trap, one more field: DeepSeek carries a registry default of `true` for
|
|
608
716
|
// annotateEmptyToolOutputs, so enrichment cannot distinguish "the client omitted it"
|
|
609
717
|
// from "the registry supplied it" either. Without this sample, an unrelated edit that
|
|
@@ -630,7 +738,9 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
630
738
|
// has no member for either field, so the add/edit form structurally cannot send them:
|
|
631
739
|
// absence in the request means "not carried", never "the user deleted it". Deletion goes
|
|
632
740
|
// through PATCH with an explicit null (#1409).
|
|
633
|
-
|
|
741
|
+
if (!submittedModelDisplayNames && existing?.modelDisplayNames) {
|
|
742
|
+
prov.modelDisplayNames = { ...existing.modelDisplayNames };
|
|
743
|
+
}
|
|
634
744
|
if (!submittedRequestPacing && existing?.requestPacing) {
|
|
635
745
|
prov.requestPacing = structuredClone(existing.requestPacing);
|
|
636
746
|
}
|
|
@@ -642,6 +752,12 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
642
752
|
if (!submittedAnnotateEmptyToolOutputs && existing?.annotateEmptyToolOutputs !== undefined) {
|
|
643
753
|
prov.annotateEmptyToolOutputs = existing.annotateEmptyToolOutputs;
|
|
644
754
|
}
|
|
755
|
+
// The provider add/edit form may omit this transport option. Preserve the stored value
|
|
756
|
+
// during a full overwrite; PATCH remains the explicit mutation path, and `!== undefined`
|
|
757
|
+
// keeps an operator's explicit false from being treated as absent.
|
|
758
|
+
if (!submittedUpstreamWebsocket && existing?.upstreamWebsocket !== undefined) {
|
|
759
|
+
prov.upstreamWebsocket = existing.upstreamWebsocket;
|
|
760
|
+
}
|
|
645
761
|
if (existing?.modelContextWindows) {
|
|
646
762
|
// When the client did send a map, its keys win and the user's other keys survive. When
|
|
647
763
|
// it did not, the stored value is the user's map alone: merging the registry seed in
|
|
@@ -656,6 +772,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
656
772
|
? { ...existing.modelAutoCompactTokenLimits, ...(prov.modelAutoCompactTokenLimits ?? {}) }
|
|
657
773
|
: { ...existing.modelAutoCompactTokenLimits };
|
|
658
774
|
}
|
|
775
|
+
// DNS validation above awaits. Re-read the live row so a dedicated alias write that
|
|
776
|
+
// completed during that wait remains authoritative instead of being overwritten by the
|
|
777
|
+
// older ownership snapshot used to admit this POST.
|
|
778
|
+
restorePersistedAliasOverlays(prov, config.providers[name]);
|
|
659
779
|
config.providers[name] = stripRegistryOnlyStaticHeaders(name, prov);
|
|
660
780
|
if (body.setDefault === true) config.defaultProvider = name;
|
|
661
781
|
save(config);
|
|
@@ -677,6 +797,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
677
797
|
try { rawBody = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
678
798
|
if (!isPlainRecord(rawBody)) return jsonResponse({ error: "provider patch body must be a plain object" }, 400);
|
|
679
799
|
const keys = Object.keys(rawBody);
|
|
800
|
+
const aliasField = PROVIDER_ALIAS_OVERLAY_FIELDS.find(field => Object.hasOwn(rawBody, field));
|
|
801
|
+
if (aliasField) return jsonResponse({ error: `${aliasField} is managed by the dedicated alias API` }, 400);
|
|
680
802
|
const hasMode = Object.hasOwn(rawBody, "codexAccountMode");
|
|
681
803
|
const hasSetDefault = Object.hasOwn(rawBody, "setDefault");
|
|
682
804
|
const canonicalBudgetOnly = name === "openai"
|
|
@@ -746,13 +868,22 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
746
868
|
if (applied.editorTouched && !pacingOnly) {
|
|
747
869
|
const providerError = canonicalBudgetOnly
|
|
748
870
|
? canonicalOpenAiBudgetPatchError(next, rawBody, keys, config)
|
|
749
|
-
: providerManagementConfigError(
|
|
871
|
+
: providerManagementConfigError(
|
|
872
|
+
name,
|
|
873
|
+
providerTransportValidationCandidate(next as unknown as Record<string, unknown>),
|
|
874
|
+
)
|
|
750
875
|
?? providerEmptyToolOutputConfigError(name, next);
|
|
751
876
|
if (providerError) return jsonResponse({ error: providerError }, 400);
|
|
752
877
|
if (!canonicalBudgetOnly) {
|
|
753
878
|
const serviceTierError = providerServiceTierConfigError(name, next);
|
|
754
879
|
if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
|
|
755
|
-
|
|
880
|
+
// Same DNS gate as POST and re-enable: the canonical built-in OpenAI forward
|
|
881
|
+
// provider may resolve through Clash/Mihomo fake-IP DNS (198.18.0.0/15), so the
|
|
882
|
+
// ordinary PATCH must not reject the very same destination the provider was
|
|
883
|
+
// created with. Loopback, RFC1918, metadata, and mixed dangerous answers still
|
|
884
|
+
// fail closed; nothing else gains the exception.
|
|
885
|
+
const allowBenchmarkAddresses = name === "openai" && isCanonicalOpenAiForwardProvider(next);
|
|
886
|
+
const resolvedError = await providerDestinationResolvedError(name, next, { allowBenchmarkAddresses });
|
|
756
887
|
if (resolvedError) return jsonResponse({ error: resolvedError }, 400);
|
|
757
888
|
}
|
|
758
889
|
} else if (applied.enablingOpenAi) {
|
|
@@ -780,7 +911,10 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
780
911
|
if (replay.editorTouched && !pacingOnly) {
|
|
781
912
|
const syncError = canonicalBudgetOnly
|
|
782
913
|
? canonicalOpenAiBudgetPatchError(replay.next, rawBody, keys, config)
|
|
783
|
-
: providerManagementConfigError(
|
|
914
|
+
: providerManagementConfigError(
|
|
915
|
+
name,
|
|
916
|
+
providerTransportValidationCandidate(replay.next as unknown as Record<string, unknown>),
|
|
917
|
+
)
|
|
784
918
|
?? providerEmptyToolOutputConfigError(name, replay.next);
|
|
785
919
|
if (syncError) {
|
|
786
920
|
replayError = syncError;
|
|
@@ -230,8 +230,11 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
230
230
|
{ method: "PUT", path: "/api/native-integrations/claude-desktop", module: "server/management/native-integration-routes", mutates: true },
|
|
231
231
|
{ method: "PUT", path: "/api/native-integrations/codex", module: "server/management/native-integration-routes", mutates: true },
|
|
232
232
|
{ method: "PUT", path: "/api/native-integrations/grok", module: "server/management/native-integration-routes", mutates: true },
|
|
233
|
+
// server/management/cursor-integration-routes
|
|
234
|
+
{ method: "GET", path: "/api/native-integrations/cursor", module: "server/management/cursor-integration-routes", mutates: false },
|
|
233
235
|
// server/management/oauth-account-routes
|
|
234
236
|
{ method: "DELETE", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
|
|
237
|
+
{ method: "DELETE", path: "/api/keys/rotate", module: "server/management/oauth-account-routes", mutates: true },
|
|
235
238
|
{ method: "DELETE", path: "/api/oauth/accounts", module: "server/management/oauth-account-routes", mutates: true },
|
|
236
239
|
{ method: "DELETE", path: "/api/providers/keys", module: "server/management/oauth-account-routes", mutates: true },
|
|
237
240
|
{ method: "GET", path: "/api/key-providers", module: "server/management/oauth-account-routes", mutates: false },
|
|
@@ -241,9 +244,13 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
241
244
|
{ method: "GET", path: "/api/oauth/providers", module: "server/management/oauth-account-routes", mutates: false },
|
|
242
245
|
{ method: "GET", path: "/api/oauth/status", module: "server/management/oauth-account-routes", mutates: false },
|
|
243
246
|
{ method: "GET", path: "/api/providers/keys", module: "server/management/oauth-account-routes", mutates: false },
|
|
247
|
+
{ method: "GET", path: "/api/providers/keychain", module: "server/management/oauth-account-routes", mutates: false },
|
|
248
|
+
{ method: "POST", path: "/api/providers/keychain", module: "server/management/oauth-account-routes", mutates: true },
|
|
244
249
|
{ method: "PATCH", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
|
|
245
250
|
{ method: "PATCH", path: "/api/oauth/accounts/pool", module: "server/management/oauth-account-routes", mutates: true },
|
|
246
251
|
{ method: "POST", path: "/api/keys", module: "server/management/oauth-account-routes", mutates: true },
|
|
252
|
+
{ method: "POST", path: "/api/keys/rotate", module: "server/management/oauth-account-routes", mutates: true },
|
|
253
|
+
{ method: "POST", path: "/api/keys/rotate/commit", module: "server/management/oauth-account-routes", mutates: true },
|
|
247
254
|
{ method: "POST", path: "/api/oauth/accounts/clear-cooldown", module: "server/management/oauth-account-routes", mutates: true },
|
|
248
255
|
{ method: "POST", path: "/api/oauth/accounts/import", module: "server/management/oauth-account-routes", mutates: true },
|
|
249
256
|
{ method: "POST", path: "/api/oauth/login", module: "server/management/oauth-account-routes", mutates: true },
|
|
@@ -275,6 +282,8 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
275
282
|
{ method: "GET", path: "/api/routing-profiles", module: "server/management/routing-profile-routes", mutates: false },
|
|
276
283
|
{ method: "POST", path: "/api/routing-profiles/dry-run", module: "server/management/routing-profile-routes", mutates: true },
|
|
277
284
|
{ method: "PUT", path: "/api/routing-profiles", module: "server/management/routing-profile-routes", mutates: true },
|
|
285
|
+
// server/management/session-routes
|
|
286
|
+
{ method: "POST", path: "/api/session/logout", module: "server/management/session-routes", mutates: true, exempt: { reason: "session-only", why: "Logs out the CURRENT gui-session and requires its own Origin and CSRF. There is nothing for a CLI verb to log out of: the CLI holds an admin token, and the admin token is refused here precisely so it cannot end a consent session it never established." } },
|
|
278
287
|
// server/management/sidebar-routes
|
|
279
288
|
{ method: "GET", path: "/api/github/star", module: "server/management/sidebar-routes", mutates: false },
|
|
280
289
|
{ method: "GET", path: "/api/update/badge", module: "server/management/sidebar-routes", mutates: false },
|
|
@@ -286,6 +295,7 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
286
295
|
{ method: "POST", path: "/api/storage/codex-logs/repair", module: "server/management/storage-log-guard-routes", mutates: true },
|
|
287
296
|
{ method: "POST", path: "/api/storage/codex-logs/unprotect", module: "server/management/storage-log-guard-routes", mutates: true },
|
|
288
297
|
// server/management/system-routes
|
|
298
|
+
{ method: "GET", path: "/api/system/health", module: "server/management/system-routes", mutates: false },
|
|
289
299
|
{ method: "GET", path: "/api/system/memory", module: "server/management/system-routes", mutates: false },
|
|
290
300
|
{ method: "GET", path: "/api/system/windows-replace-retries", module: "server/management/system-routes", mutates: false },
|
|
291
301
|
{ method: "POST", path: "/api/system/restart", module: "server/management/system-routes", mutates: true },
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { jsonResponse } from "../auth-cors";
|
|
2
|
+
import type { ManagementContext } from "./context";
|
|
3
|
+
|
|
4
|
+
export function handleSessionRoutes(ctx: ManagementContext): Response | null {
|
|
5
|
+
if (ctx.url.pathname !== "/api/session/logout" || ctx.req.method !== "POST") return null;
|
|
6
|
+
if (ctx.principal !== "gui-session") {
|
|
7
|
+
return jsonResponse({ error: "GUI session required" }, 403, ctx.req, ctx.config);
|
|
8
|
+
}
|
|
9
|
+
if (!ctx.sessionControl?.revokeCurrent(ctx.req)) {
|
|
10
|
+
return jsonResponse({ error: "GUI session not found" }, 401, ctx.req, ctx.config);
|
|
11
|
+
}
|
|
12
|
+
return jsonResponse({ ok: true }, 200, ctx.req, ctx.config);
|
|
13
|
+
}
|
|
@@ -48,7 +48,19 @@ import { acceptSystemRestart } from "./system-restart";
|
|
|
48
48
|
const ENDPOINT_SAMPLE_LIMIT = 60;
|
|
49
49
|
|
|
50
50
|
export async function handleSystemRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
51
|
-
const { req, url, config } = ctx;
|
|
51
|
+
const { req, url, config, version } = ctx;
|
|
52
|
+
if (url.pathname === "/api/system/health" && req.method === "GET") {
|
|
53
|
+
// Authenticated management counterpart to /healthz. Remote Hub deliberately keeps the
|
|
54
|
+
// unauthenticated liveness route off its management ingress, while the connected dashboard
|
|
55
|
+
// still needs bounded process identity and PID replacement evidence (#3158).
|
|
56
|
+
return jsonResponse({
|
|
57
|
+
status: "ok",
|
|
58
|
+
service: "opencodex",
|
|
59
|
+
version,
|
|
60
|
+
uptime: process.uptime(),
|
|
61
|
+
pid: process.pid,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
52
64
|
if (url.pathname === "/api/system/memory" && req.method === "GET") {
|
|
53
65
|
const usage = process.memoryUsage();
|
|
54
66
|
let jscHeap: { heapSize: number; heapCapacity: number; objectCount: number } | null = null;
|
|
@@ -72,14 +72,16 @@ import { handleSidebarRoutes } from "./management/sidebar-routes";
|
|
|
72
72
|
import { handleCodexPromptRoutes } from "./management/codex-prompt-routes";
|
|
73
73
|
import { handleIntegrationRoutes } from "./management/integration-routes";
|
|
74
74
|
import { handleNativeIntegrationRoutes } from "./management/native-integration-routes";
|
|
75
|
+
import { handleCursorIntegrationRoutes } from "./management/cursor-integration-routes";
|
|
75
76
|
import type { ManagementContext } from "./management/context";
|
|
76
|
-
import type { ManagementPrincipal } from "./management-auth";
|
|
77
|
+
import type { ManagementPrincipal, ManagementSessionControl } from "./management-auth";
|
|
77
78
|
export type { ManagementApiDeps } from "./management/context";
|
|
78
79
|
import { fetchAllModels } from "./management/shared";
|
|
79
80
|
import { CatalogGatherBusyError } from "../codex/catalog/provider-fetch";
|
|
80
81
|
import type { CatalogDisposition, ConvergeCodex } from "../codex/convergence-types";
|
|
81
82
|
import { normalizeCatalogDisposition } from "../codex/catalog-refresh-status";
|
|
82
83
|
import { managementBodyTooLargeResponse } from "./management/body";
|
|
84
|
+
import { handleSessionRoutes } from "./management/session-routes";
|
|
83
85
|
|
|
84
86
|
// installed npm version instead of a stale hardcode.
|
|
85
87
|
export const VERSION = (() => {
|
|
@@ -136,6 +138,7 @@ export async function handleManagementAPI(
|
|
|
136
138
|
config: OcxConfig,
|
|
137
139
|
deps: ManagementApiDeps = {},
|
|
138
140
|
principal?: ManagementPrincipal,
|
|
141
|
+
sessionControl?: ManagementSessionControl,
|
|
139
142
|
): Promise<Response | null> {
|
|
140
143
|
if (!isAllowedManagementOrigin(req, config)) {
|
|
141
144
|
return jsonResponse({ error: "cross-origin request blocked" }, 403, req, config);
|
|
@@ -218,10 +221,11 @@ export async function handleManagementAPI(
|
|
|
218
221
|
}
|
|
219
222
|
} catch { /* best-effort */ }
|
|
220
223
|
}
|
|
221
|
-
const ctx: ManagementContext = { req, url, config, deps, principal, convergeCodexCatalog, syncClaudeAgentDefsBestEffort };
|
|
224
|
+
const ctx: ManagementContext = { req, url, config, deps, version: VERSION, principal, sessionControl, convergeCodexCatalog, syncClaudeAgentDefsBestEffort };
|
|
222
225
|
let routed: Response | null;
|
|
223
226
|
try {
|
|
224
|
-
routed = (
|
|
227
|
+
routed = handleSessionRoutes(ctx)
|
|
228
|
+
?? (await handleConfigRoutes(ctx))
|
|
225
229
|
?? (await handleStorageLogGuardRoutes(ctx))
|
|
226
230
|
?? (await handleLogsUsageRoutes(ctx))
|
|
227
231
|
?? (await handleRequestHistoryRoutes(ctx))
|
|
@@ -231,6 +235,7 @@ export async function handleManagementAPI(
|
|
|
231
235
|
?? (await handleModelRoutes(ctx))
|
|
232
236
|
?? (await handleIntegrationRoutes(ctx))
|
|
233
237
|
?? (await handleNativeIntegrationRoutes(ctx))
|
|
238
|
+
?? (await handleCursorIntegrationRoutes(ctx))
|
|
234
239
|
?? (await handleAgentSettingsRoutes(ctx))
|
|
235
240
|
?? (await handleCodexPromptRoutes(ctx))
|
|
236
241
|
?? (await handleOauthAccountRoutes(ctx))
|