@bitkyc08/opencodex 2.57.0 → 2.59.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -10
- package/gui/dist/assets/index-C5IebErG.js +136 -0
- package/gui/dist/assets/{index-C5-RdDmD.css → index-OESInAjC.css} +1 -1
- 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 +2 -2
- package/src/adapters/base.ts +11 -1
- package/src/adapters/codebuddy/scaffold-guard.ts +5 -4
- package/src/adapters/command-code.ts +13 -4
- package/src/adapters/cursor/catalog.ts +11 -0
- package/src/adapters/cursor/cursor-errors.ts +15 -0
- package/src/adapters/cursor/discovery.ts +65 -1
- 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 +5 -1
- package/src/adapters/cursor/message-mapper.ts +3 -2
- package/src/adapters/cursor/protobuf-events.ts +110 -11
- package/src/adapters/cursor/protobuf-request.ts +27 -6
- package/src/adapters/cursor/request-builder.ts +14 -3
- package/src/adapters/cursor/text-toolcall.ts +230 -0
- package/src/adapters/cursor/thread-continuity.ts +141 -0
- package/src/adapters/cursor/tool-guidance.ts +5 -4
- package/src/adapters/cursor/types.ts +5 -0
- package/src/adapters/cursor.ts +97 -6
- package/src/adapters/devin/cloud-direct/chat.ts +11 -2
- package/src/adapters/devin/cloud-direct/index.ts +7 -0
- package/src/adapters/devin/cloud-direct/stated-reset-retry.ts +103 -0
- package/src/adapters/devin.ts +75 -13
- package/src/adapters/google-antigravity-wire.ts +29 -2
- package/src/adapters/google-http.ts +45 -13
- package/src/adapters/google.ts +23 -4
- package/src/adapters/mimo-free.ts +32 -17
- package/src/adapters/ollama-native.ts +42 -8
- package/src/adapters/openai-chat/response-events.ts +61 -0
- package/src/adapters/openai-chat.ts +5 -10
- package/src/adapters/openai-responses/passthrough.ts +40 -5
- package/src/adapters/openai-responses/request-strips.ts +43 -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/physical-send.ts +50 -0
- package/src/adapters/responses-tool-schema.ts +76 -46
- package/src/adapters/run-turn-queue.ts +17 -4
- package/src/bridge/response-json.ts +2 -2
- package/src/bridge/sse.ts +166 -25
- package/src/claude/context-windows.ts +22 -0
- package/src/claude/outbound.ts +46 -5
- package/src/cli/account-api.ts +4 -3
- package/src/cli/account-extended.ts +22 -2
- 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/config-command.ts +35 -18
- package/src/cli/dispatch.ts +71 -4
- package/src/cli/doctor.ts +197 -2
- package/src/cli/help.ts +4 -1
- package/src/cli/index.ts +132 -22
- package/src/cli/models-runtime.ts +33 -4
- 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 +72 -1
- package/src/cli/uninstall-client-state.ts +12 -0
- package/src/client/machine-api.ts +4 -3
- package/src/client/machine-listener.ts +14 -1
- package/src/clients/config-export/constants.ts +2 -3
- package/src/clients/config-export.ts +5 -5
- package/src/codex/account-store.ts +81 -5
- package/src/codex/auth-api/pool-quota-probe.ts +14 -3
- package/src/codex/auth-api/routes.ts +17 -2
- package/src/codex/auth-context.ts +58 -20
- 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 +1 -0
- package/src/codex/catalog/model-hints.ts +37 -5
- package/src/codex/catalog/parsing.ts +83 -5
- package/src/codex/catalog/reserve-warn.ts +96 -0
- package/src/codex/catalog/retained-sync.ts +19 -0
- package/src/codex/catalog/routed-gather.ts +42 -3
- package/src/codex/cli-installation-identity.ts +210 -0
- package/src/codex/cli-installation-targets.ts +158 -0
- package/src/codex/convergence.ts +5 -0
- package/src/codex/desktop-switches.ts +145 -0
- package/src/codex/history-job.ts +5 -1
- package/src/codex/history-provider.ts +37 -5
- package/src/codex/history-state-open.ts +105 -0
- package/src/codex/history-worker.ts +14 -1
- package/src/codex/inject/config-toml.ts +44 -2
- package/src/codex/inject/remove.ts +145 -7
- package/src/codex/inject/restore.ts +204 -32
- package/src/codex/inject.ts +6 -9
- package/src/codex/lineage.ts +83 -32
- package/src/codex/loopback-target.ts +40 -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-entitlements.ts +60 -1
- package/src/codex/native-profile-startup.ts +64 -20
- package/src/codex/observed-model-denials.ts +137 -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 +282 -12
- 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 +13 -15
- package/src/codex/subagent-model-fallback.ts +94 -0
- package/src/codex/windows-installation-files.ts +224 -0
- package/src/combos/failover.ts +122 -5
- package/src/config/atomic-write.ts +83 -8
- package/src/config/diagnostics.ts +21 -0
- package/src/config/load-degrade.ts +15 -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 +23 -5
- package/src/config/schema/config-schema.ts +23 -0
- package/src/config/schema/leaf-validators.ts +65 -17
- package/src/generated/compatibility-version.json +337 -201
- package/src/generated/model-metadata.ts +1 -1
- package/src/lib/bounded-body.ts +4 -2
- package/src/lib/bounded-subprocess.ts +62 -10
- package/src/lib/destination-policy.ts +48 -6
- package/src/lib/errors.ts +3 -15
- package/src/lib/local-destinations.ts +32 -5
- package/src/lib/provider-outbound.ts +3 -3
- 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 +631 -0
- package/src/lib/spend-reservation-ledger.ts +115 -9
- package/src/lib/windows-secret-acl.ts +151 -15
- package/src/lib/windows-user-principal.ts +5 -1
- package/src/lib/workflow-budget.ts +145 -8
- package/src/oauth/account-quota-rank.ts +72 -15
- package/src/oauth/generic-account-failover.ts +40 -27
- 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 +6 -0
- package/src/providers/devin-provider-merge-migration.ts +33 -12
- package/src/providers/free-directory.ts +20 -2
- package/src/providers/key-failover.ts +261 -7
- package/src/providers/model-discovery.ts +19 -7
- package/src/providers/model-rename-migration.ts +1 -0
- package/src/providers/openai-sidecar.ts +4 -0
- package/src/providers/opencode-go-transport.ts +14 -5
- package/src/providers/quota/report-cache.ts +3 -0
- package/src/providers/registry/entries-core.ts +11 -0
- package/src/providers/registry/entries-extended.ts +146 -28
- package/src/providers/registry/model-seeds.ts +136 -29
- package/src/providers/registry/types.ts +9 -0
- package/src/responses/apply-patch-envelope.ts +44 -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/hosted-tool-policy.ts +85 -2
- package/src/responses/schema.ts +9 -2
- package/src/responses/spill-store.ts +17 -0
- package/src/responses/state/body-policy.ts +25 -0
- package/src/responses/state/spill-queue.ts +8 -6
- package/src/responses/state.ts +3 -22
- package/src/router.ts +4 -0
- package/src/server/auth-cors.ts +27 -0
- package/src/server/chat-completions.ts +9 -4
- package/src/server/chat-native-sse.ts +26 -9
- package/src/server/chat-native.ts +10 -4
- package/src/server/claude-messages.ts +24 -2
- package/src/server/gui-static.ts +36 -2
- package/src/server/inbound-body-admission.ts +187 -0
- package/src/server/index/websocket-handler.ts +48 -1
- package/src/server/index.ts +15 -19
- package/src/server/management/api-access.ts +3 -4
- package/src/server/management/config-routes.ts +57 -10
- package/src/server/management/provider-capability-config.ts +35 -7
- package/src/server/management/provider-routes.ts +70 -18
- package/src/server/models-capabilities.ts +24 -3
- package/src/server/proxy-liveness.ts +97 -2
- package/src/server/relay.ts +17 -24
- package/src/server/request-log.ts +25 -1
- package/src/server/responses/adapter-continuation.ts +71 -27
- package/src/server/responses/adapter-delivery.ts +39 -8
- package/src/server/responses/adapter-dispatch.ts +52 -24
- package/src/server/responses/codex-ws-exchange.ts +65 -4
- package/src/server/responses/combo-stream-preflight.ts +68 -5
- package/src/server/responses/compact.ts +60 -11
- package/src/server/responses/core-codex-account.ts +83 -22
- package/src/server/responses/core-combo.ts +26 -0
- package/src/server/responses/core-normalize.ts +12 -5
- package/src/server/responses/core-options.ts +3 -0
- package/src/server/responses/fetch-helpers.ts +72 -3
- package/src/server/responses/native-injection-protocol.ts +42 -0
- package/src/server/responses/native-injection-replay.ts +105 -0
- package/src/server/responses/native-injection.ts +242 -0
- package/src/server/responses/native-response-control.ts +56 -0
- package/src/server/responses/native-response-json.ts +14 -0
- package/src/server/responses/native-response-output.ts +37 -0
- package/src/server/responses/native-steering-log.ts +44 -0
- package/src/server/responses/native-steering-policy.ts +49 -0
- package/src/server/responses/native-steering-replay.ts +126 -0
- package/src/server/responses/native-steering-settings.ts +76 -0
- package/src/server/responses/native-steering.ts +400 -0
- package/src/server/responses/native-tool-results.ts +130 -0
- package/src/server/responses/passthrough-delivery.ts +21 -1
- package/src/server/responses/passthrough-dispatch.ts +146 -49
- package/src/server/responses/passthrough-execution.ts +11 -1
- package/src/server/responses/request-prepare.ts +70 -0
- package/src/server/responses/request-send-budget.ts +84 -7
- package/src/server/responses/request-sidecar-auth.ts +16 -8
- package/src/server/responses/request-spend.ts +38 -9
- package/src/server/responses/request-transport.ts +13 -10
- package/src/server/responses/run-turn-execution.ts +20 -5
- package/src/server/responses/sidecar-execution.ts +2 -0
- package/src/server/responses/ws-upstream.ts +23 -2
- package/src/server/responses-custom-tool-repair.ts +2 -2
- package/src/server/sse-frame-buffer.ts +12 -10
- package/src/server/sse-payload-rewrite.ts +36 -9
- package/src/server/stop-teardown.ts +8 -1
- 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/server/ws-bridge.ts +16 -1
- package/src/service/cli.ts +29 -7
- package/src/service/guards.ts +10 -0
- package/src/service/health.ts +43 -0
- package/src/service/state.ts +7 -2
- package/src/types/accounts.ts +4 -0
- package/src/types/config.ts +104 -3
- package/src/types/provider.ts +32 -0
- package/src/types/request.ts +7 -1
- package/src/types/wire.ts +9 -1
- package/src/usage/expected-prices.ts +28 -0
- package/src/usage/log.ts +87 -4
- package/src/web-search/passthrough-bridge.ts +39 -5
- package/gui/dist/assets/index-Cz7CLdif.js +0 -128
|
@@ -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) {
|
|
@@ -17,6 +17,13 @@ import { loadPersistedCodexRuntime } from "./runtime";
|
|
|
17
17
|
import { codexRuntimeStateEpoch } from "./runtime";
|
|
18
18
|
import upstreamModelsSnapshot from "./data/upstream-models.json";
|
|
19
19
|
import { codexCredentialMutationEpoch } from "./credential-mutation-epoch";
|
|
20
|
+
import {
|
|
21
|
+
clearObservedCodexModelDenial,
|
|
22
|
+
forgetObservedCodexModelDenialsForAccount,
|
|
23
|
+
observedDeniedCodexAccountIdsForModel,
|
|
24
|
+
recordObservedCodexModelDenial,
|
|
25
|
+
resetObservedCodexModelDenialsForTests,
|
|
26
|
+
} from "./observed-model-denials";
|
|
20
27
|
|
|
21
28
|
const CODEX_MODELS_ENDPOINT = "https://chatgpt.com/backend-api/codex/models";
|
|
22
29
|
|
|
@@ -870,6 +877,11 @@ function needsEntitlementRefresh(
|
|
|
870
877
|
const cached = accountModelsCache.get(cacheKeyFor(accountId, clientVersion));
|
|
871
878
|
if (cached && cached.credentialIdentity !== credentialIdentity) {
|
|
872
879
|
invalidateCodexModelEntitlementsForAccount(accountId);
|
|
880
|
+
// The credential itself changed, so evidence gathered under the previous one answers for a
|
|
881
|
+
// different subscription. This is the only call site that knows that: the two gated-model
|
|
882
|
+
// sites in `core-codex-account.ts` invalidate a STALE roster for an unchanged credential,
|
|
883
|
+
// and clearing observed refusals there would discard the very evidence #4906 is about.
|
|
884
|
+
forgetObservedCodexModelDenialsForAccount(accountId);
|
|
873
885
|
} else if (cached && cached.expiresAt > now) {
|
|
874
886
|
return false;
|
|
875
887
|
}
|
|
@@ -1280,17 +1292,63 @@ export function cachedDeniedCodexAccountIdsForModel(
|
|
|
1280
1292
|
if (state === "granted") granted.add(accountId);
|
|
1281
1293
|
else if (state === "denied") denied.add(accountId);
|
|
1282
1294
|
}
|
|
1295
|
+
// The roster is not the only evidence, and on this path it is usually the weaker one. A cached
|
|
1296
|
+
// roster expires in five minutes and nothing on the flagship request path refetches it, so
|
|
1297
|
+
// absent an ongoing catalog sync the loop above contributes nothing at all. An upstream
|
|
1298
|
+
// refusal does not expire on that schedule and is not a snapshot of a pending answer: it is
|
|
1299
|
+
// the account's own Codex surface naming this model and declining it (#4906).
|
|
1300
|
+
for (const accountId of observedDeniedCodexAccountIdsForModel(modelId, now) ?? []) {
|
|
1301
|
+
// Under the caller's read fence, like the roster loop above. Nothing here reads account
|
|
1302
|
+
// storage, but an excluded account must stay UNKNOWN rather than denied so a profile switch
|
|
1303
|
+
// or a request-owned credential produces the same selection it does today.
|
|
1304
|
+
if (options.excludeAccountIds?.has(accountId)) continue;
|
|
1305
|
+
denied.add(accountId);
|
|
1306
|
+
}
|
|
1283
1307
|
// One account holds one entry per client version, and upstream filters the roster by that
|
|
1284
1308
|
// version. So the same account can legitimately carry a granted entry under a current client
|
|
1285
1309
|
// and a denied one under an older client that predates the model. Positive evidence is
|
|
1286
1310
|
// authoritative regardless of which version asked for it -- the same rule
|
|
1287
1311
|
// `codexModelEntitlementStateForRoster` applies within a single entry -- so a grant anywhere
|
|
1288
1312
|
// clears the denial rather than being outvoted by whichever entry the map happened to yield
|
|
1289
|
-
// last.
|
|
1313
|
+
// last. It outranks an observed refusal for the same reason: a confirmed roster that lists the
|
|
1314
|
+
// model is the newer answer, and a rollout that reaches an account must not be held back by a
|
|
1315
|
+
// refusal it has already superseded.
|
|
1290
1316
|
for (const accountId of granted) denied.delete(accountId);
|
|
1291
1317
|
return denied.size > 0 ? denied : undefined;
|
|
1292
1318
|
}
|
|
1293
1319
|
|
|
1320
|
+
/**
|
|
1321
|
+
* Record an authenticated upstream refusal as this account's own evidence about `modelId`.
|
|
1322
|
+
*
|
|
1323
|
+
* Scoped to {@link ENTITLEMENT_PREFERRED_NATIVE_OPENAI_MODELS} because that is the set whose
|
|
1324
|
+
* availability varies per account while the row stays visible, and it is the set
|
|
1325
|
+
* {@link cachedDeniedCodexAccountIdsForModel} will read back. A model outside it either has no
|
|
1326
|
+
* per-account variance or is gated by the fail-closed roster path, where an ordering preference
|
|
1327
|
+
* would change nothing.
|
|
1328
|
+
*
|
|
1329
|
+
* The caller must have matched the exact allow-listed refusal body first. A status alone is not
|
|
1330
|
+
* admissible here: 400 covers every malformed request too, and remembering one of those as an
|
|
1331
|
+
* entitlement fact would steer routing away from a perfectly capable account.
|
|
1332
|
+
*/
|
|
1333
|
+
export function recordCodexModelDenialEvidence(
|
|
1334
|
+
accountId: string | null | undefined,
|
|
1335
|
+
modelId: string | undefined,
|
|
1336
|
+
now = Date.now(),
|
|
1337
|
+
): void {
|
|
1338
|
+
if (!accountId || !modelId) return;
|
|
1339
|
+
if (!ENTITLEMENT_PREFERRED_NATIVE_OPENAI_MODELS.has(modelId)) return;
|
|
1340
|
+
recordObservedCodexModelDenial(accountId, modelId, now);
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
/** Drop the refusal evidence for a pair the account has just served successfully. */
|
|
1344
|
+
export function clearCodexModelDenialEvidence(
|
|
1345
|
+
accountId: string | null | undefined,
|
|
1346
|
+
modelId: string | undefined,
|
|
1347
|
+
): void {
|
|
1348
|
+
if (!accountId || !modelId) return;
|
|
1349
|
+
clearObservedCodexModelDenial(accountId, modelId);
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1294
1352
|
/** Synchronous projection for management/catalog readers after a discovery pass. */
|
|
1295
1353
|
export function cachedAvailableAccountGatedNativeModels(
|
|
1296
1354
|
now = Date.now(),
|
|
@@ -1351,6 +1409,7 @@ export function resetCodexModelEntitlementCacheForTests(): void {
|
|
|
1351
1409
|
negativeCredentialMemo.clear();
|
|
1352
1410
|
entitlementEnsureFlights.clear();
|
|
1353
1411
|
runtimeVersionMemo = null;
|
|
1412
|
+
resetObservedCodexModelDenialsForTests();
|
|
1354
1413
|
}
|
|
1355
1414
|
|
|
1356
1415
|
/** Test-only snapshot for proving publication fences, which cache lookup intentionally masks. */
|
|
@@ -308,6 +308,47 @@ function observeOwner(entry: StartupEntry, owner: NativeMainOwnerSnapshot): void
|
|
|
308
308
|
* Retain process ownership for one live server. The first reference acquires the
|
|
309
309
|
* canonical-home SQLite lease and owns recovery; later same-process references share it.
|
|
310
310
|
*/
|
|
311
|
+
/**
|
|
312
|
+
* Releases nobody is awaiting.
|
|
313
|
+
*
|
|
314
|
+
* A release closes the native-main owner's SQLite lease and its stable lock file, both of which
|
|
315
|
+
* live under CODEX_HOME. The normal shutdown path awaits it: `server.stop` goes through
|
|
316
|
+
* `releaseNativeMainStartupLifecycle`, which awaits the flight. The FAILED-start path does not —
|
|
317
|
+
* `startServer` must stay synchronous, so its rollback can only fire `void lifecycle.release()`
|
|
318
|
+
* and rethrow. Nothing could then wait for those handles to close, and on Windows an open handle
|
|
319
|
+
* does not delay an unlink, it refuses it outright with EPERM.
|
|
320
|
+
*
|
|
321
|
+
* That is invisible in production, where a failed start is followed by exit rather than by
|
|
322
|
+
* deleting the home. It is not invisible to a test whose cleanup removes the home it just used:
|
|
323
|
+
* `tests/server/server-management-auth.test.ts` binds a management ingress on the fixed port
|
|
324
|
+
* 10101, which nine other test files also use, so a collision on the six-shard Windows leg turns
|
|
325
|
+
* a passing start into the rollback path. The failure followed that collision across shards 1, 2
|
|
326
|
+
* and 3 while staying on the same file and line, which is what a shard-independent trigger looks
|
|
327
|
+
* like.
|
|
328
|
+
*
|
|
329
|
+
* Tracking the flight here rather than at the call site keeps `startServer` synchronous and
|
|
330
|
+
* unchanged, and gives anyone who needs the handles closed something to await.
|
|
331
|
+
*/
|
|
332
|
+
const pendingStartupReleases = new Set<Promise<void>>();
|
|
333
|
+
|
|
334
|
+
function trackStartupRelease(release: Promise<void>): Promise<void> {
|
|
335
|
+
const tracked = release.finally(() => { pendingStartupReleases.delete(tracked); });
|
|
336
|
+
pendingStartupReleases.add(tracked);
|
|
337
|
+
return tracked;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Settle every native-main startup release still in flight, including ones nobody awaited.
|
|
342
|
+
*
|
|
343
|
+
* Loops rather than awaiting a single snapshot: a release can retire an owner whose own teardown
|
|
344
|
+
* starts another, and draining only the first batch would return with handles still open.
|
|
345
|
+
*/
|
|
346
|
+
export async function flushNativeMainStartupReleases(): Promise<void> {
|
|
347
|
+
while (pendingStartupReleases.size > 0) {
|
|
348
|
+
await Promise.allSettled([...pendingStartupReleases]);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
311
352
|
export function startNativeMainStartupLifecycle(
|
|
312
353
|
deps: NativeMainStartupGateDeps = {},
|
|
313
354
|
): NativeMainStartupLifecycle {
|
|
@@ -352,29 +393,32 @@ export function startNativeMainStartupLifecycle(
|
|
|
352
393
|
}
|
|
353
394
|
entry.refs += 1;
|
|
354
395
|
let released = false;
|
|
396
|
+
const performRelease = async (): Promise<void> => {
|
|
397
|
+
if (released) return;
|
|
398
|
+
released = true;
|
|
399
|
+
entry!.refs = Math.max(0, entry!.refs - 1);
|
|
400
|
+
if (entry!.refs !== 0) return;
|
|
401
|
+
entry!.epoch += 1;
|
|
402
|
+
entry!.sweepStopping = true;
|
|
403
|
+
if (entry!.sweepTimer) clearTimeout(entry!.sweepTimer);
|
|
404
|
+
entry!.sweepTimer = undefined;
|
|
405
|
+
entry!.unsubscribe();
|
|
406
|
+
startupEntries.delete(homeId);
|
|
407
|
+
entry!.resolveAcquisition?.(snapshot);
|
|
408
|
+
entry!.resolveAcquisition = undefined;
|
|
409
|
+
// Startup convergence can transition from the exclusive recovery claim
|
|
410
|
+
// into a stage sweep. Keep the owner registered until that entire chain
|
|
411
|
+
// settles so no cleanup transaction starts untracked after owner detach.
|
|
412
|
+
await Promise.allSettled([entry!.settled]);
|
|
413
|
+
if (entry!.sweepInFlight) await Promise.allSettled([entry!.sweepInFlight]);
|
|
414
|
+
await entry!.owner.release();
|
|
415
|
+
};
|
|
355
416
|
return {
|
|
356
417
|
homeId,
|
|
357
418
|
get settled() { return entry!.settled; },
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
entry!.refs = Math.max(0, entry!.refs - 1);
|
|
362
|
-
if (entry!.refs !== 0) return;
|
|
363
|
-
entry!.epoch += 1;
|
|
364
|
-
entry!.sweepStopping = true;
|
|
365
|
-
if (entry!.sweepTimer) clearTimeout(entry!.sweepTimer);
|
|
366
|
-
entry!.sweepTimer = undefined;
|
|
367
|
-
entry!.unsubscribe();
|
|
368
|
-
startupEntries.delete(homeId);
|
|
369
|
-
entry!.resolveAcquisition?.(snapshot);
|
|
370
|
-
entry!.resolveAcquisition = undefined;
|
|
371
|
-
// Startup convergence can transition from the exclusive recovery claim
|
|
372
|
-
// into a stage sweep. Keep the owner registered until that entire chain
|
|
373
|
-
// settles so no cleanup transaction starts untracked after owner detach.
|
|
374
|
-
await Promise.allSettled([entry!.settled]);
|
|
375
|
-
if (entry!.sweepInFlight) await Promise.allSettled([entry!.sweepInFlight]);
|
|
376
|
-
await entry!.owner.release();
|
|
377
|
-
},
|
|
419
|
+
// Tracked so a caller that cannot await -- the synchronous rollback in `startServer` -- still
|
|
420
|
+
// leaves the flight drainable through `flushNativeMainStartupReleases`.
|
|
421
|
+
release: () => trackStartupRelease(performRelease()),
|
|
378
422
|
};
|
|
379
423
|
}
|
|
380
424
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-account model denials observed from an authenticated upstream refusal.
|
|
3
|
+
*
|
|
4
|
+
* [Decision Log]
|
|
5
|
+
* - 목적과 의도: Keep the one piece of account-specific model evidence that is never stale --
|
|
6
|
+
* the upstream's own refusal -- instead of discarding it after a single retry.
|
|
7
|
+
* - 기존 구현 및 제약 조건: `cachedDeniedCodexAccountIdsForModel` reads authenticated `/models`
|
|
8
|
+
* rosters, and those entries expire five minutes after they are fetched
|
|
9
|
+
* (`MODEL_ROSTER_TTL_MS`). Nothing on the flagship request path refills them, because
|
|
10
|
+
* `resolveCodexModelEntitlements` is awaited only for `ACCOUNT_GATED_NATIVE_OPENAI_MODELS`,
|
|
11
|
+
* which no longer holds the flagships. So the denial set is usually absent, the #4797
|
|
12
|
+
* ordering rules become the identity function, and the pool picks on quota alone (#4906).
|
|
13
|
+
* - 검토한 주요 대안: Fetch a roster on the flagship request path, lengthen the roster TTL, or
|
|
14
|
+
* infer availability from the account's plan name.
|
|
15
|
+
* - 선택한 방식: Record the exact upstream unsupported-model refusal per (account, model) and
|
|
16
|
+
* let selection read it alongside the roster.
|
|
17
|
+
* - 다른 대안 대신 이 방식을 선택한 이유: A roster fetch on the request path puts an
|
|
18
|
+
* authenticated upstream call in front of the most commonly requested models in the product,
|
|
19
|
+
* which is what the cache-only contract exists to prevent. A longer TTL keeps a shard's
|
|
20
|
+
* stale absence around for longer without adding any evidence. A plan name proves nothing
|
|
21
|
+
* about a grant -- `available_in_plans` for `gpt-6-astra` lists `free` while free accounts
|
|
22
|
+
* are refused -- and #3022 is what happened the last time availability was inferred rather
|
|
23
|
+
* than observed.
|
|
24
|
+
* - 장점, 단점 및 영향: A refusal is spent once and then remembered, so the pool stops
|
|
25
|
+
* re-sending a model to the account that just refused it. The evidence is confirmed rather
|
|
26
|
+
* than inferred, it is still only an ordering preference, and it is overridden by any
|
|
27
|
+
* positive roster grant for the same pair.
|
|
28
|
+
*
|
|
29
|
+
* What this deliberately is NOT: an eligibility filter. Readers treat these ids exactly like
|
|
30
|
+
* roster denials -- `withoutModelDeniedAccounts` restores them when filtering would empty the
|
|
31
|
+
* candidate list, and `preferModelEntitledAccount` leaves the active account alone when no
|
|
32
|
+
* entitled alternative exists. No model is hidden from any catalog and no request is refused
|
|
33
|
+
* before dispatch, so the 2026-09-04 owner decision that the flagships fail open is untouched.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Six hours, against a five-minute roster TTL.
|
|
38
|
+
*
|
|
39
|
+
* The asymmetry is the point. A roster entry is a snapshot of an answer that may simply not
|
|
40
|
+
* have arrived yet, so it expires quickly and absence means "unknown". A refusal is an answer:
|
|
41
|
+
* upstream named this model and this account and said no. It still expires, because a rollout
|
|
42
|
+
* can reach an account between two requests, and the two faster paths back are a positive
|
|
43
|
+
* roster grant (which overrides this outright) and a successful response from the same account
|
|
44
|
+
* for the same model (which clears the entry).
|
|
45
|
+
*/
|
|
46
|
+
const OBSERVED_DENIAL_TTL_MS = 6 * 60 * 60_000;
|
|
47
|
+
|
|
48
|
+
/** Bounded like the roster cache: pool size times flagship count, with room to spare. */
|
|
49
|
+
const OBSERVED_DENIAL_MAX_ENTRIES = 512;
|
|
50
|
+
|
|
51
|
+
/** `accountId\u0000modelId` -> expiry. Insertion order is the eviction order. */
|
|
52
|
+
const observedDenials = new Map<string, number>();
|
|
53
|
+
|
|
54
|
+
function denialKey(accountId: string, modelId: string): string {
|
|
55
|
+
return `${accountId}\u0000${modelId}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function accountIdOfDenialKey(key: string): string {
|
|
59
|
+
return key.slice(0, key.indexOf("\u0000"));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function modelIdOfDenialKey(key: string): string {
|
|
63
|
+
return key.slice(key.indexOf("\u0000") + 1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Remember that `accountId` was refused `modelId` by its own authenticated upstream.
|
|
68
|
+
*
|
|
69
|
+
* Re-recording refreshes the entry rather than extending an older one, so a pair that keeps
|
|
70
|
+
* being refused stays remembered and one that stops being refused ages out.
|
|
71
|
+
*/
|
|
72
|
+
export function recordObservedCodexModelDenial(
|
|
73
|
+
accountId: string,
|
|
74
|
+
modelId: string,
|
|
75
|
+
now = Date.now(),
|
|
76
|
+
): void {
|
|
77
|
+
const key = denialKey(accountId, modelId);
|
|
78
|
+
// Delete before set so the refreshed entry moves to the back of the eviction order.
|
|
79
|
+
observedDenials.delete(key);
|
|
80
|
+
observedDenials.set(key, now + OBSERVED_DENIAL_TTL_MS);
|
|
81
|
+
while (observedDenials.size > OBSERVED_DENIAL_MAX_ENTRIES) {
|
|
82
|
+
const oldest = observedDenials.keys().next();
|
|
83
|
+
if (oldest.done) break;
|
|
84
|
+
observedDenials.delete(oldest.value);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Forget one pair, because the account just served the model.
|
|
90
|
+
*
|
|
91
|
+
* A success is newer and stronger evidence than the refusal that preceded it: whatever the
|
|
92
|
+
* entitlement was when upstream refused, it is not that now.
|
|
93
|
+
*/
|
|
94
|
+
export function clearObservedCodexModelDenial(accountId: string, modelId: string): void {
|
|
95
|
+
observedDenials.delete(denialKey(accountId, modelId));
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Forget every pair for one account, because its credential changed.
|
|
100
|
+
*
|
|
101
|
+
* A reauthenticated account can be a different subscription entirely, so evidence gathered
|
|
102
|
+
* under the previous credential says nothing about this one -- the same reasoning
|
|
103
|
+
* `invalidateCodexModelEntitlementsForAccount` applies to cached rosters.
|
|
104
|
+
*/
|
|
105
|
+
export function forgetObservedCodexModelDenialsForAccount(accountId: string | null | undefined): void {
|
|
106
|
+
if (!accountId) return;
|
|
107
|
+
for (const key of [...observedDenials.keys()]) {
|
|
108
|
+
if (accountIdOfDenialKey(key) === accountId) observedDenials.delete(key);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Accounts refused `modelId` within the retention window.
|
|
114
|
+
*
|
|
115
|
+
* Returns `undefined` rather than an empty set when nothing is recorded, matching
|
|
116
|
+
* `cachedDeniedCodexAccountIdsForModel`: a caller must not be able to read "no evidence" as
|
|
117
|
+
* "nobody is denied".
|
|
118
|
+
*/
|
|
119
|
+
export function observedDeniedCodexAccountIdsForModel(
|
|
120
|
+
modelId: string | undefined,
|
|
121
|
+
now = Date.now(),
|
|
122
|
+
): ReadonlySet<string> | undefined {
|
|
123
|
+
if (!modelId) return undefined;
|
|
124
|
+
const denied = new Set<string>();
|
|
125
|
+
for (const [key, expiresAt] of [...observedDenials]) {
|
|
126
|
+
if (expiresAt <= now) {
|
|
127
|
+
observedDenials.delete(key);
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (modelIdOfDenialKey(key) === modelId) denied.add(accountIdOfDenialKey(key));
|
|
131
|
+
}
|
|
132
|
+
return denied.size > 0 ? denied : undefined;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function resetObservedCodexModelDenialsForTests(): void {
|
|
136
|
+
observedDenials.clear();
|
|
137
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readSync, realpathSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join, parse, relative, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
const MAX_AUTH_BYTES = 256 * 1024;
|
|
5
|
+
export const ORCA_ACCOUNT_DIRECTORY = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
6
|
+
|
|
7
|
+
export function sameLocalPath(left: string, right: string): boolean {
|
|
8
|
+
return process.platform === "win32" ? left.toLowerCase() === right.toLowerCase() : left === right;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Reject links at every component; never follow an external credential reference. */
|
|
12
|
+
export function assertPlainLocalPath(path: string): string {
|
|
13
|
+
if (/^(?:\\\\|\/\/)/.test(path)) throw new Error("Network and device credential paths are unsupported.");
|
|
14
|
+
const absolute = resolve(path);
|
|
15
|
+
let current = parse(absolute).root;
|
|
16
|
+
for (const component of relative(current, absolute).split(/[\\/]/).filter(Boolean)) {
|
|
17
|
+
current = join(current, component);
|
|
18
|
+
if (lstatSync(current).isSymbolicLink()) throw new Error("Unsafe local credential path.");
|
|
19
|
+
}
|
|
20
|
+
if (!sameLocalPath(realpathSync(absolute), absolute)) throw new Error("Unsafe local credential path.");
|
|
21
|
+
return absolute;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function readBoundedLocalFile(path: string, limit = MAX_AUTH_BYTES): string {
|
|
25
|
+
assertPlainLocalPath(path);
|
|
26
|
+
const before = lstatSync(path);
|
|
27
|
+
if (!before.isFile() || before.size > limit) throw new Error("Invalid local credential file.");
|
|
28
|
+
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
|
29
|
+
try {
|
|
30
|
+
const opened = fstatSync(fd);
|
|
31
|
+
if (!opened.isFile() || opened.size > limit || before.dev !== opened.dev || before.ino !== opened.ino) {
|
|
32
|
+
throw new Error("Local credential file changed.");
|
|
33
|
+
}
|
|
34
|
+
// The cap bounds the read, not the allocation: tiny registry files should not
|
|
35
|
+
// allocate 32 MiB on each preview/recheck. One extra byte detects concurrent growth.
|
|
36
|
+
const bytes = Buffer.alloc(opened.size + 1);
|
|
37
|
+
let length = 0;
|
|
38
|
+
while (length < bytes.length) {
|
|
39
|
+
const count = readSync(fd, bytes, length, bytes.length - length, null);
|
|
40
|
+
if (!count) break;
|
|
41
|
+
length += count;
|
|
42
|
+
}
|
|
43
|
+
if (length !== opened.size) throw new Error("Local credential file changed.");
|
|
44
|
+
assertPlainLocalPath(path);
|
|
45
|
+
const after = lstatSync(path);
|
|
46
|
+
if (after.dev !== opened.dev || after.ino !== opened.ino || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs) {
|
|
47
|
+
throw new Error("Local credential file changed.");
|
|
48
|
+
}
|
|
49
|
+
return bytes.subarray(0, length).toString("utf8");
|
|
50
|
+
} finally { closeSync(fd); }
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function object(value: unknown): Record<string, unknown> {
|
|
54
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Invalid local OAuth credential.");
|
|
55
|
+
return value as Record<string, unknown>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function parseOrcaAuth(raw: string, requireFresh = true) {
|
|
59
|
+
const auth = object(JSON.parse(raw));
|
|
60
|
+
if ((auth.auth_mode !== undefined && auth.auth_mode !== "chatgpt")
|
|
61
|
+
|| (auth.OPENAI_API_KEY !== undefined && auth.OPENAI_API_KEY !== null && auth.OPENAI_API_KEY !== "")) {
|
|
62
|
+
throw new Error("Expected a ChatGPT OAuth credential.");
|
|
63
|
+
}
|
|
64
|
+
const tokens = object(auth.tokens);
|
|
65
|
+
if (typeof tokens.access_token !== "string" || typeof tokens.account_id !== "string" || !tokens.account_id) {
|
|
66
|
+
throw new Error("Local OAuth credential lacks account identity.");
|
|
67
|
+
}
|
|
68
|
+
const parts = tokens.access_token.split(".");
|
|
69
|
+
if (parts.length !== 3) throw new Error("Invalid local OAuth bearer.");
|
|
70
|
+
const claims = object(JSON.parse(Buffer.from(parts[1]!, "base64url").toString("utf8")));
|
|
71
|
+
const identity = object(claims["https://api.openai.com/auth"]);
|
|
72
|
+
if (identity.chatgpt_account_id !== tokens.account_id || typeof claims.sub !== "string" || !claims.sub
|
|
73
|
+
|| typeof claims.exp !== "number" || !Number.isSafeInteger(claims.exp) || !Number.isSafeInteger(claims.exp * 1000)
|
|
74
|
+
|| (requireFresh && claims.exp * 1000 <= Date.now() + 60_000)) {
|
|
75
|
+
throw new Error("Local OAuth identity or expiry is invalid.");
|
|
76
|
+
}
|
|
77
|
+
return { accessToken: tokens.access_token, refreshToken: "", expiresAt: claims.exp * 1000,
|
|
78
|
+
chatgptAccountId: tokens.account_id, sourceSubject: claims.sub };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function readOrcaAuthSource(path: string) {
|
|
82
|
+
if (!isAbsolute(path) || !ORCA_ACCOUNT_DIRECTORY.test(dirname(dirname(path)).split(/[\\/]/).pop() ?? "")
|
|
83
|
+
|| dirname(path).split(/[\\/]/).pop() !== "home"
|
|
84
|
+
|| dirname(dirname(dirname(path))).split(/[\\/]/).pop() !== "codex-accounts"
|
|
85
|
+
|| path.split(/[\\/]/).pop() !== "auth.json") throw new Error("Invalid Orca credential location.");
|
|
86
|
+
try {
|
|
87
|
+
const accountDirectory = dirname(dirname(path)).split(/[\\/]/).pop()!;
|
|
88
|
+
if (readBoundedLocalFile(join(dirname(path), ".orca-managed-home"), 128).trim() !== accountDirectory) {
|
|
89
|
+
throw new Error("Orca home ownership marker does not match.");
|
|
90
|
+
}
|
|
91
|
+
return { ...parseOrcaAuth(readBoundedLocalFile(path)), sourceAuthPath: path };
|
|
92
|
+
}
|
|
93
|
+
catch { throw new Error("Orca credential unavailable; update the account in Orca and retry."); }
|
|
94
|
+
}
|