@yansigit/opencodex 2.35.2-dev.20260829.13 → 2.36.1-dev.20260829.17
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/{index-C8Tctiwl.js → index-l0pKn4-2.js} +10 -10
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/config.ts +23 -1
- package/src/generated/compatibility-version.json +17 -17
- package/src/server/index.ts +9 -2
- package/src/server/management/agent-settings-routes.ts +175 -54
- package/src/server/management/combo-routes.ts +3 -4
- package/src/server/management/config-routes.ts +45 -18
- package/src/server/management/context.ts +50 -0
- package/src/server/management/logs-usage-routes.ts +0 -1
- package/src/server/management/model-routes.ts +2 -3
- package/src/server/management/native-integration-routes.ts +17 -6
- package/src/server/management/oauth-account-routes.ts +6 -7
- package/src/server/management/provider-routes.ts +9 -15
- package/src/server/management/replit-provider-routes.ts +8 -2
- package/src/server/management/routing-profile-routes.ts +4 -6
- package/src/server/management/shared.ts +0 -1
- package/src/server/management-api.ts +12 -0
- package/src/server/responses/v2-routed-delegation-bridge.ts +45 -14
|
@@ -81,7 +81,6 @@ import {
|
|
|
81
81
|
multiAgentGuidanceEnabled,
|
|
82
82
|
providerBaseUrlConfigError,
|
|
83
83
|
providerHeadersConfigError,
|
|
84
|
-
saveConfigPreservingClaudeCode,
|
|
85
84
|
} from "../../config";
|
|
86
85
|
import {
|
|
87
86
|
clearLoginState,
|
|
@@ -148,7 +147,7 @@ import type {
|
|
|
148
147
|
|
|
149
148
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
150
149
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
151
|
-
import type
|
|
150
|
+
import { saveManagementConfig, type ManagementContext } from "./context";
|
|
152
151
|
import { listManagementModelRows, loadExportModels } from "./model-rows";
|
|
153
152
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
154
153
|
import {
|
|
@@ -177,7 +176,7 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
177
176
|
// the real store; tests that pass an in-memory fixture inject a no-op/spy. Do not
|
|
178
177
|
// bypass this seam with a dynamic config import — doing so replaced a user's
|
|
179
178
|
// ~/.opencodex/config.json with the `existing-uuid` test fixture.
|
|
180
|
-
const persistConfig = deps
|
|
179
|
+
const persistConfig = (candidate: OcxConfig) => saveManagementConfig(deps, candidate);
|
|
181
180
|
|
|
182
181
|
if (url.pathname === "/api/model-discovery" && req.method === "GET") {
|
|
183
182
|
const providers = Object.fromEntries(Object.entries(config.providers).map(([name, provider]) => [
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* Design of record: devlog/_fin/260803_integrations_toggle_all/030 (routes),
|
|
18
18
|
* 011 (Claude Code), 012 (Grok).
|
|
19
19
|
*/
|
|
20
|
-
import { loadConfig
|
|
20
|
+
import { loadConfig } from "../../config";
|
|
21
21
|
import { readRuntimePort } from "../../config/process-state";
|
|
22
22
|
import { desktopVisibleNativeSlugs, filterCatalogVisibleModels, nativeContextLimits, nativeOpenAiContextWindow, visibleNativeSlugs } from "../../codex/catalog";
|
|
23
23
|
import { providerContextCap } from "../../providers/context-cap";
|
|
@@ -31,7 +31,7 @@ import type { CodexNativeRestoreResult } from "../../codex/inject";
|
|
|
31
31
|
import type { OcxConfig } from "../../types";
|
|
32
32
|
import { jsonResponse } from "../auth-cors";
|
|
33
33
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
34
|
-
import type
|
|
34
|
+
import { ManagementPersistenceError, saveManagementConfig, type ManagementContext } from "./context";
|
|
35
35
|
|
|
36
36
|
export type NativeIntegrationClientId = "claude" | "grok" | "codex" | "claude-desktop";
|
|
37
37
|
|
|
@@ -221,9 +221,10 @@ function grokStatus(config: ManagementContext["config"]): NativeStatus {
|
|
|
221
221
|
* cannot open, which fails identically forever (audit r8 #2).
|
|
222
222
|
*/
|
|
223
223
|
function isLockContention(error: unknown): boolean {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
for (let current = error; current && typeof current === "object"; current = (current as { cause?: unknown }).cause) {
|
|
225
|
+
if ((current as { code?: unknown }).code === "SQLITE_BUSY") return true;
|
|
226
|
+
}
|
|
227
|
+
return false;
|
|
227
228
|
}
|
|
228
229
|
|
|
229
230
|
function isConfigLockError(error: unknown): boolean {
|
|
@@ -732,10 +733,20 @@ export async function handleNativeIntegrationRoutes(ctx: ManagementContext): Pro
|
|
|
732
733
|
* `deps.` first: ManagementApiDeps carries this seam so route tests with an
|
|
733
734
|
* in-memory fixture cannot overwrite the developer's real OPENCODEX_HOME.
|
|
734
735
|
*/
|
|
735
|
-
const persist = deps
|
|
736
|
+
const persist = (candidate: OcxConfig) => saveManagementConfig(deps, candidate);
|
|
736
737
|
try {
|
|
737
738
|
persist(config);
|
|
738
739
|
} catch (error) {
|
|
740
|
+
if (error instanceof ManagementPersistenceError) {
|
|
741
|
+
if (isConfigLockError(error)) {
|
|
742
|
+
error.response = isLockContention(error)
|
|
743
|
+
? refusal(409, "claude", "config_busy",
|
|
744
|
+
"Another process is saving the configuration right now. Try again in a moment.")
|
|
745
|
+
: refusal(500, "claude", "write_failed",
|
|
746
|
+
`The configuration lock could not be acquired: ${error.message}`);
|
|
747
|
+
}
|
|
748
|
+
throw error;
|
|
749
|
+
}
|
|
739
750
|
if (isConfigLockError(error)) {
|
|
740
751
|
return isLockContention(error)
|
|
741
752
|
? refusal(409, "claude", "config_busy",
|
|
@@ -12,7 +12,6 @@ import {
|
|
|
12
12
|
providerHeadersConfigError,
|
|
13
13
|
readConfigDiagnostics,
|
|
14
14
|
reconcileLiveConfigFromDisk,
|
|
15
|
-
saveConfigPreservingClaudeCode,
|
|
16
15
|
} from "../../config";
|
|
17
16
|
import {
|
|
18
17
|
clearLoginState,
|
|
@@ -68,7 +67,7 @@ import { isAzureIdentityProvider } from "../../config/provider-validation";
|
|
|
68
67
|
|
|
69
68
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
70
69
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
71
|
-
import type
|
|
70
|
+
import { saveManagementConfig, type ManagementContext } from "./context";
|
|
72
71
|
import { readManagementJsonBody, readManagementJsonBodyOr, rethrowManagementBodyTooLarge } from "./body";
|
|
73
72
|
import { codexAccountNamespaceProviderCollisionError } from "../../codex/account-namespace-match";
|
|
74
73
|
import { ACCOUNT_IMPORT_DEADLINE_MS, ACCOUNT_IMPORT_MAX_REQUEST_BYTES } from "../../oauth/account-import";
|
|
@@ -374,7 +373,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
374
373
|
enabled = body.enabled;
|
|
375
374
|
}
|
|
376
375
|
config.cursorAccountPool = { enabled };
|
|
377
|
-
|
|
376
|
+
saveManagementConfig(deps, config);
|
|
378
377
|
reconcileLiveStateStores();
|
|
379
378
|
return jsonResponse({
|
|
380
379
|
ok: true,
|
|
@@ -422,7 +421,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
422
421
|
...(strategy !== undefined ? { strategy } : {}),
|
|
423
422
|
...(stickyLimit !== undefined ? { stickyLimit } : {}),
|
|
424
423
|
};
|
|
425
|
-
|
|
424
|
+
saveManagementConfig(deps, config);
|
|
426
425
|
reconcileLiveStateStores();
|
|
427
426
|
return jsonResponse({
|
|
428
427
|
ok: true,
|
|
@@ -667,7 +666,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
667
666
|
const key = "ocx_data_" + randomBytes(20).toString("hex");
|
|
668
667
|
const entry = { id: randomUUID(), name, key, createdAt: new Date().toISOString() };
|
|
669
668
|
config.apiKeys = [...(config.apiKeys ?? []), entry];
|
|
670
|
-
|
|
669
|
+
saveManagementConfig(deps, config);
|
|
671
670
|
reconcileLiveStateStores();
|
|
672
671
|
return jsonResponse({ id: entry.id, name: entry.name, key: entry.key, createdAt: entry.createdAt }, 201, req, config);
|
|
673
672
|
}
|
|
@@ -681,7 +680,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
681
680
|
const entry = (config.apiKeys ?? []).find(k => k.id === body.id);
|
|
682
681
|
if (!entry) return jsonResponse({ error: "key not found" }, 404, req, config);
|
|
683
682
|
entry.name = nameField.value;
|
|
684
|
-
|
|
683
|
+
saveManagementConfig(deps, config);
|
|
685
684
|
reconcileLiveStateStores();
|
|
686
685
|
// Never echo key material from a rename.
|
|
687
686
|
return jsonResponse({ id: entry.id, name: entry.name, createdAt: entry.createdAt }, 200, req, config);
|
|
@@ -695,7 +694,7 @@ export async function handleOauthAccountRoutes(ctx: ManagementContext): Promise<
|
|
|
695
694
|
config.apiKeys = (config.apiKeys ?? []).filter(k => k.id !== body.id);
|
|
696
695
|
// A stale id must not read as a successful revocation.
|
|
697
696
|
if (config.apiKeys.length === before) return jsonResponse({ error: "key not found" }, 404, req, config);
|
|
698
|
-
|
|
697
|
+
saveManagementConfig(deps, config);
|
|
699
698
|
reconcileLiveStateStores();
|
|
700
699
|
return jsonResponse({ success: true }, 200, req, config);
|
|
701
700
|
}
|
|
@@ -16,7 +16,6 @@ import {
|
|
|
16
16
|
providerHeadersConfigError,
|
|
17
17
|
requestPacingConfigError,
|
|
18
18
|
readConfigAdmissionSnapshot,
|
|
19
|
-
saveConfigPreservingClaudeCode,
|
|
20
19
|
upstreamHttpVersionConfigError,
|
|
21
20
|
withConfigMutationLockSync,
|
|
22
21
|
} from "../../config";
|
|
@@ -95,7 +94,7 @@ import {
|
|
|
95
94
|
|
|
96
95
|
import { isPlainRecord, parseDebugLogQuery, tokPerSecondResult, unavailableCostReason, costResult, requestLogDto, stripRegistryOnlyStaticHeaders, fetchAllModels } from "./shared";
|
|
97
96
|
import type { MetricUnavailableReason, TokPerSecondResult, CostEstimateReason, CostResult, MetricSource } from "./shared";
|
|
98
|
-
import type
|
|
97
|
+
import { saveManagementConfig, type ManagementContext } from "./context";
|
|
99
98
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
100
99
|
import { resolveAiStudioCredentials } from "../../oauth/aistudio-credentials";
|
|
101
100
|
import { buildAiStudioHeaders, parseGoogleCookieJar } from "../../oauth/google-aistudio-auth";
|
|
@@ -743,7 +742,6 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
743
742
|
const submittedModelAutoCompactTokenLimits = Object.hasOwn(prov, "modelAutoCompactTokenLimits");
|
|
744
743
|
const submittedRequestPacing = Object.hasOwn(prov, "requestPacing");
|
|
745
744
|
enrichProviderFromCatalog(name, prov);
|
|
746
|
-
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
747
745
|
// Overwriting an existing provider must not drop its multi-key pool: carry it over, then
|
|
748
746
|
// let the (possibly new) apiKey join the pool as the active entry.
|
|
749
747
|
const existingPool = config.providers[name]?.apiKeyPool;
|
|
@@ -786,7 +784,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
786
784
|
}
|
|
787
785
|
config.providers[name] = stripRegistryOnlyStaticHeaders(name, prov);
|
|
788
786
|
if (body.setDefault === true) config.defaultProvider = name;
|
|
789
|
-
|
|
787
|
+
saveManagementConfig(deps, config);
|
|
790
788
|
reconcileLiveStateStores();
|
|
791
789
|
if (prov.apiKey && prov.apiKeyPool) {
|
|
792
790
|
const { addProviderApiKey } = await import("../../providers/api-keys");
|
|
@@ -826,9 +824,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
826
824
|
if (!provider || !isCanonicalOpenAiForwardProvider(provider)) {
|
|
827
825
|
return jsonResponse({ error: "provider openai must be the canonical built-in provider" }, 400);
|
|
828
826
|
}
|
|
829
|
-
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
830
827
|
config.providers.openai = { ...provider, codexAccountMode: mode };
|
|
831
|
-
|
|
828
|
+
saveManagementConfig(deps, config);
|
|
832
829
|
reconcileLiveStateStores();
|
|
833
830
|
(deps.clearProviderQuotaCache ?? clearProviderQuotaCache)();
|
|
834
831
|
(deps.clearThreadAccountMap ?? clearThreadAccountMap)();
|
|
@@ -853,9 +850,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
853
850
|
if (config.providers[name]!.disabled) {
|
|
854
851
|
return jsonResponse({ error: "cannot set a disabled provider as default", code: "default_provider_disabled" }, 400);
|
|
855
852
|
}
|
|
856
|
-
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
857
853
|
config.defaultProvider = name;
|
|
858
|
-
|
|
854
|
+
saveManagementConfig(deps, config);
|
|
859
855
|
reconcileLiveStateStores();
|
|
860
856
|
return jsonResponse({ success: true, name, defaultProvider: name });
|
|
861
857
|
}
|
|
@@ -926,7 +922,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
926
922
|
// A PATCH that managed headers owns the resulting block: the clear path restores
|
|
927
923
|
// registry static headers, so exact-match stripping must not erase them again.
|
|
928
924
|
config.providers[name] = replay.headersTouched ? replay.next : stripRegistryOnlyStaticHeaders(name, replay.next);
|
|
929
|
-
|
|
925
|
+
saveManagementConfig(deps, config);
|
|
930
926
|
});
|
|
931
927
|
if (replayError !== undefined) return jsonResponse({ error: replayError }, 409);
|
|
932
928
|
reconcileLiveStateStores();
|
|
@@ -1125,13 +1121,12 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1125
1121
|
combos: dependentCombos,
|
|
1126
1122
|
}, 409);
|
|
1127
1123
|
}
|
|
1128
|
-
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
1129
1124
|
if (fallbackDefault) config.defaultProvider = fallbackDefault;
|
|
1130
1125
|
delete config.providers[name];
|
|
1131
1126
|
const { dropProviderCustomModels } = await import("../../providers/provider-id-rewrite");
|
|
1132
1127
|
const droppedCustomModels = dropProviderCustomModels(config, name);
|
|
1133
1128
|
setProviderContextCap(config, name, false);
|
|
1134
|
-
|
|
1129
|
+
saveManagementConfig(deps, config);
|
|
1135
1130
|
await replaceProviderAccountSet(name, null);
|
|
1136
1131
|
reconcileLiveStateStores();
|
|
1137
1132
|
const { clearModelCache: clearCache } = await import("../../codex/model-cache");
|
|
@@ -1156,7 +1151,6 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1156
1151
|
// property access, with the route's consistent 400 response.
|
|
1157
1152
|
if (!isPlainRecord(rawBody)) return jsonResponse({ error: "provider-context-caps body must be a plain object" }, 400);
|
|
1158
1153
|
const body = rawBody as { provider?: unknown; enabled?: unknown; value?: unknown; setAll?: unknown };
|
|
1159
|
-
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
1160
1154
|
const { clearModelCache } = await import("../../codex/model-cache");
|
|
1161
1155
|
const respond = (catalogRefresh: Awaited<ReturnType<typeof convergeCodexCatalog>>) => jsonResponse({
|
|
1162
1156
|
ok: true,
|
|
@@ -1202,7 +1196,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1202
1196
|
return jsonResponse({ error: "value must be a positive number" }, 400);
|
|
1203
1197
|
}
|
|
1204
1198
|
setProviderContextCap(config, provider, body.enabled, perProviderValue);
|
|
1205
|
-
|
|
1199
|
+
saveManagementConfig(deps, config);
|
|
1206
1200
|
reconcileLiveStateStores();
|
|
1207
1201
|
clearModelCache(provider);
|
|
1208
1202
|
const catalogRefresh = await convergeCodexCatalog();
|
|
@@ -1228,7 +1222,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1228
1222
|
const affected = Object.keys(providerContextCaps(config));
|
|
1229
1223
|
const applyToAll = body.setAll === true;
|
|
1230
1224
|
setGlobalContextCapValue(config, normalizedValue, applyToAll);
|
|
1231
|
-
|
|
1225
|
+
saveManagementConfig(deps, config);
|
|
1232
1226
|
reconcileLiveStateStores();
|
|
1233
1227
|
if (applyToAll) {
|
|
1234
1228
|
for (const provider of affected) clearModelCache(provider);
|
|
@@ -1245,7 +1239,7 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
1245
1239
|
const before = Object.keys(providerContextCaps(config));
|
|
1246
1240
|
const names = Object.keys(config.providers);
|
|
1247
1241
|
setAllProviderContextCaps(config, names, body.setAll);
|
|
1248
|
-
|
|
1242
|
+
saveManagementConfig(deps, config);
|
|
1249
1243
|
reconcileLiveStateStores();
|
|
1250
1244
|
for (const provider of new Set([...before, ...names])) clearModelCache(provider);
|
|
1251
1245
|
const catalogRefresh = await convergeCodexCatalog();
|
|
@@ -9,6 +9,8 @@ function isPlainRecord(value: unknown): value is Record<string, unknown> {
|
|
|
9
9
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
const unavailableMutation = <T,>() => ({ status: "unavailable" as const, reason: "missing" as const });
|
|
13
|
+
|
|
12
14
|
export async function handleReplitProviderRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
13
15
|
const { req, url, config, deps, convergeCodexCatalog } = ctx;
|
|
14
16
|
|
|
@@ -41,7 +43,6 @@ export async function handleReplitProviderRoutes(ctx: ManagementContext): Promis
|
|
|
41
43
|
if (body.setDefault !== undefined && typeof body.setDefault !== "boolean") {
|
|
42
44
|
return jsonResponse({ error: "setDefault must be a boolean" }, 400);
|
|
43
45
|
}
|
|
44
|
-
|
|
45
46
|
const result = await installReplitProviderPair(config, {
|
|
46
47
|
origin,
|
|
47
48
|
gatewayKey,
|
|
@@ -49,10 +50,15 @@ export async function handleReplitProviderRoutes(ctx: ManagementContext): Promis
|
|
|
49
50
|
replace: body.replace === true,
|
|
50
51
|
setDefault: body.setDefault === true,
|
|
51
52
|
}, {
|
|
52
|
-
|
|
53
|
+
// Direct management dispatch has no authority to activate the service's CLI fallback.
|
|
54
|
+
mutatePersistedConfig: deps.mutatePersistedConfig ?? unavailableMutation,
|
|
53
55
|
probeFetch: deps.probeFetch,
|
|
54
56
|
});
|
|
55
57
|
|
|
58
|
+
if (!deps.mutatePersistedConfig) {
|
|
59
|
+
return jsonResponse({ error: "management persistence unavailable" }, 500, req, config);
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
if (!result.ok) {
|
|
57
63
|
const status = result.code === "provider_collision"
|
|
58
64
|
? 409
|
|
@@ -20,12 +20,12 @@ import { assemblePolicyCandidateEvidence } from "../../routing/compatibility/ass
|
|
|
20
20
|
import { activateLab, labActivationRequired } from "../../lib/lab-activation";
|
|
21
21
|
import { quotaEvidenceForCandidate } from "../../routing/quota";
|
|
22
22
|
import { routedProviderConfig } from "../../router";
|
|
23
|
-
import { deleteConfigTopLevelKey,
|
|
23
|
+
import { deleteConfigTopLevelKey, getConfigDir } from "../../config";
|
|
24
24
|
import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
|
|
25
25
|
import { isPlainRecord } from "./shared";
|
|
26
26
|
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
27
27
|
import { jsonResponse } from "../auth-cors";
|
|
28
|
-
import type
|
|
28
|
+
import { saveManagementConfig, type ManagementContext } from "./context";
|
|
29
29
|
import type { OcxConfig, OcxRoutingProfileConfig } from "../../types";
|
|
30
30
|
|
|
31
31
|
function profileDto(config: Parameters<typeof getRoutingProfile>[0], id: string): Record<string, unknown> | null {
|
|
@@ -310,8 +310,7 @@ export async function handleRoutingProfileRoutes(ctx: ManagementContext): Promis
|
|
|
310
310
|
const newPublicModel = policyPublicModelId(id, getRoutingProfile(config, id)!);
|
|
311
311
|
shouldSyncClaudeAgentDefs = migrateProfileModelReferences(config, oldPublicModel, newPublicModel);
|
|
312
312
|
}
|
|
313
|
-
|
|
314
|
-
save(config);
|
|
313
|
+
saveManagementConfig(deps, config);
|
|
315
314
|
reconcileLiveStateStores();
|
|
316
315
|
const catalogRefresh = await convergeCodexCatalog();
|
|
317
316
|
if (shouldSyncClaudeAgentDefs) await syncClaudeAgentDefsBestEffort();
|
|
@@ -338,8 +337,7 @@ export async function handleRoutingProfileRoutes(ctx: ManagementContext): Promis
|
|
|
338
337
|
delete nextProfiles[id];
|
|
339
338
|
if (Object.keys(nextProfiles).length > 0) config.routingProfiles = nextProfiles;
|
|
340
339
|
else deleteConfigTopLevelKey(config, "routingProfiles");
|
|
341
|
-
|
|
342
|
-
saveConfigPreservingClaudeCodeSafe(config);
|
|
340
|
+
saveManagementConfig(deps, config);
|
|
343
341
|
reconcileLiveStateStores();
|
|
344
342
|
const catalogRefresh = await convergeCodexCatalog();
|
|
345
343
|
return jsonResponse({ success: true, id, catalogRefresh }, 200, req, config);
|
|
@@ -75,6 +75,7 @@ import { handleIntegrationRoutes } from "./management/integration-routes";
|
|
|
75
75
|
import { handleNativeIntegrationRoutes } from "./management/native-integration-routes";
|
|
76
76
|
import type { ManagementContext } from "./management/context";
|
|
77
77
|
import type { ManagementPrincipal } from "./management-auth";
|
|
78
|
+
import { ManagementPersistenceError, MissingManagementPersistenceError } from "./management/context";
|
|
78
79
|
export type { ManagementApiDeps } from "./management/context";
|
|
79
80
|
import { fetchAllModels } from "./management/shared";
|
|
80
81
|
import { CatalogGatherBusyError } from "../codex/catalog/provider-fetch";
|
|
@@ -216,6 +217,10 @@ export async function handleManagementAPI(
|
|
|
216
217
|
} catch { /* best-effort */ }
|
|
217
218
|
}
|
|
218
219
|
const ctx: ManagementContext = { req, url, config, deps, principal, convergeCodexCatalog, syncClaudeAgentDefsBestEffort };
|
|
220
|
+
const configBeforeDispatch = (["GET", "HEAD", "OPTIONS"].includes(req.method)
|
|
221
|
+
|| (req.method === "POST" && url.pathname === "/api/providers/test"))
|
|
222
|
+
? undefined
|
|
223
|
+
: structuredClone(config);
|
|
219
224
|
let routed: Response | null;
|
|
220
225
|
try {
|
|
221
226
|
routed = (await handleConfigRoutes(ctx))
|
|
@@ -239,6 +244,13 @@ export async function handleManagementAPI(
|
|
|
239
244
|
} catch (error) {
|
|
240
245
|
const tooLarge = managementBodyTooLargeResponse(error, req, config);
|
|
241
246
|
if (tooLarge) return tooLarge;
|
|
247
|
+
if (error instanceof MissingManagementPersistenceError || error instanceof ManagementPersistenceError) {
|
|
248
|
+
if (configBeforeDispatch === undefined) throw error;
|
|
249
|
+
const response = error instanceof ManagementPersistenceError ? error.response : undefined;
|
|
250
|
+
for (const key of Object.keys(config)) delete (config as unknown as Record<string, unknown>)[key];
|
|
251
|
+
Object.assign(config, configBeforeDispatch);
|
|
252
|
+
return response ?? jsonResponse({ error: "management persistence unavailable" }, 500, req, config);
|
|
253
|
+
}
|
|
242
254
|
if (error instanceof OAuthMutationBusyError) {
|
|
243
255
|
return new Response(JSON.stringify({ error: { type: "server_error", code: "oauth_mutation_busy", message: error.message } }), {
|
|
244
256
|
status: 503,
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { isDeepStrictEqual } from "node:util";
|
|
2
1
|
import type { OcxParsedRequest, OcxTool } from "../../types";
|
|
3
2
|
import type { SsePayloadRewrite } from "../sse-payload-rewrite";
|
|
4
3
|
|
|
@@ -7,6 +6,7 @@ const NATIVE_NAMESPACE = "collaboration";
|
|
|
7
6
|
const MIRRORED_NAMES = new Set(["spawn_agent", "send_message", "followup_task"]);
|
|
8
7
|
const GUIDANCE = "Use this routed-child mirror for collaboration operations.";
|
|
9
8
|
const MAX_SSE_BINDINGS = 128;
|
|
9
|
+
const injectedGroups = new WeakSet<object>();
|
|
10
10
|
|
|
11
11
|
type RecordValue = Record<string, unknown>;
|
|
12
12
|
|
|
@@ -33,21 +33,38 @@ function rawToolLists(body: unknown, replayPrefixLength: number): unknown[][] {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
function requestStateBody(body: unknown): unknown {
|
|
36
|
-
if (!isRecord(body)
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
if (!isRecord(body)) return body;
|
|
37
|
+
const cloneTools = (tools: unknown[]) => tools.map(tool => (
|
|
38
|
+
isRecord(tool) && tool.type === "namespace" && Array.isArray(tool.tools)
|
|
39
|
+
? { ...tool, tools: [...tool.tools] }
|
|
40
|
+
: tool
|
|
41
|
+
));
|
|
42
|
+
if (!Array.isArray(body.tools) && !Array.isArray(body.input)) return body;
|
|
43
|
+
// Only tool catalogs are mutated below. Clone that narrow path so the continuation
|
|
44
|
+
// cache retains the caller's catalog without copying unrelated context.
|
|
39
45
|
return {
|
|
40
46
|
...body,
|
|
41
|
-
|
|
47
|
+
...(Array.isArray(body.tools) ? { tools: cloneTools(body.tools) } : {}),
|
|
48
|
+
...(Array.isArray(body.input) ? { input: body.input.map(item => (
|
|
42
49
|
isRecord(item) && item.type === "additional_tools" && Array.isArray(item.tools)
|
|
43
|
-
? { ...item, tools:
|
|
50
|
+
? { ...item, tools: cloneTools(item.tools) }
|
|
44
51
|
: item
|
|
45
|
-
)),
|
|
52
|
+
)) } : {}),
|
|
46
53
|
};
|
|
47
54
|
}
|
|
48
55
|
|
|
49
56
|
function mirrorTool(tool: RecordValue): RecordValue {
|
|
50
|
-
|
|
57
|
+
const parameters = isRecord(tool.parameters) ? tool.parameters : undefined;
|
|
58
|
+
const properties = isRecord(parameters?.properties) ? parameters.properties : undefined;
|
|
59
|
+
const message = isRecord(properties?.message) ? properties.message : undefined;
|
|
60
|
+
const { encrypted: _, ...plaintextMessage } = message ?? {};
|
|
61
|
+
return {
|
|
62
|
+
...tool,
|
|
63
|
+
description: `${GUIDANCE} ${tool.name}.`,
|
|
64
|
+
...(message && Object.hasOwn(message, "encrypted") ? {
|
|
65
|
+
parameters: { ...parameters, properties: { ...properties, message: plaintextMessage } },
|
|
66
|
+
} : {}),
|
|
67
|
+
};
|
|
51
68
|
}
|
|
52
69
|
|
|
53
70
|
function mirrorChildren(group: RecordValue): RecordValue[] {
|
|
@@ -85,25 +102,31 @@ export function injectV2RoutedDelegationBridge(
|
|
|
85
102
|
for (const { group } of nativeGroups) {
|
|
86
103
|
for (const child of mirrorChildren(group)) names.add(child.name as string);
|
|
87
104
|
}
|
|
105
|
+
for (const { group } of existing) {
|
|
106
|
+
for (const child of mirrorChildren(group)) names.add(child.name as string);
|
|
107
|
+
}
|
|
88
108
|
if (names.size === 0) return undefined;
|
|
89
109
|
|
|
90
110
|
const expected = nativeGroups.map(({ list, index, group }) => ({ list, index: index + 1, group: mirrorGroup(group) }));
|
|
91
|
-
const idempotent = existing.length ===
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
111
|
+
const idempotent = existing.length === nativeGroups.length && existing.every(({ list, index, group }) => (
|
|
112
|
+
injectedGroups.has(group)
|
|
113
|
+
&& nativeGroups.some(native => native.list === list && native.index + 1 === index)
|
|
114
|
+
));
|
|
95
115
|
if (existing.length > 0 && !idempotent) {
|
|
96
116
|
throw new Error("v2 routed delegation bridge namespace collision");
|
|
97
117
|
}
|
|
98
118
|
if (!idempotent) {
|
|
99
|
-
for (const { list, index, group } of [...expected].reverse())
|
|
119
|
+
for (const { list, index, group } of [...expected].reverse()) {
|
|
120
|
+
injectedGroups.add(group);
|
|
121
|
+
list.splice(index, 0, group);
|
|
122
|
+
}
|
|
100
123
|
}
|
|
101
124
|
|
|
102
125
|
const mirrorTools: OcxTool[] = [];
|
|
103
126
|
for (const name of names) {
|
|
104
127
|
const source = parsed.context.tools?.find(tool => tool.namespace === NATIVE_NAMESPACE && tool.name === name);
|
|
105
128
|
if (source) {
|
|
106
|
-
mirrorTools.push({ ...source, namespace: MIRROR_NAMESPACE
|
|
129
|
+
mirrorTools.push({ ...mirrorTool(source as unknown as RecordValue), namespace: MIRROR_NAMESPACE } as OcxTool);
|
|
107
130
|
continue;
|
|
108
131
|
}
|
|
109
132
|
const raw = nativeGroups.flatMap(entry => mirrorChildren(entry.group)).find(tool => tool.name === name);
|
|
@@ -114,7 +137,15 @@ export function injectV2RoutedDelegationBridge(
|
|
|
114
137
|
parameters: isRecord(raw?.parameters) ? raw.parameters : {},
|
|
115
138
|
});
|
|
116
139
|
}
|
|
140
|
+
for (const { group } of nativeGroups) {
|
|
141
|
+
if (Array.isArray(group.tools)) group.tools = group.tools.filter(tool => (
|
|
142
|
+
!isRecord(tool) || typeof tool.name !== "string" || !MIRRORED_NAMES.has(tool.name)
|
|
143
|
+
));
|
|
144
|
+
}
|
|
117
145
|
if (mirrorTools.length > 0) {
|
|
146
|
+
parsed.context.tools = (parsed.context.tools ?? []).filter(tool => (
|
|
147
|
+
tool.namespace !== NATIVE_NAMESPACE || !MIRRORED_NAMES.has(tool.name)
|
|
148
|
+
));
|
|
118
149
|
const present = new Set((parsed.context.tools ?? [])
|
|
119
150
|
.filter(tool => tool.namespace === MIRROR_NAMESPACE)
|
|
120
151
|
.map(tool => tool.name));
|