@bitkyc08/opencodex 2.58.0 → 2.60.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/README.md +28 -10
- package/gui/dist/assets/index-BTuCbqQd.css +1 -0
- package/gui/dist/assets/index-DoBVdPHP.js +134 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/crusoe.svg +1 -0
- package/gui/dist/provider-icons/opper.svg +3 -0
- package/package.json +4 -1
- package/src/adapters/anthropic-image-codec.ts +16 -2
- package/src/adapters/anthropic-image-normalize.ts +49 -2
- package/src/adapters/anthropic.ts +4 -1
- package/src/adapters/base.ts +34 -1
- package/src/adapters/coding-agent/turn.ts +22 -2
- package/src/adapters/command-code.ts +50 -3
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/checkpoint-store.ts +3 -0
- package/src/adapters/cursor/discovery.ts +11 -8
- package/src/adapters/cursor/effort-map.ts +16 -2
- package/src/adapters/cursor/envelope-echo.ts +55 -2
- package/src/adapters/cursor/live-transport.ts +26 -9
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-request.ts +8 -5
- package/src/adapters/cursor/request-builder.ts +21 -4
- package/src/adapters/cursor/thread-continuity.ts +105 -31
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/transport.ts +19 -0
- package/src/adapters/cursor.ts +45 -2
- package/src/adapters/devin/cloud-direct/chat.ts +14 -3
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +140 -0
- package/src/adapters/devin.ts +125 -26
- package/src/adapters/google-antigravity-replay.ts +1 -1
- package/src/adapters/google-antigravity-wire.ts +55 -4
- package/src/adapters/google-http.ts +57 -11
- package/src/adapters/google-tool-schema.ts +595 -31
- package/src/adapters/google-wire-compiler.ts +93 -10
- package/src/adapters/google-wire-shape.ts +461 -0
- package/src/adapters/google.ts +60 -10
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat-images.ts +3 -1
- package/src/adapters/openai-chat.ts +10 -11
- package/src/adapters/openai-responses/image-gen.ts +8 -6
- package/src/adapters/openai-responses/passthrough.ts +25 -4
- package/src/adapters/openai-responses/reasoning.ts +7 -0
- package/src/adapters/openai-responses/tool-output-recovery.ts +75 -0
- package/src/adapters/openai-responses/tool-schema.ts +19 -7
- package/src/adapters/opencode-go-additional-tools.ts +12 -2
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +1 -1
- package/src/bridge/sse.ts +28 -35
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +35 -4
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +26 -6
- package/src/cli/account-orca-import.ts +63 -0
- package/src/cli/account.ts +32 -4
- package/src/cli/capabilities.ts +40 -0
- package/src/cli/claude.ts +29 -1
- package/src/cli/codex-cli-update.ts +97 -2
- package/src/cli/dispatch.ts +57 -3
- package/src/cli/doctor.ts +218 -4
- package/src/cli/help.ts +4 -1
- package/src/cli/hub.ts +3 -2
- package/src/cli/index.ts +95 -22
- package/src/cli/models-runtime.ts +33 -4
- package/src/cli/opencode.ts +2 -2
- package/src/cli/provider.ts +13 -1
- package/src/cli/registry.ts +11 -1
- package/src/cli/runtime-api.ts +44 -0
- package/src/cli/start-args.ts +94 -0
- package/src/cli/system-command.ts +2 -0
- package/src/client/machine-api.ts +6 -5
- package/src/client/machine-listener.ts +16 -3
- package/src/client/runtime.ts +26 -2
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +146 -5
- package/src/codex/auth-api/account-list.ts +19 -11
- package/src/codex/auth-api/pool-quota-probe.ts +44 -10
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +16 -12
- package/src/codex/catalog/build-entries.ts +25 -4
- package/src/codex/catalog/derive-entry.ts +8 -1
- package/src/codex/catalog/effort.ts +10 -6
- package/src/codex/catalog/gather-capture.ts +22 -2
- package/src/codex/catalog/model-hints.ts +66 -33
- package/src/codex/catalog/parsing.ts +90 -5
- package/src/codex/catalog/provider-models.ts +19 -2
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +41 -26
- package/src/codex/catalog/routed-gather.ts +61 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/context-compat.ts +5 -2
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desired-state.ts +4 -1
- package/src/codex/history-job.ts +6 -6
- package/src/codex/history-provider.ts +24 -167
- package/src/codex/history-rollout-read.ts +174 -0
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject.ts +3 -2
- package/src/codex/internal/catalog-writer.ts +33 -1
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +31 -0
- package/src/codex/main-account-hard-lock.ts +2 -1
- package/src/codex/main-account.ts +10 -3
- package/src/codex/main-device-reauth.ts +17 -9
- package/src/codex/model-cache.ts +47 -0
- package/src/codex/model-entitlements.ts +80 -2
- package/src/codex/observed-model-denials.ts +230 -0
- package/src/codex/orca-auth-source.ts +94 -0
- package/src/codex/orca-import.ts +219 -0
- package/src/codex/prompt-text-probe.ts +289 -16
- package/src/codex/quota-401-recovery.ts +12 -0
- package/src/codex/quota-types.ts +65 -0
- package/src/codex/quota.ts +24 -19
- package/src/codex/routing/cooldown-math.ts +8 -47
- package/src/codex/routing/pin-drain.ts +57 -0
- package/src/codex/routing.ts +20 -16
- package/src/codex/shim.ts +1 -1
- package/src/codex/subagent-model-fallback.ts +114 -2
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +125 -5
- package/src/config/admitted-identity.ts +222 -0
- package/src/config/diagnostics.ts +43 -1
- package/src/config/feature-flags.ts +5 -0
- package/src/config/load-degrade.ts +33 -0
- package/src/config/pending-teardown.ts +8 -0
- package/src/config/process-state.ts +36 -3
- package/src/config/provider-relative-send-path.ts +16 -0
- package/src/config/proxy-env.ts +31 -7
- package/src/config/schema/compaction-triggers.ts +11 -0
- package/src/config/schema/config-schema.ts +25 -0
- package/src/config/schema/leaf-validators.ts +76 -17
- package/src/config.ts +2 -2
- package/src/generated/compatibility-version.json +410 -258
- package/src/generated/model-metadata.ts +1 -1
- package/src/grok/reset-coupons.ts +38 -19
- package/src/images/loop.ts +6 -1
- package/src/integrations/aside-profile-context.ts +37 -3
- package/src/integrations/aside-profile-journal.ts +68 -3
- package/src/integrations/aside-profiles.ts +128 -3
- package/src/integrations/mutation-plan.ts +815 -0
- package/src/integrations/writer.ts +85 -99
- package/src/lab/live/transport.ts +4 -0
- package/src/lab/live/types.ts +5 -0
- package/src/lab/subject/behavior-fingerprint.ts +1 -1
- package/src/lib/admin-secrets.ts +9 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/debug-log-buffer.ts +6 -1
- package/src/lib/debug.ts +23 -0
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +82 -15
- package/src/lib/http-response-semantics.ts +57 -0
- package/src/lib/lab-live-pinned-sender.ts +26 -12
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/pinned-http.ts +142 -2
- package/src/lib/plain-data.ts +103 -0
- package/src/lib/process-control.ts +13 -5
- package/src/lib/provider-outbound.ts +53 -5
- package/src/lib/proxy-env.ts +70 -3
- package/src/lib/request-execution-budget.ts +11 -3
- package/src/lib/response-body-inactivity.ts +193 -0
- package/src/lib/retry-delay.ts +69 -0
- package/src/lib/socks5-fetch.ts +741 -0
- package/src/lib/spend-ledger-owner.ts +364 -0
- package/src/lib/spend-reservation-ledger.ts +332 -35
- package/src/lib/windows-system-proxy.ts +16 -11
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/callback-server.ts +4 -3
- package/src/oauth/generic-account-failover.ts +41 -27
- package/src/oauth/health.ts +12 -1
- package/src/oauth/index.ts +3 -107
- package/src/oauth/login-flow-state.ts +127 -0
- package/src/oauth/orcarouter.ts +15 -2
- package/src/oauth/store.ts +8 -0
- package/src/providers/codex-capacity.ts +9 -0
- package/src/providers/derive.ts +34 -17
- package/src/providers/devin-cli-authmode-migration.ts +14 -10
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +327 -25
- package/src/providers/model-rename-migration.ts +56 -1
- package/src/providers/model-rename-startup.ts +7 -5
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/openai-virtual-models.ts +42 -2
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/antigravity.ts +22 -2
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/quota/vendor-probes-key.ts +1 -1
- package/src/providers/registry/entries-core.ts +39 -17
- package/src/providers/registry/entries-extended.ts +131 -1
- package/src/providers/registry/model-ids.ts +168 -0
- package/src/providers/registry/model-seeds.ts +87 -21
- package/src/providers/registry/types.ts +2 -0
- package/src/providers/resolved-model-policy-merge.ts +167 -0
- package/src/providers/resolved-model-policy.ts +406 -0
- package/src/providers/stale-vision-classification-migration.ts +137 -0
- package/src/responses/apply-patch-envelope.ts +32 -11
- package/src/responses/bridge-search-replay-cache.ts +152 -0
- package/src/responses/code-mode-helper-compat.ts +26 -16
- package/src/responses/custom-tool-compat.ts +1 -1
- package/src/responses/freeform-wrapper-scan.ts +279 -0
- package/src/responses/hosted-tool-policy.ts +85 -2
- package/src/responses/legacy-dotted-tool-name-repair.ts +134 -0
- package/src/responses/progressive-freeform-input.ts +130 -0
- package/src/responses/reasoning-envelope.ts +30 -0
- package/src/responses/schema.ts +9 -2
- package/src/responses/state.ts +5 -12
- package/src/responses/tool-name-aliases.ts +15 -1
- package/src/router.ts +91 -115
- package/src/routing/compatibility/behavior.ts +9 -0
- package/src/routing/compatibility/subject.ts +16 -1
- package/src/server/adapter-resolve.ts +9 -0
- package/src/server/auth-cors.ts +29 -0
- package/src/server/chat-completions.ts +13 -5
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +30 -5
- package/src/server/effort-row.ts +11 -3
- package/src/server/grok-responses-control-frame.ts +160 -1
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/serve-options.ts +86 -29
- package/src/server/index/spend-ledger-lifecycle.ts +66 -0
- package/src/server/index/websocket-handler.ts +6 -1
- package/src/server/index.ts +24 -25
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/aside-profile-routes.ts +266 -7
- package/src/server/management/config-routes.ts +48 -7
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +287 -5
- package/src/server/management/metrics-routes.ts +20 -0
- package/src/server/management/model-rows.ts +224 -12
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/management/route-registry.ts +14 -0
- package/src/server/management/shared.ts +10 -3
- package/src/server/management/system-restart.ts +7 -2
- package/src/server/management/system-routes.ts +2 -0
- package/src/server/management/usage-aggregate-cache.ts +4 -0
- package/src/server/management-api.ts +2 -0
- package/src/server/management-auth.ts +15 -1
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/readiness.ts +29 -10
- package/src/server/relay-eager.ts +24 -2
- package/src/server/relay.ts +136 -34
- package/src/server/request-log.ts +80 -3
- package/src/server/request-metrics.ts +236 -0
- package/src/server/responses/adapter-continuation.ts +74 -30
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +63 -30
- package/src/server/responses/compact.ts +89 -18
- package/src/server/responses/compaction-routing.ts +111 -0
- package/src/server/responses/core-codex-account.ts +90 -24
- package/src/server/responses/core-combo.ts +7 -7
- package/src/server/responses/core-normalize.ts +16 -15
- package/src/server/responses/core-opaque-recovery.ts +1 -0
- package/src/server/responses/core-options.ts +4 -0
- package/src/server/responses/encrypted-payload.ts +20 -2
- package/src/server/responses/fetch-helpers.ts +68 -2
- package/src/server/responses/passthrough-delivery.ts +41 -15
- package/src/server/responses/passthrough-dispatch.ts +145 -53
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/policy-fallback.ts +5 -13
- package/src/server/responses/request-prepare.ts +96 -17
- package/src/server/responses/request-send-budget.ts +89 -8
- package/src/server/responses/request-sidecar-auth.ts +17 -9
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +15 -12
- package/src/server/responses/run-turn-execution.ts +45 -8
- package/src/server/responses/sidecar-execution.ts +19 -2
- package/src/server/responses/ws-upstream.ts +16 -28
- package/src/server/responses-custom-tool-repair.ts +29 -56
- package/src/server/responses-undeclared-tool-guard.ts +31 -1
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +37 -10
- package/src/server/system-env-shell.ts +5 -1
- package/src/server/system-env.ts +7 -1
- package/src/server/workflow-refusal.ts +56 -2
- package/src/service/cli.ts +16 -6
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/tray/windows-tray.ps1 +155 -3
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +111 -6
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +9 -1
- package/src/types/tools.ts +14 -0
- package/src/types/wire.ts +9 -1
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/vision/eligibility.ts +88 -9
- package/src/vision/plan.ts +34 -10
- package/src/web-search/executor.ts +41 -2
- package/src/web-search/loop.ts +6 -1
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-BbrHOIY0.js +0 -128
- package/gui/dist/assets/index-C5-RdDmD.css +0 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How the read-only Codex history preflight opens the state store.
|
|
3
|
+
*
|
|
4
|
+
* Split out of `history-provider.ts` deliberately. That file is the single largest module in
|
|
5
|
+
* `src/codex/` and sits just under the repository's 2000-line ratchet; the reasoning below is
|
|
6
|
+
* load-bearing and long, and appending it there would have pushed the file over. Keeping the
|
|
7
|
+
* open policy in one small module also puts the decision somewhere it can be read on its own,
|
|
8
|
+
* which matters because getting it wrong is silent in both directions.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { pathToFileURL } from "node:url";
|
|
12
|
+
import { Database, constants as sqliteConstants } from "bun:sqlite";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Read-only open flags that skip WAL shared memory entirely.
|
|
16
|
+
*
|
|
17
|
+
* `immutable=1` has to arrive as a `file:` URI, and a URI filename needs SQLITE_OPEN_URI.
|
|
18
|
+
* Same idiom as the storage scanner, the log-guard inspector and the coordinator doctor, for
|
|
19
|
+
* the same reason: never touch a foreign store's WAL protocol.
|
|
20
|
+
*/
|
|
21
|
+
const IMMUTABLE_READONLY_FLAGS = sqliteConstants.SQLITE_OPEN_READONLY | sqliteConstants.SQLITE_OPEN_URI;
|
|
22
|
+
|
|
23
|
+
/** Is this the "there is no shared memory and I may not create it" open failure? */
|
|
24
|
+
export function isStateDbCantOpenError(error: unknown): boolean {
|
|
25
|
+
const code = typeof error === "object" && error && "code" in error ? String((error as { code?: unknown }).code) : "";
|
|
26
|
+
const message = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
27
|
+
// Matched on the code with a message fallback, exactly like classifyRecoverableHistoryError:
|
|
28
|
+
// the same SQLite condition reaches us as a code on some platforms and as bare text on others.
|
|
29
|
+
return code === "SQLITE_CANTOPEN" || message.includes("unable to open database file");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Which step of the primary attempt a test wants to fail. */
|
|
33
|
+
export type StateDbPreflightOpenPhase = "open" | "first-read";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Test-only knob: force the primary attempt to fail with a supplied error.
|
|
37
|
+
*
|
|
38
|
+
* The fallback below turns on a condition this repository cannot reproduce deterministically
|
|
39
|
+
* from a test: whether a plain read-only open of a cleanly-closed WAL store fails or quietly
|
|
40
|
+
* creates the `-shm` depends on the platform VFS and on the directory the store sits in.
|
|
41
|
+
* Pinning the NARROWING — sidecars absent admits the immutable read, either sidecar present
|
|
42
|
+
* still refuses — therefore needs the failure supplied rather than provoked, or the test would
|
|
43
|
+
* assert the host's SQLite build instead of this decision (#4943).
|
|
44
|
+
*
|
|
45
|
+
* The phase exists because the platforms disagree about WHEN the condition is raised, not only
|
|
46
|
+
* about whether it is: see the first-read note on `openCodexStateForPreflight`.
|
|
47
|
+
*/
|
|
48
|
+
let openFailureForTests: ((path: string, phase: StateDbPreflightOpenPhase) => unknown) | undefined;
|
|
49
|
+
export function setStateDbPreflightOpenFailureForTests(hook: typeof openFailureForTests): void {
|
|
50
|
+
openFailureForTests = hook;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Open the Codex state store for the read-only preflight.
|
|
55
|
+
*
|
|
56
|
+
* `{ readonly: true }` is tried FIRST and stays the primary path, because it is the only mode
|
|
57
|
+
* that can see a live WAL: it joins the writer's shared memory, so a thread another process
|
|
58
|
+
* just migrated to paginated history is visible here and the preflight refuses on it. An
|
|
59
|
+
* immutable open reads the last checkpointed main database instead. Making that the primary
|
|
60
|
+
* path would trade a refusal for a stale snapshot, and a refusal this preflight fails to
|
|
61
|
+
* observe is a config transition that proceeds over history Codex owns — the exact outcome the
|
|
62
|
+
* whole guard exists to prevent.
|
|
63
|
+
*
|
|
64
|
+
* A WAL store closed cleanly is the case that has no shared memory to join. SQLite cannot
|
|
65
|
+
* create the `-shm` from a read-only connection, so the open fails SQLITE_CANTOPEN on a store
|
|
66
|
+
* that is perfectly healthy, the catch-all in the preflight folds that into
|
|
67
|
+
* `history_injection_preflight_unavailable`, and `ocx sync` refuses on every attempt with no
|
|
68
|
+
* way forward (#4943).
|
|
69
|
+
*
|
|
70
|
+
* So the fallback is admitted only in the state where the absent sidecars are what make an
|
|
71
|
+
* immutable read exact rather than stale: no `-wal` and no `-shm` on disk means no writer is
|
|
72
|
+
* attached and no committed content sits outside the main database, so the main file IS the
|
|
73
|
+
* whole store and the two modes cannot disagree. Either sidecar present keeps the original
|
|
74
|
+
* error and the refusal that follows from it — a `-wal` holds content this connection would
|
|
75
|
+
* not read, and a `-shm` means a writer is attached, and neither is a store this preflight
|
|
76
|
+
* may inspect from a snapshot.
|
|
77
|
+
*
|
|
78
|
+
* The first read belongs INSIDE this attempt. `sqlite3_open_v2` does not touch page 1, so a
|
|
79
|
+
* store whose header says WAL is not inspected until the first prepare — which is where the
|
|
80
|
+
* missing shared memory is discovered on macOS, one caller frame above this function. Opening
|
|
81
|
+
* here and reading there put the classification and the failure in different scopes: the
|
|
82
|
+
* fallback was never reached, and the operator got the catch-all refusal the fix was supposed
|
|
83
|
+
* to remove. Linux hides this because its SQLite materializes the sidecars on that first read
|
|
84
|
+
* and never fails at all (#4943, macOS CI).
|
|
85
|
+
*/
|
|
86
|
+
export function openCodexStateForPreflight(resolvedPath: string): Database {
|
|
87
|
+
let db: Database | undefined;
|
|
88
|
+
try {
|
|
89
|
+
const forced = openFailureForTests?.(resolvedPath, "open");
|
|
90
|
+
if (forced) throw forced;
|
|
91
|
+
db = new Database(resolvedPath, { readonly: true });
|
|
92
|
+
const forcedRead = openFailureForTests?.(resolvedPath, "first-read");
|
|
93
|
+
if (forcedRead) throw forcedRead;
|
|
94
|
+
// Page 1, read while the failure is still this function's to classify.
|
|
95
|
+
db.query<{ tables: number }, []>("SELECT count(*) AS tables FROM sqlite_master").get();
|
|
96
|
+
return db;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
db?.close();
|
|
99
|
+
if (!isStateDbCantOpenError(error)) throw error;
|
|
100
|
+
if (existsSync(`${resolvedPath}-wal`) || existsSync(`${resolvedPath}-shm`)) throw error;
|
|
101
|
+
// pathToFileURL percent-encodes the reserved characters a naive `file:${path}` would
|
|
102
|
+
// misparse as a query or fragment.
|
|
103
|
+
return new Database(`${pathToFileURL(resolvedPath).href}?immutable=1`, IMMUTABLE_READONLY_FLAGS);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -55,6 +55,46 @@ export function applyEol(content: string, eol: "\r\n" | "\n"): string {
|
|
|
55
55
|
return eol === "\n" ? lf : lf.replace(/\n/g, "\r\n");
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
/** Label Codex shows for the injected provider when the operator has not chosen one. */
|
|
59
|
+
export const DEFAULT_CODEX_PROVIDER_DISPLAY_NAME = "OpenCodex Proxy";
|
|
60
|
+
|
|
61
|
+
/** Longest label accepted, matching the display-label policy used for provider names. */
|
|
62
|
+
const MAX_CODEX_PROVIDER_DISPLAY_NAME_LENGTH = 128;
|
|
63
|
+
|
|
64
|
+
/** Would this label put a control character into config.toml? */
|
|
65
|
+
function hasControlCharacter(value: string): boolean {
|
|
66
|
+
// Checked by code point rather than by a control-character regex, which needs a lint
|
|
67
|
+
// suppression this repository's hygiene gate rejects — and which reads no more clearly.
|
|
68
|
+
for (const character of value) {
|
|
69
|
+
const code = character.codePointAt(0) ?? 0;
|
|
70
|
+
if (code < 0x20 || code === 0x7f) return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Which label to write, given whatever the config holds.
|
|
77
|
+
*
|
|
78
|
+
* Presentation only, and deliberately separate from identity: routing resolves through the
|
|
79
|
+
* provider id `opencodex` in the root `model_provider` line and the `[model_providers.opencodex]`
|
|
80
|
+
* header, neither of which is derived from this value. So a rename cannot reroute a thread or
|
|
81
|
+
* orphan a row that already names that id (#4810).
|
|
82
|
+
*
|
|
83
|
+
* Every rejected value falls back to the default rather than being emitted or omitted. Codex
|
|
84
|
+
* refuses to load a provider with no name, so writing a blank one would break the whole config
|
|
85
|
+
* file rather than one thread — strictly worse than the branding it was meant to remove. That is
|
|
86
|
+
* also why there is no way to suppress the field: suppression here means choosing a neutral
|
|
87
|
+
* label. A control character or an over-long value is rejected for the same reason, because
|
|
88
|
+
* `tomlString` would faithfully encode something Codex may still reject.
|
|
89
|
+
*/
|
|
90
|
+
export function resolveCodexProviderDisplayName(configured?: string): string {
|
|
91
|
+
const trimmed = (configured ?? "").trim();
|
|
92
|
+
if (!trimmed) return DEFAULT_CODEX_PROVIDER_DISPLAY_NAME;
|
|
93
|
+
if (trimmed.length > MAX_CODEX_PROVIDER_DISPLAY_NAME_LENGTH) return DEFAULT_CODEX_PROVIDER_DISPLAY_NAME;
|
|
94
|
+
if (hasControlCharacter(trimmed)) return DEFAULT_CODEX_PROVIDER_DISPLAY_NAME;
|
|
95
|
+
return trimmed;
|
|
96
|
+
}
|
|
97
|
+
|
|
58
98
|
export function buildProviderTableBlock(
|
|
59
99
|
port: number,
|
|
60
100
|
supportsWebsockets?: boolean,
|
|
@@ -84,12 +124,13 @@ export function buildProviderTableBlock(
|
|
|
84
124
|
export function buildProviderTableBlockForTarget(
|
|
85
125
|
target: CodexRoutingTarget,
|
|
86
126
|
supportsWebsockets = false,
|
|
127
|
+
displayName?: string,
|
|
87
128
|
): string {
|
|
88
129
|
const lines = [
|
|
89
130
|
"",
|
|
90
131
|
OCX_SECTION_MARKER,
|
|
91
132
|
"[model_providers.opencodex]",
|
|
92
|
-
|
|
133
|
+
`name = ${tomlString(resolveCodexProviderDisplayName(displayName))}`,
|
|
93
134
|
`base_url = ${tomlString(target.baseUrl)}`,
|
|
94
135
|
'wire_api = "responses"',
|
|
95
136
|
// false only in the authless Desktop opt-in (#1107); true keeps the App/TUI account gate.
|
|
@@ -518,6 +559,7 @@ export function buildProfileFileForTarget(
|
|
|
518
559
|
catalogPath?: string | null,
|
|
519
560
|
supportsWebsockets = false,
|
|
520
561
|
fastMode?: boolean,
|
|
562
|
+
displayName?: string,
|
|
521
563
|
): string {
|
|
522
564
|
const origin = routingTargetOrigin(target);
|
|
523
565
|
const host = new URL(origin).host;
|
|
@@ -542,7 +584,7 @@ export function buildProfileFileForTarget(
|
|
|
542
584
|
];
|
|
543
585
|
if (catalogPath) lines.push(`model_catalog_json = ${tomlString(catalogPath)}`);
|
|
544
586
|
if (fastMode !== undefined) lines.push("", "[features]", `fast_mode = ${fastMode ? "true" : "false"}`);
|
|
545
|
-
lines.push(buildProviderTableBlockForTarget(target, supportsWebsockets).trimEnd(), "");
|
|
587
|
+
lines.push(buildProviderTableBlockForTarget(target, supportsWebsockets, displayName).trimEnd(), "");
|
|
546
588
|
return lines.join("\n");
|
|
547
589
|
}
|
|
548
590
|
|
package/src/codex/inject.ts
CHANGED
|
@@ -369,7 +369,7 @@ async function injectCodexConfigImpl(
|
|
|
369
369
|
content =
|
|
370
370
|
content.trimEnd() +
|
|
371
371
|
"\n" +
|
|
372
|
-
buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}));
|
|
372
|
+
buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}), config?.codexProviderDisplayName);
|
|
373
373
|
// 3) Keep existing `openai`-tagged threads reaching the proxy (see above). Ownership rules
|
|
374
374
|
// are the Design B ones: a user's own root line is never replaced.
|
|
375
375
|
if (keepRootOverrideAlongsideTable) {
|
|
@@ -430,6 +430,7 @@ async function injectCodexConfigImpl(
|
|
|
430
430
|
catalogPath,
|
|
431
431
|
websocketsEnabled(config ?? {}),
|
|
432
432
|
config?.fastMode,
|
|
433
|
+
config?.codexProviderDisplayName,
|
|
433
434
|
);
|
|
434
435
|
content = applyEol(content, eol);
|
|
435
436
|
|
|
@@ -492,7 +493,7 @@ async function injectCodexConfigImpl(
|
|
|
492
493
|
*/
|
|
493
494
|
if (hadOcxProviderTableOnDisk && !providerTableMode) {
|
|
494
495
|
content = applyEol(
|
|
495
|
-
content.trimEnd() + "\n" + buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {})),
|
|
496
|
+
content.trimEnd() + "\n" + buildProviderTableBlockForTarget(routingTarget, websocketsEnabled(config ?? {}), config?.codexProviderDisplayName),
|
|
496
497
|
eol,
|
|
497
498
|
);
|
|
498
499
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chmodSync, linkSync, mkdirSync, truncateSync, unlinkSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { chmodSync, linkSync, mkdirSync, readFileSync, truncateSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
|
|
4
4
|
import {
|
|
@@ -37,6 +37,38 @@ export interface CatalogBackupWriteIO {
|
|
|
37
37
|
|
|
38
38
|
let backupTempSequence = 0;
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Do the prepared bytes differ from what is already on disk at `prepared.path`?
|
|
42
|
+
*
|
|
43
|
+
* The single no-op rule for both files the catalog layer owns — the active catalog
|
|
44
|
+
* and Codex's models cache — so the two writers cannot drift apart. Every
|
|
45
|
+
* mtime-keyed reader has to treat a rewrite as a change: the app-server staleness
|
|
46
|
+
* classifier (#857) compares a file's mtime against each running Codex's start
|
|
47
|
+
* time, so rewriting identical bytes marks every already-running Codex as stale
|
|
48
|
+
* even though nothing changed. #1459 established this rule for the catalog; the
|
|
49
|
+
* cache is the second writer that has to apply it, because
|
|
50
|
+
* `refreshCodexModelCatalog` reports `cacheSynced` straight into the startup
|
|
51
|
+
* warning in `handleStart`.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately a Buffer rather than a decoded string: `readFileSync(path, "utf8")`
|
|
54
|
+
* substitutes U+FFFD for every invalid byte, so a file holding a raw 0x80 decodes
|
|
55
|
+
* equal to prepared content holding a legitimately encoded U+FFFD. Comparing
|
|
56
|
+
* decoded strings would then classify a malformed file as identical, skip the
|
|
57
|
+
* atomic repair write, and leave the corruption on disk.
|
|
58
|
+
*
|
|
59
|
+
* An unreadable or absent file reports "differs", so the caller performs the real
|
|
60
|
+
* write; that also converges a file that does not exist yet.
|
|
61
|
+
*/
|
|
62
|
+
export function preparedBytesDifferFromDisk(prepared: PreparedCatalogFileWrite): boolean {
|
|
63
|
+
let onDisk: Buffer;
|
|
64
|
+
try {
|
|
65
|
+
onDisk = readFileSync(prepared.path);
|
|
66
|
+
} catch {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return !onDisk.equals(Buffer.from(prepared.content, "utf8"));
|
|
70
|
+
}
|
|
71
|
+
|
|
40
72
|
function isMissingPathError(error: unknown): boolean {
|
|
41
73
|
return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
|
|
42
74
|
}
|
package/src/codex/lineage.ts
CHANGED
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
*
|
|
15
15
|
* What it is for, and what it is not for:
|
|
16
16
|
*
|
|
17
|
-
* - FIRST PLACEMENT.
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* - FIRST PLACEMENT. Largely subsumed by cohort keying (#4780): a member of a tree that any
|
|
18
|
+
* other member has already bound resolves to that same binding, so there is nothing to place.
|
|
19
|
+
* `pickLineageServingAccount` in ./routing remains for the one case cohort keying cannot
|
|
20
|
+
* unify -- a session-less chain whose parent is not recorded in this scope -- and is gated on
|
|
21
|
+
* the parent's key actually differing from this request's.
|
|
20
22
|
* - COST ATTRIBUTION. {@link codexThreadLineageLookup} and {@link codexLineageRootForRequest}
|
|
21
23
|
* answer which root workflow a conversation belongs to, so a grandchild's spend aggregates
|
|
22
24
|
* onto the root. No budget is implemented here.
|
|
@@ -155,30 +157,83 @@ export function codexConversationKeyFor(familyId: string, threadId: string): str
|
|
|
155
157
|
.digest("base64url")}`;
|
|
156
158
|
}
|
|
157
159
|
|
|
160
|
+
/**
|
|
161
|
+
* The cohort anchor's key: the same string on both sides of the derivation.
|
|
162
|
+
*
|
|
163
|
+
* A cohort is identified by one value the whole tree shares, so the key is that value keyed
|
|
164
|
+
* against itself rather than against a member. Using {@link codexConversationKeyFor} keeps one
|
|
165
|
+
* derivation in the module, which is what guarantees a lineage record's `conversationKey` stays
|
|
166
|
+
* byte-identical to the key the thread actually binds under.
|
|
167
|
+
*/
|
|
168
|
+
function cohortKeyFromAnchor(sessionId: string | undefined, fallbackAnchor: string): string {
|
|
169
|
+
const anchor = sessionId ?? fallbackAnchor;
|
|
170
|
+
return codexConversationKeyFor(anchor, anchor);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* The cohort this request belongs to, or undefined when it names no cohort at all.
|
|
175
|
+
*
|
|
176
|
+
* The session IS the cohort, which is exactly how upstream keys the prompt cache:
|
|
177
|
+
* `prompt_cache_key()` returns `responses_metadata.session_id` (or `{source}:{parent_thread_id}`
|
|
178
|
+
* for an internal session), and `AgentControl.session_id` is the root thread's id, shared with
|
|
179
|
+
* every sub-agent spawned from that root. Two requests carrying the same `prompt_cache_key` must
|
|
180
|
+
* not be served by different accounts, and keying on the session is what makes that structural
|
|
181
|
+
* rather than a hint (#4780).
|
|
182
|
+
*
|
|
183
|
+
* Without a session there is nothing in the headers that names the tree, so the cohort is
|
|
184
|
+
* whatever the parent is already bound to. That lookup is what keeps a chain of parent-only
|
|
185
|
+
* turns converging on one key: anchoring each depth on its own parent would split the cohort
|
|
186
|
+
* again at every hop. When the parent has not been seen in this scope the parent id anchors it,
|
|
187
|
+
* which is the same key that parent derives for itself.
|
|
188
|
+
*/
|
|
189
|
+
function cohortConversationKey(
|
|
190
|
+
headers: Headers,
|
|
191
|
+
sessionId: string | undefined,
|
|
192
|
+
parentThreadId: string | undefined,
|
|
193
|
+
now: number,
|
|
194
|
+
): string | undefined {
|
|
195
|
+
if (sessionId !== undefined) return cohortKeyFromAnchor(sessionId, sessionId);
|
|
196
|
+
if (parentThreadId === undefined) return undefined;
|
|
197
|
+
const recorded = liveLineageRecord(
|
|
198
|
+
lineageByScope.get(codexLineageScopeKey(headers)),
|
|
199
|
+
parentThreadId,
|
|
200
|
+
now,
|
|
201
|
+
);
|
|
202
|
+
return recorded?.conversationKey ?? cohortKeyFromAnchor(undefined, parentThreadId);
|
|
203
|
+
}
|
|
204
|
+
|
|
158
205
|
/**
|
|
159
206
|
* Resolve a request's conversation identity, or undefined when it carries no bindable thread
|
|
160
207
|
* identity at all.
|
|
161
208
|
*
|
|
162
|
-
* The set of requests that produce NO key is deliberately unchanged
|
|
163
|
-
* bare `thread-id` with neither a session nor a parent stays unbound, exactly as the
|
|
164
|
-
* fallback required both halves of its pair. Only the VALUE moves
|
|
165
|
-
*
|
|
209
|
+
* The set of requests that produce NO key is deliberately unchanged, through both #4546 and
|
|
210
|
+
* #4780: a bare `thread-id` with neither a session nor a parent stays unbound, exactly as the
|
|
211
|
+
* Desktop fallback required both halves of its pair. Only the VALUE moves.
|
|
212
|
+
*
|
|
213
|
+
* Every member of one tree resolves to the SAME key, because the cohort is the binding unit
|
|
214
|
+
* (#4780):
|
|
215
|
+
*
|
|
216
|
+
* - root (`session-id` + `thread-id`) -> the session's cohort key;
|
|
217
|
+
* - child and grandchild (parent + own `thread-id`) -> the same cohort key, since they carry the
|
|
218
|
+
* same session;
|
|
219
|
+
* - parent-only (no `thread-id`) -> the same cohort key, derived from the session or, without
|
|
220
|
+
* one, read from the parent's record.
|
|
166
221
|
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
*
|
|
172
|
-
*
|
|
222
|
+
* THIS IS NOT A REVERT OF #4546 wp8, and reading it as one would flip it straight back. wp8
|
|
223
|
+
* fixed a real incoherence: a child keyed under the RAW parent id, which is a different identity
|
|
224
|
+
* from the root's own `app:HMAC(session, thread)` binding, so siblings shared an entry unrelated
|
|
225
|
+
* to the root's and a grandchild keying on its own parent landed on a key nobody had ever bound.
|
|
226
|
+
* A cohort key cannot produce that, because the root's own binding IS the cohort key: there is
|
|
227
|
+
* one identity for the tree rather than two competing ones. What wp8 additionally gave each
|
|
228
|
+
* thread -- a binding of its own -- is what #4780 deliberately gives up, and the reason is that
|
|
229
|
+
* upstream never agreed to it: `prompt_cache_key` is keyed on the session the whole tree shares,
|
|
230
|
+
* so a proxy that splits the tree makes every split member assert a warm prefix that is cold on
|
|
231
|
+
* its account.
|
|
173
232
|
*
|
|
174
|
-
*
|
|
175
|
-
* conversation, so it
|
|
176
|
-
*
|
|
177
|
-
* and the
|
|
178
|
-
* exactly why deriving the key looks correct until a caller whose session differs from its thread
|
|
179
|
-
* starts a COLD conversation on every parent-only turn and overwrites the parent's record on the
|
|
180
|
-
* way through. So the recorded key wins, the session-derived key is the fallback that reproduces
|
|
181
|
-
* it when the parent has not been seen in this scope, and the raw parent id is never the answer.
|
|
233
|
+
* The parent-only case keeps the trap it always had. Such a turn belongs to the parent's
|
|
234
|
+
* conversation, so it must land on the binding the parent is already using. With a session in
|
|
235
|
+
* hand that is immediate, since both derive the same cohort key. Without one, the recorded key
|
|
236
|
+
* wins and the parent id anchors the fallback; the raw parent id is never the answer.
|
|
182
237
|
*/
|
|
183
238
|
export function codexConversationIdentity(
|
|
184
239
|
headers: Headers,
|
|
@@ -190,24 +245,20 @@ export function codexConversationIdentity(
|
|
|
190
245
|
|
|
191
246
|
if (threadId === undefined) {
|
|
192
247
|
if (parentThreadId === undefined) return undefined;
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
parentThreadId,
|
|
196
|
-
now,
|
|
197
|
-
);
|
|
248
|
+
const parentOnlyKey = cohortConversationKey(headers, sessionId, parentThreadId, now);
|
|
249
|
+
if (parentOnlyKey === undefined) return undefined;
|
|
198
250
|
return {
|
|
199
|
-
conversationKey:
|
|
200
|
-
?? codexConversationKeyFor(sessionId ?? parentThreadId, parentThreadId),
|
|
251
|
+
conversationKey: parentOnlyKey,
|
|
201
252
|
recordThreadId: parentThreadId,
|
|
202
253
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
203
254
|
legacyConversationKey: parentThreadId,
|
|
204
255
|
declaresParent: false,
|
|
205
256
|
};
|
|
206
257
|
}
|
|
207
|
-
const
|
|
208
|
-
if (
|
|
258
|
+
const conversationKey = cohortConversationKey(headers, sessionId, parentThreadId, now);
|
|
259
|
+
if (conversationKey === undefined) return undefined;
|
|
209
260
|
return {
|
|
210
|
-
conversationKey
|
|
261
|
+
conversationKey,
|
|
211
262
|
recordThreadId: threadId,
|
|
212
263
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
213
264
|
...(parentThreadId !== undefined ? { parentThreadId } : {}),
|
|
@@ -313,7 +364,7 @@ function lineageFor(
|
|
|
313
364
|
const parentConversationKey = parentThreadId === undefined
|
|
314
365
|
? undefined
|
|
315
366
|
: parentRecord?.conversationKey
|
|
316
|
-
??
|
|
367
|
+
?? cohortKeyFromAnchor(identity.sessionId, parentThreadId);
|
|
317
368
|
const rootSessionKey = parentConversationKey === undefined
|
|
318
369
|
? identity.conversationKey
|
|
319
370
|
: parentRecord?.rootSessionKey ?? parentConversationKey;
|
|
@@ -5,6 +5,37 @@ import { NATIVE_RESERVE_MODEL } from "./catalog/native-models";
|
|
|
5
5
|
export const CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE =
|
|
6
6
|
"Luna Reserve compatibility is only available as a conversation model, not a vision helper. Choose another vision model.";
|
|
7
7
|
|
|
8
|
+
export const CODEX_RESERVE_OPT_IN_REQUIRED_MESSAGE =
|
|
9
|
+
"Luna Reserve (gpt-reserve) is not forwarded without the local Desktop authless opt-in."
|
|
10
|
+
+ " OpenCodex holds no Reserve entitlement to send for this request, so the upstream would answer with a"
|
|
11
|
+
+ " usage-limit error that names neither the cause nor the fix."
|
|
12
|
+
+ " Enable the opt-in with 'ocx system settings --desktop-authless on' (or set codexDesktopAuthless to true"
|
|
13
|
+
+ " in config.json), then retry. Choose another model to keep working without it.";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Strict complement of {@link isCodexReserveRequestEligible} for the FLAG reason ONLY (#4940).
|
|
17
|
+
*
|
|
18
|
+
* Read the two together. This answers a narrower question: Reserve was asked for, every ingress
|
|
19
|
+
* condition eligibility requires already holds, and the single missing piece is the operator
|
|
20
|
+
* opt-in. Flipping `codexDesktopAuthless` to true therefore always turns a true here into a true
|
|
21
|
+
* from {@link isCodexReserveRequestEligible}, which is what makes it honest for the refusal to
|
|
22
|
+
* name that one setting. The other two ineligibility reasons are deliberately not covered: a
|
|
23
|
+
* client role and a non-loopback admission source are different situations, and the correct
|
|
24
|
+
* answer for both is still to forward exactly as before.
|
|
25
|
+
*
|
|
26
|
+
* Callers classify the concrete destination as canonical forward before using this predicate, the
|
|
27
|
+
* same obligation {@link isCodexReserveHelperUnsupported} carries. An operator who has aliased or
|
|
28
|
+
* routed `gpt-reserve` onto some other provider owns a path that works, and it must keep working.
|
|
29
|
+
*/
|
|
30
|
+
export function isCodexReserveOptInMissing(
|
|
31
|
+
config: Pick<OcxConfig, "codexDesktopAuthless" | "runtimeRole">,
|
|
32
|
+
modelId: string,
|
|
33
|
+
admission: Pick<DataPlaneAdmission, "source"> | undefined,
|
|
34
|
+
): boolean {
|
|
35
|
+
return modelId === NATIVE_RESERVE_MODEL && config.codexDesktopAuthless !== true
|
|
36
|
+
&& config.runtimeRole !== "client" && admission?.source === "loopback";
|
|
37
|
+
}
|
|
38
|
+
|
|
8
39
|
/** Callers classify the concrete destination as canonical forward before using this predicate. */
|
|
9
40
|
export function isCodexReserveHelperUnsupported(
|
|
10
41
|
config: Pick<OcxConfig, "codexDesktopAuthless" | "runtimeRole">,
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types";
|
|
2
2
|
import { getMainPolicyQuota } from "./quota";
|
|
3
|
+
import { MAIN_ACCOUNT_HARD_LOCK_PERCENT } from "./quota-types";
|
|
3
4
|
|
|
4
|
-
export
|
|
5
|
+
export { MAIN_ACCOUNT_HARD_LOCK_PERCENT };
|
|
5
6
|
|
|
6
7
|
export interface MainAccountHardLockStatus {
|
|
7
8
|
enabled: boolean;
|
|
@@ -296,7 +296,10 @@ function persistNativeMainReauthTokens(
|
|
|
296
296
|
* main account's reauth quarantine for the new credential generation.
|
|
297
297
|
*/
|
|
298
298
|
export function beginNativeMainReauth(): {
|
|
299
|
-
commit: (
|
|
299
|
+
commit: (
|
|
300
|
+
tokens: NativeMainReauthTokens,
|
|
301
|
+
options?: { signal?: AbortSignal },
|
|
302
|
+
) => Promise<{ chatgptAccountId: string }>;
|
|
300
303
|
} {
|
|
301
304
|
const expected = readMainAuthJsonCredential();
|
|
302
305
|
if (!expected || !expected.chatgptAccountId) {
|
|
@@ -305,7 +308,10 @@ export function beginNativeMainReauth(): {
|
|
|
305
308
|
);
|
|
306
309
|
}
|
|
307
310
|
return {
|
|
308
|
-
async commit(
|
|
311
|
+
async commit(
|
|
312
|
+
tokens: NativeMainReauthTokens,
|
|
313
|
+
options: { signal?: AbortSignal } = {},
|
|
314
|
+
): Promise<{ chatgptAccountId: string }> {
|
|
309
315
|
if (!tokens.accessToken || !tokens.refreshToken || !tokens.idToken) {
|
|
310
316
|
throw new NativeMainReauthUnavailableError("Device grant did not produce a complete token set");
|
|
311
317
|
}
|
|
@@ -322,11 +328,12 @@ export function beginNativeMainReauth(): {
|
|
|
322
328
|
"Native main traffic is blocked by startup or recovery state",
|
|
323
329
|
);
|
|
324
330
|
}
|
|
331
|
+
if (options.signal?.aborted) throw options.signal.reason;
|
|
325
332
|
assertMainAuthJsonSnapshotUnchanged(expected);
|
|
326
333
|
persistNativeMainReauthTokens(expected, tokens);
|
|
327
334
|
clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
328
335
|
return { chatgptAccountId: tokens.chatgptAccountId };
|
|
329
|
-
}, { waitMs: 30_000 });
|
|
336
|
+
}, { waitMs: 30_000, signal: options.signal });
|
|
330
337
|
},
|
|
331
338
|
};
|
|
332
339
|
}
|
|
@@ -54,7 +54,9 @@ interface ActiveFlow {
|
|
|
54
54
|
/** Set once auth.json has been replaced; cancellation can no longer win. */
|
|
55
55
|
published: boolean;
|
|
56
56
|
/** Snapshot-holding commit prepared at start; closure-private identity. */
|
|
57
|
-
prepared?: {
|
|
57
|
+
prepared?: {
|
|
58
|
+
commit: (tokens: NativeMainReauthTokens, options?: { signal?: AbortSignal }) => Promise<{ chatgptAccountId: string }>;
|
|
59
|
+
};
|
|
58
60
|
}
|
|
59
61
|
|
|
60
62
|
/** Bounded terminal retention so status/cancel stay answerable after completion. */
|
|
@@ -65,7 +67,9 @@ const terminalFlows = new Map<string, { status: MainDeviceReauthStatus; expiresA
|
|
|
65
67
|
|
|
66
68
|
export interface MainDeviceReauthDeps {
|
|
67
69
|
login?: (ctrl: OAuthController) => Promise<NativeDeviceLogin>;
|
|
68
|
-
beginCommit?: () => {
|
|
70
|
+
beginCommit?: () => {
|
|
71
|
+
commit: (tokens: NativeMainReauthTokens, options?: { signal?: AbortSignal }) => Promise<{ chatgptAccountId: string }>;
|
|
72
|
+
};
|
|
69
73
|
flowId?: () => string;
|
|
70
74
|
now?: () => number;
|
|
71
75
|
}
|
|
@@ -81,10 +85,13 @@ function sweepTerminal(now: number): void {
|
|
|
81
85
|
}
|
|
82
86
|
|
|
83
87
|
function finish(flow: ActiveFlow, status: MainDeviceReauthStatus, now: number): void {
|
|
84
|
-
//
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
+
// Terminal results are first-write-wins, with one exception (080): once the
|
|
89
|
+
// commit actually replaced auth.json, the honest terminal is succeeded. The
|
|
90
|
+
// signal now fences the claim wait and the pre-write recheck, but it cannot
|
|
91
|
+
// fence the gap between the synchronous write and this call — the claim
|
|
92
|
+
// teardown and the promise resolution both yield, so a cancel arriving there
|
|
93
|
+
// would otherwise report "cancelled" for a credential that was replaced and
|
|
94
|
+
// a reauth quarantine that was cleared.
|
|
88
95
|
if (isTerminal(flow.status)) {
|
|
89
96
|
if (!(flow.published && status.status === "succeeded")) return;
|
|
90
97
|
}
|
|
@@ -165,15 +172,16 @@ export function startMainDeviceReauth(deps: MainDeviceReauthDeps = {}): MainDevi
|
|
|
165
172
|
if (flow.controller.signal.aborted) return;
|
|
166
173
|
if (!isTerminal(flow.status)) flow.status = { flowId, status: "committing" };
|
|
167
174
|
// Recheck immediately before the write: a cancel that landed while the
|
|
168
|
-
// grant was resolving must not reach auth.json.
|
|
169
|
-
//
|
|
175
|
+
// grant was resolving must not reach auth.json. The commit itself is
|
|
176
|
+
// fenced by the same signal, so a cancel delivered while the claim
|
|
177
|
+
// waits aborts the publication instead of racing it.
|
|
170
178
|
if (activeFlow !== flow || flow.controller.signal.aborted || isTerminal(flow.status)) return;
|
|
171
179
|
await flow.prepared!.commit({
|
|
172
180
|
accessToken: grant.credential.access,
|
|
173
181
|
refreshToken: grant.credential.refresh,
|
|
174
182
|
idToken: grant.idToken,
|
|
175
183
|
chatgptAccountId: grant.credential.accountId!,
|
|
176
|
-
});
|
|
184
|
+
}, { signal: flow.controller.signal });
|
|
177
185
|
flow.published = true;
|
|
178
186
|
finish(flow, { flowId, status: "succeeded", credentialUpdated: true }, clock());
|
|
179
187
|
} catch (error) {
|