@yansigit/opencodex 2.31.3 → 2.33.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 (176) hide show
  1. package/README.md +2 -2
  2. package/bin/ocx.mjs +99 -70
  3. package/gui/dist/assets/index-DKLr4LTE.js +102 -0
  4. package/gui/dist/assets/index-DrSQdTRd.css +1 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +6 -5
  7. package/src/adapters/anthropic.ts +25 -13
  8. package/src/adapters/azure.ts +20 -4
  9. package/src/adapters/base.ts +5 -1
  10. package/src/adapters/command-code.ts +42 -10
  11. package/src/adapters/cursor/live-models.ts +8 -0
  12. package/src/adapters/cursor/live-transport.ts +1 -1
  13. package/src/adapters/cursor/native-exec-desktop.ts +16 -0
  14. package/src/adapters/cursor/protobuf-events.ts +158 -7
  15. package/src/adapters/cursor/protobuf-request.ts +33 -15
  16. package/src/adapters/cursor/request-builder.ts +4 -3
  17. package/src/adapters/cursor/tool-definitions.ts +27 -1
  18. package/src/adapters/cursor/types.ts +4 -3
  19. package/src/adapters/cursor.ts +9 -0
  20. package/src/adapters/google-antigravity-replay.ts +2 -2
  21. package/src/adapters/google-antigravity-wire.ts +7 -0
  22. package/src/adapters/google-errors.ts +6 -2
  23. package/src/adapters/google-http.ts +30 -7
  24. package/src/adapters/google-truncation.ts +5 -0
  25. package/src/adapters/google-wire-compiler.ts +38 -6
  26. package/src/adapters/google.ts +154 -33
  27. package/src/adapters/kiro-tools.ts +20 -9
  28. package/src/adapters/kiro.ts +0 -3
  29. package/src/adapters/openai-chat.ts +9 -0
  30. package/src/adapters/openai-responses.ts +4 -1
  31. package/src/adapters/tool-catalog-nudge.ts +1 -1
  32. package/src/adapters/xai-web-search.ts +7 -2
  33. package/src/bridge.ts +133 -24
  34. package/src/claude/context-windows.ts +16 -9
  35. package/src/cli/dispatch.ts +50 -2
  36. package/src/cli/doctor.ts +26 -13
  37. package/src/cli/help.ts +4 -3
  38. package/src/cli/index.ts +20 -6
  39. package/src/cli/models.ts +13 -3
  40. package/src/cli/observe.ts +20 -5
  41. package/src/cli/provider.ts +8 -1
  42. package/src/cli/registry.ts +7 -5
  43. package/src/cli/status.ts +25 -1
  44. package/src/cli/system-restart-client.ts +1 -1
  45. package/src/cli/usage-report.ts +134 -0
  46. package/src/codex/app-server-processes.ts +3 -1
  47. package/src/codex/auth-api.ts +4 -2
  48. package/src/codex/autostart-health.ts +16 -0
  49. package/src/codex/catalog/aggregation.ts +12 -0
  50. package/src/codex/catalog/effort.ts +42 -12
  51. package/src/codex/catalog/metadata.ts +27 -1
  52. package/src/codex/catalog/model-metadata.ts +593 -0
  53. package/src/codex/catalog/parsing.ts +71 -27
  54. package/src/codex/catalog/provider-fetch.ts +189 -33
  55. package/src/codex/catalog/sync.ts +6 -5
  56. package/src/codex/convergence.ts +5 -0
  57. package/src/codex/desktop-app-restart.ts +342 -0
  58. package/src/codex/history-job.ts +32 -3
  59. package/src/codex/history-manifest.ts +112 -0
  60. package/src/codex/history-migration-guardian.ts +5 -5
  61. package/src/codex/history-provider.ts +825 -247
  62. package/src/codex/history-worker.ts +8 -5
  63. package/src/codex/inject.ts +49 -21
  64. package/src/codex/injected-marker.ts +1 -1
  65. package/src/codex/internal/history-writer.ts +4 -3
  66. package/src/codex/native-profile-startup.ts +157 -27
  67. package/src/codex/native-residue.ts +26 -33
  68. package/src/codex/shim.ts +56 -3
  69. package/src/combos/failover.ts +27 -0
  70. package/src/compatibility/index.ts +26 -0
  71. package/src/compatibility/manifest.ts +253 -0
  72. package/src/compatibility/openai-responses.ts +81 -0
  73. package/src/config/atomic-write.ts +219 -0
  74. package/src/config/paths.ts +40 -0
  75. package/src/config/process-state.ts +308 -0
  76. package/src/config/provider-validation.ts +214 -0
  77. package/src/config.ts +153 -814
  78. package/src/generated/compatibility-version.json +232 -148
  79. package/src/images/loop.ts +37 -6
  80. package/src/images/plan.ts +5 -4
  81. package/src/integrations/ownership-policy.ts +141 -0
  82. package/src/integrations/ownership.ts +10 -0
  83. package/src/integrations/state.ts +44 -5
  84. package/src/integrations/writer.ts +6 -0
  85. package/src/lib/azure-identity.ts +154 -0
  86. package/src/lib/bounded-body.ts +14 -2
  87. package/src/lib/debug.ts +42 -0
  88. package/src/lib/errors.ts +14 -0
  89. package/src/lib/process-control.ts +2 -1
  90. package/src/lib/provider-outbound.ts +45 -33
  91. package/src/lib/provider-tls-profile.ts +309 -0
  92. package/src/lib/proxy-env.ts +49 -0
  93. package/src/lib/redact.ts +10 -1
  94. package/src/lib/state-store-registrations.ts +2 -0
  95. package/src/lib/tool-argument-integers.ts +56 -5
  96. package/src/oauth/antigravity-routing.ts +282 -236
  97. package/src/oauth/callback-server.ts +22 -2
  98. package/src/oauth/command-code.ts +5 -16
  99. package/src/oauth/google-antigravity.ts +42 -5
  100. package/src/oauth/health.ts +1 -1
  101. package/src/oauth/index.ts +15 -3
  102. package/src/oauth/kimi.ts +9 -1
  103. package/src/oauth/open-browser-choice.ts +26 -0
  104. package/src/oauth/store.ts +6 -0
  105. package/src/providers/antigravity-quota.ts +3 -1
  106. package/src/providers/api-keys.ts +2 -1
  107. package/src/providers/auto-compact-budget.ts +65 -0
  108. package/src/providers/derive.ts +4 -0
  109. package/src/providers/key-failover.ts +5 -1
  110. package/src/providers/openai-tiers.ts +5 -0
  111. package/src/providers/provider-id-rewrite.ts +1 -0
  112. package/src/providers/quota.ts +59 -13
  113. package/src/providers/registry.ts +4 -2
  114. package/src/providers/request-pacing.ts +33 -6
  115. package/src/providers/xai-transport.ts +21 -0
  116. package/src/reasoning-effort.ts +19 -2
  117. package/src/responses/apply-patch-envelope.ts +63 -0
  118. package/src/responses/custom-tool-compat.ts +132 -38
  119. package/src/responses/google-provider-options.ts +36 -0
  120. package/src/responses/namespace-tool-compat.ts +84 -4
  121. package/src/responses/parser.ts +14 -2
  122. package/src/responses/provider-opaque-metadata.ts +3 -3
  123. package/src/responses/reasoning-replay-cache.ts +81 -3
  124. package/src/responses/schema.ts +37 -0
  125. package/src/responses/state.ts +94 -4
  126. package/src/router.ts +8 -2
  127. package/src/server/auth-cors.ts +37 -7
  128. package/src/server/images.ts +19 -35
  129. package/src/server/index.ts +102 -21
  130. package/src/server/local-management-read-client.ts +1 -1
  131. package/src/server/local-provider-reload-client.ts +1 -1
  132. package/src/server/management/agent-settings-routes.ts +206 -16
  133. package/src/server/management/combo-routes.ts +6 -0
  134. package/src/server/management/config-routes.ts +35 -6
  135. package/src/server/management/context.ts +1 -1
  136. package/src/server/management/logs-usage-routes.ts +27 -6
  137. package/src/server/management/model-routes.ts +8 -4
  138. package/src/server/management/model-rows.ts +4 -0
  139. package/src/server/management/native-integration-routes.ts +2 -1
  140. package/src/server/management/oauth-account-routes.ts +25 -4
  141. package/src/server/management/provider-capability-config.ts +1 -1
  142. package/src/server/management/provider-routes.ts +113 -15
  143. package/src/server/management/routing-profile-routes.ts +3 -0
  144. package/src/server/management/system-restart.ts +1 -1
  145. package/src/server/port-reclaim.ts +1 -1
  146. package/src/server/proxy-liveness.ts +2 -1
  147. package/src/server/request-log-conversation.ts +30 -0
  148. package/src/server/request-log.ts +21 -0
  149. package/src/server/responses/agent-task-recovery.ts +1 -1
  150. package/src/server/responses/codex-auth-error.ts +55 -0
  151. package/src/server/responses/combo-stream-preflight.ts +171 -0
  152. package/src/server/responses/compact.ts +36 -22
  153. package/src/server/responses/core.ts +584 -247
  154. package/src/server/responses/empty-completion-guard.ts +35 -6
  155. package/src/server/responses/fetch-helpers.ts +20 -102
  156. package/src/server/responses/v2-native-parent-override.ts +59 -0
  157. package/src/server/responses/ws-upstream.ts +75 -2
  158. package/src/server/responses-custom-tool-repair.ts +41 -5
  159. package/src/server/responses-undeclared-tool-guard.ts +241 -18
  160. package/src/service.ts +9 -5
  161. package/src/types/config.ts +16 -1
  162. package/src/types/provider.ts +16 -0
  163. package/src/types/request.ts +34 -1
  164. package/src/types/tools.ts +114 -11
  165. package/src/types.ts +7 -1
  166. package/src/update/index.ts +5 -4
  167. package/src/update/job.ts +3 -1
  168. package/src/update/transactional-install.mjs +8 -1
  169. package/src/usage/log.ts +16 -8
  170. package/src/usage/summary.ts +201 -8
  171. package/src/vision/describe.ts +18 -13
  172. package/src/web-search/executor.ts +10 -3
  173. package/src/web-search/gemini-executor.ts +6 -4
  174. package/src/web-search/loop.ts +42 -6
  175. package/gui/dist/assets/index-CGoDO3uO.css +0 -1
  176. package/gui/dist/assets/index-Cxt5fZMP.js +0 -102
