@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.
- package/bin/ocx.mjs +58 -6
- package/gui/dist/assets/index-D-lchsPw.js +112 -0
- package/gui/dist/assets/index-uvENYLin.css +1 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/baseten.svg +13 -0
- package/gui/dist/provider-icons/bizrouter.svg +41 -0
- package/gui/dist/provider-icons/cerebras.svg +26 -0
- package/gui/dist/provider-icons/deepinfra.svg +75 -0
- package/gui/dist/provider-icons/digitalocean.svg +10 -0
- package/gui/dist/provider-icons/featherless.svg +4 -0
- package/gui/dist/provider-icons/gajae-code.svg +410 -0
- package/gui/dist/provider-icons/hermes-agent.svg +207 -0
- package/gui/dist/provider-icons/hyperbolic.svg +18 -0
- package/gui/dist/provider-icons/kilo.svg +13 -0
- package/gui/dist/provider-icons/litellm.svg +1 -0
- package/gui/dist/provider-icons/minimax.svg +1 -0
- package/gui/dist/provider-icons/nanogpt.svg +74 -0
- package/gui/dist/provider-icons/nebius.svg +1 -0
- package/gui/dist/provider-icons/neuralwatt.svg +27 -0
- package/gui/dist/provider-icons/nous.svg +149 -0
- package/gui/dist/provider-icons/novita.svg +32 -0
- package/gui/dist/provider-icons/orcarouter.svg +175 -0
- package/gui/dist/provider-icons/parallel.svg +13 -0
- package/gui/dist/provider-icons/sambanova.svg +276 -0
- package/gui/dist/provider-icons/scaleway.svg +11 -0
- package/gui/dist/provider-icons/siliconflow.svg +18 -0
- package/gui/dist/provider-icons/synthetic.svg +12 -0
- package/gui/dist/provider-icons/together.svg +18 -0
- package/gui/dist/provider-icons/umans.svg +30 -0
- package/gui/dist/provider-icons/venice.svg +165 -0
- package/gui/dist/provider-icons/vultr.svg +15 -0
- package/gui/dist/provider-icons/zai.svg +218 -0
- package/gui/dist/provider-icons/zenmux.svg +1 -0
- package/package.json +1 -1
- package/src/adapters/cursor/live-models.ts +1 -0
- package/src/adapters/openai-responses.ts +72 -10
- package/src/bridge.ts +15 -15
- package/src/cli/dispatch.ts +54 -18
- package/src/cli/index.ts +420 -30
- package/src/cli/integrations.ts +24 -2
- package/src/cli/uninstall-plan.ts +86 -0
- package/src/codex/account-store.ts +121 -8
- package/src/codex/auth-api.ts +202 -33
- package/src/codex/catalog/provider-fetch.ts +76 -9
- package/src/codex/history-job.ts +10 -0
- package/src/codex/history-manifest.ts +35 -2
- package/src/codex/history-provider.ts +196 -19
- package/src/codex/history-worker.ts +3 -0
- package/src/codex/quota-401-recovery.ts +190 -0
- package/src/codex/quota-recovery-timing.ts +28 -0
- package/src/codex/quota.ts +6 -0
- package/src/codex/routing.ts +68 -16
- package/src/codex/subagent-model-fallback.ts +4 -1
- package/src/config/pending-teardown-names.d.mts +8 -0
- package/src/config/pending-teardown-names.mjs +69 -0
- package/src/config/pending-teardown.ts +286 -0
- package/src/generated/compatibility-version.json +84 -32
- package/src/integrations/journal.ts +12 -1
- package/src/integrations/writer.ts +82 -7
- package/src/lib/process-control.ts +30 -5
- package/src/lib/state-store-registrations.ts +8 -0
- package/src/oauth/index.ts +220 -13
- package/src/oauth/store.ts +220 -18
- package/src/responses/spill-store.ts +20 -0
- package/src/responses/state.ts +288 -2
- package/src/server/management/integration-routes.ts +36 -5
- package/src/server/management-api.ts +66 -12
- package/src/server/responses/encrypted-payload.ts +18 -1
- package/src/server/stop-teardown.ts +84 -0
- package/src/service.ts +155 -20
- package/src/update/index.ts +36 -4
- package/src/update/proxy-liveness-probe.d.mts +6 -0
- package/src/update/proxy-liveness-probe.mjs +84 -0
- package/src/update/stop-contract.d.mts +2 -0
- package/src/update/stop-contract.mjs +15 -0
- package/src/update/stop-decision.d.mts +10 -0
- package/src/update/stop-decision.mjs +34 -0
- package/src/vision/eligibility.ts +19 -1
- package/src/vision/index.ts +4 -18
- package/gui/dist/assets/index-C14iCj_Q.js +0 -112
- package/gui/dist/assets/index-D7PIz7_g.css +0 -1
package/src/oauth/store.ts
CHANGED
|
@@ -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
|
|
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
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
180
|
+
return uncertainOAuthRefreshIntent(provider, accountId);
|
|
111
181
|
}
|
|
112
182
|
}
|
|
113
|
-
export function writeOAuthRefreshIntent(provider: string, accountId: string, generation: string, createdAt = Date.now(), flightId?: string):
|
|
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 = {
|
|
118
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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) {
|