@bitkyc08/opencodex 2.38.0 → 2.39.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 (81) hide show
  1. package/bin/ocx.mjs +58 -6
  2. package/gui/dist/assets/index-D-lchsPw.js +112 -0
  3. package/gui/dist/assets/index-uvENYLin.css +1 -0
  4. package/gui/dist/index.html +2 -2
  5. package/gui/dist/provider-icons/baseten.svg +13 -0
  6. package/gui/dist/provider-icons/bizrouter.svg +41 -0
  7. package/gui/dist/provider-icons/cerebras.svg +26 -0
  8. package/gui/dist/provider-icons/deepinfra.svg +75 -0
  9. package/gui/dist/provider-icons/digitalocean.svg +10 -0
  10. package/gui/dist/provider-icons/featherless.svg +4 -0
  11. package/gui/dist/provider-icons/gajae-code.svg +410 -0
  12. package/gui/dist/provider-icons/hermes-agent.svg +207 -0
  13. package/gui/dist/provider-icons/hyperbolic.svg +18 -0
  14. package/gui/dist/provider-icons/kilo.svg +13 -0
  15. package/gui/dist/provider-icons/litellm.svg +1 -0
  16. package/gui/dist/provider-icons/minimax.svg +1 -0
  17. package/gui/dist/provider-icons/nanogpt.svg +74 -0
  18. package/gui/dist/provider-icons/nebius.svg +1 -0
  19. package/gui/dist/provider-icons/neuralwatt.svg +27 -0
  20. package/gui/dist/provider-icons/nous.svg +149 -0
  21. package/gui/dist/provider-icons/novita.svg +32 -0
  22. package/gui/dist/provider-icons/orcarouter.svg +175 -0
  23. package/gui/dist/provider-icons/parallel.svg +13 -0
  24. package/gui/dist/provider-icons/sambanova.svg +276 -0
  25. package/gui/dist/provider-icons/scaleway.svg +11 -0
  26. package/gui/dist/provider-icons/siliconflow.svg +18 -0
  27. package/gui/dist/provider-icons/synthetic.svg +12 -0
  28. package/gui/dist/provider-icons/together.svg +18 -0
  29. package/gui/dist/provider-icons/umans.svg +30 -0
  30. package/gui/dist/provider-icons/venice.svg +165 -0
  31. package/gui/dist/provider-icons/vultr.svg +15 -0
  32. package/gui/dist/provider-icons/zai.svg +218 -0
  33. package/gui/dist/provider-icons/zenmux.svg +1 -0
  34. package/package.json +1 -1
  35. package/src/adapters/cursor/live-models.ts +1 -0
  36. package/src/adapters/openai-responses.ts +72 -10
  37. package/src/bridge.ts +15 -15
  38. package/src/cli/dispatch.ts +54 -18
  39. package/src/cli/index.ts +420 -30
  40. package/src/cli/integrations.ts +24 -2
  41. package/src/cli/uninstall-plan.ts +86 -0
  42. package/src/codex/account-store.ts +121 -8
  43. package/src/codex/auth-api.ts +202 -33
  44. package/src/codex/catalog/provider-fetch.ts +76 -9
  45. package/src/codex/history-job.ts +10 -0
  46. package/src/codex/history-manifest.ts +35 -2
  47. package/src/codex/history-provider.ts +196 -19
  48. package/src/codex/history-worker.ts +3 -0
  49. package/src/codex/quota-401-recovery.ts +190 -0
  50. package/src/codex/quota-recovery-timing.ts +28 -0
  51. package/src/codex/quota.ts +6 -0
  52. package/src/codex/routing.ts +68 -16
  53. package/src/codex/subagent-model-fallback.ts +4 -1
  54. package/src/config/pending-teardown-names.d.mts +8 -0
  55. package/src/config/pending-teardown-names.mjs +69 -0
  56. package/src/config/pending-teardown.ts +286 -0
  57. package/src/generated/compatibility-version.json +84 -32
  58. package/src/integrations/journal.ts +12 -1
  59. package/src/integrations/writer.ts +82 -7
  60. package/src/lib/process-control.ts +30 -5
  61. package/src/lib/state-store-registrations.ts +8 -0
  62. package/src/oauth/index.ts +220 -13
  63. package/src/oauth/store.ts +220 -18
  64. package/src/responses/spill-store.ts +20 -0
  65. package/src/responses/state.ts +288 -2
  66. package/src/server/management/integration-routes.ts +36 -5
  67. package/src/server/management-api.ts +66 -12
  68. package/src/server/responses/encrypted-payload.ts +18 -1
  69. package/src/server/stop-teardown.ts +84 -0
  70. package/src/service.ts +155 -20
  71. package/src/update/index.ts +36 -4
  72. package/src/update/proxy-liveness-probe.d.mts +6 -0
  73. package/src/update/proxy-liveness-probe.mjs +84 -0
  74. package/src/update/stop-contract.d.mts +2 -0
  75. package/src/update/stop-contract.mjs +15 -0
  76. package/src/update/stop-decision.d.mts +10 -0
  77. package/src/update/stop-decision.mjs +34 -0
  78. package/src/vision/eligibility.ts +19 -1
  79. package/src/vision/index.ts +4 -18
  80. package/gui/dist/assets/index-C14iCj_Q.js +0 -112
  81. package/gui/dist/assets/index-D7PIz7_g.css +0 -1
