@bitkyc08/opencodex 2.58.0 → 2.59.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 (188) hide show
  1. package/README.md +28 -10
  2. package/gui/dist/assets/index-C5IebErG.js +136 -0
  3. package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/crusoe.svg +1 -0
  6. package/gui/dist/provider-icons/opper.svg +3 -0
  7. package/package.json +1 -1
  8. package/src/adapters/base.ts +11 -1
  9. package/src/adapters/cursor/catalog.ts +11 -0
  10. package/src/adapters/cursor/effort-map.ts +16 -2
  11. package/src/adapters/cursor/envelope-echo.ts +55 -2
  12. package/src/adapters/cursor/message-mapper.ts +3 -2
  13. package/src/adapters/cursor/protobuf-request.ts +8 -5
  14. package/src/adapters/cursor/request-builder.ts +14 -3
  15. package/src/adapters/cursor/thread-continuity.ts +105 -31
  16. package/src/adapters/cursor/tool-guidance.ts +5 -4
  17. package/src/adapters/cursor.ts +42 -1
  18. package/src/adapters/devin/cloud-direct/chat.ts +11 -2
  19. package/src/adapters/devin/cloud-direct/index.ts +7 -0
  20. package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
  21. package/src/adapters/devin.ts +75 -13
  22. package/src/adapters/google-antigravity-wire.ts +29 -2
  23. package/src/adapters/google-http.ts +8 -1
  24. package/src/adapters/google.ts +23 -4
  25. package/src/adapters/openai-chat/response-events.ts +61 -0
  26. package/src/adapters/openai-chat.ts +5 -10
  27. package/src/adapters/openai-responses/passthrough.ts +10 -1
  28. package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
  29. package/src/adapters/openai-responses/tool-schema.ts +19 -7
  30. package/src/adapters/responses-tool-schema.ts +76 -46
  31. package/src/adapters/run-turn-queue.ts +17 -4
  32. package/src/bridge/response-json.ts +1 -1
  33. package/src/bridge/sse.ts +165 -24
  34. package/src/claude/context-windows.ts +22 -0
  35. package/src/claude/outbound.ts +35 -4
  36. package/src/cli/account-api.ts +4 -3
  37. package/src/cli/account-extended.ts +22 -2
  38. package/src/cli/account-orca-import.ts +63 -0
  39. package/src/cli/account.ts +32 -4
  40. package/src/cli/capabilities.ts +40 -0
  41. package/src/cli/claude.ts +29 -1
  42. package/src/cli/codex-cli-update.ts +97 -2
  43. package/src/cli/dispatch.ts +54 -0
  44. package/src/cli/doctor.ts +197 -2
  45. package/src/cli/help.ts +4 -1
  46. package/src/cli/index.ts +88 -20
  47. package/src/cli/models-runtime.ts +33 -4
  48. package/src/cli/registry.ts +11 -1
  49. package/src/cli/runtime-api.ts +44 -0
  50. package/src/cli/start-args.ts +94 -0
  51. package/src/cli/system-command.ts +2 -0
  52. package/src/client/machine-api.ts +4 -3
  53. package/src/client/machine-listener.ts +14 -1
  54. package/src/clients/config-export/constants.ts +2 -3
  55. package/src/clients/config-export.ts +5 -5
  56. package/src/codex/account-store.ts +81 -5
  57. package/src/codex/auth-api/pool-quota-probe.ts +14 -3
  58. package/src/codex/auth-api/routes.ts +17 -2
  59. package/src/codex/auth-context.ts +16 -12
  60. package/src/codex/catalog/build-entries.ts +25 -4
  61. package/src/codex/catalog/derive-entry.ts +8 -1
  62. package/src/codex/catalog/effort.ts +10 -6
  63. package/src/codex/catalog/gather-capture.ts +1 -0
  64. package/src/codex/catalog/model-hints.ts +37 -5
  65. package/src/codex/catalog/parsing.ts +83 -5
  66. package/src/codex/catalog/reserve-warn.ts +96 -0
  67. package/src/codex/catalog/retained-sync.ts +19 -0
  68. package/src/codex/catalog/routed-gather.ts +42 -3
  69. package/src/codex/cli-installation-identity.ts +210 -0
  70. package/src/codex/cli-installation-targets.ts +158 -0
  71. package/src/codex/convergence.ts +5 -0
  72. package/src/codex/history-provider.ts +4 -1
  73. package/src/codex/history-state-open.ts +105 -0
  74. package/src/codex/inject/config-toml.ts +44 -2
  75. package/src/codex/inject.ts +3 -2
  76. package/src/codex/lineage.ts +83 -32
  77. package/src/codex/loopback-target.ts +31 -0
  78. package/src/codex/main-account-hard-lock.ts +2 -1
  79. package/src/codex/main-account.ts +10 -3
  80. package/src/codex/main-device-reauth.ts +17 -9
  81. package/src/codex/model-entitlements.ts +60 -1
  82. package/src/codex/observed-model-denials.ts +137 -0
  83. package/src/codex/orca-auth-source.ts +94 -0
  84. package/src/codex/orca-import.ts +219 -0
  85. package/src/codex/prompt-text-probe.ts +282 -12
  86. package/src/codex/quota-401-recovery.ts +12 -0
  87. package/src/codex/quota-types.ts +65 -0
  88. package/src/codex/quota.ts +24 -19
  89. package/src/codex/routing/cooldown-math.ts +8 -47
  90. package/src/codex/routing/pin-drain.ts +57 -0
  91. package/src/codex/routing.ts +13 -15
  92. package/src/codex/subagent-model-fallback.ts +94 -0
  93. package/src/codex/windows-installation-files.ts +224 -0
  94. package/src/combos/failover.ts +122 -5
  95. package/src/config/diagnostics.ts +21 -0
  96. package/src/config/load-degrade.ts +15 -0
  97. package/src/config/pending-teardown.ts +8 -0
  98. package/src/config/process-state.ts +36 -3
  99. package/src/config/provider-relative-send-path.ts +16 -0
  100. package/src/config/proxy-env.ts +23 -5
  101. package/src/config/schema/config-schema.ts +21 -0
  102. package/src/config/schema/leaf-validators.ts +64 -17
  103. package/src/generated/compatibility-version.json +235 -163
  104. package/src/generated/model-metadata.ts +1 -1
  105. package/src/lib/bounded-body.ts +4 -2
  106. package/src/lib/destination-policy.ts +48 -6
  107. package/src/lib/errors.ts +3 -15
  108. package/src/lib/local-destinations.ts +32 -5
  109. package/src/lib/provider-outbound.ts +3 -3
  110. package/src/lib/proxy-env.ts +70 -3
  111. package/src/lib/request-execution-budget.ts +11 -3
  112. package/src/lib/response-body-inactivity.ts +193 -0
  113. package/src/lib/retry-delay.ts +69 -0
  114. package/src/lib/socks5-fetch.ts +631 -0
  115. package/src/lib/spend-reservation-ledger.ts +115 -9
  116. package/src/lib/workflow-budget.ts +145 -8
  117. package/src/oauth/account-quota-rank.ts +72 -15
  118. package/src/oauth/generic-account-failover.ts +40 -27
  119. package/src/oauth/orcarouter.ts +15 -2
  120. package/src/oauth/store.ts +8 -0
  121. package/src/providers/codex-capacity.ts +9 -0
  122. package/src/providers/devin-provider-merge-migration.ts +33 -12
  123. package/src/providers/free-directory.ts +20 -2
  124. package/src/providers/key-failover.ts +261 -7
  125. package/src/providers/model-rename-migration.ts +1 -0
  126. package/src/providers/openai-sidecar.ts +4 -0
  127. package/src/providers/opencode-go-transport.ts +14 -5
  128. package/src/providers/quota/report-cache.ts +3 -0
  129. package/src/providers/registry/entries-extended.ts +96 -0
  130. package/src/providers/registry/model-seeds.ts +78 -21
  131. package/src/responses/apply-patch-envelope.ts +44 -11
  132. package/src/responses/bridge-search-replay-cache.ts +152 -0
  133. package/src/responses/code-mode-helper-compat.ts +26 -16
  134. package/src/responses/custom-tool-compat.ts +1 -1
  135. package/src/responses/hosted-tool-policy.ts +85 -2
  136. package/src/responses/schema.ts +9 -2
  137. package/src/server/auth-cors.ts +26 -0
  138. package/src/server/chat-completions.ts +9 -4
  139. package/src/server/chat-native-sse.ts +26 -9
  140. package/src/server/chat-native.ts +10 -4
  141. package/src/server/claude-messages.ts +24 -2
  142. package/src/server/gui-static.ts +36 -2
  143. package/src/server/inbound-body-admission.ts +187 -0
  144. package/src/server/index.ts +15 -19
  145. package/src/server/management/api-access.ts +3 -4
  146. package/src/server/management/config-routes.ts +31 -6
  147. package/src/server/management/provider-capability-config.ts +35 -7
  148. package/src/server/management/provider-routes.ts +70 -18
  149. package/src/server/proxy-liveness.ts +97 -2
  150. package/src/server/relay.ts +17 -24
  151. package/src/server/request-log.ts +25 -1
  152. package/src/server/responses/adapter-continuation.ts +71 -27
  153. package/src/server/responses/adapter-delivery.ts +39 -8
  154. package/src/server/responses/adapter-dispatch.ts +52 -24
  155. package/src/server/responses/compact.ts +60 -11
  156. package/src/server/responses/core-codex-account.ts +83 -22
  157. package/src/server/responses/core-normalize.ts +12 -5
  158. package/src/server/responses/fetch-helpers.ts +68 -2
  159. package/src/server/responses/passthrough-delivery.ts +10 -1
  160. package/src/server/responses/passthrough-dispatch.ts +113 -48
  161. package/src/server/responses/passthrough-execution.ts +11 -1
  162. package/src/server/responses/request-prepare.ts +29 -0
  163. package/src/server/responses/request-send-budget.ts +84 -7
  164. package/src/server/responses/request-sidecar-auth.ts +16 -8
  165. package/src/server/responses/request-spend.ts +38 -9
  166. package/src/server/responses/request-transport.ts +13 -10
  167. package/src/server/responses/run-turn-execution.ts +20 -5
  168. package/src/server/responses/sidecar-execution.ts +2 -0
  169. package/src/server/responses/ws-upstream.ts +2 -1
  170. package/src/server/responses-custom-tool-repair.ts +2 -2
  171. package/src/server/sse-frame-buffer.ts +12 -10
  172. package/src/server/sse-payload-rewrite.ts +36 -9
  173. package/src/server/system-env-shell.ts +5 -1
  174. package/src/server/system-env.ts +7 -1
  175. package/src/server/workflow-refusal.ts +56 -2
  176. package/src/service/cli.ts +16 -6
  177. package/src/service/guards.ts +10 -0
  178. package/src/service/health.ts +43 -0
  179. package/src/service/state.ts +7 -2
  180. package/src/types/accounts.ts +4 -0
  181. package/src/types/config.ts +100 -3
  182. package/src/types/provider.ts +19 -0
  183. package/src/types/request.ts +7 -1
  184. package/src/types/wire.ts +9 -1
  185. package/src/usage/expected-prices.ts +28 -0
  186. package/src/usage/log.ts +87 -4
  187. package/src/web-search/passthrough-bridge.ts +39 -5
  188. package/gui/dist/assets/index-BbrHOIY0.js +0 -128
