@bitkyc08/opencodex 2.26.0 → 2.27.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/gui/dist/assets/{index-RL6b1bTV.js → index-7jlKgmJd.js} +14 -14
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/base.ts +14 -2
- package/src/adapters/command-code.ts +4 -3
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/live-transport.ts +14 -1
- package/src/adapters/google.ts +1 -1
- package/src/adapters/openai-chat.ts +38 -6
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/bridge.ts +11 -5
- package/src/cli/doctor.ts +76 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/models.ts +13 -6
- package/src/codex/app-server-processes.ts +269 -37
- package/src/codex/auth-context.ts +53 -1
- package/src/codex/catalog/aggregation.ts +3 -0
- package/src/codex/catalog/parsing.ts +20 -3
- package/src/codex/catalog/provider-fetch.ts +8 -0
- package/src/codex/catalog/sync.ts +6 -4
- package/src/codex/log-guard/path-safety.ts +52 -3
- package/src/codex/native-profile-startup.ts +100 -2
- package/src/codex/user-identity.ts +21 -1
- package/src/config/provider-name.ts +24 -0
- package/src/config.ts +11 -24
- package/src/generated/compatibility-version.json +73 -45
- package/src/images/loop.ts +11 -4
- package/src/lib/state-store-registrations.ts +8 -2
- package/src/providers/antigravity-models.ts +70 -5
- package/src/providers/derive.ts +12 -2
- package/src/providers/registry.ts +46 -1
- package/src/providers/service-tier.ts +34 -7
- package/src/responses/parser.ts +56 -2
- package/src/responses/state.ts +162 -5
- package/src/router.ts +10 -3
- package/src/routing/compatibility/behavior.ts +3 -3
- package/src/routing/profile.ts +1 -1
- package/src/server/index.ts +5 -0
- package/src/server/management/shared.ts +3 -1
- package/src/server/responses/collaboration.ts +34 -9
- package/src/server/responses/core.ts +13 -4
- package/src/server/responses/input-admission.ts +7 -2
- package/src/service-manager-probe.ts +99 -0
- package/src/service.ts +86 -6
- package/src/tray/windows.ts +25 -5
- package/src/types/accounts.ts +37 -0
- package/src/types/config.ts +818 -0
- package/src/types/provider.ts +521 -0
- package/src/types/request.ts +358 -0
- package/src/types/tools.ts +131 -0
- package/src/types/wire.ts +80 -0
- package/src/types.ts +103 -1883
- package/src/usage/cost.ts +37 -1
- package/src/web-search/loop.ts +11 -4
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import { withNativeMainExclusiveClaim } from "./native-main-claim";
|
|
16
16
|
import { scrubNativeMainAuthTempResidues } from "./native-main-auth-temp";
|
|
17
17
|
import { NATIVE_STAGE_SWEEP_INTERVAL_MS } from "./native-profile-stage-store";
|
|
18
|
+
import type { NativeCodexOwnership } from "../integrations/native/ownership-preflight";
|
|
18
19
|
|
|
19
20
|
export type NativeMainStartupGateSnapshot =
|
|
20
21
|
| { status: "ready"; homeId: string | null }
|
|
@@ -31,6 +32,10 @@ export type NativeMainStartupGateSnapshot =
|
|
|
31
32
|
| "stage-cleanup-required";
|
|
32
33
|
};
|
|
33
34
|
|
|
35
|
+
/** The settled reason a blocked gate carries, named so consumers can hold one without the union. */
|
|
36
|
+
export type NativeMainStartupBlockReason =
|
|
37
|
+
Extract<NativeMainStartupGateSnapshot, { status: "blocked" }>["reason"];
|
|
38
|
+
|
|
34
39
|
export interface NativeMainStartupGateDeps {
|
|
35
40
|
manager?: NativeProfileManager;
|
|
36
41
|
/** Test-only barrier used to prove admission stays closed while startup recovery is pending. */
|
|
@@ -306,6 +311,75 @@ export function startNativeMainStartupLifecycle(
|
|
|
306
311
|
};
|
|
307
312
|
}
|
|
308
313
|
|
|
314
|
+
/**
|
|
315
|
+
* How many times a service-ownership fence will re-ask before it stops asking (#2108).
|
|
316
|
+
*
|
|
317
|
+
* A host that is permanently unaskable must not re-probe on every request forever, and a
|
|
318
|
+
* host that recovers usually does so within the first few. The budget belongs to the
|
|
319
|
+
* REASON, not to an individual fence: raising a second fence deliberately does not hand
|
|
320
|
+
* out a fresh allowance, or a caller looping over fences could spin the probe forever.
|
|
321
|
+
* It is dropped when the last fence for that reason releases.
|
|
322
|
+
*/
|
|
323
|
+
export const NATIVE_MAIN_OWNERSHIP_RETRY_LIMIT = 5;
|
|
324
|
+
|
|
325
|
+
/** Reprobe hooks for the fences currently held, keyed by the reason they were raised for. */
|
|
326
|
+
const serviceOwnershipReprobes = new Map<NativeMainServiceOwnershipBlockReason, ServiceOwnershipReprobe>();
|
|
327
|
+
|
|
328
|
+
interface ServiceOwnershipReprobe {
|
|
329
|
+
readonly probe: () => NativeCodexOwnership;
|
|
330
|
+
attempts: number;
|
|
331
|
+
/** The fence that installed this hook; only its own release may drop the entry. */
|
|
332
|
+
readonly owner: NativeMainStartupLifecycle;
|
|
333
|
+
/** Releases the fence that installed this hook, exactly once. */
|
|
334
|
+
readonly spend: () => void;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Test-only: the retry budget is module state and would otherwise leak across tests. */
|
|
338
|
+
export function __resetNativeMainOwnershipRetries(): void {
|
|
339
|
+
for (const entry of serviceOwnershipReprobes.values()) entry.attempts = 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Re-ask whether this host is still unownable, and drop the fence if it is not.
|
|
344
|
+
*
|
|
345
|
+
* `startServer` takes the ownership verdict once, at boot, and holds it for the process
|
|
346
|
+
* lifetime. For `foreign-ownership` that is correct — a foreign owner is a fact, and
|
|
347
|
+
* re-asking would only hand a determined caller a second chance. For `ownership-unknown`
|
|
348
|
+
* it is wrong: that verdict means the probe could not answer, so waiting cannot help,
|
|
349
|
+
* which is precisely why the #2108 reporter had to run `ocx restart` after every reboot.
|
|
350
|
+
*
|
|
351
|
+
* The re-probe is demand-driven rather than timed: it runs when something asks whether
|
|
352
|
+
* native-main is fenced, which is usually a request but is also the background token
|
|
353
|
+
* guardian's warmup. It is capped so a permanently unaskable host cannot spin.
|
|
354
|
+
*
|
|
355
|
+
* The probe is synchronous `spawnSync` with a bounded timeout, and this function is on a
|
|
356
|
+
* request path, so the cap is what keeps a wedged host from paying that cost repeatedly.
|
|
357
|
+
*/
|
|
358
|
+
function reprobeServiceOwnership(reason: NativeMainServiceOwnershipBlockReason): boolean {
|
|
359
|
+
if (reason !== "ownership-unknown") return false;
|
|
360
|
+
const entry = serviceOwnershipReprobes.get(reason);
|
|
361
|
+
if (!entry) return false;
|
|
362
|
+
if (entry.attempts >= NATIVE_MAIN_OWNERSHIP_RETRY_LIMIT) return false;
|
|
363
|
+
entry.attempts += 1;
|
|
364
|
+
let answer: NativeCodexOwnership;
|
|
365
|
+
try {
|
|
366
|
+
answer = entry.probe();
|
|
367
|
+
} catch {
|
|
368
|
+
// An inspection that throws is not evidence the host became ownable.
|
|
369
|
+
return false;
|
|
370
|
+
}
|
|
371
|
+
if (answer !== "owned") return false;
|
|
372
|
+
// Release through the fence that installed this hook, and only that one.
|
|
373
|
+
//
|
|
374
|
+
// Several servers can hold a fence for the same reason while only one carries a hook, so
|
|
375
|
+
// clearing the shared refcount here would unblock fences this probe never spoke for.
|
|
376
|
+
// Decrementing here directly is just as wrong the other way: that fence's own release()
|
|
377
|
+
// would then pay a second time for one fence, leaving the count short. Delegating to the
|
|
378
|
+
// fence's idempotent release keeps exactly one payment per fence.
|
|
379
|
+
entry.spend();
|
|
380
|
+
return true;
|
|
381
|
+
}
|
|
382
|
+
|
|
309
383
|
function activeServiceOwnershipBlockReason(): NativeMainServiceOwnershipBlockReason | null {
|
|
310
384
|
if ((serviceOwnershipRefs.get("foreign-ownership") ?? 0) > 0) return "foreign-ownership";
|
|
311
385
|
if ((serviceOwnershipRefs.get("ownership-unknown") ?? 0) > 0) return "ownership-unknown";
|
|
@@ -321,10 +395,11 @@ function serviceOwnershipSnapshot(
|
|
|
321
395
|
/** Close native-main admission without resolving or creating any CODEX_HOME artifacts. */
|
|
322
396
|
export function blockNativeMainStartupForUnownedServiceHome(
|
|
323
397
|
reason: NativeMainServiceOwnershipBlockReason,
|
|
398
|
+
options?: { reprobe?: () => NativeCodexOwnership },
|
|
324
399
|
): NativeMainStartupLifecycle {
|
|
325
400
|
serviceOwnershipRefs.set(reason, (serviceOwnershipRefs.get(reason) ?? 0) + 1);
|
|
326
401
|
let released = false;
|
|
327
|
-
|
|
402
|
+
const lifecycle: NativeMainStartupLifecycle = {
|
|
328
403
|
homeId: null,
|
|
329
404
|
settled: Promise.resolve(serviceOwnershipSnapshot(reason)),
|
|
330
405
|
async release() {
|
|
@@ -333,8 +408,25 @@ export function blockNativeMainStartupForUnownedServiceHome(
|
|
|
333
408
|
const remaining = Math.max(0, (serviceOwnershipRefs.get(reason) ?? 0) - 1);
|
|
334
409
|
if (remaining === 0) serviceOwnershipRefs.delete(reason);
|
|
335
410
|
else serviceOwnershipRefs.set(reason, remaining);
|
|
411
|
+
if (serviceOwnershipReprobes.get(reason)?.owner === lifecycle) {
|
|
412
|
+
serviceOwnershipReprobes.delete(reason);
|
|
413
|
+
}
|
|
336
414
|
},
|
|
337
415
|
};
|
|
416
|
+
// Do NOT reset an existing budget: keying the reprobe by reason means a caller raising
|
|
417
|
+
// fences in a loop would otherwise be handed a fresh allowance each time and could spin
|
|
418
|
+
// the probe forever. But once the holder is gone its entry is removed above, so a LATER
|
|
419
|
+
// fence installs its own hook — a server started after an earlier probe must not be left
|
|
420
|
+
// needing `ocx restart`, which is the very symptom this exists to remove.
|
|
421
|
+
if (options?.reprobe && reason === "ownership-unknown" && !serviceOwnershipReprobes.has(reason)) {
|
|
422
|
+
serviceOwnershipReprobes.set(reason, {
|
|
423
|
+
probe: options.reprobe,
|
|
424
|
+
attempts: 0,
|
|
425
|
+
owner: lifecycle,
|
|
426
|
+
spend: () => { void lifecycle.release(); },
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
return lifecycle;
|
|
338
430
|
}
|
|
339
431
|
|
|
340
432
|
export function bindNativeMainStartupLifecycle(server: object, lifecycle: NativeMainStartupLifecycle): void {
|
|
@@ -349,7 +441,13 @@ export async function releaseNativeMainStartupLifecycle(server: object): Promise
|
|
|
349
441
|
}
|
|
350
442
|
|
|
351
443
|
export function isNativeMainTrafficBlocked(): boolean {
|
|
352
|
-
|
|
444
|
+
const reason = activeServiceOwnershipBlockReason();
|
|
445
|
+
if (reason !== null && reprobeServiceOwnership(reason)) {
|
|
446
|
+
// The host became ownable after boot (#2108): the fence lifts here rather than
|
|
447
|
+
// waiting for the restart the reporter had to perform by hand.
|
|
448
|
+
return activeServiceOwnershipBlockReason() !== null || snapshot.status === "blocked";
|
|
449
|
+
}
|
|
450
|
+
return reason !== null || snapshot.status === "blocked";
|
|
353
451
|
}
|
|
354
452
|
|
|
355
453
|
/**
|
|
@@ -39,6 +39,26 @@ const SID_PATTERN = /^S-1-(?:\d+-)+\d+$/i;
|
|
|
39
39
|
*/
|
|
40
40
|
const WINDOWS_POWERSHELL_LOOKUP_TIMEOUT_MS = 8_000;
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* The same budget, widened for a contended CI runner.
|
|
44
|
+
*
|
|
45
|
+
* 8s is a generous ceiling for `powershell.exe -Command` on a real desktop and is not one
|
|
46
|
+
* on a GitHub Windows runner executing a quarter of this suite: the composed-acceptance
|
|
47
|
+
* cases fail there with "Windows effective-account lookup timed out" while the child is
|
|
48
|
+
* still starting. That is runner contention, not a hung lookup, and the budget exists to
|
|
49
|
+
* bound the latter.
|
|
50
|
+
*
|
|
51
|
+
* Gated on `CI` alone. A user's machine keeps the 8s ceiling exactly as before, so the
|
|
52
|
+
* recoverable-refusal contract this budget protects is unchanged where it matters.
|
|
53
|
+
*/
|
|
54
|
+
const WINDOWS_POWERSHELL_LOOKUP_TIMEOUT_CI_MS = 30_000;
|
|
55
|
+
|
|
56
|
+
function windowsIdentityLookupTimeoutMs(): number {
|
|
57
|
+
return process.env.CI === "true"
|
|
58
|
+
? WINDOWS_POWERSHELL_LOOKUP_TIMEOUT_CI_MS
|
|
59
|
+
: WINDOWS_POWERSHELL_LOOKUP_TIMEOUT_MS;
|
|
60
|
+
}
|
|
61
|
+
|
|
42
62
|
/**
|
|
43
63
|
* FOLDERID_LocalAppData, and the flag that makes the lookup ignore the caller's
|
|
44
64
|
* environment.
|
|
@@ -108,7 +128,7 @@ function windowsIdentityPowerShellSpawnOptions(): {
|
|
|
108
128
|
stdin: "ignore",
|
|
109
129
|
stdout: "pipe",
|
|
110
130
|
stderr: "pipe",
|
|
111
|
-
timeout:
|
|
131
|
+
timeout: windowsIdentityLookupTimeoutMs(),
|
|
112
132
|
windowsHide: true,
|
|
113
133
|
};
|
|
114
134
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const RESERVED_PROVIDER_NAMES = new Set([
|
|
2
|
+
// JavaScript prototype-pollution guards.
|
|
3
|
+
"__proto__",
|
|
4
|
+
"prototype",
|
|
5
|
+
"constructor",
|
|
6
|
+
// System-reserved routing namespace (resolved before provider/account
|
|
7
|
+
// namespaces in routeModelInternal). "combo" is intentionally NOT reserved:
|
|
8
|
+
// a physical provider named `combo` is a supported pattern (combo aliases
|
|
9
|
+
// hosted on the combo provider), and the combo selector only wins when an
|
|
10
|
+
// actual combo id matches.
|
|
11
|
+
"policy",
|
|
12
|
+
]);
|
|
13
|
+
const PROVIDER_NAME_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,62}[A-Za-z0-9])?$/;
|
|
14
|
+
|
|
15
|
+
export function isValidProviderName(name: string): boolean {
|
|
16
|
+
const trimmed = name.trim();
|
|
17
|
+
return trimmed === name
|
|
18
|
+
&& PROVIDER_NAME_PATTERN.test(name)
|
|
19
|
+
&& !RESERVED_PROVIDER_NAMES.has(name.toLowerCase());
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function hasOwnProvider(providers: Record<string, unknown>, name: string): boolean {
|
|
23
|
+
return Object.prototype.hasOwnProperty.call(providers, name);
|
|
24
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { homedir } from "node:os";
|
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
6
|
import { Database } from "bun:sqlite";
|
|
7
7
|
import * as z from "zod/v4";
|
|
8
|
+
import { isValidProviderName, hasOwnProvider } from "./config/provider-name";
|
|
8
9
|
import {
|
|
9
10
|
bumpConfigGenerationAtPath,
|
|
10
11
|
bumpCurrentConfigGeneration,
|
|
@@ -74,6 +75,7 @@ import {
|
|
|
74
75
|
getProviderRegistryEntry,
|
|
75
76
|
providerMatchesRegistryTransport,
|
|
76
77
|
providerModelWireDefault,
|
|
78
|
+
registryModelServiceTierCapabilityApplies,
|
|
77
79
|
} from "./providers/registry";
|
|
78
80
|
import { resolveOpenAiVirtualModel } from "./providers/openai-virtual-models";
|
|
79
81
|
import { parseDesktopProfile } from "./claude/desktop-profile";
|
|
@@ -726,6 +728,11 @@ const providerConfigSchema = z.object({
|
|
|
726
728
|
.optional(),
|
|
727
729
|
retryOn429: retryOn429PolicySchema.optional(),
|
|
728
730
|
codexAccountMode: z.enum(["pool", "direct"]).optional(),
|
|
731
|
+
// Validated rather than passed through: this schema ends in `.passthrough()`, so an
|
|
732
|
+
// undeclared key survives verbatim. A misspelled `codexToolMode` therefore used to be
|
|
733
|
+
// accepted, persisted, and then silently resolved to the `code_mode_only` default — the
|
|
734
|
+
// operator asked for shell mode, got code mode, and was told nothing (#2106).
|
|
735
|
+
codexToolMode: z.enum(["code_mode_only", "shell"]).optional(),
|
|
729
736
|
responsesItemIdRepair: z.object({
|
|
730
737
|
message: z.array(z.string().min(1)).optional(),
|
|
731
738
|
reasoning: z.array(z.string().min(1)).optional(),
|
|
@@ -735,19 +742,6 @@ const providerConfigSchema = z.object({
|
|
|
735
742
|
responsesSnapshotRepair: z.boolean().optional(),
|
|
736
743
|
}).passthrough();
|
|
737
744
|
|
|
738
|
-
const RESERVED_PROVIDER_NAMES = new Set([
|
|
739
|
-
// JavaScript prototype-pollution guards.
|
|
740
|
-
"__proto__",
|
|
741
|
-
"prototype",
|
|
742
|
-
"constructor",
|
|
743
|
-
// System-reserved routing namespace (resolved before provider/account
|
|
744
|
-
// namespaces in routeModelInternal). "combo" is intentionally NOT reserved:
|
|
745
|
-
// a physical provider named `combo` is a supported pattern (combo aliases
|
|
746
|
-
// hosted on the combo provider), and the combo selector only wins when an
|
|
747
|
-
// actual combo id matches.
|
|
748
|
-
"policy",
|
|
749
|
-
]);
|
|
750
|
-
const PROVIDER_NAME_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]{0,62}[A-Za-z0-9])?$/;
|
|
751
745
|
const HEADER_NAME_PATTERN = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
|
|
752
746
|
const SENSITIVE_PROVIDER_HEADERS = new Set([
|
|
753
747
|
"authorization",
|
|
@@ -759,16 +753,7 @@ const SENSITIVE_PROVIDER_HEADERS = new Set([
|
|
|
759
753
|
"x-amz-security-token",
|
|
760
754
|
]);
|
|
761
755
|
|
|
762
|
-
export
|
|
763
|
-
const trimmed = name.trim();
|
|
764
|
-
return trimmed === name
|
|
765
|
-
&& PROVIDER_NAME_PATTERN.test(name)
|
|
766
|
-
&& !RESERVED_PROVIDER_NAMES.has(name.toLowerCase());
|
|
767
|
-
}
|
|
768
|
-
|
|
769
|
-
export function hasOwnProvider(providers: Record<string, unknown>, name: string): boolean {
|
|
770
|
-
return Object.prototype.hasOwnProperty.call(providers, name);
|
|
771
|
-
}
|
|
756
|
+
export { isValidProviderName, hasOwnProvider } from "./config/provider-name";
|
|
772
757
|
|
|
773
758
|
export function providerBaseUrlConfigError(baseUrl: string): string | null {
|
|
774
759
|
try {
|
|
@@ -2159,7 +2144,9 @@ function inheritedFastWireConflictProviderNames(
|
|
|
2159
2144
|
if (!registry) continue;
|
|
2160
2145
|
const effectiveProviderCapability = provider.supportsServiceTier ?? registry.supportsServiceTier;
|
|
2161
2146
|
const effectiveModelCapabilities = {
|
|
2162
|
-
...(registry
|
|
2147
|
+
...(registryModelServiceTierCapabilityApplies(registry, provider)
|
|
2148
|
+
? registry.modelSupportsServiceTier ?? {}
|
|
2149
|
+
: {}),
|
|
2163
2150
|
...(provider.modelSupportsServiceTier ?? {}),
|
|
2164
2151
|
};
|
|
2165
2152
|
if (
|