@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.
Files changed (55) hide show
  1. package/gui/dist/assets/{index-RL6b1bTV.js → index-7jlKgmJd.js} +14 -14
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +1 -1
  4. package/src/adapters/anthropic.ts +1 -1
  5. package/src/adapters/base.ts +14 -2
  6. package/src/adapters/command-code.ts +4 -3
  7. package/src/adapters/cursor/cursor-errors.ts +15 -0
  8. package/src/adapters/cursor/live-transport.ts +14 -1
  9. package/src/adapters/google.ts +1 -1
  10. package/src/adapters/openai-chat.ts +38 -6
  11. package/src/adapters/tool-catalog-nudge.ts +1 -1
  12. package/src/bridge.ts +11 -5
  13. package/src/cli/doctor.ts +76 -0
  14. package/src/cli/help.ts +2 -0
  15. package/src/cli/models.ts +13 -6
  16. package/src/codex/app-server-processes.ts +269 -37
  17. package/src/codex/auth-context.ts +53 -1
  18. package/src/codex/catalog/aggregation.ts +3 -0
  19. package/src/codex/catalog/parsing.ts +20 -3
  20. package/src/codex/catalog/provider-fetch.ts +8 -0
  21. package/src/codex/catalog/sync.ts +6 -4
  22. package/src/codex/log-guard/path-safety.ts +52 -3
  23. package/src/codex/native-profile-startup.ts +100 -2
  24. package/src/codex/user-identity.ts +21 -1
  25. package/src/config/provider-name.ts +24 -0
  26. package/src/config.ts +11 -24
  27. package/src/generated/compatibility-version.json +73 -45
  28. package/src/images/loop.ts +11 -4
  29. package/src/lib/state-store-registrations.ts +8 -2
  30. package/src/providers/antigravity-models.ts +70 -5
  31. package/src/providers/derive.ts +12 -2
  32. package/src/providers/registry.ts +46 -1
  33. package/src/providers/service-tier.ts +34 -7
  34. package/src/responses/parser.ts +56 -2
  35. package/src/responses/state.ts +162 -5
  36. package/src/router.ts +10 -3
  37. package/src/routing/compatibility/behavior.ts +3 -3
  38. package/src/routing/profile.ts +1 -1
  39. package/src/server/index.ts +5 -0
  40. package/src/server/management/shared.ts +3 -1
  41. package/src/server/responses/collaboration.ts +34 -9
  42. package/src/server/responses/core.ts +13 -4
  43. package/src/server/responses/input-admission.ts +7 -2
  44. package/src/service-manager-probe.ts +99 -0
  45. package/src/service.ts +86 -6
  46. package/src/tray/windows.ts +25 -5
  47. package/src/types/accounts.ts +37 -0
  48. package/src/types/config.ts +818 -0
  49. package/src/types/provider.ts +521 -0
  50. package/src/types/request.ts +358 -0
  51. package/src/types/tools.ts +131 -0
  52. package/src/types/wire.ts +80 -0
  53. package/src/types.ts +103 -1883
  54. package/src/usage/cost.ts +37 -1
  55. 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
- return {
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
- return activeServiceOwnershipBlockReason() !== null || snapshot.status === "blocked";
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: WINDOWS_POWERSHELL_LOOKUP_TIMEOUT_MS,
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 function isValidProviderName(name: string): boolean {
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.modelSupportsServiceTier ?? {}),
2147
+ ...(registryModelServiceTierCapabilityApplies(registry, provider)
2148
+ ? registry.modelSupportsServiceTier ?? {}
2149
+ : {}),
2163
2150
  ...(provider.modelSupportsServiceTier ?? {}),
2164
2151
  };
2165
2152
  if (