@@ -0,0 +1,224 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { Library, Symbols } from "bun:ffi";
3
+
4
+ const KERNEL_SYMBOLS = {
5
+ GetDriveTypeW: { args: ["ptr"], returns: "u32" },
6
+ GetVolumeNameForVolumeMountPointW: { args: ["ptr", "ptr", "u32"], returns: "i32" },
7
+ CreateFileW: { args: ["ptr", "u32", "u32", "ptr", "u32", "u32", "u64"], returns: "u64" },
8
+ GetFileInformationByHandleEx: { args: ["u64", "i32", "ptr", "u32"], returns: "i32" },
9
+ GetFileType: { args: ["u64"], returns: "u32" },
10
+ ReadFile: { args: ["u64", "ptr", "u32", "ptr", "ptr"], returns: "i32" },
11
+ CloseHandle: { args: ["u64"], returns: "i32" },
12
+ } as const satisfies Symbols;
13
+ const NT_SYMBOLS = {
14
+ NtCreateFile: { args: ["ptr", "u32", "ptr", "ptr", "ptr", "u32", "u32", "u32", "u32", "ptr", "u32"], returns: "i32" },
15
+ } as const satisfies Symbols;
16
+
17
+ export interface WindowsInstallationFileRequest {
18
+ readonly path: string;
19
+ readonly maxBytes: number;
20
+ readonly hashOnly?: boolean;
21
+ }
22
+ export interface WindowsInstallationFileIdentity {
23
+ readonly volumeSerial: string;
24
+ readonly fileId: string;
25
+ readonly size: number;
26
+ readonly lastWriteTime: string;
27
+ readonly changeTime: string;
28
+ }
29
+ export type WindowsInstallationFilesResult =
30
+ | { kind: "observed"; files: {
31
+ path: string; identity: WindowsInstallationFileIdentity; bytes: Uint8Array; digest: string;
32
+ }[] }
33
+ | { kind: "refused"; reason: "unsupported-platform" | "invalid-request" | "native-api-unavailable"
34
+ | "volume-unavailable" | "open-refused" | "reparse-point" | "not-regular-file"
35
+ | "size-limit" | "read-failed" | "identity-changed" | "inspection-failed" };
36
+
37
+ type Refusal = Extract<WindowsInstallationFilesResult, { kind: "refused" }>["reason"];
38
+ class InspectionRefusal extends Error {
39
+ constructor(readonly reason: Refusal) { super(reason); }
40
+ }
41
+ const MIB = 1024 * 1024;
42
+ const REPARSE = 0x400;
43
+ const DIRECTORY = 0x10;
44
+ const INVALID_HANDLE = 0xffffffffffffffffn;
45
+ let openedForTests: (() => void) | undefined;
46
+ /** Runs after handles are held, for real Windows sharing/rename regression fixtures. */
47
+ export function setWindowsInstallationFilesOpenedForTests(callback?: () => void): void {
48
+ openedForTests = callback;
49
+ }
50
+
51
+ function components(path: string): { drive: string; names: string[] } | null {
52
+ if (typeof path !== "string" || path.length > 8192 || !/^[A-Za-z]:[\\/]/.test(path)) return null;
53
+ const names = path.slice(3).split(/[\\/]/);
54
+ if (!names.length || names.length > 64 || names.some(name => !name || name === "." || name === ".."
55
+ || /[\x00-\x1f<>:"|?*]/.test(name) || /[. ]$/.test(name)
56
+ || /^(?:con|prn|aux|nul|com[1-9¹²³]|lpt[1-9¹²³])(?:\.|$)/i.test(name))) return null;
57
+ return { drive: `${path[0]!.toUpperCase()}:\\`, names };
58
+ }
59
+
60
+ /**
61
+ * An explicit, read-only snapshot; never installation authority or a runtime-selection proof.
62
+ * All component opens are relative to held directory handles, not checked-then-reopened paths.
63
+ * Windows x64 only: the UNICODE_STRING/OBJECT_ATTRIBUTES layouts below are the x64 ABI.
64
+ * No candidate runs and no shell/compiler is started. Disk/driver latency is not a hard deadline.
65
+ * API contracts: learn.microsoft.com/windows/win32/api/winternl/nf-winternl-ntcreatefile
66
+ * and /windows/win32/api/fileapi/nf-fileapi-getvolumenameforvolumemountpointw.
67
+ */
68
+ export async function inspectWindowsInstallationFiles(
69
+ requests: readonly WindowsInstallationFileRequest[],
70
+ ): Promise<WindowsInstallationFilesResult> {
71
+ if (!Array.isArray(requests) || requests.length === 0 || requests.length > 12) {
72
+ return { kind: "refused", reason: "invalid-request" };
73
+ }
74
+ let ceiling = 0;
75
+ const parsed = requests.map(request => {
76
+ const parsedPath = request && components(request.path);
77
+ if (!parsedPath || !Number.isSafeInteger(request.maxBytes) || request.maxBytes < 0
78
+ || (request.hashOnly !== undefined && typeof request.hashOnly !== "boolean")
79
+ || request.maxBytes > (request.hashOnly ? 256 * MIB : MIB)) return null;
80
+ ceiling += request.maxBytes;
81
+ return parsedPath;
82
+ });
83
+ if (parsed.some(path => path === null) || ceiling > 300 * MIB) {
84
+ return { kind: "refused", reason: "invalid-request" };
85
+ }
86
+ if (process.platform !== "win32" || process.arch !== "x64") {
87
+ return { kind: "refused", reason: "unsupported-platform" };
88
+ }
89
+ // Loading FFI is itself opt-in; importing this module performs no Windows inspection.
90
+ let ffi: typeof import("bun:ffi");
91
+ try { ffi = await import("bun:ffi"); } catch { return { kind: "refused", reason: "native-api-unavailable" }; }
92
+ let kernel: Library<typeof KERNEL_SYMBOLS> | undefined;
93
+ let nt: Library<typeof NT_SYMBOLS> | undefined;
94
+ const handles: bigint[] = [];
95
+ try {
96
+ try {
97
+ kernel = ffi.dlopen("kernel32.dll", KERNEL_SYMBOLS);
98
+ nt = ffi.dlopen("ntdll.dll", NT_SYMBOLS);
99
+ } catch { throw new InspectionRefusal("native-api-unavailable"); }
100
+ const k = kernel.symbols;
101
+ const wide = (text: string): Buffer => Buffer.from(`${text}\0`, "utf16le");
102
+ const keep = (handle: bigint): bigint => {
103
+ if (handle === 0n || handle === INVALID_HANDLE) throw new InspectionRefusal("open-refused");
104
+ handles.push(handle);
105
+ return handle;
106
+ };
107
+ const info = (handle: bigint, infoClass: number, length: number): Buffer => {
108
+ const buffer = Buffer.alloc(length);
109
+ if (!k.GetFileInformationByHandleEx!(handle, infoClass, ffi.ptr(buffer), length)) {
110
+ throw new InspectionRefusal("inspection-failed");
111
+ }
112
+ return buffer;
113
+ };
114
+ const inspect = (handle: bigint, directory: boolean): WindowsInstallationFileIdentity => {
115
+ if (k.GetFileType!(handle) !== 1) throw new InspectionRefusal("not-regular-file"); // FILE_TYPE_DISK
116
+ const basic = info(handle, 0, 40); // FILE_BASIC_INFO: 4 LARGE_INTEGERs, attributes.
117
+ const attributes = basic.readUInt32LE(32);
118
+ if (attributes & REPARSE) throw new InspectionRefusal("reparse-point");
119
+ if (Boolean(attributes & DIRECTORY) !== directory) throw new InspectionRefusal("not-regular-file");
120
+ const id = info(handle, 18, 24); // FILE_ID_INFO: volume serial and 128-bit file ID.
121
+ const standard = info(handle, 1, 24); // FILE_STANDARD_INFO.EndOfFile
122
+ const size = standard.readBigInt64LE(8);
123
+ if (size < 0n || size > BigInt(Number.MAX_SAFE_INTEGER)) throw new InspectionRefusal("size-limit");
124
+ return { volumeSerial: id.readBigUInt64LE(0).toString(16), fileId: id.subarray(8, 24).toString("hex"),
125
+ size: Number(size), lastWriteTime: basic.readBigInt64LE(16).toString(), changeTime: basic.readBigInt64LE(24).toString() };
126
+ };
127
+ const roots = new Map<string, bigint>();
128
+ const root = (drive: string): bigint => {
129
+ const held = roots.get(drive);
130
+ if (held !== undefined) return held;
131
+ const mount = wide(drive);
132
+ const name = Buffer.alloc(100 * 2);
133
+ if (![2, 3, 5, 6].includes(Number(k.GetDriveTypeW!(ffi.ptr(mount))))) {
134
+ throw new InspectionRefusal("volume-unavailable");
135
+ }
136
+ // Root-only volume management query, never a candidate-controlled ancestor traversal.
137
+ // This API does not support SMB; only a strict local volume GUID reaches CreateFileW.
138
+ if (!k.GetVolumeNameForVolumeMountPointW!(ffi.ptr(mount), ffi.ptr(name), 100)) {
139
+ throw new InspectionRefusal("volume-unavailable");
140
+ }
141
+ const guid = name.toString("utf16le").split("\0", 1)[0]!;
142
+ if (!/^\\\\\?\\Volume\{[0-9a-f-]{36}\}\\$/i.test(guid)) throw new InspectionRefusal("volume-unavailable");
143
+ const path = wide(guid);
144
+ const handle = keep(BigInt(k.CreateFileW!(ffi.ptr(path), 0x100081, 1, null, 3, 0x02200000, 0n)));
145
+ inspect(handle, true);
146
+ roots.set(drive, handle);
147
+ return handle;
148
+ };
149
+ const relativeOpen = (parent: bigint, component: string, directory: boolean): bigint => {
150
+ const name = wide(component);
151
+ const unicode = Buffer.alloc(16);
152
+ unicode.writeUInt16LE(name.length - 2, 0);
153
+ unicode.writeUInt16LE(name.length, 2);
154
+ unicode.writeBigUInt64LE(BigInt(ffi.ptr(name)), 8);
155
+ const attributes = Buffer.alloc(48);
156
+ attributes.writeUInt32LE(48, 0);
157
+ attributes.writeBigUInt64LE(parent, 8);
158
+ attributes.writeBigUInt64LE(BigInt(ffi.ptr(unicode)), 16);
159
+ attributes.writeUInt32LE(0x40, 24); // OBJ_CASE_INSENSITIVE; one component only.
160
+ const output = Buffer.alloc(8);
161
+ const status = Buffer.alloc(16);
162
+ // FILE_OPEN; FILE_SYNCHRONOUS_IO_NONALERT; FILE_OPEN_REPARSE_POINT.
163
+ // Share READ only: while held, writes/reparse edits and delete/rename opens are refused.
164
+ const result = nt!.symbols.NtCreateFile!(ffi.ptr(output), 0x100081, ffi.ptr(attributes),
165
+ ffi.ptr(status), null, 0, 1, 1, 0x200020 | (directory ? 1 : 0), null, 0);
166
+ if (result < 0) throw new InspectionRefusal("open-refused");
167
+ const handle = keep(output.readBigUInt64LE(0));
168
+ inspect(handle, directory);
169
+ return handle;
170
+ };
171
+ const directories = new Map<string, bigint>();
172
+ const files = requests.map((request, index) => {
173
+ const { drive, names } = parsed[index]!;
174
+ let parent = root(drive);
175
+ let key = drive;
176
+ for (const component of names.slice(0, -1)) {
177
+ key += `${component}\\`;
178
+ let handle = directories.get(key);
179
+ if (handle === undefined) {
180
+ handle = relativeOpen(parent, component, true);
181
+ directories.set(key, handle);
182
+ }
183
+ parent = handle;
184
+ }
185
+ const handle = relativeOpen(parent, names[names.length - 1]!, false);
186
+ const identity = inspect(handle, false);
187
+ if (identity.size > request.maxBytes) throw new InspectionRefusal("size-limit");
188
+ return { request, handle, identity };
189
+ });
190
+ openedForTests?.();
191
+ const observed = files.map(({ request, handle, identity }) => {
192
+ const hash = createHash("sha256");
193
+ const bytes = request.hashOnly ? new Uint8Array() : new Uint8Array(identity.size);
194
+ const chunk = Buffer.alloc(Math.min(MIB, Math.max(1, identity.size)));
195
+ const read = Buffer.alloc(4);
196
+ let offset = 0;
197
+ while (offset < identity.size) {
198
+ const length = Math.min(chunk.length, identity.size - offset);
199
+ if (!k.ReadFile!(handle, ffi.ptr(chunk), length, ffi.ptr(read), null)) throw new InspectionRefusal("read-failed");
200
+ const count = read.readUInt32LE(0);
201
+ if (!count || count > length) throw new InspectionRefusal("read-failed");
202
+ hash.update(chunk.subarray(0, count));
203
+ if (!request.hashOnly) bytes.set(chunk.subarray(0, count), offset);
204
+ offset += count;
205
+ }
206
+ if (!k.ReadFile!(handle, ffi.ptr(chunk), 1, ffi.ptr(read), null) || read.readUInt32LE(0) !== 0) {
207
+ throw new InspectionRefusal("identity-changed");
208
+ }
209
+ return { path: request.path, identity, bytes, digest: hash.digest("hex") };
210
+ });
211
+ for (const file of files) {
212
+ if (JSON.stringify(inspect(file.handle, false)) !== JSON.stringify(file.identity)) {
213
+ throw new InspectionRefusal("identity-changed");
214
+ }
215
+ }
216
+ return { kind: "observed", files: observed };
217
+ } catch (error) {
218
+ return { kind: "refused", reason: error instanceof InspectionRefusal ? error.reason : "inspection-failed" };
219
+ } finally {
220
+ for (const handle of handles.reverse()) kernel?.symbols.CloseHandle?.(handle);
221
+ nt?.close();
222
+ kernel?.close();
223
+ }
224
+ }
@@ -118,23 +118,29 @@ function parseHttpDate(value: string, now: number): number | undefined {
118
118
  export function parseRetryAfterMs(
119
119
  value: string | null | undefined,
120
120
  now = Date.now(),
121
- options?: { preserveImmediate?: boolean },
121
+ options?: { preserveImmediate?: boolean; preserveServerDelay?: boolean },
122
122
  ): number | undefined {
123
123
  const text = value?.trim();
124
124
  if (!text) return undefined;
125
+ // A local wait ceiling must not make an explicit upstream reset expire early.
126
+ // Keep legacy bounded parsing for other callers. The opt-in stores a timestamp;
127
+ // the combo picker still independently limits how long a live request waits.
128
+ const maximum = options?.preserveServerDelay === true
129
+ ? Number.MAX_SAFE_INTEGER - Math.max(0, now)
130
+ : MAX_COOLDOWN_MS;
125
131
  if (/^\d+(?:\.\d+)?$/.test(text)) {
126
132
  const seconds = Number(text);
127
133
  if (
128
134
  Number.isFinite(seconds)
129
135
  && (seconds > 0 || (options?.preserveImmediate && seconds === 0))
130
136
  ) {
131
- return Math.min(Math.max(Math.ceil(seconds * 1000), 1), MAX_COOLDOWN_MS);
137
+ return Math.min(Math.max(Math.ceil(seconds * 1000), 1), maximum);
132
138
  }
133
139
  }
134
140
  const timestamp = parseHttpDate(text, now);
135
141
  if (timestamp === undefined) return undefined;
136
142
  const delay = timestamp - now;
137
- if (delay > 0) return Math.min(delay, MAX_COOLDOWN_MS);
143
+ if (delay > 0) return Math.min(delay, maximum);
138
144
  return options?.preserveImmediate ? 1 : undefined;
139
145
  }
140
146
 
@@ -215,7 +221,11 @@ export function coolComboTarget(
215
221
  // A server-provided Retry-After is authoritative, including an immediate `0` directive.
216
222
  // A quota reset is the next-most-specific signal (#3256); configured and default cooldowns
217
223
  // are only fallbacks when upstream supplied neither usable value.
218
- const cooldownMs = parseRetryAfterMs(options?.retryAfter, now, { preserveImmediate: true })
224
+ const serverDelayMs = parseRetryAfterMs(options?.retryAfter, now, {
225
+ preserveImmediate: true,
226
+ preserveServerDelay: true,
227
+ });
228
+ const cooldownMs = serverDelayMs
219
229
  ?? parseResetCooldownMs(options?.resetAt, now)
220
230
  ?? options?.cooldownMs
221
231
  ?? (isTransientRequestRateLimit({
@@ -224,7 +234,9 @@ export function coolComboTarget(
224
234
  message: options?.message,
225
235
  }) ? COMBO_REQUEST_RATE_COOLDOWN_MS : DEFAULT_COOLDOWN_MS);
226
236
  targetCooldowns.set(cooldownMapKey(comboId, target), {
227
- cooldownUntil: now + Math.min(Math.max(cooldownMs, 1), MAX_COOLDOWN_MS),
237
+ // Only the locally chosen fallback is capped at ten minutes. An explicit
238
+ // server lower bound (including one hour) remains authoritative.
239
+ cooldownUntil: now + (serverDelayMs ?? Math.min(Math.max(cooldownMs, 1), MAX_COOLDOWN_MS)),
228
240
  });
229
241
  sweepExpiredOnWrite(now);
230
242
  }
@@ -394,6 +406,103 @@ function isRequestLocalTargetIncompatibility(status: number, message: string, co
394
406
  return false;
395
407
  }
396
408
 
409
+ /**
410
+ * Codes a gateway uses when it declines a request field it cannot serve.
411
+ *
412
+ * Wider than the set {@link isRequestLocalTargetIncompatibility} accepts, by exactly one member:
413
+ * Alibaba's gateway reports `invalid_parameter_error`. It is kept in its own set rather than
414
+ * added to the shared one, because that set also governs the `user` parameter and image-input
415
+ * branches and widening it there would admit shapes those branches were reasoned about without.
416
+ */
417
+ const RESPONSE_FORMAT_REFUSAL_CODES = new Set([
418
+ "",
419
+ "invalid_request_error",
420
+ "invalid_parameter_error",
421
+ "unsupported_parameter",
422
+ "unsupported_value",
423
+ ]);
424
+
425
+ /**
426
+ * Does this message say the target cannot PROVIDE `response_format`, rather than that the
427
+ * request's `response_format` was malformed?
428
+ *
429
+ * That distinction is the whole point of #4903 and it is why neither obvious option was taken.
430
+ * Hopping on every 400 would replay a genuinely malformed request against every remaining
431
+ * target. Dropping `response_format` would silently change the output contract the caller
432
+ * asked for, on a path whose entire purpose is a structured result.
433
+ *
434
+ * So both halves are required: the message must name the field, AND it must say the field is
435
+ * unavailable or unsupported. "Invalid schema for response_format" names the field and claims
436
+ * nothing about capability, so it stays terminal.
437
+ *
438
+ * `param` may be absent or explicitly null -- the reported gateway sends `param: null` -- but a
439
+ * param naming a DIFFERENT field contradicts the message and fails closed.
440
+ */
441
+ function namesResponseFormatIncapability(message: string, param: unknown): boolean {
442
+ if (param !== undefined && param !== null && param !== "response_format") return false;
443
+ const text = message.toLowerCase();
444
+ if (!text.includes("response_format")) return false;
445
+ return /(unavailable|not available|unsupported|not supported|does not support|doesn't support|cannot be used|is not enabled)/u
446
+ .test(text);
447
+ }
448
+
449
+ /**
450
+ * A `response_format` capability gap is target-local: this model cannot produce the requested
451
+ * output shape, which says nothing about the next target in the combo.
452
+ *
453
+ * Reported against a shadow title-generation call, where a combo's first target rejects
454
+ * `response_format` and the chain stops instead of trying the target behind it (#4903).
455
+ *
456
+ * The next target receives the SAME request, `response_format` included, so a target that can
457
+ * honour the contract honours it and one that cannot is skipped in turn. Traversal stays finite
458
+ * because combo excludes each attempted target and policy tries each candidate once.
459
+ *
460
+ * The envelope is bounded exactly like {@link isRequestLocalTargetIncompatibility}: an intact
461
+ * provider JSON object, a depth budget, `type: "invalid_request_error"`, and a code from a
462
+ * closed set. Nothing is inferred from echoed prompt text and no field is removed.
463
+ */
464
+ function isResponseFormatCapabilityRefusal(
465
+ status: number,
466
+ message: string,
467
+ code?: string | null,
468
+ ): boolean {
469
+ if (status !== 400 || message.length > 16_384) return false;
470
+ if (!RESPONSE_FORMAT_REFUSAL_CODES.has(normalizedFailureCode(code))) return false;
471
+ let text = message.trim();
472
+ for (let depth = 0; depth < 3; depth += 1) {
473
+ if (text.startsWith("Provider error 400: ")) {
474
+ text = text.slice("Provider error 400: ".length).trim();
475
+ }
476
+ // A chat gateway can report the refusal inside a single SSE frame, and the combo consumer
477
+ // keeps the raw text when that frame stops the error object from being extracted -- which is
478
+ // why the reported classification text reads `data: {"error":...}` and why the structured
479
+ // code arrives undefined. Exactly one `data:` prefix is removed, and only when the body is
480
+ // one line: this unwraps a single frame rather than parsing a stream, so a multi-event body
481
+ // is left alone and still fails closed.
482
+ if (text.startsWith("data:") && !text.includes("\n")) {
483
+ text = text.slice("data:".length).trim();
484
+ }
485
+ let payload: unknown;
486
+ try { payload = JSON.parse(text); } catch { return false; }
487
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) return false;
488
+ const error = (payload as Record<string, unknown>).error;
489
+ if (!error || typeof error !== "object" || Array.isArray(error)) return false;
490
+ const e = error as Record<string, unknown>;
491
+ if (e.code !== undefined && e.code !== null && typeof e.code !== "string") return false;
492
+ if (typeof e.message !== "string") return false;
493
+ // Our own wrapper, re-wrapped by a downstream hop. Peel it and look again, within budget.
494
+ if (e.message.startsWith("Provider error 400: ") && e.param === undefined) {
495
+ text = e.message;
496
+ continue;
497
+ }
498
+ if (e.type !== "invalid_request_error") return false;
499
+ const errorCode = normalizedFailureCode(typeof e.code === "string" ? e.code : undefined);
500
+ if (!RESPONSE_FORMAT_REFUSAL_CODES.has(errorCode)) return false;
501
+ return namesResponseFormatIncapability(e.message, e.param);
502
+ }
503
+ return false;
504
+ }
505
+
397
506
  export function comboFailureCooldownScope(
398
507
  status: number,
399
508
  message: string,
@@ -411,6 +520,9 @@ export function comboFailureCooldownScope(
411
520
  || isProviderTargetContextOverflow(status, message, options?.code)
412
521
  || isDefiniteContextOverflow(status, message)
413
522
  || isRequestLocalTargetIncompatibility(status, message, options?.code)
523
+ // A capability gap says the target is healthy and the request did not fit it, which is the
524
+ // same reason every other entry here refuses to cool a target.
525
+ || isResponseFormatCapabilityRefusal(status, message, options?.code)
414
526
  ) return "none";
415
527
  if (isProviderScopedQuotaCap(status, message, options?.code)) return "provider";
416
528
  // A rejected or unpaid credential is provider-wide evidence: every target that routes
@@ -595,6 +707,11 @@ export function comboFailureDecision(
595
707
  // per-request cap, not provider-wide evidence), so keep its hop verdict explicit here.
596
708
  if (failureCode === "free_rate_limited") return "hop";
597
709
  if (isRequestLocalTargetIncompatibility(status, message, options?.code)) return "hop";
710
+ // Must precede the generic `invalid_request_error` stop below, which is where this refusal
711
+ // ended the chain: the gateway reports `type: "invalid_request_error"`, so the classifier
712
+ // reaches that list and returns terminal before anything can ask whether the next target
713
+ // could have served the request (#4903).
714
+ if (isResponseFormatCapabilityRefusal(status, message, options?.code)) return "hop";
598
715
  if (["origin_rejected", "context_length_exceeded", "invalid_request_error"].includes(error.code ?? "")) {
599
716
  return "stop";
600
717
  }
@@ -34,6 +34,7 @@ import {
34
34
  malformedQuotaResetNotifyWarning,
35
35
  malformedCatalogAutoRefreshWarning,
36
36
  malformedCodexPoolWarning,
37
+ malformedSpendWarning,
37
38
  rawConfigRecord,
38
39
  malformedNativeSubagentFields,
39
40
  malformedNativeSubagentFieldWarning,
@@ -58,6 +59,7 @@ import {
58
59
  quotaResetNotifySchema,
59
60
  remoteGuiConfigSchema,
60
61
  runtimeRoleSchema,
62
+ spendSchema,
61
63
  } from "./schema/leaf-validators";
62
64
 
63
65
  export type ConfigDiagnostics = {
@@ -122,6 +124,8 @@ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): Conf
122
124
  if (catalogRefreshWarning) warnings.push(catalogRefreshWarning);
123
125
  const codexPoolWarning = malformedCodexPoolWarning(rawParsed);
124
126
  if (codexPoolWarning) warnings.push(codexPoolWarning);
127
+ const spendWarning = malformedSpendWarning(rawParsed);
128
+ if (spendWarning) warnings.push(spendWarning);
125
129
  const plaintextWarning = malformedPlaintextV2AgentMessagesWarning(rawParsed);
126
130
  if (plaintextWarning) warnings.push(plaintextWarning);
127
131
  if (syncDisabledReason) {
@@ -295,6 +299,22 @@ function catalogAutoRefreshError(value: unknown): string | null {
295
299
  return `schema_invalid: catalogAutoRefresh${field ? `.${field}` : ""}: ${issue?.message ?? "invalid configuration"}`;
296
300
  }
297
301
 
302
+ /**
303
+ * The read path degrades a malformed spend section to undefined, which means no ceiling is
304
+ * enforced. Reject it on write so `ocx config set` cannot store a budget that reads back as
305
+ * configured and refuses nothing -- a ceiling that is not enforced looks identical to one
306
+ * nothing has reached.
307
+ */
308
+ function spendError(value: unknown): string | null {
309
+ const raw = rawConfigRecord(value);
310
+ if (!raw || !Object.hasOwn(raw, "spend") || raw.spend === undefined) return null;
311
+ const result = spendSchema.safeParse(raw.spend);
312
+ if (result.success) return null;
313
+ const issue = result.error.issues[0];
314
+ const field = issue?.path.join(".");
315
+ return `schema_invalid: spend${field ? `.${field}` : ""}: ${issue?.message ?? "invalid configuration"}`;
316
+ }
317
+
298
318
  /**
299
319
  * The read path degrades a malformed pool policy to undefined, which for an exclusion policy means
300
320
  * the excluded accounts quietly keep serving traffic. Reject it on write so `ocx config set` cannot
@@ -551,6 +571,7 @@ export function validateConfigCandidate(value: unknown): { ok: true; config: Ocx
551
571
  ?? agentTaskRecoveryError(value)
552
572
  ?? quotaResetNotifyError(value)
553
573
  ?? catalogAutoRefreshError(value)
574
+ ?? spendError(value)
554
575
  ?? codexPoolError(value)
555
576
  ?? googleAntigravityStaticCatalogVersionError(value)
556
577
  ?? codexAccountPrioritiesError(value)
@@ -33,6 +33,7 @@ import {
33
33
  remoteGuiConfigSchema,
34
34
  retryOn429PolicySchema,
35
35
  runtimeRoleSchema,
36
+ spendSchema,
36
37
  } from "./schema/leaf-validators";
37
38
  import { hasWarnedInheritedFastWireConflict, markWarnedInheritedFastWireConflict } from "./warn-memo";
38
39
 
@@ -636,6 +637,20 @@ export function malformedCatalogAutoRefreshWarning(rawParsed: unknown): string |
636
637
  return `catalogAutoRefresh${field ? `.${field}` : ""} ignored: invalid catalog auto-refresh configuration`;
637
638
  }
638
639
 
640
+ /**
641
+ * The same silent-in-the-wrong-direction failure, and the most expensive instance of it here:
642
+ * a dropped spend section means the ceilings are not enforced, and an unenforced ceiling is
643
+ * indistinguishable from one nothing has reached. The operator finds out from the bill.
644
+ */
645
+ export function malformedSpendWarning(rawParsed: unknown): string | null {
646
+ const raw = rawConfigRecord(rawParsed);
647
+ if (!raw || !Object.hasOwn(raw, "spend")) return null;
648
+ const result = spendSchema.safeParse(raw.spend);
649
+ if (result.success) return null;
650
+ const field = result.error.issues[0]?.path.join(".");
651
+ return `spend${field ? `.${field}` : ""} ignored: invalid spend ceiling configuration, so no token ceiling is enforced`;
652
+ }
653
+
639
654
  /**
640
655
  * Same silent-in-the-wrong-direction failure as the notification block: a dropped pool policy means
641
656
  * the accounts the operator meant to exclude keep taking traffic, and the only visible symptom is
@@ -295,6 +295,14 @@ export function quarantinePendingTeardown(nonce: string): string | null {
295
295
  * alone. Only an abandoned obligation is a candidate, and a VALID one still has to prove
296
296
  * its endpoint is down before anything is restored — an invalid one never can, which is
297
297
  * what {@link quarantinePendingTeardown} exists for.
298
+ *
299
+ * `isAlive` must answer "is the OWNER still running", which is not "is this PID alive".
300
+ * PIDs are reused: after the owner exits its number can be handed to an unrelated process,
301
+ * and a bare liveness probe then reports the owner as running for as long as that process
302
+ * lives. The receipt is neither recovered nor quarantined nor reported while both updater
303
+ * gates keep refusing on it, which is a permanent fail-closed with no way forward (#4897).
304
+ * `handleStop` therefore passes an identity-aware predicate; do not substitute a bare
305
+ * `process.kill(pid, 0)` here for cheapness.
298
306
  */
299
307
  export function isPendingTeardownAbandoned(
300
308
  read: PendingTeardownRead | TeardownScanFailure,
@@ -110,16 +110,29 @@ export function removeRuntimePortIfPidIs(snapshotPid: number | null): void {
110
110
  try { unlinkSync(getRuntimePortPath()); } catch { /* ignore */ }
111
111
  }
112
112
 
113
- export function isOcxStartCommandLine(commandLine: string): boolean {
113
+ /**
114
+ * Does this command line belong to an opencodex process at all, whatever it is doing?
115
+ *
116
+ * {@link isOcxStartCommandLine} answers the narrower question the pidfile needs — "is this
117
+ * the proxy?" — by additionally requiring the `start` verb. Ownership of a pending-teardown
118
+ * receipt is the broader question: the owner is whichever invocation claimed it, which is an
119
+ * `ocx stop` or the `ocx update` worker that drove it, never an `ocx start`. Asking the
120
+ * start-shaped question there would call every real owner foreign.
121
+ */
122
+ export function isOcxCommandLine(commandLine: string): boolean {
114
123
  const normalized = commandLine.toLowerCase().replace(/\\/g, "/");
115
124
  // Keep legacy source launches and npm's in-place Windows rename recognizable:
116
125
  // a service wrapper may respawn from `.opencodex-*` during a global update.
117
- const hasOcxEntrypoint = normalized.includes("src/cli.ts")
126
+ return normalized.includes("src/cli.ts")
118
127
  || normalized.includes("src/cli/index.ts")
119
128
  || normalized.includes("@bitkyc08/opencodex")
120
129
  || /@bitkyc08\/\.opencodex-/.test(normalized)
121
130
  || /(?:^|[\s/"'])(?:ocx|opencodex)(?:\.cmd)?(?:$|[\s"'])/.test(normalized);
122
- return hasOcxEntrypoint && /(?:^|[\s"'])start(?:$|[\s"'])/.test(normalized);
131
+ }
132
+
133
+ export function isOcxStartCommandLine(commandLine: string): boolean {
134
+ const normalized = commandLine.toLowerCase().replace(/\\/g, "/");
135
+ return isOcxCommandLine(commandLine) && /(?:^|[\s"'])start(?:$|[\s"'])/.test(normalized);
123
136
  }
124
137
 
125
138
  /** Avoid spawning WMIC/PowerShell on every short liveness poll. */
@@ -227,6 +240,26 @@ export function verifyPidIdentity(candidatePid: number): number | null {
227
240
  return isLikelyOcxStartProcess(candidatePid) ? candidatePid : null;
228
241
  }
229
242
 
243
+ /**
244
+ * Is this PID an opencodex process, rather than merely a live one?
245
+ *
246
+ * `process.kill(pid, 0)` answers "does this number name a process", which is not the same
247
+ * question. PIDs are reused, so a recorded owner that exited can have its number handed to
248
+ * an unrelated process, and bare liveness then reports the owner as still running forever.
249
+ *
250
+ * Deliberately uncached, unlike {@link isLikelyOcxStartProcess}. That cache exists because
251
+ * liveness polling asks about the same PID many times a second; this is asked once per
252
+ * outstanding teardown receipt in a short-lived `ocx stop`, so the cache would only add an
253
+ * unswept map. The Windows WMIC/PowerShell probe cost is the point rather than a regression:
254
+ * the runtime contract requires the identity check, not the cheap one, before acting on
255
+ * stale state.
256
+ */
257
+ export function isLikelyOcxProcess(pid: number): boolean {
258
+ const commandLine = readProcessCommandLine(pid);
259
+ if (commandLine === undefined) return false;
260
+ return isOcxCommandLine(commandLine);
261
+ }
262
+
230
263
  type ProcessCommandLineExec = (
231
264
  executable: string,
232
265
  args: string[],
@@ -0,0 +1,16 @@
1
+ /** Shared, initialization-independent shape check for provider send-path overrides. */
2
+ export function providerRelativeSendPathConfigError(
3
+ field: "responsesPath" | "chatCompletionsPath",
4
+ value: unknown,
5
+ ): string | null {
6
+ if (value === undefined) return null;
7
+ if (typeof value !== "string") return `${field} must be a string`;
8
+ if (/^[A-Za-z][A-Za-z0-9+.-]*:/.test(value) || value.includes("://")) {
9
+ return `${field} must be a relative path without a URL scheme`;
10
+ }
11
+ if (!value.startsWith("/")) return `${field} must start with /`;
12
+ if (value.includes("?") || value.includes("#")) {
13
+ return `${field} must not include query strings or fragments`;
14
+ }
15
+ return null;
16
+ }
@@ -1,3 +1,5 @@
1
+ import { configureSocks5Fetch } from "../lib/proxy-env";
2
+ import { redactUrlForLog } from "../lib/redact";
1
3
  import { join } from "node:path";
2
4
  import { DEFAULT_SUBAGENT_MODELS, SUBAGENT_MODELS_VERSION } from "./subagent-models";
3
5
  import { MULTI_AGENT_SURFACE_ADVISORY_VERSION } from "./multi-agent-surface";
@@ -102,10 +104,15 @@ function warnProxyConfigDiscardOnce(kind: "proxy" | "noProxy" | "noProxyElements
102
104
  * variables win; config fills missing scheme proxies, which take precedence over ALL_PROXY for WS.
103
105
  * localhost/127.0.0.1 are appended to NO_PROXY so the CLI's own health checks and
104
106
  * running-proxy API calls stay direct. Call once per process entry that makes outbound provider
105
- * requests (server start, catalog sync).
107
+ * requests (server start, catalog sync). `announce` prints the one-line startup banner naming
108
+ * the outbound proxy actually in effect; only server start passes it, because on catalog sync
109
+ * that line is noise. The URL is redacted because a proxy URL can carry credentials.
106
110
  */
107
- export function applyProxyEnv(config: OcxConfig): void {
111
+ export function applyProxyEnv(config: OcxConfig, announce = false): void {
108
112
  applyProxyEnvWith(config);
113
+ if (!announce) return;
114
+ const outbound = process.env.ALL_PROXY?.trim() || process.env.HTTPS_PROXY?.trim() || process.env.HTTP_PROXY?.trim();
115
+ if (outbound) console.log(` outbound proxy: ${redactUrlForLog(outbound)}`);
109
116
  }
110
117
 
111
118
  /** Test seam for `proxy: "auto"`: the registry reader and platform are injectable. */
@@ -123,6 +130,7 @@ export function applyProxyEnvWith(
123
130
  let proxy = typeof rawProxy === "string" ? resolveEnvValue(rawProxy) : undefined;
124
131
  if (!proxy) {
125
132
  if (rawProxy !== undefined) warnProxyConfigDiscardOnce("proxy");
133
+ configureSocks5Fetch();
126
134
  return;
127
135
  }
128
136
  if (proxy.trim().toLowerCase() === "auto") {
@@ -151,8 +159,18 @@ export function applyProxyEnvWith(
151
159
  }
152
160
  }
153
161
  if (proxy) {
154
- if (!process.env.HTTP_PROXY?.trim() && !process.env.http_proxy?.trim()) process.env.HTTP_PROXY = proxy;
155
- if (!process.env.HTTPS_PROXY?.trim() && !process.env.https_proxy?.trim()) process.env.HTTPS_PROXY = proxy;
162
+ if (/^socks/i.test(proxy.trim()) && !/^socks5h?:\/\//i.test(proxy.trim())) {
163
+ throw new Error("Only SOCKS5 proxy URLs are supported; use socks5://host:port");
164
+ }
165
+ if (/^socks5h?:\/\//i.test(proxy.trim())) {
166
+ for (const key of ["HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "ALL_PROXY", "all_proxy"] as const) {
167
+ delete process.env[key];
168
+ }
169
+ process.env.ALL_PROXY = proxy;
170
+ } else {
171
+ if (!process.env.HTTP_PROXY?.trim() && !process.env.http_proxy?.trim()) process.env.HTTP_PROXY = proxy;
172
+ if (!process.env.HTTPS_PROXY?.trim() && !process.env.https_proxy?.trim()) process.env.HTTPS_PROXY = proxy;
173
+ }
156
174
  }
157
175
  const existing = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
158
176
  const entries = existing.split(",").map(s => s.trim()).filter(Boolean);
@@ -184,5 +202,5 @@ export function applyProxyEnvWith(
184
202
  }
185
203
  }
186
204
  process.env.NO_PROXY = entries.join(",");
205
+ configureSocks5Fetch();
187
206
  }
188
-