@bitkyc08/opencodex 2.36.0-preview.20260829 → 2.36.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/gui/dist/assets/index-Cy7Z_pl0.css +1 -0
- package/gui/dist/assets/index-DO8liQVL.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +2 -1
- package/src/AGENTS.md +2 -1
- package/src/adapters/agentrouter.ts +50 -0
- package/src/adapters/anthropic.ts +1 -51
- package/src/adapters/cursor/call-id.ts +76 -8
- package/src/adapters/cursor/checkpoint-store.ts +6 -1
- package/src/adapters/cursor/cursor-errors.ts +44 -0
- package/src/adapters/cursor/native-exec.ts +13 -0
- package/src/adapters/cursor/protobuf-request.ts +651 -29
- package/src/adapters/cursor/tool-result-normalize.ts +3 -3
- package/src/adapters/cursor/transport-retry.ts +5 -1
- package/src/adapters/cursor.ts +15 -1
- package/src/adapters/empty-tool-output-annotation.ts +43 -0
- package/src/adapters/exec-tool-result-normalize.ts +70 -5
- package/src/adapters/google.ts +22 -2
- package/src/adapters/kiro.ts +26 -2
- package/src/adapters/ollama-native-url.ts +111 -0
- package/src/adapters/ollama-native.ts +1131 -0
- package/src/adapters/openai-chat.ts +30 -7
- package/src/adapters/openai-responses.ts +72 -4
- package/src/adapters/registry.ts +7 -0
- package/src/adapters/xai-web-search.ts +58 -0
- package/src/claude/desktop-3p.ts +21 -1
- package/src/claude/desktop-policy.ts +149 -0
- package/src/cli/account.ts +16 -2
- package/src/cli/claude-desktop.ts +13 -3
- package/src/cli/combo.ts +8 -5
- package/src/cli/doctor.ts +77 -11
- package/src/cli/help.ts +1 -1
- package/src/cli/index.ts +16 -0
- package/src/cli/models.ts +20 -3
- package/src/cli/registry.ts +2 -1
- package/src/cli/status.ts +140 -2
- package/src/cli/storage.ts +10 -1
- package/src/codex/account-runtime-state.ts +39 -5
- package/src/codex/account-store.ts +393 -13
- package/src/codex/account-usability.ts +11 -4
- package/src/codex/app-server-processes.ts +46 -5
- package/src/codex/auth-context.ts +160 -32
- package/src/codex/catalog/bundled.ts +7 -5
- package/src/codex/catalog/metadata.ts +1 -1
- package/src/codex/catalog/parsing.ts +57 -1
- package/src/codex/catalog/provider-fetch.ts +61 -4
- package/src/codex/catalog/sync.ts +4 -3
- package/src/codex/convergence.ts +3 -2
- package/src/codex/data/upstream-models.json +40 -8
- package/src/codex/inject-coordination.ts +111 -14
- package/src/codex/integration-record.ts +12 -2
- package/src/codex/main-account.ts +225 -1
- package/src/codex/model-entitlements.ts +339 -27
- package/src/codex/prompt-layers.ts +346 -7
- package/src/codex/prompt-text-probe.ts +272 -21
- package/src/codex/routing.ts +693 -132
- package/src/codex/runtime.ts +12 -0
- package/src/codex/subagent-model-fallback.ts +62 -24
- package/src/codex/user-identity.ts +33 -25
- package/src/combos/index.ts +1 -0
- package/src/combos/reset-window.ts +46 -0
- package/src/combos/resolve.ts +84 -2
- package/src/combos/types.ts +5 -2
- package/src/config/atomic-write.ts +104 -22
- package/src/config/provider-validation.ts +11 -0
- package/src/config.ts +75 -3
- package/src/generated/compatibility-version.json +207 -131
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/catalog.ts +71 -0
- package/src/grok/effort.ts +83 -0
- package/src/grok/inject.ts +952 -127
- package/src/grok/models.ts +56 -0
- package/src/grok/status.ts +21 -8
- package/src/grok/sync.ts +10 -18
- package/src/images/loop.ts +6 -3
- package/src/integrations/native/ownership-preflight.ts +4 -1
- package/src/lab/fabric/producer-isolate.ts +36 -3
- package/src/lib/destination-policy.ts +93 -7
- package/src/lib/redact.ts +6 -1
- package/src/lib/shadow-call.ts +38 -3
- package/src/lib/test-home-guard.ts +18 -3
- package/src/lib/upstream-retry.ts +43 -6
- package/src/lib/windows-secret-acl.ts +66 -0
- package/src/lib/windows-text.ts +28 -2
- package/src/lib/windows-user-principal.ts +35 -23
- package/src/oauth/account-quota-rank.ts +107 -0
- package/src/oauth/anthropic-routing.ts +125 -30
- package/src/oauth/chatgpt.ts +5 -1
- package/src/oauth/generic-account-failover.ts +114 -7
- package/src/oauth/index.ts +15 -8
- package/src/oauth/store.ts +16 -0
- package/src/providers/account-quota-disk.ts +79 -0
- package/src/providers/command-code-efforts.ts +24 -0
- package/src/providers/derive.ts +6 -0
- package/src/providers/key-failover.ts +33 -1
- package/src/providers/kiro-usage.ts +272 -0
- package/src/providers/ollama-show.ts +311 -0
- package/src/providers/openai-sidecar.ts +5 -0
- package/src/providers/quota-routing-cache.ts +32 -0
- package/src/providers/quota-types.ts +36 -0
- package/src/providers/quota-wire.ts +102 -0
- package/src/providers/quota.ts +208 -147
- package/src/providers/registry.ts +68 -8
- package/src/providers/slug-codec.ts +12 -4
- package/src/providers/vercel-gateway-routing.ts +108 -0
- package/src/router.ts +22 -12
- package/src/server/auth-cors.ts +26 -0
- package/src/server/catalog-download.ts +73 -0
- package/src/server/chat-native.ts +12 -2
- package/src/server/gui-static.ts +4 -1
- package/src/server/index.ts +132 -9
- package/src/server/management/agent-settings-routes.ts +38 -5
- package/src/server/management/codex-prompt-routes.ts +7 -1
- package/src/server/management/combo-routes.ts +10 -1
- package/src/server/management/config-routes.ts +9 -1
- package/src/server/management/context.ts +5 -0
- package/src/server/management/model-routes.ts +16 -6
- package/src/server/management/native-integration-routes.ts +12 -17
- package/src/server/management/oauth-account-routes.ts +13 -0
- package/src/server/management/provider-routes.ts +32 -5
- package/src/server/management/routing-profile-routes.ts +15 -0
- package/src/server/management/shadow-call-validation.ts +29 -0
- package/src/server/management-api.ts +7 -3
- package/src/server/request-log.ts +3 -5
- package/src/server/responses/agent-task-recovery-cache.ts +8 -0
- package/src/server/responses/agent-task-recovery.ts +52 -20
- package/src/server/responses/codex-auth-error.ts +26 -0
- package/src/server/responses/compact.ts +345 -10
- package/src/server/responses/core.ts +736 -108
- package/src/server/responses/empty-completion-guard.ts +16 -0
- package/src/server/responses/fetch-helpers.ts +42 -0
- package/src/server/responses/policy-fallback.ts +11 -6
- package/src/server/responses-undeclared-tool-guard.ts +16 -3
- package/src/server/startup-health-cache.ts +59 -13
- package/src/service-manager-probe.ts +115 -9
- package/src/service.ts +139 -40
- package/src/storage/cleanup.ts +10 -0
- package/src/storage/storage-mutation-coordinator.ts +14 -3
- package/src/tray/windows-tray.ps1 +10 -4
- package/src/tray/windows.ts +30 -2
- package/src/types/config.ts +27 -14
- package/src/types/provider.ts +54 -0
- package/src/types/tools.ts +13 -3
- package/src/types.ts +4 -0
- package/src/usage/summary.ts +421 -177
- package/src/vision/anthropic-describe.ts +3 -3
- package/src/vision/describe.ts +5 -3
- package/src/web-search/anthropic-executor.ts +9 -2
- package/src/web-search/exa-executor.ts +3 -3
- package/src/web-search/executor.ts +8 -3
- package/src/web-search/gemini-executor.ts +3 -3
- package/src/web-search/loop.ts +11 -3
- package/src/web-search/xai-executor.ts +3 -3
- package/gui/dist/assets/index-DQ-Ie18T.css +0 -1
- package/gui/dist/assets/index-DX0RGj3e.js +0 -112
|
@@ -21,6 +21,10 @@
|
|
|
21
21
|
"multi_agent_version": "v2",
|
|
22
22
|
"use_responses_lite": true,
|
|
23
23
|
"include_skills_usage_instructions": false,
|
|
24
|
+
"node_repl_disabled": false,
|
|
25
|
+
"node_repl_auto_review_required": false,
|
|
26
|
+
"include_plugin_usage_instructions": true,
|
|
27
|
+
"include_apps_usage_instructions": true,
|
|
24
28
|
"auto_review_model_override": null,
|
|
25
29
|
"context_window": 372000,
|
|
26
30
|
"max_context_window": 372000,
|
|
@@ -57,7 +61,7 @@
|
|
|
57
61
|
"description": "Maximum reasoning with automatic task delegation"
|
|
58
62
|
}
|
|
59
63
|
],
|
|
60
|
-
"shell_type": "
|
|
64
|
+
"shell_type": "unified_exec",
|
|
61
65
|
"visibility": "list",
|
|
62
66
|
"minimal_client_version": "0.142.2",
|
|
63
67
|
"supported_in_api": true,
|
|
@@ -135,6 +139,10 @@
|
|
|
135
139
|
"multi_agent_version": "v2",
|
|
136
140
|
"use_responses_lite": true,
|
|
137
141
|
"include_skills_usage_instructions": false,
|
|
142
|
+
"node_repl_disabled": false,
|
|
143
|
+
"node_repl_auto_review_required": false,
|
|
144
|
+
"include_plugin_usage_instructions": true,
|
|
145
|
+
"include_apps_usage_instructions": true,
|
|
138
146
|
"auto_review_model_override": null,
|
|
139
147
|
"context_window": 372000,
|
|
140
148
|
"max_context_window": 372000,
|
|
@@ -171,7 +179,7 @@
|
|
|
171
179
|
"description": "Maximum reasoning with automatic task delegation"
|
|
172
180
|
}
|
|
173
181
|
],
|
|
174
|
-
"shell_type": "
|
|
182
|
+
"shell_type": "unified_exec",
|
|
175
183
|
"visibility": "list",
|
|
176
184
|
"minimal_client_version": "0.142.2",
|
|
177
185
|
"supported_in_api": true,
|
|
@@ -247,6 +255,10 @@
|
|
|
247
255
|
"multi_agent_version": "v1",
|
|
248
256
|
"use_responses_lite": true,
|
|
249
257
|
"include_skills_usage_instructions": false,
|
|
258
|
+
"node_repl_disabled": false,
|
|
259
|
+
"node_repl_auto_review_required": false,
|
|
260
|
+
"include_plugin_usage_instructions": true,
|
|
261
|
+
"include_apps_usage_instructions": true,
|
|
250
262
|
"auto_review_model_override": null,
|
|
251
263
|
"context_window": 372000,
|
|
252
264
|
"max_context_window": 372000,
|
|
@@ -279,7 +291,7 @@
|
|
|
279
291
|
"description": "Maximum reasoning depth for the hardest problems"
|
|
280
292
|
}
|
|
281
293
|
],
|
|
282
|
-
"shell_type": "
|
|
294
|
+
"shell_type": "unified_exec",
|
|
283
295
|
"visibility": "list",
|
|
284
296
|
"minimal_client_version": "0.142.2",
|
|
285
297
|
"supported_in_api": true,
|
|
@@ -355,6 +367,10 @@
|
|
|
355
367
|
"multi_agent_version": null,
|
|
356
368
|
"use_responses_lite": false,
|
|
357
369
|
"include_skills_usage_instructions": true,
|
|
370
|
+
"node_repl_disabled": false,
|
|
371
|
+
"node_repl_auto_review_required": false,
|
|
372
|
+
"include_plugin_usage_instructions": true,
|
|
373
|
+
"include_apps_usage_instructions": true,
|
|
358
374
|
"auto_review_model_override": null,
|
|
359
375
|
"context_window": 272000,
|
|
360
376
|
"max_context_window": 272000,
|
|
@@ -383,7 +399,7 @@
|
|
|
383
399
|
"description": "Extra high reasoning depth for complex problems"
|
|
384
400
|
}
|
|
385
401
|
],
|
|
386
|
-
"shell_type": "
|
|
402
|
+
"shell_type": "unified_exec",
|
|
387
403
|
"visibility": "list",
|
|
388
404
|
"minimal_client_version": "0.124.0",
|
|
389
405
|
"supported_in_api": true,
|
|
@@ -461,6 +477,10 @@
|
|
|
461
477
|
"multi_agent_version": null,
|
|
462
478
|
"use_responses_lite": false,
|
|
463
479
|
"include_skills_usage_instructions": false,
|
|
480
|
+
"node_repl_disabled": false,
|
|
481
|
+
"node_repl_auto_review_required": false,
|
|
482
|
+
"include_plugin_usage_instructions": true,
|
|
483
|
+
"include_apps_usage_instructions": true,
|
|
464
484
|
"auto_review_model_override": null,
|
|
465
485
|
"context_window": 272000,
|
|
466
486
|
"max_context_window": 1000000,
|
|
@@ -489,7 +509,7 @@
|
|
|
489
509
|
"description": "Extra high reasoning depth for complex problems"
|
|
490
510
|
}
|
|
491
511
|
],
|
|
492
|
-
"shell_type": "
|
|
512
|
+
"shell_type": "unified_exec",
|
|
493
513
|
"visibility": "list",
|
|
494
514
|
"minimal_client_version": "0.98.0",
|
|
495
515
|
"supported_in_api": true,
|
|
@@ -562,6 +582,10 @@
|
|
|
562
582
|
"multi_agent_version": null,
|
|
563
583
|
"use_responses_lite": false,
|
|
564
584
|
"include_skills_usage_instructions": false,
|
|
585
|
+
"node_repl_disabled": false,
|
|
586
|
+
"node_repl_auto_review_required": false,
|
|
587
|
+
"include_plugin_usage_instructions": true,
|
|
588
|
+
"include_apps_usage_instructions": true,
|
|
565
589
|
"auto_review_model_override": null,
|
|
566
590
|
"context_window": 272000,
|
|
567
591
|
"max_context_window": 272000,
|
|
@@ -590,7 +614,7 @@
|
|
|
590
614
|
"description": "Extra high reasoning depth for complex problems"
|
|
591
615
|
}
|
|
592
616
|
],
|
|
593
|
-
"shell_type": "
|
|
617
|
+
"shell_type": "unified_exec",
|
|
594
618
|
"visibility": "list",
|
|
595
619
|
"minimal_client_version": "0.98.0",
|
|
596
620
|
"supported_in_api": true,
|
|
@@ -658,6 +682,10 @@
|
|
|
658
682
|
"multi_agent_version": null,
|
|
659
683
|
"use_responses_lite": false,
|
|
660
684
|
"include_skills_usage_instructions": false,
|
|
685
|
+
"node_repl_disabled": false,
|
|
686
|
+
"node_repl_auto_review_required": false,
|
|
687
|
+
"include_plugin_usage_instructions": true,
|
|
688
|
+
"include_apps_usage_instructions": true,
|
|
661
689
|
"auto_review_model_override": null,
|
|
662
690
|
"context_window": 272000,
|
|
663
691
|
"max_context_window": 272000,
|
|
@@ -686,7 +714,7 @@
|
|
|
686
714
|
"description": "Extra high reasoning for complex problems"
|
|
687
715
|
}
|
|
688
716
|
],
|
|
689
|
-
"shell_type": "
|
|
717
|
+
"shell_type": "unified_exec",
|
|
690
718
|
"visibility": "list",
|
|
691
719
|
"minimal_client_version": "0.0.1",
|
|
692
720
|
"supported_in_api": true,
|
|
@@ -754,6 +782,10 @@
|
|
|
754
782
|
"multi_agent_version": null,
|
|
755
783
|
"use_responses_lite": false,
|
|
756
784
|
"include_skills_usage_instructions": false,
|
|
785
|
+
"node_repl_disabled": false,
|
|
786
|
+
"node_repl_auto_review_required": false,
|
|
787
|
+
"include_plugin_usage_instructions": true,
|
|
788
|
+
"include_apps_usage_instructions": true,
|
|
757
789
|
"auto_review_model_override": null,
|
|
758
790
|
"context_window": 272000,
|
|
759
791
|
"max_context_window": 1000000,
|
|
@@ -782,7 +814,7 @@
|
|
|
782
814
|
"description": "Extra high reasoning depth for complex problems"
|
|
783
815
|
}
|
|
784
816
|
],
|
|
785
|
-
"shell_type": "
|
|
817
|
+
"shell_type": "unified_exec",
|
|
786
818
|
"visibility": "hide",
|
|
787
819
|
"minimal_client_version": "0.98.0",
|
|
788
820
|
"supported_in_api": true,
|
|
@@ -16,6 +16,7 @@ import { CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "./paths";
|
|
|
16
16
|
import type {
|
|
17
17
|
CodexArtifactId,
|
|
18
18
|
CodexProvenanceEntry,
|
|
19
|
+
CodexProvenanceLedger,
|
|
19
20
|
} from "./convergence-types";
|
|
20
21
|
import {
|
|
21
22
|
codexWriteCoordination,
|
|
@@ -196,6 +197,41 @@ export function captureCodexPreImages(): CodexPreImages {
|
|
|
196
197
|
*/
|
|
197
198
|
export const CODEX_PROVENANCE_MAX_TRANSACTIONS = 16;
|
|
198
199
|
|
|
200
|
+
/**
|
|
201
|
+
* How many serialized bytes of evidence the ledger keeps.
|
|
202
|
+
*
|
|
203
|
+
* The transaction window alone bounds the ledger only if each transaction is small, and a
|
|
204
|
+
* baseline embeds the artifact's exact bytes. Those artifacts sit outside this proxy's trust
|
|
205
|
+
* boundary: a native `config.toml` grown to hundreds of megabytes is copied into the record as
|
|
206
|
+
* base64 and re-serialized on every append, so a single oversized file turns a 16-transaction
|
|
207
|
+
* window into a multi-gigabyte write.
|
|
208
|
+
*
|
|
209
|
+
* The size is derived from the window above rather than picked: the 25 KB `config.toml` that
|
|
210
|
+
* comment describes measures 100.8 KiB per transaction, so a full 16-transaction window is
|
|
211
|
+
* 1.58 MiB. 4 MiB leaves that ordinary window untouched with room to spare while still refusing
|
|
212
|
+
* the pathological case, and a regression test pins the ordinary window so the two cannot drift
|
|
213
|
+
* apart silently.
|
|
214
|
+
*/
|
|
215
|
+
export const CODEX_PROVENANCE_MAX_BYTES = 4 * 1024 * 1024;
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Measure the representation the integration-record writer actually emits.
|
|
219
|
+
*
|
|
220
|
+
* The minimal wrapper reproduces the final indentation depth of `provenance.entries`, including
|
|
221
|
+
* structured unknown extension fields preserved from older records. Its fixed wrapper makes this
|
|
222
|
+
* slightly conservative as a ledger-only ceiling, which is preferable to admitting an entry that
|
|
223
|
+
* expands past the limit when `JSON.stringify(record, null, 2)` writes it.
|
|
224
|
+
*/
|
|
225
|
+
function serializedProvenanceBytes(
|
|
226
|
+
entries: readonly CodexProvenanceEntry[],
|
|
227
|
+
ledger: CodexProvenanceLedger | undefined,
|
|
228
|
+
): number {
|
|
229
|
+
return Buffer.byteLength(`${JSON.stringify({
|
|
230
|
+
version: 1,
|
|
231
|
+
provenance: { ...ledger, entries },
|
|
232
|
+
}, null, 2)}\n`, "utf-8");
|
|
233
|
+
}
|
|
234
|
+
|
|
199
235
|
function provenanceBaseline(bytes: string | null): CodexProvenanceEntry["baseline"] {
|
|
200
236
|
if (bytes === null) return { kind: "absent" };
|
|
201
237
|
return {
|
|
@@ -214,21 +250,66 @@ function provenancePostImage(path: string): string | null {
|
|
|
214
250
|
}
|
|
215
251
|
|
|
216
252
|
/**
|
|
217
|
-
* Keep the newest
|
|
253
|
+
* Keep the newest transactions that fit both the transaction window and the byte ceiling, whole.
|
|
218
254
|
*
|
|
219
255
|
* Trimming by ENTRY count would cut a transaction in half and leave evidence that says a
|
|
220
256
|
* transaction touched two artifacts when it touched three — worse than dropping it outright,
|
|
221
|
-
* because a partial record still reads as complete. Order is preserved; only whole
|
|
222
|
-
*
|
|
257
|
+
* because a partial record still reads as complete. Order is preserved; only whole transactions
|
|
258
|
+
* are removed.
|
|
259
|
+
*
|
|
260
|
+
* The byte ceiling applies the same rule: a transaction that does not fit is omitted whole,
|
|
261
|
+
* including the newest one — a record silently truncated to fit would be read as a faithful
|
|
262
|
+
* pre-image. An oversized transaction is skipped rather than ending the scan, so one pathological
|
|
263
|
+
* artifact cannot erase the smaller transactions that still fit and are still diagnosable.
|
|
223
264
|
*/
|
|
224
265
|
export function boundProvenanceEntries(
|
|
225
266
|
entries: readonly CodexProvenanceEntry[],
|
|
226
267
|
maxTransactions = CODEX_PROVENANCE_MAX_TRANSACTIONS,
|
|
268
|
+
maxBytes = CODEX_PROVENANCE_MAX_BYTES,
|
|
269
|
+
ledger?: CodexProvenanceLedger,
|
|
227
270
|
): readonly CodexProvenanceEntry[] {
|
|
228
|
-
const
|
|
229
|
-
for (const entry of entries)
|
|
230
|
-
|
|
231
|
-
|
|
271
|
+
const transactions = new Map<string, CodexProvenanceEntry[]>();
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
const transaction = transactions.get(entry.txId);
|
|
274
|
+
if (transaction) transaction.push(entry);
|
|
275
|
+
else transactions.set(entry.txId, [entry]);
|
|
276
|
+
}
|
|
277
|
+
const order = [...transactions.keys()];
|
|
278
|
+
if (order.length <= maxTransactions) {
|
|
279
|
+
const baselineBytes = entries.reduce(
|
|
280
|
+
(total, entry) => total + (entry.baseline.kind === "present" ? entry.baseline.bytesBase64.length : 0),
|
|
281
|
+
0,
|
|
282
|
+
);
|
|
283
|
+
if (
|
|
284
|
+
baselineBytes <= maxBytes
|
|
285
|
+
&& serializedProvenanceBytes(entries, ledger) <= maxBytes
|
|
286
|
+
) return entries;
|
|
287
|
+
}
|
|
288
|
+
// Newest first, so the transactions anyone diagnoses against are the ones that fit.
|
|
289
|
+
const keep = new Set<string>();
|
|
290
|
+
let baselineBytes = 0;
|
|
291
|
+
for (let i = order.length - 1; i >= 0 && keep.size < maxTransactions; i--) {
|
|
292
|
+
const txId = order[i]!;
|
|
293
|
+
const txEntries = transactions.get(txId)!;
|
|
294
|
+
// Measure the embedded pre-images first: a pathological baseline is refused without
|
|
295
|
+
// serializing it, so the ceiling does not itself allocate the payload it exists to reject.
|
|
296
|
+
const transactionBaselineBytes = txEntries.reduce(
|
|
297
|
+
(total, entry) => total + (entry.baseline.kind === "present" ? entry.baseline.bytesBase64.length : 0),
|
|
298
|
+
0,
|
|
299
|
+
);
|
|
300
|
+
if (baselineBytes + transactionBaselineBytes > maxBytes) continue;
|
|
301
|
+
const candidateKeep = new Set(keep);
|
|
302
|
+
candidateKeep.add(txId);
|
|
303
|
+
// Reuse the already-grouped transactions: this avoids another full-ledger filter on each
|
|
304
|
+
// iteration while preserving the original transaction and entry order.
|
|
305
|
+
const candidateEntries = order.flatMap(id =>
|
|
306
|
+
candidateKeep.has(id) ? transactions.get(id)! : []
|
|
307
|
+
);
|
|
308
|
+
if (serializedProvenanceBytes(candidateEntries, ledger) > maxBytes) continue;
|
|
309
|
+
baselineBytes += transactionBaselineBytes;
|
|
310
|
+
keep.add(txId);
|
|
311
|
+
}
|
|
312
|
+
if (keep.size === order.length) return entries;
|
|
232
313
|
return entries.filter(entry => keep.has(entry.txId));
|
|
233
314
|
}
|
|
234
315
|
|
|
@@ -250,13 +331,29 @@ export function recordCodexNativeTransactionProvenance(
|
|
|
250
331
|
txId,
|
|
251
332
|
at,
|
|
252
333
|
}));
|
|
253
|
-
return updateIntegrationRecord(record =>
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
334
|
+
return updateIntegrationRecord(record => {
|
|
335
|
+
const previousLedger = record.provenance;
|
|
336
|
+
// Unknown ledger extensions are forward-compatible and must be preserved. If those fixed
|
|
337
|
+
// fields alone exceed the ceiling, no entry selection can make the write compliant. Keep the
|
|
338
|
+
// existing record byte-for-byte instead of deleting all known evidence and rewriting the
|
|
339
|
+
// same oversized extension on every native transaction.
|
|
340
|
+
if (
|
|
341
|
+
previousLedger
|
|
342
|
+
&& serializedProvenanceBytes([], previousLedger) > CODEX_PROVENANCE_MAX_BYTES
|
|
343
|
+
) return record;
|
|
344
|
+
return {
|
|
345
|
+
...record,
|
|
346
|
+
provenance: {
|
|
347
|
+
...previousLedger,
|
|
348
|
+
entries: boundProvenanceEntries(
|
|
349
|
+
[...(previousLedger?.entries ?? []), ...entries],
|
|
350
|
+
CODEX_PROVENANCE_MAX_TRANSACTIONS,
|
|
351
|
+
CODEX_PROVENANCE_MAX_BYTES,
|
|
352
|
+
previousLedger,
|
|
353
|
+
),
|
|
354
|
+
},
|
|
355
|
+
};
|
|
356
|
+
});
|
|
260
357
|
}
|
|
261
358
|
|
|
262
359
|
/**
|
|
@@ -204,12 +204,17 @@ function mergeEntry(previous: CodexProvenanceEntry, next: CodexProvenanceEntry):
|
|
|
204
204
|
function mergeLedger(previous: CodexProvenanceLedger, next: CodexProvenanceLedger): CodexProvenanceLedger {
|
|
205
205
|
const unused = new Set(previous.entries.map((_, index) => index));
|
|
206
206
|
const entries = next.entries.map((entry, nextIndex) => {
|
|
207
|
-
|
|
207
|
+
// Extensions follow provenance identity, never position. A bounded ledger may drop an
|
|
208
|
+
// oversized transaction, shifting an unrelated entry into that slot, and even within one
|
|
209
|
+
// transaction and timestamp the artifact may differ. Since this search already requires
|
|
210
|
+
// txId, timestamp, and artifact identity, any remaining positional match is by definition
|
|
211
|
+
// a different artifact, so preserving its unknown entry/artifact/baseline fields would
|
|
212
|
+
// attach false evidence and could regrow the record past the measured byte ceiling.
|
|
213
|
+
const previousIndex = previous.entries.findIndex((candidate, index) =>
|
|
208
214
|
unused.has(index)
|
|
209
215
|
&& candidate.txId === entry.txId
|
|
210
216
|
&& candidate.at === entry.at
|
|
211
217
|
&& knownArtifactIdentity(candidate.artifact) === knownArtifactIdentity(entry.artifact));
|
|
212
|
-
if (previousIndex < 0 && unused.has(nextIndex)) previousIndex = nextIndex;
|
|
213
218
|
if (previousIndex < 0) return entry;
|
|
214
219
|
unused.delete(previousIndex);
|
|
215
220
|
return mergeEntry(previous.entries[previousIndex]!, entry);
|
|
@@ -245,6 +250,11 @@ export const updateIntegrationRecord = (
|
|
|
245
250
|
if (read.kind === "invalid") return read;
|
|
246
251
|
const previous: CodexIntegrationRecord = read.kind === "ready" ? read.record : { version: 1 };
|
|
247
252
|
const proposed = mutate(previous);
|
|
253
|
+
// The mutator can explicitly decline an update by returning the exact record it received.
|
|
254
|
+
// This matters when a preserved forward-compatible extension already exceeds a caller's
|
|
255
|
+
// write budget: rewriting the same oversized bytes would amplify I/O while deleting known
|
|
256
|
+
// entries cannot make that irreducible overhead fit.
|
|
257
|
+
if (proposed === previous) return { kind: "updated" as const, record: previous };
|
|
248
258
|
if (!validateRecord(proposed)) {
|
|
249
259
|
return { kind: "invalid", message: "Codex integration record update produced a malformed v1 shape" };
|
|
250
260
|
}
|
|
@@ -1,7 +1,23 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import { readCodexTokens } from "./auth-collision";
|
|
2
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
decodeJwtPayload,
|
|
7
|
+
extractAccountId,
|
|
8
|
+
refreshChatGPTToken,
|
|
9
|
+
} from "../oauth/chatgpt";
|
|
10
|
+
import type { OAuthCredentials } from "../oauth/types";
|
|
3
11
|
import { extractChatgptPlanType } from "./plan";
|
|
4
12
|
import { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
13
|
+
import {
|
|
14
|
+
refreshGrantFingerprintForToken,
|
|
15
|
+
withCodexRefreshFileLock,
|
|
16
|
+
} from "./account-store";
|
|
17
|
+
import { atomicWriteFile, resolveWriteTarget } from "../config/atomic-write";
|
|
18
|
+
import { resolveCodexHomeDir } from "./home";
|
|
19
|
+
import { assertNotRealCodexHomeUnderTest } from "../lib/test-home-guard";
|
|
20
|
+
import { clearAccountNeedsReauth } from "./account-runtime-state";
|
|
5
21
|
|
|
6
22
|
export { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
7
23
|
|
|
@@ -12,6 +28,214 @@ export { MAIN_CODEX_ACCOUNT_ID } from "./account-id";
|
|
|
12
28
|
*/
|
|
13
29
|
let mainAccountPlan: string | null = null;
|
|
14
30
|
let jwtPlanAttempted = false;
|
|
31
|
+
const MAIN_TOKEN_REFRESH_SKEW_MS = 60_000;
|
|
32
|
+
let beforeMainAuthJsonRenameForTests: (() => void) | null = null;
|
|
33
|
+
|
|
34
|
+
type MainAuthJsonCredential = {
|
|
35
|
+
path: string;
|
|
36
|
+
rawSha256: string;
|
|
37
|
+
root: Record<string, unknown>;
|
|
38
|
+
tokens: Record<string, unknown>;
|
|
39
|
+
accessToken?: string;
|
|
40
|
+
refreshToken?: string;
|
|
41
|
+
chatgptAccountId: string;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export interface NativeMainRefreshDependencies {
|
|
45
|
+
refreshToken?: (refreshToken: string, options: { signal: AbortSignal }) => Promise<OAuthCredentials>;
|
|
46
|
+
signal?: AbortSignal;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export class MainAuthJsonChangedDuringRefreshError extends Error {
|
|
50
|
+
constructor() {
|
|
51
|
+
super("Codex auth.json changed while its token was refreshing");
|
|
52
|
+
this.name = "MainAuthJsonChangedDuringRefreshError";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class MainAccountTokenRefreshError extends Error {
|
|
57
|
+
constructor(readonly reason: "reauth" | "transient", options?: ErrorOptions) {
|
|
58
|
+
super(reason === "reauth"
|
|
59
|
+
? "Codex main account needs reauthentication"
|
|
60
|
+
: "Codex main token refresh did not complete", options);
|
|
61
|
+
this.name = "MainAccountTokenRefreshError";
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function nonEmptyString(value: unknown): string | undefined {
|
|
66
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function sha256(value: string): string {
|
|
70
|
+
return createHash("sha256").update(value).digest("hex");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readMainAuthJsonCredential(): MainAuthJsonCredential | null {
|
|
74
|
+
const path = resolveWriteTarget(join(resolveCodexHomeDir(), "auth.json"));
|
|
75
|
+
let raw: string;
|
|
76
|
+
try {
|
|
77
|
+
raw = readFileSync(path, "utf8");
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
try {
|
|
82
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
83
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return null;
|
|
84
|
+
const root = parsed as Record<string, unknown>;
|
|
85
|
+
const tokenValue = root.tokens;
|
|
86
|
+
if (!tokenValue || typeof tokenValue !== "object" || Array.isArray(tokenValue)) return null;
|
|
87
|
+
const tokens = tokenValue as Record<string, unknown>;
|
|
88
|
+
const accessToken = nonEmptyString(tokens.access_token);
|
|
89
|
+
const refreshToken = nonEmptyString(tokens.refresh_token);
|
|
90
|
+
if (!accessToken && !refreshToken) return null;
|
|
91
|
+
const idToken = nonEmptyString(tokens.id_token);
|
|
92
|
+
const chatgptAccountId = extractAccountId(idToken, accessToken)
|
|
93
|
+
?? nonEmptyString(tokens.account_id)
|
|
94
|
+
?? "";
|
|
95
|
+
return {
|
|
96
|
+
path,
|
|
97
|
+
rawSha256: sha256(raw),
|
|
98
|
+
root,
|
|
99
|
+
tokens,
|
|
100
|
+
...(accessToken ? { accessToken } : {}),
|
|
101
|
+
...(refreshToken ? { refreshToken } : {}),
|
|
102
|
+
chatgptAccountId,
|
|
103
|
+
};
|
|
104
|
+
} catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function mainAccessTokenFresh(accessToken: string | undefined, now: number, skewMs: number): boolean {
|
|
110
|
+
if (!accessToken) return false;
|
|
111
|
+
const payload = decodeJwtPayload(accessToken);
|
|
112
|
+
const exp = typeof payload?.exp === "number" ? payload.exp * 1000 : undefined;
|
|
113
|
+
return exp === undefined || exp > now + skewMs;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A refresh grant makes native main routeable even when the current access token is expired. */
|
|
117
|
+
export function isMainAccountCredentialUsable(now = Date.now()): boolean {
|
|
118
|
+
const current = readMainAuthJsonCredential();
|
|
119
|
+
return !!current?.refreshToken || mainAccessTokenFresh(current?.accessToken, now, 0);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function hasMainAccountRefreshGrant(): boolean {
|
|
123
|
+
return !!readMainAuthJsonCredential()?.refreshToken;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function assertMainAuthJsonSnapshotUnchanged(expected: MainAuthJsonCredential): void {
|
|
127
|
+
const current = readMainAuthJsonCredential();
|
|
128
|
+
if (!current || current.path !== expected.path || current.rawSha256 !== expected.rawSha256) {
|
|
129
|
+
throw new MainAuthJsonChangedDuringRefreshError();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function persistRefreshedMainAuthJson(
|
|
134
|
+
expected: MainAuthJsonCredential,
|
|
135
|
+
refreshed: OAuthCredentials,
|
|
136
|
+
): { accessToken: string; chatgptAccountId: string } {
|
|
137
|
+
assertNotRealCodexHomeUnderTest(resolveCodexHomeDir());
|
|
138
|
+
const accessToken = refreshed.access;
|
|
139
|
+
const refreshToken = refreshed.refresh || expected.refreshToken!;
|
|
140
|
+
const chatgptAccountId = refreshed.accountId
|
|
141
|
+
?? extractAccountId(undefined, accessToken)
|
|
142
|
+
?? expected.chatgptAccountId;
|
|
143
|
+
const tokens = {
|
|
144
|
+
...expected.tokens,
|
|
145
|
+
access_token: accessToken,
|
|
146
|
+
refresh_token: refreshToken,
|
|
147
|
+
account_id: chatgptAccountId,
|
|
148
|
+
};
|
|
149
|
+
atomicWriteFile(
|
|
150
|
+
expected.path,
|
|
151
|
+
JSON.stringify({ ...expected.root, tokens }, null, 2) + "\n",
|
|
152
|
+
undefined,
|
|
153
|
+
{
|
|
154
|
+
beforeRename: () => {
|
|
155
|
+
assertMainAuthJsonSnapshotUnchanged(expected);
|
|
156
|
+
const hook = beforeMainAuthJsonRenameForTests;
|
|
157
|
+
beforeMainAuthJsonRenameForTests = null;
|
|
158
|
+
hook?.();
|
|
159
|
+
},
|
|
160
|
+
validateBeforeRename: () => assertMainAuthJsonSnapshotUnchanged(expected),
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
return { accessToken, chatgptAccountId };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function setMainAuthJsonBeforeRenameHookForTests(hook: (() => void) | null): void {
|
|
167
|
+
beforeMainAuthJsonRenameForTests = hook;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
async function resolveMainAccountToken(
|
|
171
|
+
dependencies: NativeMainRefreshDependencies = {},
|
|
172
|
+
rejectedAccessToken?: string,
|
|
173
|
+
): Promise<{ accessToken: string; chatgptAccountId: string } | null> {
|
|
174
|
+
const initial = readMainAuthJsonCredential();
|
|
175
|
+
if (!initial) return null;
|
|
176
|
+
const now = Date.now();
|
|
177
|
+
if (initial.accessToken !== rejectedAccessToken
|
|
178
|
+
&& mainAccessTokenFresh(initial.accessToken, now, MAIN_TOKEN_REFRESH_SKEW_MS)) {
|
|
179
|
+
return { accessToken: initial.accessToken!, chatgptAccountId: initial.chatgptAccountId };
|
|
180
|
+
}
|
|
181
|
+
if (!initial.refreshToken) {
|
|
182
|
+
return initial.accessToken !== rejectedAccessToken
|
|
183
|
+
&& mainAccessTokenFresh(initial.accessToken, now, 0)
|
|
184
|
+
? { accessToken: initial.accessToken!, chatgptAccountId: initial.chatgptAccountId }
|
|
185
|
+
: null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const signal = dependencies.signal
|
|
189
|
+
? AbortSignal.any([dependencies.signal, AbortSignal.timeout(30_000)])
|
|
190
|
+
: AbortSignal.timeout(30_000);
|
|
191
|
+
const lockKey = refreshGrantFingerprintForToken(initial.refreshToken);
|
|
192
|
+
return withCodexRefreshFileLock(lockKey, signal, async () => {
|
|
193
|
+
const locked = readMainAuthJsonCredential();
|
|
194
|
+
if (!locked) throw new MainAuthJsonChangedDuringRefreshError();
|
|
195
|
+
if (!locked.refreshToken
|
|
196
|
+
|| refreshGrantFingerprintForToken(locked.refreshToken) !== lockKey) {
|
|
197
|
+
if (locked.accessToken !== rejectedAccessToken
|
|
198
|
+
&& mainAccessTokenFresh(locked.accessToken, Date.now(), 0)) {
|
|
199
|
+
return { accessToken: locked.accessToken!, chatgptAccountId: locked.chatgptAccountId };
|
|
200
|
+
}
|
|
201
|
+
throw new MainAuthJsonChangedDuringRefreshError();
|
|
202
|
+
}
|
|
203
|
+
if (locked.accessToken !== rejectedAccessToken
|
|
204
|
+
&& mainAccessTokenFresh(locked.accessToken, Date.now(), MAIN_TOKEN_REFRESH_SKEW_MS)) {
|
|
205
|
+
return { accessToken: locked.accessToken!, chatgptAccountId: locked.chatgptAccountId };
|
|
206
|
+
}
|
|
207
|
+
const refresh = dependencies.refreshToken
|
|
208
|
+
?? ((refreshToken: string, options: { signal: AbortSignal }) => refreshChatGPTToken(refreshToken, options));
|
|
209
|
+
let refreshed: OAuthCredentials;
|
|
210
|
+
try {
|
|
211
|
+
refreshed = await refresh(locked.refreshToken, { signal });
|
|
212
|
+
} catch (cause) {
|
|
213
|
+
const message = cause instanceof Error ? cause.message.toLowerCase() : "";
|
|
214
|
+
const reason = /invalid_grant|invalidated|revoked|expired/.test(message)
|
|
215
|
+
? "reauth" as const
|
|
216
|
+
: "transient" as const;
|
|
217
|
+
throw new MainAccountTokenRefreshError(reason, { cause });
|
|
218
|
+
}
|
|
219
|
+
const result = persistRefreshedMainAuthJson(locked, refreshed);
|
|
220
|
+
clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
221
|
+
return result;
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/** Refresh the CLI-owned native credential before upstream I/O and publish it atomically. */
|
|
226
|
+
export function getValidMainAccountToken(
|
|
227
|
+
dependencies: NativeMainRefreshDependencies = {},
|
|
228
|
+
): Promise<{ accessToken: string; chatgptAccountId: string } | null> {
|
|
229
|
+
return resolveMainAccountToken(dependencies);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Force refresh after upstream rejected this exact bearer once. */
|
|
233
|
+
export function forceRefreshMainAccountToken(
|
|
234
|
+
rejectedAccessToken: string,
|
|
235
|
+
dependencies: NativeMainRefreshDependencies = {},
|
|
236
|
+
): Promise<{ accessToken: string; chatgptAccountId: string } | null> {
|
|
237
|
+
return resolveMainAccountToken(dependencies, rejectedAccessToken);
|
|
238
|
+
}
|
|
15
239
|
|
|
16
240
|
export function setMainAccountPlan(plan: string | null): void {
|
|
17
241
|
mainAccountPlan = plan;
|