@@ -19,7 +19,7 @@
19
19
  import { createHash, randomUUID } from "node:crypto";
20
20
  import { chmodSync, closeSync, copyFileSync, existsSync, fstatSync, mkdirSync, openSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
21
21
  import { join } from "node:path";
22
- import { getConfigDir, atomicWriteFile, backupInvalidConfig, hardenConfigDir, hardenExistingSecret } from "../config";
22
+ import { getConfigDir, atomicWriteFile, backupInvalidConfig, hardenConfigDir, hardenExistingSecret, withConfigMutationLockSync } from "../config";
23
23
  import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
24
24
  import { recordOwnedConfigPath } from "../lib/config-ownership";
25
25
  import { MAX_PENDING_OAUTH_MUTATIONS } from "../lib/translator-budget";
@@ -67,23 +67,93 @@ export function getAuthRefreshIntentLockPath(provider: string, accountId: string
67
67
  export function getAuthRefreshIntentPath(provider: string, accountId: string): string {
68
68
  return `${getAuthRefreshIntentLockPath(provider, accountId)}.json`;
69
69
  }
70
- export interface OAuthRefreshIntent { version: 1; provider: string; accountId: string; generation: string; createdAt: number; flightId?: string; staleOwner?: true; uncertain?: true }
70
+ export type OAuthRefreshIntentCleanupPending = "pre-dispatch" | "definitive-rejection";
71
+ export interface OAuthRefreshIntent {
72
+ version: 1;
73
+ provider: string;
74
+ accountId: string;
75
+ generation: string;
76
+ createdAt: number;
77
+ attemptId?: string;
78
+ flightId?: string;
79
+ staleOwner?: true;
80
+ uncertain?: true;
81
+ cleanupPending?: OAuthRefreshIntentCleanupPending;
82
+ }
83
+
84
+ export class OAuthRefreshIntentIOError extends Error {
85
+ readonly code = "OAUTH_REFRESH_INTENT_IO";
86
+ constructor(
87
+ readonly operation: "write-intent" | "mark-cleanup-pending" | "clear-cleanup-pending" | "resume-cleanup",
88
+ cause?: unknown,
89
+ readonly refreshError?: unknown,
90
+ ) {
91
+ super(`OAuth refresh intent ${operation} failed`, cause ? { cause } : undefined);
92
+ this.name = "OAuthRefreshIntentIOError";
93
+ }
94
+ }
95
+
96
+ // Both compare-and-swap sites in this file — the OAuth file lock and the refresh-intent
97
+ // file — must agree on what "the same file" means, so they share one snapshot shape and
98
+ // one identity check (`snapshot`/`sameSnapshot` below) rather than two copies that can drift.
99
+ type OAuthRefreshIntentFileSnapshot = LockSnapshot;
100
+
101
+ export interface OAuthRefreshIntentMutationHooks {
102
+ beforeMarkCommit?: () => void;
103
+ beforeClearRecheck?: () => void;
104
+ }
105
+
106
+ class OAuthRefreshIntentChangedError extends Error {}
107
+
108
+ function uncertainOAuthRefreshIntent(provider: string, accountId: string): OAuthRefreshIntent {
109
+ return { version: 1, provider, accountId, generation: "", createdAt: 0, uncertain: true };
110
+ }
111
+
71
112
  function parseOAuthRefreshIntent(
72
113
  provider: string,
73
114
  accountId: string,
74
115
  raw: string,
75
116
  ): OAuthRefreshIntent {
76
- const value = JSON.parse(raw) as Partial<OAuthRefreshIntent>;
117
+ const parsed: unknown = JSON.parse(raw);
118
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
119
+ return uncertainOAuthRefreshIntent(provider, accountId);
120
+ }
121
+ const value = parsed as Partial<OAuthRefreshIntent>;
77
122
  if (
78
123
  value.version !== 1
79
124
  || value.provider !== provider
80
125
  || value.accountId !== accountId
81
126
  || typeof value.generation !== "string"
127
+ || !/^[0-9a-f]{64}$/.test(value.generation)
82
128
  || typeof value.createdAt !== "number"
83
- || (value.flightId !== undefined && typeof value.flightId !== "string")
129
+ || !Number.isFinite(value.createdAt)
130
+ || !Number.isInteger(value.createdAt)
131
+ || value.createdAt < 0
132
+ || (
133
+ value.attemptId !== undefined
134
+ && (typeof value.attemptId !== "string" || value.attemptId.length === 0 || value.attemptId.length > 128)
135
+ )
136
+ || (
137
+ value.flightId !== undefined
138
+ && (typeof value.flightId !== "string" || value.flightId.length === 0 || value.flightId.length > 128)
139
+ )
84
140
  || (value.staleOwner !== undefined && value.staleOwner !== true)
141
+ || (value.uncertain !== undefined && value.uncertain !== true)
142
+ || (
143
+ value.cleanupPending !== undefined
144
+ && value.cleanupPending !== "pre-dispatch"
145
+ && value.cleanupPending !== "definitive-rejection"
146
+ )
147
+ || (
148
+ value.cleanupPending !== undefined
149
+ && (
150
+ value.attemptId === undefined
151
+ || value.uncertain === true
152
+ || value.staleOwner === true
153
+ )
154
+ )
85
155
  ) {
86
- return { version: 1, provider, accountId, generation: "", createdAt: 0, uncertain: true };
156
+ return uncertainOAuthRefreshIntent(provider, accountId);
87
157
  }
88
158
  return value as OAuthRefreshIntent;
89
159
  }
@@ -96,7 +166,7 @@ export function readOAuthRefreshIntent(provider: string, accountId: string): OAu
96
166
  return parseOAuthRefreshIntent(provider, accountId, readFileSync(path, "utf8"));
97
167
  } catch (error) {
98
168
  if (errorCode(error) === "ENOENT") return undefined;
99
- return { version: 1, provider, accountId, generation: "", createdAt: 0, uncertain: true };
169
+ return uncertainOAuthRefreshIntent(provider, accountId);
100
170
  }