@@ -7,12 +7,14 @@ import {
7
7
  resolvePublicAddresses,
8
8
  } from "./destination-policy";
9
9
  import { pinnedHttpGet, pinnedHttpPost } from "./pinned-http";
10
- import { outboundProxyConfigured } from "./proxy-env";
10
+ import { noProxyMatches, outboundProxyConfigured, proxyForUrl } from "./proxy-env";
11
11
  import { publicProviderBaseUrl } from "./provider-url";
12
+ import { antigravityOAuthDestinationConfigError, isCanonicalAntigravityUrl, providerTlsFetch } from "./provider-tls-profile";
13
+ import { waitForProviderRequestSlot } from "../providers/request-pacing";
12
14
 
13
15
  type ProviderGetInit = Omit<RequestInit, "body" | "method" | "redirect">;
14
16
  type ProviderPostInit = ProviderGetInit & { body: string };
15
- type ProviderOutboundConfig = Pick<OcxProviderConfig, "baseUrl" | "allowPrivateNetwork"> & {
17
+ type ProviderOutboundConfig = Pick<OcxProviderConfig, "baseUrl" | "allowPrivateNetwork"> & Partial<Pick<OcxProviderConfig, "adapter" | "authMode" | "googleMode" | "tlsProfile">> & {
16
18
  fetch?: typeof globalThis.fetch;
17
19
  };
18
20
  export interface ProviderOutboundDependencies {
@@ -40,36 +42,26 @@ function normalizeProxyHostname(hostname: string): string {
40
42
  : normalized;
41
43
  }
42
44
 
43
- function noProxyMatches(url: URL): boolean {
44
- const raw = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
45
- const hostname = normalizeProxyHostname(url.hostname);
46
- const port = url.port || (url.protocol === "https:" ? "443" : "80");
47
- for (const rawEntry of raw.split(",")) {
48
- let entry = rawEntry.trim().toLowerCase();
49
- if (!entry) continue;
50
- if (entry === "*") return true;
51
- entry = entry.replace(/^https?:\/\//, "").split("/", 1)[0]!;
52
-
53
- let entryHost = entry;
54
- let entryPort = "";
55
- const bracketed = /^\[([^\]]+)](?::(\d+))?$/.exec(entry);
56
- if (bracketed) {
57
- entryHost = bracketed[1]!;
58
- entryPort = bracketed[2] ?? "";
59
- } else if ((entry.match(/:/g)?.length ?? 0) === 1) {
60
- const separator = entry.lastIndexOf(":");
61
- const possiblePort = entry.slice(separator + 1);
62
- if (/^\d+$/.test(possiblePort)) {
63
- entryHost = entry.slice(0, separator);
64
- entryPort = possiblePort;
65
- }
66
- }
67
- if (entryPort && entryPort !== port) continue;
68
- entryHost = normalizeProxyHostname(entryHost.replace(/^\*?\./, ""));
69
- if (!entryHost) continue;
70
- if (hostname === entryHost || hostname.endsWith(`.${entryHost}`)) return true;
45
+ function antigravityProfileFetch(
46
+ name: string,
47
+ provider: ProviderOutboundConfig,
48
+ url: string,
49
+ ): typeof globalThis.fetch | undefined {
50
+ if (name !== "google-antigravity"
51
+ || provider.fetch
52
+ || provider.tlsProfile !== "antigravity-browser"
53
+ || provider.adapter !== "google"
54
+ || provider.authMode !== "oauth"
55
+ || provider.googleMode !== "cloud-code-assist"
56
+ || !isCanonicalAntigravityUrl(provider.baseUrl)
57
+ || !isCanonicalAntigravityUrl(url)) {
58
+ return undefined;
71
59
  }
72
- return false;
60
+ const nativeFetch = providerTlsFetch(name, provider as Parameters<typeof providerTlsFetch>[1], globalThis.fetch);
61
+ return (async (input: Parameters<typeof globalThis.fetch>[0], init?: RequestInit): Promise<Response> => {
62
+ await waitForProviderRequestSlot(name, provider as OcxProviderConfig, undefined, init?.signal ?? undefined);
63
+ return nativeFetch(input, init);
64
+ }) as typeof globalThis.fetch;
73
65
  }
74
66
 
75
67
  let proxyBoundaryWarned = false;
@@ -112,11 +104,31 @@ async function providerOutboundRequest(
112
104
  init: ProviderGetInit | ProviderPostInit,
113
105
  dependencies: ProviderOutboundDependencies = {},
114
106
  ): Promise<Response> {
107
+ const antigravityBaseError = antigravityOAuthDestinationConfigError(name, provider);
108
+ if (antigravityBaseError) throw new ProviderOutboundPolicyError(`provider ${name} ${antigravityBaseError}`);
109
+ if (name === "google-antigravity" && !isCanonicalAntigravityUrl(url)) {
110
+ throw new ProviderOutboundPolicyError("provider google-antigravity requires a canonical Antigravity HTTPS destination for OAuth");
111
+ }
115
112
  const postUrl = method === "POST" ? new URL(url) : undefined;
116
113
  if (postUrl?.protocol !== undefined && postUrl.protocol !== "https:") {
117
114
  throw new ProviderOutboundPolicyError("provider POST URL must use HTTPS");
118
115
  }
119
- if (provider.fetch) {
116
+ const profiledFetch = antigravityProfileFetch(name, provider, url);
117
+ const fetchOverride = provider.fetch ?? profiledFetch;
118
+ if (profiledFetch) {
119
+ // Keep the profiled executor behind the same pre-dispatch DNS boundary as
120
+ // the pinned path. A proxy owns peer selection, so match the existing
121
+ // providerOutbound proxy boundary and deliberately skip local resolution.
122
+ if (!proxyForUrl(url)) {
123
+ const resolveAddresses = dependencies.resolveAddresses ?? resolvePublicAddresses;
124
+ await resolveAddresses(url, {
125
+ context: "provider URL",
126
+ allowPrivateNetwork: false,
127
+ });
128
+ }
129
+ return fetchOverride!(url, { ...init, method, redirect: "manual" });
130
+ }
131
+ if (fetchOverride) {
120
132
  // A caller-owned executor cannot be peer-pinned here. This branch keeps literal/config
121
133
  // checks and redirect blocking, but does not provide the resolved-address guarantees of
122
134
  // the built-in transport. Main-request migration must define that executor contract first.
@@ -135,7 +147,7 @@ async function providerOutboundRequest(
135
147
  });
136
148
  if (destinationError) throw new ProviderOutboundPolicyError(destinationError);
137
149
  }
138
- return provider.fetch(url, { ...init, method, redirect: "manual" });
150
+ return fetchOverride(url, { ...init, method, redirect: "manual" });
139
151
  }
140
152
  const parsed = postUrl ?? new URL(url);
141
153
  const proxyConfigured = configuredProxyFor();
@@ -0,0 +1,309 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { OcxProviderConfig, ProviderTlsProfile } from "../types";
3
+ import { resolvePublicAddresses } from "./destination-policy";
4
+ import { registerOptionalShutdownHook } from "./optional-shutdown-hooks";
5
+ import { proxyForUrl } from "./proxy-env";
6
+ import { redactErrorMessage } from "./redact";
7
+
8
+ export const ANTIGRAVITY_TLS_HOSTS = new Set([
9
+ "daily-cloudcode-pa.googleapis.com",
10
+ "cloudcode-pa.googleapis.com",
11
+ ]);
12
+
13
+ export type ProviderTlsProfileStatus = "disabled" | "active" | "fallback";
14
+
15
+ interface WreqTransport {
16
+ close(): Promise<void>;
17
+ }
18
+
19
+ interface WreqModule {
20
+ createTransport(options: {
21
+ browser: "chrome_142";
22
+ os: "windows" | "macos" | "linux";
23
+ proxy?: string;
24
+ }): Promise<WreqTransport>;
25
+ fetch(input: string | URL | Request, init?: Record<string, unknown>): Promise<unknown>;
26
+ }
27
+
28
+ interface InitializedTransport {
29
+ module: WreqModule;
30
+ transport: WreqTransport;
31
+ }
32
+
33
+ type ProviderTlsContract = Pick<OcxProviderConfig, "adapter" | "authMode" | "googleMode" | "baseUrl" | "tlsProfile">;
34
+
35
+ type AntigravityOAuthDestinationContract = Pick<OcxProviderConfig, "baseUrl"> & Partial<Pick<OcxProviderConfig, "adapter" | "authMode" | "googleMode">>;
36
+
37
+ export interface ProviderTlsRuntimeForTest {
38
+ importWreq: () => Promise<WreqModule>;
39
+ resolveDestination?: typeof resolvePublicAddresses;
40
+ }
41
+
42
+ const defaultRuntime: ProviderTlsRuntimeForTest = {
43
+ importWreq: async () => await import("wreq-js") as unknown as WreqModule,
44
+ };
45
+
46
+ let runtime = defaultRuntime;
47
+ const statusByProvider = new Map<string, { status: ProviderTlsProfileStatus; fingerprint: string }>();
48
+ const transports = new Map<string, Promise<InitializedTransport | undefined>>();
49
+ let shutdownDetach: (() => void) | undefined;
50
+ let fallbackWarned = false;
51
+
52
+ function providerTlsContractFingerprint(providerName: string, provider: ProviderTlsContract): string {
53
+ return createHash("sha256").update(JSON.stringify([
54
+ providerName,
55
+ provider.adapter,
56
+ provider.authMode,
57
+ provider.googleMode,
58
+ provider.baseUrl,
59
+ provider.tlsProfile,
60
+ ])).digest("hex");
61
+ }
62
+
63
+ function setStatus(providerName: string, provider: ProviderTlsContract, status: ProviderTlsProfileStatus): void {
64
+ statusByProvider.set(providerName, { status, fingerprint: providerTlsContractFingerprint(providerName, provider) });
65
+ }
66
+
67
+ const SAFE_NATIVE_ERROR_NAMES = new Set(["AbortError", "TimeoutError"]);
68
+ const SAFE_NATIVE_ERROR_CODES = new Set([
69
+ "ABORT_ERR", "ECONNREFUSED", "ECONNRESET", "EAI_AGAIN", "ENETUNREACH", "ENOTFOUND",
70
+ "ETIMEDOUT", "UND_ERR_CONNECT_TIMEOUT", "UND_ERR_SOCKET",
71
+ ]);
72
+
73
+ function safeProviderTlsError(error: unknown): Error {
74
+ const raw = error instanceof Error ? error.message : String(error);
75
+ const safe = new Error(redactErrorMessage(raw));
76
+ if (!error || typeof error !== "object") return safe;
77
+ const native = error as { name?: unknown; code?: unknown };
78
+ if (typeof native.name === "string" && SAFE_NATIVE_ERROR_NAMES.has(native.name)) {
79
+ safe.name = native.name;
80
+ }
81
+ if (typeof native.code === "string" && SAFE_NATIVE_ERROR_CODES.has(native.code)) {
82
+ Object.defineProperty(safe, "code", {
83
+ configurable: true,
84
+ enumerable: false,
85
+ value: native.code,
86
+ writable: false,
87
+ });
88
+ }
89
+ return safe;
90
+ }
91
+
92
+ function warnFallbackOnce(): void {
93
+ if (fallbackWarned) return;
94
+ fallbackWarned = true;
95
+ console.warn("[opencodex] Antigravity TLS profile requested → fallback to Bun fetch; native transport initialization failed");
96
+ }
97
+
98
+ function emulationOs(): "windows" | "macos" | "linux" {
99
+ if (process.platform === "win32") return "windows";
100
+ if (process.platform === "darwin") return "macos";
101
+ if (process.platform === "linux") return "linux";
102
+ throw new Error("unsupported host operating system for Antigravity TLS profile");
103
+ }
104
+
105
+ function profileIsSupported(providerName: string, provider: Pick<OcxProviderConfig, "adapter" | "authMode" | "googleMode" | "baseUrl" | "tlsProfile">): boolean {
106
+ return providerName === "google-antigravity"
107
+ && provider.adapter === "google"
108
+ && provider.authMode === "oauth"
109
+ && provider.googleMode === "cloud-code-assist"
110
+ && isCanonicalAntigravityUrl(provider.baseUrl)
111
+ && provider.tlsProfile === "antigravity-browser";
112
+ }
113
+
114
+ export function providerTlsProfileConfigError(
115
+ providerName: string,
116
+ provider: Pick<OcxProviderConfig, "adapter" | "authMode" | "googleMode" | "baseUrl" | "tlsProfile">,
117
+ ): string | null {
118
+ if (provider.tlsProfile === undefined) return null;
119
+ if (provider.tlsProfile !== "antigravity-browser") {
120
+ return "tlsProfile must be antigravity-browser";
121
+ }
122
+ if (providerName !== "google-antigravity") return "tlsProfile antigravity-browser is valid only for google-antigravity";
123
+ if (provider.adapter !== "google" || provider.authMode !== "oauth") {
124
+ return "tlsProfile antigravity-browser requires Google OAuth authentication";
125
+ }
126
+ if (provider.googleMode !== "cloud-code-assist") {
127
+ return "tlsProfile antigravity-browser requires Google Cloud Code Assist mode";
128
+ }
129
+ if (!isCanonicalAntigravityUrl(provider.baseUrl)) {
130
+ return "tlsProfile antigravity-browser requires a canonical Antigravity HTTPS destination";
131
+ }
132
+ return null;
133
+ }
134
+
135
+ export function isCanonicalAntigravityUrl(input: string | URL): boolean {
136
+ try {
137
+ const url = new URL(input);
138
+ return url.protocol === "https:"
139
+ && (url.port === "" || url.port === "443")
140
+ && url.username === ""
141
+ && url.password === ""
142
+ && ANTIGRAVITY_TLS_HOSTS.has(url.hostname.toLowerCase());
143
+ } catch {
144
+ return false;
145
+ }
146
+ }
147
+
148
+ export function isAntigravityOAuthProvider(
149
+ providerName: string,
150
+ provider: Pick<OcxProviderConfig, "authMode">,
151
+ ): boolean {
152
+ return providerName === "google-antigravity"
153
+ && (provider.authMode === undefined || provider.authMode === "oauth");
154
+ }
155
+
156
+ /** Same-name Antigravity rows are OAuth destinations even when legacy config omitted authMode. */
157
+ export function antigravityOAuthDestinationConfigError(
158
+ providerName: string,
159
+ provider: AntigravityOAuthDestinationContract,
160
+ ): string | null {
161
+ if (providerName !== "google-antigravity") return null;
162
+ if (provider.googleMode !== undefined && provider.googleMode !== "cloud-code-assist") {
163
+ return "requires Google Cloud Code Assist mode for OAuth";
164
+ }
165
+ if (!isCanonicalAntigravityUrl(provider.baseUrl)) {
166
+ return "requires a canonical Antigravity HTTPS destination for OAuth";
167
+ }
168
+ return null;
169
+ }
170
+
171
+ export function getProviderTlsProfileStatus(providerName: string, provider: ProviderTlsContract): ProviderTlsProfileStatus {
172
+ const current = statusByProvider.get(providerName);
173
+ return current?.fingerprint === providerTlsContractFingerprint(providerName, provider)
174
+ ? current.status
175
+ : "disabled";
176
+ }
177
+
178
+ export function clearProviderTlsProfileStatus(providerName: string): void {
179
+ statusByProvider.delete(providerName);
180
+ }
181
+
182
+ function registerShutdownHook(): void {
183
+ if (shutdownDetach) return;
184
+ shutdownDetach = registerOptionalShutdownHook("provider-tls-profile", () => {
185
+ const pending = [...transports.values()];
186
+ transports.clear();
187
+ shutdownDetach = undefined;
188
+ for (const transport of pending) {
189
+ void transport.then(value => value?.transport.close()).catch(() => undefined);
190
+ }
191
+ });
192
+ }
193
+
194
+ function transportKey(proxy: string | undefined): string {
195
+ return `${process.platform}:${proxy ?? "direct"}`;
196
+ }
197
+
198
+ async function getTransport(proxy: string | undefined): Promise<InitializedTransport | undefined> {
199
+ const key = transportKey(proxy);
200
+ const existing = transports.get(key);
201
+ if (existing) return existing;
202
+ const pending = (async () => {
203
+ let transport: WreqTransport | undefined;
204
+ try {
205
+ const module = await runtime.importWreq();
206
+ transport = await module.createTransport({
207
+ browser: "chrome_142",
208
+ os: emulationOs(),
209
+ ...(proxy ? { proxy } : {}),
210
+ });
211
+ registerShutdownHook();
212
+ return { module, transport };
213
+ } catch {
214
+ if (transport) await transport.close().catch(() => undefined);
215
+ return undefined;
216
+ }
217
+ })();
218
+ transports.set(key, pending);
219
+ return pending;
220
+ }
221
+
222
+ export function providerTlsFetch(
223
+ providerName: string,
224
+ provider: Pick<OcxProviderConfig, "adapter" | "authMode" | "googleMode" | "baseUrl" | "tlsProfile">,
225
+ bunFetch: typeof globalThis.fetch,
226
+ ): typeof globalThis.fetch {
227
+ const antigravityError = antigravityOAuthDestinationConfigError(providerName, provider);
228
+ if (antigravityError) {
229
+ return (async () => {
230
+ throw new Error(`provider ${providerName} ${antigravityError}`);
231
+ }) as unknown as typeof globalThis.fetch;
232
+ }
233
+ if (provider.tlsProfile === undefined) {
234
+ setStatus(providerName, provider, "disabled");
235
+ return bunFetch;
236
+ }
237
+ if (!profileIsSupported(providerName, provider)) {
238
+ setStatus(providerName, provider, "fallback");
239
+ warnFallbackOnce();
240
+ return bunFetch;
241
+ }
242
+ return (async (input: string | URL | Request, init?: RequestInit): Promise<Response> => {
243
+ const url = typeof input === "string" || input instanceof URL ? input : input.url;
244
+ if (!isCanonicalAntigravityUrl(url)) return bunFetch(input, init);
245
+ const proxy = proxyForUrl(url);
246
+ // A direct profiled request must pass the same public-destination gate as the
247
+ // ordinary provider executor before wreq can send its bearer-bearing request.
248
+ // Proxied requests deliberately leave peer selection to the configured proxy,
249
+ // matching providerOutbound's proxy boundary (and its inability to pin a peer).
250
+ if (!proxy) {
251
+ await (runtime.resolveDestination ?? resolvePublicAddresses)(String(url), {
252
+ context: "Antigravity profile URL",
253
+ allowPrivateNetwork: false,
254
+ });
255
+ }
256
+ const initialized = await getTransport(proxy);
257
+ if (!initialized) {
258
+ setStatus(providerName, provider, "fallback");
259
+ warnFallbackOnce();
260
+ return bunFetch(input, { ...init, redirect: "manual" });
261
+ }
262
+ setStatus(providerName, provider, "active");
263
+ const wreqInit = {
264
+ ...init,
265
+ transport: initialized.transport,
266
+ disableDefaultHeaders: true,
267
+ cookieMode: "ephemeral" as const,
268
+ redirect: "manual" as const,
269
+ };
270
+ try {
271
+ return await initialized.module.fetch(input, wreqInit) as Response;
272
+ } catch (error) {
273
+ // Native errors are post-dispatch failures. Redact at this boundary and
274
+ // never replay through Bun, since generation may already have started.
275
+ throw safeProviderTlsError(error);
276
+ }
277
+ }) as typeof globalThis.fetch;
278
+ }
279
+
280
+ export function setProviderTlsRuntimeForTest(next: ProviderTlsRuntimeForTest | undefined): void {
281
+ if (next === undefined) {
282
+ runtime = defaultRuntime;
283
+ return;
284
+ }
285
+ // Test callers normally replace only the native module. Keep those tests
286
+ // deterministic in offline sandboxes while allowing explicit DNS policy
287
+ // regressions to inject a rejecting resolver.
288
+ runtime = {
289
+ ...defaultRuntime,
290
+ resolveDestination: async (url: string) => ({
291
+ hostname: new URL(url).hostname,
292
+ addresses: [{ address: "142.250.1.1", family: 4 }],
293
+ privateNetwork: false,
294
+ }),
295
+ ...next,
296
+ };
297
+ }
298
+
299
+ export function resetProviderTlsProfileForTests(): void {
300
+ for (const pending of transports.values()) void pending.then(value => value?.transport.close()).catch(() => undefined);
301
+ transports.clear();
302
+ shutdownDetach?.();
303
+ shutdownDetach = undefined;
304
+ statusByProvider.clear();
305
+ fallbackWarned = false;
306
+ runtime = defaultRuntime;
307
+ }
308
+
309
+ export type { ProviderTlsProfile };
@@ -16,3 +16,52 @@ export function outboundProxyConfigured(
16
16
  ): boolean {
17
17
  return OUTBOUND_PROXY_ENV_KEYS.some(key => proxyEnvPresent(key, env));
18
18
  }
19
+
20
+ function proxyValue(key: ProxyEnvKey, env: ProxyEnvMap): string | undefined {
21
+ const value = env[key]?.trim() || env[key.toLowerCase()]?.trim();
22
+ return value || undefined;
23
+ }
24
+
25
+ export function noProxyMatches(url: URL, env: ProxyEnvMap = process.env): boolean {
26
+ const raw = (env.NO_PROXY ?? env.no_proxy ?? "").trim();
27
+ const hostname = url.hostname.trim().toLowerCase().replace(/^\[|\]$/g, "").replace(/\.+$/, "");
28
+ const port = url.port || (url.protocol === "https:" ? "443" : "80");
29
+ for (const value of raw.split(",")) {
30
+ let entry = value.trim().toLowerCase();
31
+ if (!entry) continue;
32
+ if (entry === "*") return true;
33
+ entry = entry.replace(/^https?:\/\//, "").split("/", 1)[0]!;
34
+ let entryHost = entry;
35
+ let entryPort = "";
36
+ const bracketed = /^\[([^\]]+)](?::(\d+))?$/.exec(entry);
37
+ if (bracketed) {
38
+ entryHost = bracketed[1]!;
39
+ entryPort = bracketed[2] ?? "";
40
+ } else if ((entry.match(/:/g)?.length ?? 0) === 1) {
41
+ const separator = entry.lastIndexOf(":");
42
+ const possiblePort = entry.slice(separator + 1);
43
+ if (/^\d+$/.test(possiblePort)) {
44
+ entryHost = entry.slice(0, separator);
45
+ entryPort = possiblePort;
46
+ }
47
+ }
48
+ if (entryPort && entryPort !== port) continue;
49
+ entryHost = entryHost.replace(/^\*?\./, "").replace(/^\[|\]$/g, "").replace(/\.+$/, "");
50
+ if (entryHost && (hostname === entryHost || hostname.endsWith(`.${entryHost}`))) return true;
51
+ }
52
+ return false;
53
+ }
54
+
55
+ /** Select the standard proxy for a URL without exposing its credentials. */
56
+ export function proxyForUrl(url: string | URL, env: ProxyEnvMap = process.env): string | undefined {
57
+ let parsed: URL;
58
+ try {
59
+ parsed = new URL(url);
60
+ } catch {
61
+ return undefined;
62
+ }
63
+ if (noProxyMatches(parsed, env)) return undefined;
64
+ if (parsed.protocol === "https:") return proxyValue("HTTPS_PROXY", env) ?? proxyValue("ALL_PROXY", env);
65
+ if (parsed.protocol === "http:") return proxyValue("HTTP_PROXY", env) ?? proxyValue("ALL_PROXY", env);
66
+ return undefined;
67
+ }
package/src/lib/redact.ts CHANGED
@@ -490,10 +490,19 @@ export function redactUrlForLog(url: string): string {
490
490
  parsed.hash = "";
491
491
  return parsed.toString();
492
492
  } catch {
493
- return redactSecretString(url.split("?")[0] ?? url);
493
+ const stripped = (url.split(/[?#]/)[0] ?? url).replace(/:\/\/[^@\s]+@/g, "://");
494
+ return redactSecretString(stripped);
494
495
  }
495
496
  }
496
497
 
498
+ /** Redact credentials and userinfo from arbitrary diagnostic text. */
499
+ export function redactErrorMessage(value: string): string {
500
+ return redactSecretString(value).replace(
501
+ /[a-z][a-z0-9+.-]*:\/\/[^\s"'<>]+/gi,
502
+ match => redactUrlForLog(match),
503
+ );
504
+ }
505
+
497
506
  const USER_HOME_PATH_PATTERNS: Array<[RegExp, string]> = [
498
507
  // Windows: C:\Users\<name>\... -> C:\Users\[USER]\...
499
508
  [/([A-Za-z]:\\Users\\)[^\\/]+/gi, "$1[USER]"],
@@ -6,6 +6,7 @@ import {
6
6
  import { reconcileCodexReauthState } from "../codex/account-runtime-state";
7
7
  import { reconcileCatalogWarningMemos } from "../codex/catalog/aggregation";
8
8
  import { reconcileProviderFetchWarnings } from "../codex/catalog/provider-fetch";
9
+ import { reconcileModelMetadataSnapshot } from "../codex/catalog/model-metadata";
9
10
  import { reconcileModelCacheGeneration } from "../codex/model-cache";
10
11
  import { reconcilePoolRotationState } from "../codex/pool-rotation";
11
12
  import { reconcileCodexQuotaAccounts } from "../codex/quota";
@@ -103,6 +104,7 @@ export const STATE_STORE_REGISTRATIONS = [
103
104
  { name: "provider-quota-history", reconcileGeneration: reconcileProviderAccountQuotaRows },
104
105
  { name: "codex-routing-health", reconcileGeneration: reconcileCodexRoutingHealth },
105
106
  { name: "model-cache-history", reconcileGeneration: reconcileModelCacheGeneration },
107
+ { name: "model-metadata-snapshot", reconcileGeneration: reconcileModelMetadataSnapshot },
106
108
  { name: "pool-rotation", reconcileGeneration: reconcilePoolRotationState },
107
109
  { name: "combo-rotation", reconcileGeneration: reconcileComboRotationState },
108
110
  { name: "guardian-backoff", reconcileGeneration: reconcileGuardianBackoff },
@@ -59,6 +59,35 @@ function declaresString(schema: SchemaNode): boolean {
59
59
  return Array.isArray(type) && type.includes("string");
60
60
  }
61
61
 
62
+ // Issue #2316: Codex advertises `multi_agent_v1__wait_agent`'s `timeout_ms` as a JSON
63
+ // Schema `number`, but its Rust runtime deserializes the field as `u64` and rejects
64
+ // `120000.0` with "invalid type: floating point `120000.0`, expected u64" before the
65
+ // tool ever runs. The schema lookup is not the problem — the error text comes from
66
+ // Codex's own deserializer, so the call reached it — the problem is that `number`
67
+ // alone never authorizes the integral-float repair below.
68
+ //
69
+ // This allowlist is deliberately one field wide. It names only what has a live
70
+ // reproduction against a real `u64`, because the repair is only unambiguous for a
71
+ // field that cannot legitimately hold a fraction. Generic names (`start`, `end`,
72
+ // `priority`, `port`) would silently rewrite a third-party tool's fractional value,
73
+ // so a new entry needs its own evidence, not a plausible-sounding name.
74
+ //
75
+ // Cursor's sibling `yield_time_ms` (src/adapters/cursor/tool-definitions.ts) is also
76
+ // declared `number`; it is NOT included here because no rejection has been captured
77
+ // against it. It gets its own change when it gets its own reproduction.
78
+ const U64_NUMBER_FIELDS = new Set(["timeout_ms"]);
79
+
80
+ // Issue #2443 / #2451: Codex Desktop's bare `wait` tool has the same schema/runtime
81
+ // split for `yield_time_ms` and `max_tokens`. Scope these names to that bare tool so a
82
+ // third-party or namespaced tool can still use fractional values legitimately.
83
+ // #2448 allowlisted the hyphenated `yield-time_ms`; live Grok 4.6 calls and the
84
+ // advertised wait schema both use the underscore form, so that name is the one
85
+ // with a captured u64 rejection. Cursor still uses the same underscore name on a
86
+ // namespaced tool; the wait-only map keeps that path byte-identical.
87
+ const U64_NUMBER_FIELDS_BY_TOOL = new Map<string, ReadonlySet<string>>([
88
+ ["wait", new Set(["yield_time_ms", "max_tokens"])],
89
+ ]);
90
+
62
91
  /** True when the node accepts a JSON number (`integer` or `number`), so a numeric
63
92
  * value is already schema-valid and must not be rewritten into a string. */
64
93
  function declaresNumeric(schema: SchemaNode): boolean {
@@ -118,7 +147,14 @@ interface CoerceResult {
118
147
  changed: boolean;
119
148
  }
120
149
 
121
- function coerceValue(value: unknown, schema: SchemaNode | undefined, root: SchemaNode, depth: number): CoerceResult {
150
+ function coerceValue(
151
+ value: unknown,
152
+ schema: SchemaNode | undefined,
153
+ root: SchemaNode,
154
+ depth: number,
155
+ toolName?: string,
156
+ propertyName?: string,
157
+ ): CoerceResult {
122
158
  // A hostile or deeply nested schema must not blow the stack.
123
159
  if (depth > 64) return { value, changed: false };
124
160
  const resolved = schema ? resolveRef(schema, root, new Set()) : undefined;
@@ -126,7 +162,19 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
126
162
  if (typeof value === "number") {
127
163
  if (!resolved) return { value, changed: false };
128
164
  const branches = compositionBranches(resolved);
129
- const integerDeclared = declaresInteger(resolved) || branches.some(declaresInteger);
165
+ // #2316: a known Codex-native u64 field counts as integer-declared even when the
166
+ // advertised schema says `number`, but only when the field really is numeric —
167
+ // an allowlisted name over a string-typed field is a disagreement, not a repair.
168
+ const nativeU64Field = propertyName !== undefined
169
+ && (
170
+ U64_NUMBER_FIELDS.has(propertyName)
171
+ || U64_NUMBER_FIELDS_BY_TOOL.get(toolName ?? "")?.has(propertyName) === true
172
+ );
173
+ const nativeU64Declared = nativeU64Field
174
+ && (declaresNumeric(resolved) || branches.some(declaresNumeric));
175
+ const integerDeclared = declaresInteger(resolved)
176
+ || branches.some(declaresInteger)
177
+ || nativeU64Declared;
130
178
  if (!integerDeclared && safelyIntegral(value)) {
131
179
  // Issue #1938: a bare integer in a string-only field has exactly one faithful
132
180
  // string reading. A field that also accepts a numeric type keeps the number.
@@ -148,7 +196,9 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
148
196
  const itemSchema = resolved ? asSchema(resolved.items) : undefined;
149
197
  let changed = false;
150
198
  const next = value.map(entry => {
151
- const result = coerceValue(entry, itemSchema, root, depth + 1);
199
+ // Array items have no property name of their own; passing the array's key would
200
+ // let `timeout_ms: [1.5]` inherit the allowlist. Items are judged by schema only.
201
+ const result = coerceValue(entry, itemSchema, root, depth + 1, toolName);
152
202
  if (result.changed) changed = true;
153
203
  return result.value;
154
204
  });
@@ -164,7 +214,7 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
164
214
  const next: Record<string, unknown> = {};
165
215
  for (const [key, entry] of Object.entries(object)) {
166
216
  const childSchema = asSchema(properties?.[key]) ?? additional;
167
- const result = coerceValue(entry, childSchema, root, depth + 1);
217
+ const result = coerceValue(entry, childSchema, root, depth + 1, toolName, key);
168
218
  if (result.changed) changed = true;
169
219
  next[key] = result.value;
170
220
  }
@@ -182,6 +232,7 @@ function coerceValue(value: unknown, schema: SchemaNode | undefined, root: Schem
182
232
  export function coerceIntegerToolArguments(
183
233
  args: string,
184
234
  parameters: Record<string, unknown> | undefined,
235
+ toolName?: string,
185
236
  ): string {
186
237
  if (!parameters || !args) return args;
187
238
  // Cheap reject: a payload with no digit cannot need either repair (integral-float
@@ -196,7 +247,7 @@ export function coerceIntegerToolArguments(
196
247
  return args;
197
248
  }
198
249
  const root = parameters as SchemaNode;
199
- const result = coerceValue(parsed, root, root, 0);
250
+ const result = coerceValue(parsed, root, root, 0, toolName);
200
251
  if (!result.changed) return args;
201
252
  return JSON.stringify(result.value);
202
253
  }