@yansigit/opencodex 2.31.3 → 2.32.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/bin/ocx.mjs +99 -70
- package/gui/dist/assets/index-BG43zwVe.js +102 -0
- package/gui/dist/assets/index-CiSI-jrP.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +6 -8
- package/src/adapters/base.ts +2 -0
- package/src/adapters/command-code.ts +2 -3
- package/src/adapters/cursor/live-models.ts +8 -0
- package/src/adapters/cursor/native-exec-desktop.ts +16 -0
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/google.ts +6 -7
- package/src/adapters/kiro.ts +0 -3
- package/src/adapters/openai-responses.ts +3 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-web-search.ts +7 -2
- package/src/bridge.ts +21 -15
- package/src/cli/dispatch.ts +50 -2
- package/src/cli/doctor.ts +24 -11
- package/src/cli/help.ts +4 -3
- package/src/cli/index.ts +11 -4
- package/src/cli/models.ts +13 -3
- package/src/cli/observe.ts +20 -5
- package/src/cli/provider.ts +2 -1
- package/src/cli/registry.ts +7 -5
- package/src/cli/status.ts +2 -1
- package/src/cli/system-restart-client.ts +1 -1
- package/src/cli/usage-report.ts +134 -0
- package/src/codex/app-server-processes.ts +3 -1
- package/src/codex/catalog/aggregation.ts +13 -1
- package/src/codex/catalog/effort.ts +24 -9
- package/src/codex/catalog/model-metadata.ts +566 -0
- package/src/codex/catalog/parsing.ts +33 -0
- package/src/codex/catalog/provider-fetch.ts +123 -32
- package/src/codex/catalog/sync.ts +5 -4
- package/src/codex/desktop-app-restart.ts +342 -0
- package/src/codex/history-job.ts +32 -3
- package/src/codex/history-manifest.ts +112 -0
- package/src/codex/history-migration-guardian.ts +5 -5
- package/src/codex/history-provider.ts +825 -247
- package/src/codex/history-worker.ts +8 -5
- package/src/codex/inject.ts +49 -21
- package/src/codex/injected-marker.ts +1 -1
- package/src/codex/internal/history-writer.ts +4 -3
- package/src/codex/native-profile-startup.ts +157 -27
- package/src/codex/native-residue.ts +26 -33
- package/src/combos/failover.ts +27 -0
- package/src/compatibility/index.ts +26 -0
- package/src/compatibility/manifest.ts +253 -0
- package/src/compatibility/openai-responses.ts +81 -0
- package/src/config/atomic-write.ts +219 -0
- package/src/config/paths.ts +40 -0
- package/src/config/process-state.ts +308 -0
- package/src/config/provider-validation.ts +177 -0
- package/src/config.ts +75 -812
- package/src/generated/compatibility-version.json +145 -85
- package/src/images/plan.ts +5 -4
- package/src/integrations/ownership-policy.ts +141 -0
- package/src/integrations/ownership.ts +10 -0
- package/src/integrations/state.ts +44 -5
- package/src/integrations/writer.ts +6 -0
- package/src/lib/bounded-body.ts +14 -2
- package/src/lib/process-control.ts +2 -1
- package/src/lib/state-store-registrations.ts +2 -0
- package/src/lib/tool-argument-integers.ts +56 -5
- package/src/oauth/health.ts +1 -1
- package/src/providers/registry.ts +1 -1
- package/src/reasoning-effort.ts +19 -2
- package/src/responses/apply-patch-envelope.ts +63 -0
- package/src/responses/custom-tool-compat.ts +132 -38
- package/src/responses/parser.ts +3 -2
- package/src/responses/reasoning-replay-cache.ts +81 -3
- package/src/server/auth-cors.ts +9 -7
- package/src/server/index.ts +102 -21
- package/src/server/local-management-read-client.ts +1 -1
- package/src/server/local-provider-reload-client.ts +1 -1
- package/src/server/management/agent-settings-routes.ts +1 -1
- package/src/server/management/config-routes.ts +4 -1
- package/src/server/management/context.ts +1 -1
- package/src/server/management/logs-usage-routes.ts +27 -6
- package/src/server/management/model-routes.ts +8 -4
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/provider-capability-config.ts +1 -1
- package/src/server/management/system-restart.ts +1 -1
- package/src/server/port-reclaim.ts +1 -1
- package/src/server/proxy-liveness.ts +2 -1
- package/src/server/request-log-conversation.ts +30 -0
- package/src/server/responses/codex-auth-error.ts +55 -0
- package/src/server/responses/combo-stream-preflight.ts +171 -0
- package/src/server/responses/compact.ts +6 -21
- package/src/server/responses/core.ts +225 -94
- package/src/server/responses/fetch-helpers.ts +2 -97
- package/src/server/responses-custom-tool-repair.ts +41 -5
- package/src/server/responses-undeclared-tool-guard.ts +156 -15
- package/src/service.ts +8 -4
- package/src/types/request.ts +6 -1
- package/src/types/tools.ts +87 -11
- package/src/types.ts +1 -1
- package/src/update/index.ts +5 -4
- package/src/update/job.ts +3 -1
- package/src/update/transactional-install.mjs +8 -1
- package/src/usage/log.ts +16 -8
- package/src/usage/summary.ts +201 -8
- package/src/vision/describe.ts +18 -13
- package/src/web-search/executor.ts +10 -3
- package/gui/dist/assets/index-CGoDO3uO.css +0 -1
- package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { namespacedToolName } from "../types";
|
|
2
|
+
import {
|
|
3
|
+
normalizeApplyPatchDelimiters,
|
|
4
|
+
repairFreeformToolInput,
|
|
5
|
+
unwrapFreeformToolInput,
|
|
6
|
+
} from "./apply-patch-envelope";
|
|
2
7
|
import { collectResponsesToolGroups } from "./tool-groups";
|
|
3
8
|
|
|
4
9
|
const ROUTED_CUSTOM_TOOL_PASSTHROUGH = new Set(["apply_patch"]);
|
|
@@ -15,19 +20,42 @@ function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
|
15
20
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
16
21
|
}
|
|
17
22
|
|
|
18
|
-
function customToolInput(argumentsText: unknown): string {
|
|
19
|
-
if (typeof argumentsText !== "string") return "";
|
|
20
|
-
try {
|
|
21
|
-
const parsed = JSON.parse(argumentsText) as unknown;
|
|
22
|
-
if (isPlainObject(parsed) && typeof parsed.input === "string") return parsed.input;
|
|
23
|
-
} catch { /* malformed arguments stay visible to the client */ }
|
|
24
|
-
return argumentsText;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
23
|
function customToolWireName(namespace: string | undefined, name: string): string {
|
|
28
24
|
return namespace === BUILTIN_FUNCTIONS_NAMESPACE ? name : namespacedToolName(namespace, name);
|
|
29
25
|
}
|
|
30
26
|
|
|
27
|
+
function toolChoiceAllowsRoutedCustomTool(
|
|
28
|
+
body: unknown,
|
|
29
|
+
wireName: string,
|
|
30
|
+
candidateNames: ReadonlySet<string>,
|
|
31
|
+
): boolean {
|
|
32
|
+
if (!isPlainObject(body)) return true;
|
|
33
|
+
const choice = body.tool_choice;
|
|
34
|
+
if (choice === undefined || choice === null || choice === "auto" || choice === "required") {
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
if (choice === "none") return false;
|
|
38
|
+
if (!isPlainObject(choice)) return true;
|
|
39
|
+
|
|
40
|
+
const selectorAllows = (selector: unknown): boolean => {
|
|
41
|
+
if (!isPlainObject(selector) || typeof selector.name !== "string") return false;
|
|
42
|
+
if (selector.type !== "custom") return false;
|
|
43
|
+
if (typeof selector.namespace === "string") {
|
|
44
|
+
return customToolWireName(selector.namespace, selector.name) === wireName;
|
|
45
|
+
}
|
|
46
|
+
if (selector.name === wireName) return true;
|
|
47
|
+
const suffix = `__${selector.name}`;
|
|
48
|
+
const candidates = [...candidateNames].filter(name => name.endsWith(suffix));
|
|
49
|
+
return candidates.length === 1 && candidates[0] === wireName;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if (choice.type === "function" || choice.type === "custom") return selectorAllows(choice);
|
|
53
|
+
if (choice.type === "allowed_tools" && Array.isArray(choice.tools)) {
|
|
54
|
+
return choice.tools.some(selectorAllows);
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
31
59
|
/** Final upstream identity of a call, including a namespace restored by an earlier rewrite. */
|
|
32
60
|
export function routedCustomToolWireName(value: unknown): string | undefined {
|
|
33
61
|
if (!isPlainObject(value) || typeof value.name !== "string") return undefined;
|
|
@@ -38,12 +66,13 @@ export function routedCustomToolWireName(value: unknown): string | undefined {
|
|
|
38
66
|
}
|
|
39
67
|
|
|
40
68
|
/**
|
|
41
|
-
* Names of
|
|
42
|
-
*
|
|
69
|
+
* Names of custom declarations after namespace lowering. The selection flag separates converted
|
|
70
|
+
* names from native passthrough names while keeping same-named function and custom children distinct.
|
|
43
71
|
*/
|
|
44
72
|
function collectRoutedCustomToolWireNames(
|
|
45
73
|
body: unknown,
|
|
46
74
|
supportsResponsesCustomTools?: boolean,
|
|
75
|
+
passthrough = false,
|
|
47
76
|
): Set<string> {
|
|
48
77
|
const names = new Set<string>();
|
|
49
78
|
const groups = collectResponsesToolGroups(body);
|
|
@@ -64,7 +93,7 @@ function collectRoutedCustomToolWireNames(
|
|
|
64
93
|
if (
|
|
65
94
|
tool.type === "custom"
|
|
66
95
|
&& typeof tool.name === "string"
|
|
67
|
-
&&
|
|
96
|
+
&& routedCustomToolPassesThrough(tool.name, supportsResponsesCustomTools) === passthrough
|
|
68
97
|
) {
|
|
69
98
|
names.add(tool.name);
|
|
70
99
|
continue;
|
|
@@ -77,7 +106,8 @@ function collectRoutedCustomToolWireNames(
|
|
|
77
106
|
isPlainObject(child)
|
|
78
107
|
&& child.type === "custom"
|
|
79
108
|
&& typeof child.name === "string"
|
|
80
|
-
&&
|
|
109
|
+
&& routedCustomToolPassesThrough(child.name, supportsResponsesCustomTools) === passthrough
|
|
110
|
+
&& (!passthrough || tool.name === BUILTIN_FUNCTIONS_NAMESPACE)
|
|
81
111
|
&& !(tool.name === BUILTIN_FUNCTIONS_NAMESPACE && bareWireNames.has(child.name))
|
|
82
112
|
) names.add(customToolWireName(tool.name, child.name));
|
|
83
113
|
}
|
|
@@ -85,7 +115,6 @@ function collectRoutedCustomToolWireNames(
|
|
|
85
115
|
}
|
|
86
116
|
return names;
|
|
87
117
|
}
|
|
88
|
-
|
|
89
118
|
export function customToolItemId(id: unknown): unknown {
|
|
90
119
|
if (typeof id !== "string") return id;
|
|
91
120
|
return id.startsWith("fc_") ? `ctc_${id.slice(3)}` : id;
|
|
@@ -203,64 +232,129 @@ export function rewriteRoutedCustomToolsForUpstream(
|
|
|
203
232
|
): {
|
|
204
233
|
body: unknown;
|
|
205
234
|
names: Set<string>;
|
|
235
|
+
repairNames: Set<string>;
|
|
206
236
|
} {
|
|
207
237
|
const conversionNames = collectRoutedCustomToolNames(body, supportsResponsesCustomTools);
|
|
208
238
|
const names = collectRoutedCustomToolWireNames(body, supportsResponsesCustomTools);
|
|
209
|
-
|
|
239
|
+
const repairNames = collectRoutedCustomToolWireNames(body, supportsResponsesCustomTools, true);
|
|
240
|
+
for (const name of repairNames) {
|
|
241
|
+
if (!toolChoiceAllowsRoutedCustomTool(body, name, repairNames)) repairNames.delete(name);
|
|
242
|
+
}
|
|
243
|
+
if (conversionNames.size === 0) return { body, names, repairNames };
|
|
210
244
|
const callIds = new Set<string>();
|
|
211
245
|
collectConvertedCallIds(body, conversionNames, callIds);
|
|
212
|
-
return { body: rewriteForUpstream(body, conversionNames, callIds), names };
|
|
246
|
+
return { body: rewriteForUpstream(body, conversionNames, callIds), names, repairNames };
|
|
213
247
|
}
|
|
214
248
|
|
|
215
249
|
export function restoreRoutedCustomCalls(
|
|
216
250
|
value: unknown,
|
|
217
251
|
names: ReadonlySet<string>,
|
|
252
|
+
repairNames: ReadonlySet<string> = new Set(),
|
|
218
253
|
): { value: unknown; changed: boolean } {
|
|
219
|
-
if (
|
|
254
|
+
if (!isPlainObject(value)) return { value, changed: false };
|
|
255
|
+
|
|
256
|
+
const restoreItem = (item: unknown): { value: unknown; changed: boolean } => {
|
|
257
|
+
if (!isPlainObject(item)) return { value: item, changed: false };
|
|
258
|
+
const wireName = routedCustomToolWireName(item);
|
|
259
|
+
if (
|
|
260
|
+
item.type === "function_call"
|
|
261
|
+
&& typeof item.name === "string"
|
|
262
|
+
&& wireName !== undefined
|
|
263
|
+
&& names.has(wireName)
|
|
264
|
+
) {
|
|
265
|
+
const restored: Record<string, unknown> = {
|
|
266
|
+
...item,
|
|
267
|
+
type: "custom_tool_call",
|
|
268
|
+
id: customToolItemId(item.id),
|
|
269
|
+
input: repairFreeformToolInput(
|
|
270
|
+
item.arguments,
|
|
271
|
+
item.name,
|
|
272
|
+
typeof item.namespace === "string" ? item.namespace : undefined,
|
|
273
|
+
),
|
|
274
|
+
};
|
|
275
|
+
delete restored.arguments;
|
|
276
|
+
return { value: restored, changed: true };
|
|
277
|
+
}
|
|
278
|
+
if (
|
|
279
|
+
item.type === "custom_tool_call"
|
|
280
|
+
&& typeof item.name === "string"
|
|
281
|
+
&& wireName !== undefined
|
|
282
|
+
&& repairNames.has(wireName)
|
|
283
|
+
&& typeof item.input === "string"
|
|
284
|
+
) {
|
|
285
|
+
const input = normalizeApplyPatchDelimiters(item.input);
|
|
286
|
+
if (input !== item.input) return { value: { ...item, input }, changed: true };
|
|
287
|
+
}
|
|
288
|
+
return { value: item, changed: false };
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const restoreOutput = (output: unknown): { value: unknown; changed: boolean } => {
|
|
292
|
+
if (!Array.isArray(output)) return { value: output, changed: false };
|
|
220
293
|
let changed = false;
|
|
221
|
-
const restored =
|
|
222
|
-
const result =
|
|
294
|
+
const restored = output.map(item => {
|
|
295
|
+
const result = restoreItem(item);
|
|
223
296
|
changed ||= result.changed;
|
|
224
297
|
return result.value;
|
|
225
298
|
});
|
|
226
|
-
return changed ? { value: restored, changed: true } : { value, changed: false };
|
|
227
|
-
}
|
|
228
|
-
if (!isPlainObject(value)) return { value, changed: false };
|
|
299
|
+
return changed ? { value: restored, changed: true } : { value: output, changed: false };
|
|
300
|
+
};
|
|
229
301
|
|
|
230
302
|
let changed = false;
|
|
231
|
-
const restored: Record<string, unknown> = {};
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
restored
|
|
235
|
-
changed
|
|
303
|
+
const restored: Record<string, unknown> = { ...value };
|
|
304
|
+
const output = restoreOutput(value.output);
|
|
305
|
+
if (output.changed) {
|
|
306
|
+
restored.output = output.value;
|
|
307
|
+
changed = true;
|
|
236
308
|
}
|
|
237
309
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
310
|
+
if (
|
|
311
|
+
(value.type === "response.output_item.added" || value.type === "response.output_item.done")
|
|
312
|
+
&& isPlainObject(value.item)
|
|
313
|
+
) {
|
|
314
|
+
const item = restoreItem(value.item);
|
|
315
|
+
if (item.changed) {
|
|
316
|
+
restored.item = item.value;
|
|
317
|
+
changed = true;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (
|
|
322
|
+
typeof value.type === "string"
|
|
323
|
+
&& value.type.startsWith("response.")
|
|
324
|
+
&& isPlainObject(value.response)
|
|
325
|
+
) {
|
|
326
|
+
const response = restoreRoutedCustomCalls(value.response, names, repairNames);
|
|
327
|
+
if (response.changed) {
|
|
328
|
+
restored.response = response.value;
|
|
329
|
+
changed = true;
|
|
330
|
+
}
|
|
245
331
|
}
|
|
332
|
+
|
|
246
333
|
return changed ? { value: restored, changed: true } : { value, changed: false };
|
|
247
334
|
}
|
|
248
335
|
|
|
249
336
|
export function restoreRoutedCustomCallsInJson(
|
|
250
337
|
text: string,
|
|
251
338
|
names: ReadonlySet<string>,
|
|
339
|
+
repairNames: ReadonlySet<string> = new Set(),
|
|
252
340
|
): string {
|
|
253
|
-
if (names.size === 0) return text;
|
|
341
|
+
if (names.size === 0 && repairNames.size === 0) return text;
|
|
254
342
|
let payload: unknown;
|
|
255
343
|
try {
|
|
256
344
|
payload = JSON.parse(text);
|
|
257
345
|
} catch {
|
|
258
346
|
return text;
|
|
259
347
|
}
|
|
260
|
-
const restored = restoreRoutedCustomCalls(payload, names);
|
|
348
|
+
const restored = restoreRoutedCustomCalls(payload, names, repairNames);
|
|
261
349
|
return restored.changed ? JSON.stringify(restored.value) : text;
|
|
262
350
|
}
|
|
263
351
|
|
|
264
|
-
export function unwrapRoutedCustomToolArguments(
|
|
265
|
-
|
|
352
|
+
export function unwrapRoutedCustomToolArguments(
|
|
353
|
+
argumentsText: unknown,
|
|
354
|
+
toolName = "",
|
|
355
|
+
namespace?: string,
|
|
356
|
+
): string {
|
|
357
|
+
return toolName
|
|
358
|
+
? repairFreeformToolInput(argumentsText, toolName, namespace)
|
|
359
|
+
: unwrapFreeformToolInput(argumentsText);
|
|
266
360
|
}
|
package/src/responses/parser.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
OcxToolCall,
|
|
12
12
|
OcxReasoningReplayScopeRef,
|
|
13
13
|
} from "../types";
|
|
14
|
-
import {
|
|
14
|
+
import { createToolChoiceResolver, namespacedToolName } from "../types";
|
|
15
15
|
import { responsesRequestSchema } from "./schema";
|
|
16
16
|
import { providerMetadataFromResponsesFunctionCall } from "./provider-opaque-metadata";
|
|
17
17
|
import { lookupReplayThoughtSignature } from "./thought-signature-replay";
|
|
@@ -758,8 +758,9 @@ export function parseRequest(
|
|
|
758
758
|
const tc = mapToolChoice(data.tool_choice);
|
|
759
759
|
if (tc && typeof tc === "object") {
|
|
760
760
|
const selectors = "allowedTools" in tc ? tc.allowedTools : [tc.name];
|
|
761
|
+
const resolver = createToolChoiceResolver(mergedTools);
|
|
761
762
|
for (const selector of selectors) {
|
|
762
|
-
if (
|
|
763
|
+
if (resolver.candidateCount(selector) > 1) {
|
|
763
764
|
throw new Error(`ambiguous tool_choice name: ${selector}`);
|
|
764
765
|
}
|
|
765
766
|
}
|
|
@@ -29,6 +29,7 @@ import type {
|
|
|
29
29
|
const MAX_ENTRIES = 64;
|
|
30
30
|
const MAX_TOTAL_BYTES = 256 * 1024;
|
|
31
31
|
const TTL_MS = 60 * 60 * 1000;
|
|
32
|
+
const OPAQUE_BLOB_REJECTION_TTL_MS = 5 * 60 * 1000;
|
|
32
33
|
const replayIdentityKey = randomBytes(32);
|
|
33
34
|
const CREDENTIAL_HEADER_NAMES = new Set([
|
|
34
35
|
"authorization",
|
|
@@ -58,10 +59,17 @@ interface ServingIdentityEntry {
|
|
|
58
59
|
at: number;
|
|
59
60
|
}
|
|
60
61
|
|
|
62
|
+
interface OpaqueBlobRejectionEntry {
|
|
63
|
+
bytes: number;
|
|
64
|
+
at: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
61
67
|
const entries = new Map<string, CacheEntry>();
|
|
62
68
|
const servingIdentities = new Map<string, ServingIdentityEntry>();
|
|
69
|
+
const opaqueBlobRejections = new Map<string, OpaqueBlobRejectionEntry>();
|
|
63
70
|
let totalBytes = 0;
|
|
64
71
|
let servingIdentityTotalBytes = 0;
|
|
72
|
+
let opaqueBlobRejectionTotalBytes = 0;
|
|
65
73
|
let clockForTests: (() => number) | null = null;
|
|
66
74
|
|
|
67
75
|
const now = (): number => clockForTests?.() ?? Date.now();
|
|
@@ -142,11 +150,31 @@ function servingIdentityFor(
|
|
|
142
150
|
return { threadId, identity: JSON.stringify(identityTuple) };
|
|
143
151
|
}
|
|
144
152
|
|
|
153
|
+
function opaqueBlobRejectionKeyFor(
|
|
154
|
+
scope: OcxReasoningReplayScopeRef | undefined,
|
|
155
|
+
): string | undefined {
|
|
156
|
+
const current = servingIdentityFor(scope);
|
|
157
|
+
return current ? JSON.stringify([current.threadId, current.identity]) : undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function deleteOpaqueBlobRejection(key: string): void {
|
|
161
|
+
const entry = opaqueBlobRejections.get(key);
|
|
162
|
+
if (!entry) return;
|
|
163
|
+
opaqueBlobRejections.delete(key);
|
|
164
|
+
opaqueBlobRejectionTotalBytes -= entry.bytes;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function sweepExpiredOpaqueBlobRejections(at: number): void {
|
|
168
|
+
for (const [key, entry] of opaqueBlobRejections) {
|
|
169
|
+
if (at - entry.at >= OPAQUE_BLOB_REJECTION_TTL_MS) deleteOpaqueBlobRejection(key);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
145
173
|
/**
|
|
146
|
-
* Compare this request's route with the last successfully serving route for its
|
|
174
|
+
* Compare this request's route with the last successfully serving route for its conversation.
|
|
147
175
|
* A live mismatch means replayed opaque reasoning was minted by another backend and must not be
|
|
148
176
|
* forwarded to this one. Comparison deliberately does not refresh or replace the recorded route:
|
|
149
|
-
* a failed candidate request did not serve the
|
|
177
|
+
* a failed candidate request did not serve the conversation.
|
|
150
178
|
*
|
|
151
179
|
* Serving provenance uses restart-stable destination and credential dimensions so token
|
|
152
180
|
* generations and other volatile credential material cannot create false route changes. Missing
|
|
@@ -164,7 +192,7 @@ export function reasoningReplayServingIdentityChanged(
|
|
|
164
192
|
return previous !== undefined && previous.identity !== current.identity;
|
|
165
193
|
}
|
|
166
194
|
|
|
167
|
-
/** Record the route only after it has successfully served the
|
|
195
|
+
/** Record the route only after it has successfully served the conversation. */
|
|
168
196
|
export function commitReasoningReplayServingIdentity(
|
|
169
197
|
scope: OcxReasoningReplayScopeRef | undefined,
|
|
170
198
|
): void {
|
|
@@ -200,6 +228,54 @@ export function commitReasoningReplayServingIdentity(
|
|
|
200
228
|
}
|
|
201
229
|
}
|
|
202
230
|
|
|
231
|
+
/**
|
|
232
|
+
* Whether this exact conversation and durable serving identity previously rejected opaque replay.
|
|
233
|
+
*
|
|
234
|
+
* The five serving dimensions deliberately match the serving record. Missing durable destination
|
|
235
|
+
* or credential identity is unknown and never falls back to process-local dimensions. The five
|
|
236
|
+
* minute TTL is shorter than the serving record's hour: a stale memo silently degrades reasoning,
|
|
237
|
+
* while expiry costs one visible recovery round trip and can safely re-establish the memo.
|
|
238
|
+
*/
|
|
239
|
+
export function reasoningReplayOpaqueBlobRejectionMemoized(
|
|
240
|
+
scope: OcxReasoningReplayScopeRef | undefined,
|
|
241
|
+
): boolean {
|
|
242
|
+
const key = opaqueBlobRejectionKeyFor(scope);
|
|
243
|
+
if (!key) return false;
|
|
244
|
+
const at = now();
|
|
245
|
+
sweepExpiredOpaqueBlobRejections(at);
|
|
246
|
+
return opaqueBlobRejections.has(key);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** Record only after a blobless retry succeeded for this durable serving identity. */
|
|
250
|
+
export function rememberReasoningReplayOpaqueBlobRejection(
|
|
251
|
+
scope: OcxReasoningReplayScopeRef | undefined,
|
|
252
|
+
): void {
|
|
253
|
+
const key = opaqueBlobRejectionKeyFor(scope);
|
|
254
|
+
if (!key) return;
|
|
255
|
+
const bytes = Buffer.byteLength(key, "utf8");
|
|
256
|
+
if (bytes > MAX_TOTAL_BYTES) return;
|
|
257
|
+
const at = now();
|
|
258
|
+
sweepExpiredOpaqueBlobRejections(at);
|
|
259
|
+
if (opaqueBlobRejections.has(key)) deleteOpaqueBlobRejection(key);
|
|
260
|
+
opaqueBlobRejections.set(key, { bytes, at });
|
|
261
|
+
opaqueBlobRejectionTotalBytes += bytes;
|
|
262
|
+
while (
|
|
263
|
+
(opaqueBlobRejectionTotalBytes > MAX_TOTAL_BYTES || opaqueBlobRejections.size > MAX_ENTRIES)
|
|
264
|
+
&& opaqueBlobRejections.size > 1
|
|
265
|
+
) {
|
|
266
|
+
let oldestKey: string | undefined;
|
|
267
|
+
let oldestAt = Infinity;
|
|
268
|
+
for (const [candidateKey, entry] of opaqueBlobRejections) {
|
|
269
|
+
if (entry.at < oldestAt) {
|
|
270
|
+
oldestAt = entry.at;
|
|
271
|
+
oldestKey = candidateKey;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (oldestKey === undefined) break;
|
|
275
|
+
deleteOpaqueBlobRejection(oldestKey);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
203
279
|
function processLocalIdentity(domain: string, material: string): string {
|
|
204
280
|
return createHmac("sha256", replayIdentityKey)
|
|
205
281
|
.update(domain)
|
|
@@ -420,7 +496,9 @@ export function peekReasoningForCall(
|
|
|
420
496
|
export function clearReasoningReplayCacheForTests(clock?: (() => number) | null): void {
|
|
421
497
|
entries.clear();
|
|
422
498
|
servingIdentities.clear();
|
|
499
|
+
opaqueBlobRejections.clear();
|
|
423
500
|
totalBytes = 0;
|
|
424
501
|
servingIdentityTotalBytes = 0;
|
|
502
|
+
opaqueBlobRejectionTotalBytes = 0;
|
|
425
503
|
clockForTests = clock ?? null;
|
|
426
504
|
}
|
package/src/server/auth-cors.ts
CHANGED
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import { timingSafeEqual } from "node:crypto";
|
|
2
2
|
import { formatErrorResponse } from "../bridge";
|
|
3
|
+
import {
|
|
4
|
+
codexAutoStartEnabled,
|
|
5
|
+
modelPreferHostedToolsConfigError,
|
|
6
|
+
providerModelCostsConfigError,
|
|
7
|
+
requestPacingConfigError,
|
|
8
|
+
retryOn429PolicyConfigError,
|
|
9
|
+
sanitizeModelCostsForDisplay,
|
|
10
|
+
} from "../config";
|
|
3
11
|
import {
|
|
4
12
|
apiKeyTransportConfigError,
|
|
5
13
|
booleanRecordConfigError,
|
|
6
14
|
modelAdapterRecordConfigError,
|
|
7
|
-
modelPreferHostedToolsConfigError,
|
|
8
|
-
codexAutoStartEnabled,
|
|
9
15
|
nonBlankStringArrayConfigError,
|
|
10
16
|
positiveIntegerConfigError,
|
|
11
17
|
positiveIntegerRecordConfigError,
|
|
12
18
|
providerBaseUrlConfigError,
|
|
13
19
|
providerHeadersConfigError,
|
|
14
|
-
providerModelCostsConfigError,
|
|
15
20
|
reasoningSummaryDeliveryRecordConfigError,
|
|
16
|
-
retryOn429PolicyConfigError,
|
|
17
|
-
requestPacingConfigError,
|
|
18
|
-
sanitizeModelCostsForDisplay,
|
|
19
21
|
upstreamHttpVersionConfigError,
|
|
20
|
-
} from "../config";
|
|
22
|
+
} from "../config/provider-validation";
|
|
21
23
|
import { providerDestinationConfigError } from "../lib/destination-policy";
|
|
22
24
|
import { redactSecretString } from "../lib/redact";
|
|
23
25
|
import { effectiveGoogleMode, getProviderRegistryEntry, providerCodexAccountMode, providerMatchesRegistryTransport, registryEntryForProviderDestination } from "../providers/registry";
|
package/src/server/index.ts
CHANGED
|
@@ -22,10 +22,11 @@ import {
|
|
|
22
22
|
import { reconcileOAuthProviders } from "../oauth";
|
|
23
23
|
import { withCatalogWriteSerialization } from "../codex/catalog-write-serialization";
|
|
24
24
|
import { invalidateCodexModelsCacheWithPermit } from "../codex/catalog/sync";
|
|
25
|
-
import {
|
|
25
|
+
import { currentServiceHomes, serviceStatePathsForOpenCodexHome } from "../service";
|
|
26
26
|
import { shouldSyncCodexOnStart } from "../codex/desired-state";
|
|
27
27
|
import {
|
|
28
28
|
inspectNativeCodexOwnership,
|
|
29
|
+
type NativeCodexOwnership,
|
|
29
30
|
type OwnershipInspection,
|
|
30
31
|
} from "../integrations/native/ownership-preflight";
|
|
31
32
|
import { registerCodexCooldownRecoveryProbeWorker } from "../codex/auth-api";
|
|
@@ -175,8 +176,8 @@ import { runClaudeAuthModeMigration } from "../claude/auth-mode-migration";
|
|
|
175
176
|
import {
|
|
176
177
|
bindNativeMainStartupLifecycle,
|
|
177
178
|
blockNativeMainStartupForUnownedServiceHome,
|
|
179
|
+
prepareNativeMainStartupLifecycle,
|
|
178
180
|
releaseNativeMainStartupLifecycle,
|
|
179
|
-
startNativeMainStartupLifecycle,
|
|
180
181
|
type NativeMainStartupGateDeps,
|
|
181
182
|
type NativeMainStartupLifecycle,
|
|
182
183
|
} from "../codex/native-profile-startup";
|
|
@@ -444,6 +445,8 @@ export interface StartServerDeps {
|
|
|
444
445
|
nativeMainStartup?: NativeMainStartupGateDeps;
|
|
445
446
|
/** Test-only ownership evidence; production inspects the installed service state. */
|
|
446
447
|
inspectNativeCodexOwnership?: typeof inspectNativeCodexOwnership;
|
|
448
|
+
/** Test-only service-home resolver; production resolves the current homes directly. */
|
|
449
|
+
resolveServiceHomes?: typeof currentServiceHomes;
|
|
447
450
|
/** Test-only seam for an upstream that cannot complete its WebSocket close handshake. */
|
|
448
451
|
liveSidebandWebSocketFactory?: LiveSidebandWebSocketFactory;
|
|
449
452
|
/** Test-only seam; production derives a fresh local-attestation secret per process. */
|
|
@@ -452,9 +455,22 @@ export interface StartServerDeps {
|
|
|
452
455
|
readinessGate?: ReadinessGate;
|
|
453
456
|
}
|
|
454
457
|
|
|
455
|
-
function inspectStartupOwnership(
|
|
458
|
+
function inspectStartupOwnership(
|
|
459
|
+
deps: StartServerDeps,
|
|
460
|
+
currentHomes: ReturnType<typeof currentServiceHomes> | null,
|
|
461
|
+
statePaths: readonly string[] | null,
|
|
462
|
+
): OwnershipInspection {
|
|
456
463
|
try {
|
|
457
|
-
|
|
464
|
+
if (currentHomes === null || statePaths === null) {
|
|
465
|
+
return {
|
|
466
|
+
ownership: "unknown",
|
|
467
|
+
reason: "startup service-home resolution failed",
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
if (deps.inspectNativeCodexOwnership) {
|
|
471
|
+
return deps.inspectNativeCodexOwnership({ currentHomes, statePaths });
|
|
472
|
+
}
|
|
473
|
+
return inspectNativeCodexOwnership({ currentHomes, statePaths });
|
|
458
474
|
} catch {
|
|
459
475
|
return {
|
|
460
476
|
ownership: "unknown",
|
|
@@ -541,15 +557,27 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
541
557
|
// journal, or credential path. Both positive foreign evidence and an unprovable
|
|
542
558
|
// ownership state are non-authority.
|
|
543
559
|
startupCacheInvalidationWrote = false;
|
|
544
|
-
const
|
|
560
|
+
const resolveServiceHomes = deps.resolveServiceHomes ?? currentServiceHomes;
|
|
561
|
+
let startupOwnershipHomes: ReturnType<typeof currentServiceHomes> | null = null;
|
|
562
|
+
let startupOwnershipStatePaths: readonly string[] | null = null;
|
|
563
|
+
try {
|
|
564
|
+
const homes = resolveServiceHomes();
|
|
565
|
+
const statePaths = serviceStatePathsForOpenCodexHome(homes.opencodexHome);
|
|
566
|
+
startupOwnershipHomes = homes;
|
|
567
|
+
startupOwnershipStatePaths = statePaths;
|
|
568
|
+
} catch { /* inspection below stays unknown */ }
|
|
569
|
+
const startupCacheOwnership = inspectStartupOwnership(
|
|
570
|
+
deps,
|
|
571
|
+
startupOwnershipHomes,
|
|
572
|
+
startupOwnershipStatePaths,
|
|
573
|
+
);
|
|
545
574
|
// Startup cache invalidation is best-effort and must never block the server from
|
|
546
|
-
// serving. It now takes K so it cannot race a convergence commit
|
|
547
|
-
//
|
|
548
|
-
//
|
|
549
|
-
|
|
550
|
-
if (startupCacheOwnership.ownership === "owned") {
|
|
575
|
+
// serving. It now takes K so it cannot race a convergence commit. Use the home
|
|
576
|
+
// paired with the ownership inspection; re-reading ambient CODEX_HOME here could
|
|
577
|
+
// invalidate a different installation after an environment or mount change.
|
|
578
|
+
if (startupCacheOwnership.ownership === "owned" && startupOwnershipHomes !== null) {
|
|
551
579
|
try {
|
|
552
|
-
const startupCodexHome =
|
|
580
|
+
const startupCodexHome = startupOwnershipHomes.codexHome;
|
|
553
581
|
// #1046: record whether this actually rewrote the cache. `handleStart` ORs this
|
|
554
582
|
// with the later startup sync and warns ONCE about stale app-servers; warning
|
|
555
583
|
// here instead would read a catalog mtime the sync is about to move.
|
|
@@ -706,18 +734,71 @@ export function startServer(port?: number, deps: StartServerDeps = {}): Server<W
|
|
|
706
734
|
// clients; no Codex request can use this lifecycle in that state.
|
|
707
735
|
// Re-probe here instead of trusting the earlier cache decision: startup work
|
|
708
736
|
// between the two sites must not widen the service-install race.
|
|
709
|
-
const nativeOwnership = inspectStartupOwnership(deps);
|
|
737
|
+
const nativeOwnership = inspectStartupOwnership(deps, startupOwnershipHomes, startupOwnershipStatePaths);
|
|
738
|
+
const preparedNativeMainLifecycle = nativeOwnership.ownership !== "foreign"
|
|
739
|
+
&& startupOwnershipHomes !== null
|
|
740
|
+
? prepareNativeMainStartupLifecycle(
|
|
741
|
+
deps.nativeMainStartup,
|
|
742
|
+
{ codexHome: startupOwnershipHomes.codexHome, configDir: startupOwnershipHomes.opencodexHome },
|
|
743
|
+
)
|
|
744
|
+
: null;
|
|
745
|
+
let retryOwnershipHomes = startupOwnershipHomes;
|
|
746
|
+
let retryOwnershipStatePaths = startupOwnershipStatePaths;
|
|
747
|
+
let retryPreparedNativeMainLifecycle = preparedNativeMainLifecycle;
|
|
748
|
+
const reprobeNativeOwnership = (): NativeCodexOwnership => {
|
|
749
|
+
// If startup could not resolve the homes at all, preserve a bounded retry
|
|
750
|
+
// without guessing an authority. The first successful resolution is pinned
|
|
751
|
+
// together with its service-state paths before ownership is inspected.
|
|
752
|
+
if (retryOwnershipHomes === null || retryOwnershipStatePaths === null) {
|
|
753
|
+
try {
|
|
754
|
+
const homes = resolveServiceHomes();
|
|
755
|
+
const statePaths = serviceStatePathsForOpenCodexHome(homes.opencodexHome);
|
|
756
|
+
retryOwnershipHomes = homes;
|
|
757
|
+
retryOwnershipStatePaths = statePaths;
|
|
758
|
+
} catch {
|
|
759
|
+
return "unknown";
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
const homes = retryOwnershipHomes;
|
|
763
|
+
const statePaths = retryOwnershipStatePaths;
|
|
764
|
+
const answer = inspectStartupOwnership(deps, homes, statePaths).ownership;
|
|
765
|
+
if (answer !== "owned") return answer;
|
|
766
|
+
retryPreparedNativeMainLifecycle ??= prepareNativeMainStartupLifecycle(
|
|
767
|
+
deps.nativeMainStartup,
|
|
768
|
+
{ codexHome: homes.codexHome, configDir: homes.opencodexHome },
|
|
769
|
+
);
|
|
770
|
+
// An ownership verdict without a lifecycle bound to that same home is not
|
|
771
|
+
// enough to reopen native-main admission.
|
|
772
|
+
return retryPreparedNativeMainLifecycle ? "owned" : "unknown";
|
|
773
|
+
};
|
|
774
|
+
const ownershipRetryOptions = {
|
|
775
|
+
reprobe: reprobeNativeOwnership,
|
|
776
|
+
expectedHomeId: () => retryPreparedNativeMainLifecycle?.homeId ?? null,
|
|
777
|
+
startOwnedLifecycle: () => {
|
|
778
|
+
if (!retryPreparedNativeMainLifecycle) {
|
|
779
|
+
throw new Error("Native-main ownership became known before its startup lifecycle was prepared.");
|
|
780
|
+
}
|
|
781
|
+
return retryPreparedNativeMainLifecycle.start();
|
|
782
|
+
},
|
|
783
|
+
};
|
|
710
784
|
const nativeMainLifecycle: NativeMainStartupLifecycle = shouldSyncCodexOnStart(config)
|
|
711
785
|
? nativeOwnership.ownership === "owned"
|
|
712
|
-
?
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
786
|
+
? preparedNativeMainLifecycle
|
|
787
|
+
? preparedNativeMainLifecycle.start()
|
|
788
|
+
: blockNativeMainStartupForUnownedServiceHome(
|
|
789
|
+
"ownership-unknown",
|
|
790
|
+
ownershipRetryOptions,
|
|
791
|
+
)
|
|
792
|
+
: nativeOwnership.ownership === "foreign"
|
|
793
|
+
? blockNativeMainStartupForUnownedServiceHome("foreign-ownership")
|
|
794
|
+
: blockNativeMainStartupForUnownedServiceHome(
|
|
795
|
+
"ownership-unknown",
|
|
796
|
+
// #2108: an `unknown` verdict means the probe could not answer, not that this host
|
|
797
|
+
// is unownable. Hand the fence a way to re-ask so a host that becomes answerable
|
|
798
|
+
// after boot reopens on its own instead of needing `ocx restart`. A `foreign`
|
|
799
|
+
// verdict ignores this by design — that one is a fact, not a question.
|
|
800
|
+
ownershipRetryOptions,
|
|
801
|
+
)
|
|
721
802
|
: {
|
|
722
803
|
homeId: null,
|
|
723
804
|
settled: Promise.resolve({ status: "ready", homeId: null }),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readRuntimePort, type RuntimePortState } from "../config";
|
|
1
|
+
import { readRuntimePort, type RuntimePortState } from "../config/process-state";
|
|
2
2
|
import { createLocalAttestationChallenge } from "../lib/local-management-attestation";
|
|
3
3
|
import {
|
|
4
4
|
LOCAL_MANAGEMENT_CAPABILITY_HEADER,
|
|
@@ -155,7 +155,7 @@ function runGrokApplyFlight(): Promise<unknown> {
|
|
|
155
155
|
flight.promise = (grokApplyTestHooks?.run ?? (async () => {
|
|
156
156
|
const [{ syncGrokConfig }, { readRuntimePort }] = await Promise.all([
|
|
157
157
|
import("../../grok/sync"),
|
|
158
|
-
import("../../config"),
|
|
158
|
+
import("../../config/process-state"),
|
|
159
159
|
]);
|
|
160
160
|
const currentConfig = loadConfig();
|
|
161
161
|
const runtime = readRuntimePort(process.pid);
|
|
@@ -490,7 +490,10 @@ export async function handleConfigRoutes(ctx: ManagementContext): Promise<Respon
|
|
|
490
490
|
if (url.pathname === "/api/sync" && req.method === "POST") {
|
|
491
491
|
const { syncModelsToCodex } = await import("../../codex/sync");
|
|
492
492
|
const { attachStaleAppServerHint } = await import("../../codex/app-server-processes");
|
|
493
|
-
const { readRuntimePort, loadConfig } = await
|
|
493
|
+
const [{ readRuntimePort }, { loadConfig }] = await Promise.all([
|
|
494
|
+
import("../../config/process-state"),
|
|
495
|
+
import("../../config"),
|
|
496
|
+
]);
|
|
494
497
|
// Never use the server-captured startup object for a durable integration
|
|
495
498
|
// decision. A toggle may have persisted while this process was gathering.
|
|
496
499
|
const runtime = readRuntimePort(process.pid);
|
|
@@ -8,7 +8,7 @@ import type { ManagementPrincipal } from "../management-auth";
|
|
|
8
8
|
import type { CatalogModel } from "../../codex/catalog";
|
|
9
9
|
import type { injectGrokConfig } from "../../grok/inject";
|
|
10
10
|
import type { removeDesktop3pStandardPivot, writeDesktop3pConfig } from "../../claude/desktop-3p";
|
|
11
|
-
import type { RuntimePortState } from "../../config";
|
|
11
|
+
import type { RuntimePortState } from "../../config/process-state";
|
|
12
12
|
import type { CatalogDisposition, ConvergeCodex } from "../../codex/convergence-types";
|
|
13
13
|
import type {
|
|
14
14
|
performCodexRestart,
|