101
171
  }
102
172
 
@@ -107,27 +177,159 @@ export function peekOAuthRefreshIntent(provider: string, accountId: string): OAu
107
177
  return parseOAuthRefreshIntent(provider, accountId, readFileSync(path, "utf8"));
108
178
  } catch (error) {
109
179
  if (errorCode(error) === "ENOENT") return undefined;
110
- return { version: 1, provider, accountId, generation: "", createdAt: 0, uncertain: true };
180
+ return uncertainOAuthRefreshIntent(provider, accountId);
111
181
  }
112
182
  }
113
- export function writeOAuthRefreshIntent(provider: string, accountId: string, generation: string, createdAt = Date.now(), flightId?: string): void {
183
+ export function writeOAuthRefreshIntent(provider: string, accountId: string, generation: string, createdAt = Date.now(), flightId?: string): OAuthRefreshIntent {
114
184
  const dir = getConfigDir();
115
185
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
116
186
  hardenConfigDir();
117
- const intent: OAuthRefreshIntent = { version: 1, provider, accountId, generation, createdAt, ...(flightId ? { flightId } : {}) };
118
- atomicWriteFile(getAuthRefreshIntentPath(provider, accountId), `${JSON.stringify(intent)}\n`);
187
+ const intent: OAuthRefreshIntent = {
188
+ version: 1,
189
+ provider,
190
+ accountId,
191
+ generation,
192
+ createdAt,
193
+ attemptId: randomUUID(),
194
+ ...(flightId ? { flightId } : {}),
195
+ };
196
+ return withConfigMutationLockSync(() => {
197
+ if (readOAuthRefreshIntent(provider, accountId) !== undefined) {
198
+ throw new OAuthRefreshIntentIOError(
199
+ "write-intent",
200
+ new Error("An OAuth refresh intent already exists; refusing to overwrite its replay guard"),
201
+ );
202
+ }
203
+ atomicWriteFile(getAuthRefreshIntentPath(provider, accountId), `${JSON.stringify(intent)}\n`);
204
+ return intent;
205
+ });
206
+ }
207
+
208
+ function sameOAuthRefreshIntentAttempt(current: OAuthRefreshIntent, expected: OAuthRefreshIntent): boolean {
209
+ return current.provider === expected.provider
210
+ && current.accountId === expected.accountId
211
+ && current.generation === expected.generation
212
+ && current.createdAt === expected.createdAt
213
+ && current.attemptId === expected.attemptId
214
+ && current.flightId === expected.flightId;
215
+ }
216
+
217
+ function exactOAuthRefreshIntentFile(
218
+ provider: string,
219
+ accountId: string,
220
+ ): { intent: OAuthRefreshIntent; snapshot: OAuthRefreshIntentFileSnapshot } | undefined {
221
+ const path = getAuthRefreshIntentPath(provider, accountId);
222
+ try {
223
+ hardenConfigDir();
224
+ hardenExistingSecret(path);
225
+ const file = snapshot(path);
226
+ let intent: OAuthRefreshIntent;
227
+ try { intent = parseOAuthRefreshIntent(provider, accountId, file.bytes); }
228
+ catch { intent = uncertainOAuthRefreshIntent(provider, accountId); }
229
+ return { intent, snapshot: file };
230
+ } catch (error) {
231
+ if (errorCode(error) === "ENOENT") return undefined;
232
+ throw error;
233
+ }
234
+ }
235
+
236
+ export function markOAuthRefreshIntentCleanupPending(
237
+ provider: string,
238
+ accountId: string,
239
+ expected: OAuthRefreshIntent,
240
+ cleanupPending: OAuthRefreshIntentCleanupPending,
241
+ hooks: OAuthRefreshIntentMutationHooks = {},
242
+ ): OAuthRefreshIntent | undefined {
243
+ return withConfigMutationLockSync(() => {
244
+ const path = getAuthRefreshIntentPath(provider, accountId);
245
+ const observed = exactOAuthRefreshIntentFile(provider, accountId);
246
+ const current = observed?.intent;
247
+ if (
248
+ !observed
249
+ || !current
250
+ || expected.attemptId === undefined
251
+ || current.uncertain
252
+ || current.staleOwner
253
+ || !sameOAuthRefreshIntentAttempt(current, expected)
254
+ || (current.cleanupPending !== undefined && current.cleanupPending !== cleanupPending)
255
+ ) return undefined;
256
+ const marked: OAuthRefreshIntent = { ...current, cleanupPending };
257
+ try {
258
+ atomicWriteFile(path, `${JSON.stringify(marked)}\n`, undefined, {
259
+ beforeRename: hooks.beforeMarkCommit,
260
+ validateBeforeRename: targetPath => {
261
+ let latest: OAuthRefreshIntentFileSnapshot;
262
+ try { latest = snapshot(targetPath); }
263
+ catch (error) {
264
+ throw new OAuthRefreshIntentChangedError("OAuth refresh intent changed before marker commit", { cause: error });
265
+ }
266
+ if (!sameSnapshot(observed.snapshot, latest)) {
267
+ throw new OAuthRefreshIntentChangedError("OAuth refresh intent changed before marker commit");
268
+ }
269
+ },
270
+ });
271
+ } catch (error) {
272
+ if (error instanceof OAuthRefreshIntentChangedError) return undefined;
273
+ throw error;
274
+ }
275
+ return marked;
276
+ });
119
277
  }
278
+
279
+ export function clearOAuthRefreshIntentIfMatch(
280
+ provider: string,
281
+ accountId: string,
282
+ expected: OAuthRefreshIntent,
283
+ hooks: OAuthRefreshIntentMutationHooks = {},
284
+ ): boolean {
285
+ return withConfigMutationLockSync(() => {
286
+ const path = getAuthRefreshIntentPath(provider, accountId);
287
+ const observed = exactOAuthRefreshIntentFile(provider, accountId);
288
+ if (!observed) return true;
289
+ const current = observed.intent;
290
+ if (
291
+ current.uncertain
292
+ || expected.uncertain
293
+ || current.attemptId === undefined
294
+ || expected.attemptId === undefined
295
+ || current.uncertain !== expected.uncertain
296
+ || current.staleOwner !== expected.staleOwner
297
+ || current.cleanupPending !== expected.cleanupPending
298
+ || !sameOAuthRefreshIntentAttempt(current, expected)
299
+ ) return false;
300
+ hooks.beforeClearRecheck?.();
301
+ let latest: OAuthRefreshIntentFileSnapshot;
302
+ try { latest = snapshot(path); }
303
+ catch (error) {
304
+ if (errorCode(error) === "ENOENT") return true;
305
+ throw error;
306
+ }
307
+ if (!sameSnapshot(observed.snapshot, latest)) return false;
308
+ try { unlinkSync(path); return true; }
309
+ catch (error) { if (errorCode(error) === "ENOENT") return true; throw error; }
310
+ });
311
+ }
312
+
120
313
  export function markOAuthRefreshIntentStaleOwner(provider: string, accountId: string, generation: string, flightId: string): boolean {
121
- const current = readOAuthRefreshIntent(provider, accountId);
122
- if (current?.uncertain || current?.generation !== generation || current.flightId !== flightId) return false;
123
- atomicWriteFile(getAuthRefreshIntentPath(provider, accountId), `${JSON.stringify({ ...current, staleOwner: true })}\n`);
124
- return true;
314
+ return withConfigMutationLockSync(() => {
315
+ const current = readOAuthRefreshIntent(provider, accountId);
316
+ if (
317
+ current?.uncertain
318
+ || current?.cleanupPending
319
+ || current?.generation !== generation
320
+ || current.flightId !== flightId
321
+ ) return false;
322
+ atomicWriteFile(getAuthRefreshIntentPath(provider, accountId), `${JSON.stringify({ ...current, staleOwner: true })}\n`);
323
+ return true;
324
+ });
125
325
  }
126
326
  export function clearOAuthRefreshIntent(provider: string, accountId: string, generation: string): boolean {
127
- const current = readOAuthRefreshIntent(provider, accountId);
128
- if (!current || current.generation !== generation) return false;
129
- try { unlinkSync(getAuthRefreshIntentPath(provider, accountId)); return true; }
130
- catch (error) { if (errorCode(error) === "ENOENT") return false; throw error; }
327
+ return withConfigMutationLockSync(() => {
328
+ const current = readOAuthRefreshIntent(provider, accountId);
329
+ if (!current || current.generation !== generation) return false;
330
+ try { unlinkSync(getAuthRefreshIntentPath(provider, accountId)); return true; }
331
+ catch (error) { if (errorCode(error) === "ENOENT") return false; throw error; }
332
+ });
131
333
  }
132
334
  export function credentialGeneration(cred: OAuthCredentials): string {
133
335
  return createHash("sha256").update(JSON.stringify([cred.refresh, cred.access, cred.expires])).digest("hex");
@@ -470,6 +470,26 @@ function serializedSpill(
470
470
  };
471
471
  }
472
472
 
473
+ /**
474
+ * Exact on-disk payload size this spill WOULD occupy, measured before publication.
475
+ *
476
+ * Callers that reserve disk against a cap need the real envelope, not the resident
477
+ * measurement: the resident figure omits the `version` field the published payload
478
+ * carries, so pricing an admission by it undercounts and lets a request that sits exactly
479
+ * at the cap still exceed it. Shares `serializedSpill` rather than describing it, so the
480
+ * two cannot drift.
481
+ */
482
+ export function prospectiveResponseSpillBytes(
483
+ responseId: string,
484
+ state: Omit<ResponseSpillPayload, "version" | "responseId">,
485
+ ): number | null {
486
+ try {
487
+ return serializedSpill(responseId, state).bytes.byteLength;
488
+ } catch {
489
+ return null;
490
+ }
491
+ }
492
+
473
493
  function responseSpillWriteError(cause: unknown): NodeJS.ErrnoException {
474
494
  const error = new Error("Response spill write failed", { cause }) as NodeJS.ErrnoException;
475
495
  if (cause && typeof cause === "object" && "code" in cause) {