@bitkyc08/opencodex 2.35.0 → 2.36.0-preview.20260830
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-Cy7Z_pl0.css +1 -0
- package/gui/dist/assets/index-DPl4nBMA.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/AGENTS.md +2 -1
- package/src/adapters/agentrouter.ts +50 -0
- package/src/adapters/anthropic.ts +1 -51
- package/src/adapters/cursor/call-id.ts +76 -8
- package/src/adapters/cursor/checkpoint-store.ts +6 -1
- package/src/adapters/cursor/cursor-errors.ts +44 -0
- package/src/adapters/cursor/native-exec.ts +13 -0
- package/src/adapters/cursor/protobuf-request.ts +651 -29
- package/src/adapters/cursor/tool-result-normalize.ts +3 -3
- package/src/adapters/cursor/transport-retry.ts +5 -1
- package/src/adapters/cursor.ts +15 -1
- package/src/adapters/empty-tool-output-annotation.ts +43 -0
- package/src/adapters/exec-tool-result-normalize.ts +70 -5
- package/src/adapters/google.ts +22 -2
- package/src/adapters/kiro.ts +26 -2
- package/src/adapters/ollama-native-url.ts +111 -0
- package/src/adapters/ollama-native.ts +1131 -0
- package/src/adapters/openai-chat.ts +30 -7
- package/src/adapters/openai-responses.ts +72 -4
- package/src/adapters/registry.ts +7 -0
- package/src/adapters/xai-web-search.ts +58 -0
- package/src/claude/desktop-3p.ts +21 -1
- package/src/claude/desktop-policy.ts +149 -0
- package/src/cli/account.ts +16 -2
- package/src/cli/claude-desktop.ts +13 -3
- package/src/cli/combo.ts +8 -5
- package/src/cli/doctor.ts +77 -11
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +16 -0
- package/src/cli/models.ts +20 -3
- package/src/cli/registry.ts +2 -1
- package/src/cli/status.ts +140 -2
- package/src/cli/storage.ts +10 -1
- package/src/codex/account-runtime-state.ts +39 -5
- package/src/codex/account-store.ts +393 -13
- package/src/codex/account-usability.ts +11 -4
- package/src/codex/app-server-processes.ts +46 -5
- package/src/codex/auth-context.ts +160 -32
- package/src/codex/catalog/bundled.ts +7 -5
- package/src/codex/catalog/metadata.ts +1 -1
- package/src/codex/catalog/parsing.ts +57 -1
- package/src/codex/catalog/provider-fetch.ts +61 -4
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/convergence.ts +3 -2
- package/src/codex/data/upstream-models.json +40 -8
- package/src/codex/inject-coordination.ts +111 -14
- package/src/codex/integration-record.ts +12 -2
- package/src/codex/main-account.ts +225 -1
- package/src/codex/model-entitlements.ts +339 -27
- package/src/codex/prompt-layers.ts +346 -7
- package/src/codex/prompt-text-probe.ts +272 -21
- package/src/codex/routing.ts +693 -132
- package/src/codex/runtime.ts +12 -0
- package/src/codex/subagent-model-fallback.ts +62 -24
- package/src/codex/user-identity.ts +33 -25
- package/src/combos/index.ts +1 -0
- package/src/combos/reset-window.ts +46 -0
- package/src/combos/resolve.ts +84 -2
- package/src/combos/types.ts +5 -2
- package/src/config/atomic-write.ts +104 -22
- package/src/config/provider-validation.ts +11 -0
- package/src/config.ts +75 -3
- package/src/generated/compatibility-version.json +207 -131
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/catalog.ts +71 -0
- package/src/grok/effort.ts +83 -0
- package/src/grok/inject.ts +952 -127
- package/src/grok/models.ts +56 -0
- package/src/grok/status.ts +21 -8
- package/src/grok/sync.ts +10 -18
- package/src/images/loop.ts +6 -3
- package/src/integrations/native/ownership-preflight.ts +4 -1
- package/src/lab/fabric/producer-isolate.ts +36 -3
- package/src/lib/destination-policy.ts +93 -7
- package/src/lib/redact.ts +6 -1
- package/src/lib/shadow-call.ts +38 -3
- package/src/lib/test-home-guard.ts +18 -3
- package/src/lib/upstream-retry.ts +43 -6
- package/src/lib/windows-secret-acl.ts +66 -0
- package/src/lib/windows-text.ts +28 -2
- package/src/lib/windows-user-principal.ts +35 -23
- package/src/oauth/account-quota-rank.ts +107 -0
- package/src/oauth/anthropic-routing.ts +125 -30
- package/src/oauth/chatgpt.ts +5 -1
- package/src/oauth/generic-account-failover.ts +114 -7
- package/src/oauth/index.ts +15 -8
- package/src/oauth/store.ts +16 -0
- package/src/providers/account-quota-disk.ts +79 -0
- package/src/providers/command-code-efforts.ts +24 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +33 -1
- package/src/providers/kiro-usage.ts +272 -0
- package/src/providers/ollama-show.ts +311 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota-routing-cache.ts +32 -0
- package/src/providers/quota-types.ts +36 -0
- package/src/providers/quota-wire.ts +102 -0
- package/src/providers/quota.ts +208 -147
- package/src/providers/registry.ts +68 -8
- package/src/providers/slug-codec.ts +12 -4
- package/src/providers/vercel-gateway-routing.ts +108 -0
- package/src/router.ts +22 -12
- package/src/server/auth-cors.ts +26 -0
- package/src/server/catalog-download.ts +73 -0
- package/src/server/chat-native.ts +12 -2
- package/src/server/gui-static.ts +4 -1
- package/src/server/index.ts +132 -9
- package/src/server/management/agent-settings-routes.ts +38 -5
- package/src/server/management/codex-prompt-routes.ts +7 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +9 -1
- package/src/server/management/context.ts +5 -0
- package/src/server/management/model-routes.ts +16 -6
- package/src/server/management/native-integration-routes.ts +12 -17
- package/src/server/management/oauth-account-routes.ts +13 -0
- package/src/server/management/provider-routes.ts +32 -5
- package/src/server/management/routing-profile-routes.ts +15 -0
- package/src/server/management/shadow-call-validation.ts +29 -0
- package/src/server/management-api.ts +7 -3
- package/src/server/request-log.ts +3 -5
- package/src/server/responses/agent-task-recovery-cache.ts +8 -0
- package/src/server/responses/agent-task-recovery.ts +52 -20
- package/src/server/responses/codex-auth-error.ts +26 -0
- package/src/server/responses/compact.ts +345 -10
- package/src/server/responses/core.ts +736 -108
- package/src/server/responses/empty-completion-guard.ts +16 -0
- package/src/server/responses/fetch-helpers.ts +42 -0
- package/src/server/responses/policy-fallback.ts +11 -6
- package/src/server/responses-undeclared-tool-guard.ts +16 -3
- package/src/server/startup-health-cache.ts +59 -13
- package/src/service-manager-probe.ts +115 -9
- package/src/service.ts +139 -40
- package/src/storage/cleanup.ts +10 -0
- package/src/storage/storage-mutation-coordinator.ts +14 -3
- package/src/tray/windows-tray.ps1 +10 -4
- package/src/tray/windows.ts +30 -2
- package/src/types/config.ts +27 -14
- package/src/types/provider.ts +54 -0
- package/src/types/tools.ts +13 -3
- package/src/types.ts +4 -0
- package/src/usage/summary.ts +421 -177
- package/src/vision/anthropic-describe.ts +3 -3
- package/src/vision/describe.ts +5 -3
- package/src/web-search/anthropic-executor.ts +9 -2
- package/src/web-search/exa-executor.ts +3 -3
- package/src/web-search/executor.ts +8 -3
- package/src/web-search/gemini-executor.ts +3 -3
- package/src/web-search/loop.ts +11 -3
- package/src/web-search/xai-executor.ts +3 -3
- package/gui/dist/assets/index-DNdRKXK9.js +0 -112
- package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
|
@@ -77,6 +77,7 @@ import { estimateComboCost, estimateRequestCost, normalizeCostTokens, tokensPerS
|
|
|
77
77
|
import type { PersistedUsageAttempt } from "../../usage/log";
|
|
78
78
|
import { isAllowedRequestOrigin, jsonResponse, providerManagementConfigError, publicProviderBaseUrl, safeConfigDTO } from "../auth-cors";
|
|
79
79
|
import { providerServiceTierConfigError } from "./provider-capability-config";
|
|
80
|
+
import { providerEmptyToolOutputConfigError } from "../../config/provider-validation";
|
|
80
81
|
import { applySystemEnvToggle } from "../system-env";
|
|
81
82
|
import {
|
|
82
83
|
LOCAL_PROVIDER_RELOAD_NAME_HEADER,
|
|
@@ -187,6 +188,17 @@ function applyProviderPatchFields(
|
|
|
187
188
|
next.liveModels = rawBody.liveModels;
|
|
188
189
|
touched = true;
|
|
189
190
|
}
|
|
191
|
+
if (Object.hasOwn(rawBody, "annotateEmptyToolOutputs")) {
|
|
192
|
+
const value = rawBody.annotateEmptyToolOutputs;
|
|
193
|
+
if (value === null) {
|
|
194
|
+
delete next.annotateEmptyToolOutputs;
|
|
195
|
+
} else if (typeof value === "boolean") {
|
|
196
|
+
next.annotateEmptyToolOutputs = value;
|
|
197
|
+
} else {
|
|
198
|
+
return { error: "annotateEmptyToolOutputs must be a boolean or null" };
|
|
199
|
+
}
|
|
200
|
+
touched = true;
|
|
201
|
+
}
|
|
190
202
|
if (Object.hasOwn(rawBody, "xaiResponsesOptIn")) {
|
|
191
203
|
if (name !== "xai") return { error: "xaiResponsesOptIn is valid only for provider xai" };
|
|
192
204
|
if (typeof rawBody.xaiResponsesOptIn !== "boolean") {
|
|
@@ -412,7 +424,8 @@ function canonicalOpenAiBudgetPatchError(
|
|
|
412
424
|
}
|
|
413
425
|
const applied = applyProviderPatchFields("openai", seed, rawBody, keys, config);
|
|
414
426
|
if ("error" in applied) return applied.error;
|
|
415
|
-
return providerManagementConfigError("openai", applied.next)
|
|
427
|
+
return providerManagementConfigError("openai", applied.next)
|
|
428
|
+
?? providerEmptyToolOutputConfigError("openai", applied.next);
|
|
416
429
|
}
|
|
417
430
|
|
|
418
431
|
export async function handleProviderRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
@@ -484,7 +497,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
484
497
|
return jsonResponse({ error: "provider reload target unavailable" }, 404);
|
|
485
498
|
}
|
|
486
499
|
const provider = diskConfig.providers[name]!;
|
|
487
|
-
const providerError = providerManagementConfigError(name, provider)
|
|
500
|
+
const providerError = providerManagementConfigError(name, provider)
|
|
501
|
+
?? providerEmptyToolOutputConfigError(name, provider);
|
|
488
502
|
if (providerError) return jsonResponse({ error: "provider reload target invalid" }, 409);
|
|
489
503
|
const namespaceCollision = codexAccountNamespaceProviderCollisionError(
|
|
490
504
|
diskConfig.codexAccountNamespaces,
|
|
@@ -543,7 +557,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
543
557
|
let body: { name?: unknown; provider?: unknown; setDefault?: boolean };
|
|
544
558
|
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
545
559
|
const name = typeof body.name === "string" ? body.name.trim() : "";
|
|
546
|
-
const providerError = providerManagementConfigError(name, body.provider)
|
|
560
|
+
const providerError = providerManagementConfigError(name, body.provider)
|
|
561
|
+
?? providerEmptyToolOutputConfigError(name, body.provider);
|
|
547
562
|
if (providerError) return jsonResponse({ error: providerError }, 400);
|
|
548
563
|
const serviceTierError = providerServiceTierConfigError(name, body.provider);
|
|
549
564
|
if (serviceTierError) return jsonResponse({ error: serviceTierError }, 400);
|
|
@@ -585,6 +600,11 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
585
600
|
const submittedModelContextWindows = Object.hasOwn(prov, "modelContextWindows");
|
|
586
601
|
const submittedModelAutoCompactTokenLimits = Object.hasOwn(prov, "modelAutoCompactTokenLimits");
|
|
587
602
|
const submittedRequestPacing = Object.hasOwn(prov, "requestPacing");
|
|
603
|
+
// Same trap, one more field: DeepSeek carries a registry default of `true` for
|
|
604
|
+
// annotateEmptyToolOutputs, so enrichment cannot distinguish "the client omitted it"
|
|
605
|
+
// from "the registry supplied it" either. Without this sample, an unrelated edit that
|
|
606
|
+
// omits the key resurrects the registry default over an operator's explicit `false`.
|
|
607
|
+
const submittedAnnotateEmptyToolOutputs = Object.hasOwn(prov, "annotateEmptyToolOutputs");
|
|
588
608
|
enrichProviderFromCatalog(name, prov);
|
|
589
609
|
const { saveConfigPreservingClaudeCode: save } = await import("../../config");
|
|
590
610
|
// Overwriting an existing provider must not drop its multi-key pool: carry it over, then
|
|
@@ -613,6 +633,11 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
613
633
|
if (!submittedContextWindow && existing?.contextWindow !== undefined) {
|
|
614
634
|
prov.contextWindow = existing.contextWindow;
|
|
615
635
|
}
|
|
636
|
+
// `!== undefined` rather than a truthiness test: the whole point of this field is that
|
|
637
|
+
// an explicit `false` must survive, and `false` is falsy.
|
|
638
|
+
if (!submittedAnnotateEmptyToolOutputs && existing?.annotateEmptyToolOutputs !== undefined) {
|
|
639
|
+
prov.annotateEmptyToolOutputs = existing.annotateEmptyToolOutputs;
|
|
640
|
+
}
|
|
616
641
|
if (existing?.modelContextWindows) {
|
|
617
642
|
// When the client did send a map, its keys win and the user's other keys survive. When
|
|
618
643
|
// it did not, the stored value is the user's map alone: merging the registry seed in
|
|
@@ -717,7 +742,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
717
742
|
if (applied.editorTouched && !pacingOnly) {
|
|
718
743
|
const providerError = canonicalBudgetOnly
|
|
719
744
|
? canonicalOpenAiBudgetPatchError(next, rawBody, keys, config)
|
|
720
|
-
: providerManagementConfigError(name, next)
|
|
745
|
+
: providerManagementConfigError(name, next)
|
|
746
|
+
?? providerEmptyToolOutputConfigError(name, next);
|
|
721
747
|
if (providerError) return jsonResponse({ error: providerError }, 400);
|
|
722
748
|
if (!canonicalBudgetOnly) {
|
|
723
749
|
const serviceTierError = providerServiceTierConfigError(name, next);
|
|
@@ -750,7 +776,8 @@ export async function handleProviderRoutes(ctx: ManagementContext): Promise<Resp
|
|
|
750
776
|
if (replay.editorTouched && !pacingOnly) {
|
|
751
777
|
const syncError = canonicalBudgetOnly
|
|
752
778
|
? canonicalOpenAiBudgetPatchError(replay.next, rawBody, keys, config)
|
|
753
|
-
: providerManagementConfigError(name, replay.next)
|
|
779
|
+
: providerManagementConfigError(name, replay.next)
|
|
780
|
+
?? providerEmptyToolOutputConfigError(name, replay.next);
|
|
754
781
|
if (syncError) {
|
|
755
782
|
replayError = syncError;
|
|
756
783
|
return;
|
|
@@ -27,6 +27,7 @@ import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
|
27
27
|
import { jsonResponse } from "../auth-cors";
|
|
28
28
|
import type { ManagementContext } from "./context";
|
|
29
29
|
import type { OcxConfig, OcxRoutingProfileConfig } from "../../types";
|
|
30
|
+
import { shadowCallTargetError } from "./shadow-call-validation";
|
|
30
31
|
|
|
31
32
|
function profileDto(config: Parameters<typeof getRoutingProfile>[0], id: string): Record<string, unknown> | null {
|
|
32
33
|
const profile = getRoutingProfile(config, id);
|
|
@@ -276,10 +277,12 @@ export async function handleRoutingProfileRoutes(ctx: ManagementContext): Promis
|
|
|
276
277
|
}
|
|
277
278
|
|
|
278
279
|
const previousProfile = mode === "update" ? getRoutingProfile(config, id) : undefined;
|
|
280
|
+
let aliasMigration: { oldPublicModel: string; newPublicModel: string } | undefined;
|
|
279
281
|
if (mode === "update" && previousProfile) {
|
|
280
282
|
const oldPublicModel = policyPublicModelId(id, previousProfile);
|
|
281
283
|
const newProfile = normalizeRoutingProfile(id, body.profile as OcxRoutingProfileConfig);
|
|
282
284
|
const newPublicModel = policyPublicModelId(id, newProfile);
|
|
285
|
+
aliasMigration = { oldPublicModel, newPublicModel };
|
|
283
286
|
const collision = modelMapMigrationCollision(config, oldPublicModel, newPublicModel);
|
|
284
287
|
if (collision) {
|
|
285
288
|
return jsonResponse({
|
|
@@ -289,6 +292,18 @@ export async function handleRoutingProfileRoutes(ctx: ManagementContext): Promis
|
|
|
289
292
|
}
|
|
290
293
|
const nextProfiles = { ...(config.routingProfiles ?? {}) };
|
|
291
294
|
nextProfiles[id] = storedProfile(id, body.profile as OcxRoutingProfileConfig);
|
|
295
|
+
const currentShadowTarget = config.shadowCallIntercept?.model;
|
|
296
|
+
if (aliasMigration && currentShadowTarget === aliasMigration.oldPublicModel) {
|
|
297
|
+
const targetError = shadowCallTargetError(
|
|
298
|
+
{ ...config, routingProfiles: nextProfiles },
|
|
299
|
+
aliasMigration.newPublicModel,
|
|
300
|
+
);
|
|
301
|
+
if (targetError) {
|
|
302
|
+
return jsonResponse({
|
|
303
|
+
error: { code: "invalid_shadow_call_target", message: targetError },
|
|
304
|
+
}, 400, req, config);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
292
307
|
config.routingProfiles = nextProfiles;
|
|
293
308
|
// Creating the first profile on a process started profile-less must install the
|
|
294
309
|
// compatibility provider now; activation is synchronous and idempotent per configDir.
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { shadowCallTargetsIntersect, shadowSourceModels } from "../../lib/shadow-call";
|
|
2
|
+
import { OPENAI_CODEX_PROVIDER_ID } from "../../providers/openai-tiers";
|
|
3
|
+
import { routeConcreteModel, routeModel } from "../../router";
|
|
4
|
+
import type { OcxConfig } from "../../types";
|
|
5
|
+
|
|
6
|
+
/** Validate a prospective persisted shadow-call target against its resolved source identities. */
|
|
7
|
+
export function shadowCallTargetError(config: OcxConfig, targetModel: string | undefined): string | null {
|
|
8
|
+
if (!targetModel) return null;
|
|
9
|
+
|
|
10
|
+
let target;
|
|
11
|
+
try {
|
|
12
|
+
target = routeModel(config, targetModel);
|
|
13
|
+
} catch {
|
|
14
|
+
return "model must resolve to a configured provider";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const intersectsSource = shadowSourceModels(config.shadowCallIntercept?.sourceModels).some(sourceModel => {
|
|
18
|
+
let source = { providerName: OPENAI_CODEX_PROVIDER_ID, modelId: sourceModel };
|
|
19
|
+
try {
|
|
20
|
+
const resolved = routeConcreteModel(config, sourceModel);
|
|
21
|
+
source = { providerName: resolved.providerName, modelId: sourceModel };
|
|
22
|
+
} catch { /* Unconfigured native Codex source models remain OpenAI-owned. */ }
|
|
23
|
+
return shadowCallTargetsIntersect(source, target);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
return intersectsSource
|
|
27
|
+
? "shadow-call target must not intersect a source model"
|
|
28
|
+
: null;
|
|
29
|
+
}
|
|
@@ -197,7 +197,7 @@ export async function handleManagementAPI(
|
|
|
197
197
|
try {
|
|
198
198
|
const { injectClaudeAgentDefs } = await import("../claude/agents-inject");
|
|
199
199
|
if (config.claudeCode?.enabled === false || config.claudeCode?.injectAgents === false) {
|
|
200
|
-
injectClaudeAgentDefs(config, {});
|
|
200
|
+
injectClaudeAgentDefs(config, {}, deps.claudeAgentConfigDir);
|
|
201
201
|
return;
|
|
202
202
|
}
|
|
203
203
|
try {
|
|
@@ -206,11 +206,15 @@ export async function handleManagementAPI(
|
|
|
206
206
|
import("../claude/context-windows"),
|
|
207
207
|
import("../codex/catalog"),
|
|
208
208
|
]);
|
|
209
|
-
injectClaudeAgentDefs(
|
|
209
|
+
injectClaudeAgentDefs(
|
|
210
|
+
config,
|
|
211
|
+
buildClaudeContextWindows([...visibleNativeSlugs(config)], models, nativeContextLimits(config)),
|
|
212
|
+
deps.claudeAgentConfigDir,
|
|
213
|
+
);
|
|
210
214
|
} catch {
|
|
211
215
|
// Keep routes available through a provider-discovery blip. A later
|
|
212
216
|
// launch-time sync restores any context markers missing from this pass.
|
|
213
|
-
injectClaudeAgentDefs(config, {});
|
|
217
|
+
injectClaudeAgentDefs(config, {}, deps.claudeAgentConfigDir);
|
|
214
218
|
}
|
|
215
219
|
} catch { /* best-effort */ }
|
|
216
220
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
2
3
|
import type { ResponsesTerminalStatus } from "../bridge";
|
|
3
4
|
import {
|
|
4
5
|
classifyError,
|
|
@@ -201,7 +202,6 @@ const requestLog: RequestLogEntry[] = [];
|
|
|
201
202
|
const MAX_LOG_SIZE = 2000;
|
|
202
203
|
const requestLogEntryBytes = new WeakMap<RequestLogEntry, number>();
|
|
203
204
|
let requestLogBytes = 0;
|
|
204
|
-
let requestLogSeq = 0;
|
|
205
205
|
/** True after hydrateRequestLogsFromDisk ran once in this process. */
|
|
206
206
|
let requestLogsHydratedFromDisk = false;
|
|
207
207
|
|
|
@@ -430,9 +430,8 @@ export function addRequestLog(entry: RequestLogEntry) {
|
|
|
430
430
|
}
|
|
431
431
|
}
|
|
432
432
|
|
|
433
|
-
export function nextRequestLogId(
|
|
434
|
-
|
|
435
|
-
return `ocx-${timestamp.toString(36)}-${requestLogSeq.toString(36)}`;
|
|
433
|
+
export function nextRequestLogId(_timestamp = Date.now()): string {
|
|
434
|
+
return `ocx-${randomBytes(16).toString("hex")}`;
|
|
436
435
|
}
|
|
437
436
|
|
|
438
437
|
/**
|
|
@@ -1308,6 +1307,5 @@ export function getRequestLogEntries(): RequestLogEntry[] { return requestLog; }
|
|
|
1308
1307
|
export function clearRequestLogsForTests(): void {
|
|
1309
1308
|
requestLog.length = 0;
|
|
1310
1309
|
requestLogBytes = 0;
|
|
1311
|
-
requestLogSeq = 0;
|
|
1312
1310
|
requestLogsHydratedFromDisk = false;
|
|
1313
1311
|
}
|
|
@@ -128,6 +128,10 @@ export async function resolveCachedAgentTaskRecovery(
|
|
|
128
128
|
return flight ? waitForRecoveryFlight(flight, abortSignal) : null;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
export function discardCachedAgentTaskRecovery(key: string): void {
|
|
132
|
+
deleteRecoveryCacheEntry(key);
|
|
133
|
+
}
|
|
134
|
+
|
|
131
135
|
export function resetAgentTaskRecoveryCache(): void {
|
|
132
136
|
for (const flight of RECOVERY_FLIGHTS.values()) {
|
|
133
137
|
flight.controller.abort(new DOMException("Recovery state reset", "AbortError"));
|
|
@@ -141,3 +145,7 @@ export function agentTaskRecoveryWaiterCountForTests(): number {
|
|
|
141
145
|
for (const flight of RECOVERY_FLIGHTS.values()) count += flight.waiters;
|
|
142
146
|
return count;
|
|
143
147
|
}
|
|
148
|
+
|
|
149
|
+
export function agentTaskRecoveryCacheSnapshotForTests(): { entries: number; bytes: number } {
|
|
150
|
+
return { entries: RECOVERY_CACHE.size, bytes: recoveryCacheBytes };
|
|
151
|
+
}
|
|
@@ -5,6 +5,7 @@ import { readBoundedResponseBody } from "../../lib/bounded-body";
|
|
|
5
5
|
import { isApiAuthRequired, isProxyAdmissionSecret } from "../auth-cors";
|
|
6
6
|
import { structurallyValidFernetTokens } from "./encrypted-payload";
|
|
7
7
|
import {
|
|
8
|
+
discardCachedAgentTaskRecovery,
|
|
8
9
|
resetAgentTaskRecoveryCache,
|
|
9
10
|
resolveCachedAgentTaskRecovery,
|
|
10
11
|
} from "./agent-task-recovery-cache";
|
|
@@ -266,6 +267,38 @@ function recoveryAdmission(req: Request, config: OcxConfig): RecoveryAdmission |
|
|
|
266
267
|
return { headers, cacheScope };
|
|
267
268
|
}
|
|
268
269
|
|
|
270
|
+
interface AdmittedRecovery {
|
|
271
|
+
envelope: AgentEnvelope;
|
|
272
|
+
admission: RecoveryAdmission;
|
|
273
|
+
cacheKey: string;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function admittedRecovery(
|
|
277
|
+
req: Request,
|
|
278
|
+
input: unknown,
|
|
279
|
+
config: OcxConfig,
|
|
280
|
+
parentThreadId?: string | null,
|
|
281
|
+
): AdmittedRecovery | null {
|
|
282
|
+
const envelope = findEnvelope(input);
|
|
283
|
+
if (!envelope) return null;
|
|
284
|
+
const admission = recoveryAdmission(req, config);
|
|
285
|
+
if (!admission) return null;
|
|
286
|
+
const cacheKey = createHash("sha256")
|
|
287
|
+
.update(admission.cacheScope)
|
|
288
|
+
.update("\0")
|
|
289
|
+
.update(parentThreadId ?? "")
|
|
290
|
+
.update("\0")
|
|
291
|
+
.update(envelope.messageType)
|
|
292
|
+
.update("\0")
|
|
293
|
+
.update(envelope.taskName)
|
|
294
|
+
.update("\0")
|
|
295
|
+
.update(envelope.sender)
|
|
296
|
+
.update("\0")
|
|
297
|
+
.update(envelope.ciphertext)
|
|
298
|
+
.digest("hex");
|
|
299
|
+
return { envelope, admission, cacheKey };
|
|
300
|
+
}
|
|
301
|
+
|
|
269
302
|
function recoveryPayload(envelope: AgentEnvelope, model: string): string {
|
|
270
303
|
return JSON.stringify({
|
|
271
304
|
model,
|
|
@@ -430,34 +463,33 @@ export async function recoverEncryptedAgentTask(
|
|
|
430
463
|
config: OcxConfig,
|
|
431
464
|
context: { parentThreadId?: string | null; abortSignal?: AbortSignal } = {},
|
|
432
465
|
): Promise<boolean> {
|
|
433
|
-
const envelope = findEnvelope(input);
|
|
434
|
-
if (!envelope) return false;
|
|
435
466
|
// Admission is deliberately checked before cache access. A cache hit must not
|
|
436
467
|
// turn this process into a plaintext oracle for an unauthenticated caller.
|
|
437
|
-
const
|
|
438
|
-
if (!
|
|
439
|
-
|
|
440
|
-
const cacheKey = createHash("sha256")
|
|
441
|
-
.update(admission.cacheScope)
|
|
442
|
-
.update("\0")
|
|
443
|
-
.update(context.parentThreadId ?? "")
|
|
444
|
-
.update("\0")
|
|
445
|
-
.update(envelope.messageType)
|
|
446
|
-
.update("\0")
|
|
447
|
-
.update(envelope.taskName)
|
|
448
|
-
.update("\0")
|
|
449
|
-
.update(envelope.sender)
|
|
450
|
-
.update("\0")
|
|
451
|
-
.update(envelope.ciphertext)
|
|
452
|
-
.digest("hex");
|
|
468
|
+
const admitted = admittedRecovery(req, input, config, context.parentThreadId);
|
|
469
|
+
if (!admitted) return false;
|
|
470
|
+
const { admission, cacheKey, envelope } = admitted;
|
|
453
471
|
const assignment = await resolveCachedAgentTaskRecovery(
|
|
454
472
|
cacheKey,
|
|
455
473
|
options.cacheEntries ?? 200,
|
|
456
474
|
signal => requestRecovery(admission, envelope, options, signal),
|
|
457
475
|
context.abortSignal,
|
|
458
476
|
);
|
|
459
|
-
if (!assignment
|
|
460
|
-
|
|
477
|
+
if (!assignment) return false;
|
|
478
|
+
if (context.abortSignal?.aborted || !injectAssignment(input, envelope, assignment)) {
|
|
479
|
+
discardCachedAgentTaskRecovery(cacheKey);
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
return true;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
export function discardEncryptedAgentTaskRecovery(
|
|
486
|
+
req: Request,
|
|
487
|
+
input: unknown,
|
|
488
|
+
config: OcxConfig,
|
|
489
|
+
context: { parentThreadId?: string | null } = {},
|
|
490
|
+
): void {
|
|
491
|
+
const admitted = admittedRecovery(req, input, config, context.parentThreadId);
|
|
492
|
+
if (admitted) discardCachedAgentTaskRecovery(admitted.cacheKey);
|
|
461
493
|
}
|
|
462
494
|
|
|
463
495
|
export function resetAgentTaskRecoveryState(): void {
|
|
@@ -10,12 +10,35 @@ import {
|
|
|
10
10
|
CodexPoolAuthenticationError,
|
|
11
11
|
CodexThreadAffinityExpiredError,
|
|
12
12
|
} from "../../codex/auth-context";
|
|
13
|
+
import {
|
|
14
|
+
MAIN_CODEX_ACCOUNT_ID,
|
|
15
|
+
MainAccountTokenRefreshError,
|
|
16
|
+
MainAuthJsonChangedDuringRefreshError,
|
|
17
|
+
} from "../../codex/main-account";
|
|
13
18
|
|
|
14
19
|
export interface CodexAuthContextErrorResponseOptions {
|
|
15
20
|
accountSelector?: string;
|
|
16
21
|
now: number;
|
|
17
22
|
}
|
|
18
23
|
|
|
24
|
+
export function nativeMainRefreshFailureResponse(error: unknown): Response {
|
|
25
|
+
if (error instanceof MainAccountTokenRefreshError && error.reason === "reauth") {
|
|
26
|
+
return formatErrorResponse(401, "authentication_error", "Codex main account needs reauthentication");
|
|
27
|
+
}
|
|
28
|
+
if (error instanceof MainAccountTokenRefreshError
|
|
29
|
+
|| error instanceof MainAuthJsonChangedDuringRefreshError) {
|
|
30
|
+
const response = formatErrorResponse(
|
|
31
|
+
503,
|
|
32
|
+
"server_busy",
|
|
33
|
+
"Codex main credential refresh did not complete; retry this request",
|
|
34
|
+
);
|
|
35
|
+
const headers = new Headers(response.headers);
|
|
36
|
+
headers.set("Retry-After", "1");
|
|
37
|
+
return new Response(response.body, { status: response.status, headers });
|
|
38
|
+
}
|
|
39
|
+
return formatErrorResponse(401, "authentication_error", "No usable Codex main credential to serve this request");
|
|
40
|
+
}
|
|
41
|
+
|
|
19
42
|
/** Shared HTTP contract for Codex auth-context failures on Responses surfaces. */
|
|
20
43
|
export function mapCodexAuthContextErrorToResponse(
|
|
21
44
|
error: unknown,
|
|
@@ -35,6 +58,9 @@ export function mapCodexAuthContextErrorToResponse(
|
|
|
35
58
|
);
|
|
36
59
|
}
|
|
37
60
|
if (error instanceof CodexAuthContextError) {
|
|
61
|
+
if (error.accountId === MAIN_CODEX_ACCOUNT_ID) {
|
|
62
|
+
return nativeMainRefreshFailureResponse(error.cause);
|
|
63
|
+
}
|
|
38
64
|
return formatErrorResponse(
|
|
39
65
|
401,
|
|
40
66
|
"authentication_error",
|