@bitkyc08/opencodex 2.43.0 → 2.44.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 +25 -17
- package/gui/dist/assets/index-B7_K1Hsj.js +115 -0
- package/gui/dist/assets/index-ltx3L-WS.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +23 -2
- package/src/adapters/cursor/native-exec.ts +63 -11
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/cursor/tool-schemas.ts +59 -0
- package/src/adapters/kiro.ts +54 -8
- package/src/adapters/openai-chat.ts +23 -6
- package/src/adapters/openai-responses.ts +3 -1
- package/src/adapters/opencode-go.ts +35 -0
- package/src/claude/agents-inject.ts +12 -5
- package/src/claude/desktop-3p-library.ts +89 -0
- package/src/claude/desktop-3p.ts +166 -95
- package/src/claude/desktop-discovery-inputs.ts +44 -0
- package/src/claude/desktop-profile.ts +1 -1
- package/src/claude/desktop-remote-store-artifact.ts +185 -0
- package/src/claude/desktop-remote-store-io.ts +91 -0
- package/src/claude/desktop-remote-store-state.ts +127 -0
- package/src/claude/desktop-remote-store.ts +269 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +1 -1
- package/src/claude/model-info.ts +16 -3
- package/src/cli/account-api.ts +7 -1
- package/src/cli/aside-profiles.ts +17 -0
- package/src/cli/capabilities.ts +39 -0
- package/src/cli/claude-agent-startup-sync.ts +3 -1
- package/src/cli/claude-desktop.ts +127 -18
- package/src/cli/connect.ts +27 -14
- package/src/cli/dispatch.ts +30 -16
- package/src/cli/export-command.ts +3 -1
- package/src/cli/index.ts +28 -16
- package/src/cli/integrations.ts +46 -12
- package/src/cli/opencode.ts +11 -4
- package/src/cli/runtime-api.ts +7 -2
- package/src/cli/uninstall-client-state.ts +78 -0
- package/src/client/connect.ts +476 -223
- package/src/client/hub-client.ts +81 -0
- package/src/client/hub-relay.ts +38 -6
- package/src/client/lifecycle-lock.ts +129 -0
- package/src/client/state.ts +71 -42
- package/src/clients/aside-profiles.ts +224 -0
- package/src/codex/account-lifecycle.ts +5 -9
- package/src/codex/auth-api.ts +50 -4
- package/src/codex/catalog/metadata.ts +0 -2
- package/src/codex/catalog/provider-fetch.ts +7 -5
- package/src/codex/catalog/sync.ts +88 -24
- package/src/codex/convergence.ts +2 -0
- package/src/codex/inject.ts +45 -14
- package/src/codex/journal.ts +6 -6
- package/src/codex/quota-refresh-outcome.ts +27 -0
- package/src/codex/shim.ts +7 -1
- package/src/combos/resolve.ts +10 -7
- package/src/config.ts +18 -5
- package/src/generated/compatibility-version.json +209 -105
- package/src/images/loop.ts +5 -2
- package/src/integrations/aside-profile-context.ts +270 -0
- package/src/integrations/aside-profile-journal.ts +229 -0
- package/src/integrations/aside-profiles.ts +176 -0
- package/src/integrations/catalog-refresh.ts +40 -0
- package/src/integrations/config-io.ts +16 -1
- package/src/integrations/owned-refresh.ts +14 -2
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +3 -1
- package/src/lab/fabric/producer-isolate.ts +84 -46
- package/src/lib/account-selection-events.ts +32 -0
- package/src/lib/errors.ts +9 -1
- package/src/lib/provider-outbound.ts +2 -45
- package/src/lib/proxy-env.ts +67 -0
- package/src/oauth/account-quota-rank.ts +9 -1
- package/src/oauth/anthropic-routing.ts +91 -23
- package/src/oauth/generic-account-failover.ts +15 -53
- package/src/oauth/index.ts +67 -68
- package/src/oauth/pool-settings-capability.ts +2 -1
- package/src/oauth/store.ts +91 -7
- package/src/oauth/types.ts +8 -0
- package/src/providers/api-key-selection.ts +110 -0
- package/src/providers/api-keys.ts +38 -31
- package/src/providers/context-cap.ts +27 -3
- package/src/providers/key-failover.ts +27 -17
- package/src/providers/provider-id-rewrite.ts +10 -8
- package/src/providers/registry.ts +1 -0
- package/src/responses/custom-tool-compat.ts +10 -0
- package/src/responses/function-call-compat.ts +173 -0
- package/src/responses/namespace-tool-compat.ts +27 -4
- package/src/responses/parser.ts +8 -0
- package/src/responses/task-input.ts +36 -0
- package/src/responses/tool-name-aliases.ts +79 -0
- package/src/router.ts +2 -0
- package/src/routing/capability.ts +10 -3
- package/src/routing/compatibility/assemble.ts +16 -1
- package/src/routing/evaluator.ts +5 -1
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-native.ts +44 -9
- package/src/server/claude-messages.ts +71 -36
- package/src/server/grok-responses-snapshot-repair.ts +333 -0
- package/src/server/index.ts +26 -10
- package/src/server/management/account-selection-stream.ts +70 -0
- package/src/server/management/aside-profile-routes.ts +231 -0
- package/src/server/management/config-routes.ts +36 -41
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +61 -3
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/model-routes.ts +20 -9
- package/src/server/management/model-rows.ts +10 -2
- package/src/server/management/oauth-account-routes.ts +7 -0
- package/src/server/management/provider-routes.ts +10 -8
- package/src/server/management/route-registry.ts +20 -2
- package/src/server/management-auth.ts +16 -0
- package/src/server/relay-eager.ts +17 -5
- package/src/server/relay.ts +43 -4
- package/src/server/request-log.ts +29 -1
- package/src/server/responses/agent-task-recovery-cache.ts +11 -0
- package/src/server/responses/agent-task-recovery.ts +24 -4
- package/src/server/responses/codex-ws-pool.ts +4 -4
- package/src/server/responses/codex-ws-session.ts +2 -2
- package/src/server/responses/collaboration.ts +2 -1
- package/src/server/responses/combo-stream-preflight.ts +26 -7
- package/src/server/responses/core.ts +668 -151
- package/src/server/responses/fetch-helpers.ts +6 -1
- package/src/server/responses/ws-upstream.ts +8 -3
- package/src/server/responses-custom-tool-repair.ts +29 -9
- package/src/server/responses-function-tool-repair.ts +183 -0
- package/src/server/responses-snapshot-codec.ts +14 -0
- package/src/server/responses-snapshot-repair.ts +1 -13
- package/src/server/responses-undeclared-tool-guard.ts +1 -68
- package/src/server/sse-payload-rewrite.ts +3 -1
- package/src/service.ts +6 -2
- package/src/types/config.ts +23 -16
- package/src/types/provider.ts +14 -3
- package/src/types/request.ts +2 -0
- package/src/usage/log.ts +9 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/index-DS1NE4Jn.css +0 -1
- package/gui/dist/assets/index-Djowl68T.js +0 -112
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { lstatSync, readFileSync, readlinkSync, realpathSync, statSync, type Stats } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
4
|
+
import type { IntegrationIO } from "../integrations/config-io";
|
|
5
|
+
import { asideHomeDir, ClientPathError } from "./config-export";
|
|
6
|
+
|
|
7
|
+
export interface AsideProfile {
|
|
8
|
+
id: number;
|
|
9
|
+
name?: string;
|
|
10
|
+
current: boolean;
|
|
11
|
+
root: string;
|
|
12
|
+
configPath: string;
|
|
13
|
+
detectDir: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const MAX_PROFILES = 128;
|
|
17
|
+
const MAX_MANIFEST_BYTES = 4 * 1024 * 1024;
|
|
18
|
+
const MAX_LEAF_LINKS = 40;
|
|
19
|
+
|
|
20
|
+
function refuse(message: string): never {
|
|
21
|
+
// Never include manifest contents or underlying filesystem error messages.
|
|
22
|
+
throw new ClientPathError(`Aside profile: ${message}`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isId(value: unknown): value is number {
|
|
26
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 && !Object.is(value, -0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function object(value: unknown): value is Record<string, unknown> {
|
|
30
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function inspect(path: string, follow = false): Stats | null {
|
|
34
|
+
try {
|
|
35
|
+
return follow ? statSync(path) : lstatSync(path);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return null;
|
|
38
|
+
return refuse("a filesystem boundary could not be inspected.");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function canonical(path: string): string {
|
|
43
|
+
try { return realpathSync.native(path); } catch {
|
|
44
|
+
return refuse("a filesystem boundary could not be resolved.");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Resolve a peer's leaf link even when its final model file does not exist yet. */
|
|
49
|
+
function leafDestination(path: string): string | null {
|
|
50
|
+
const visited = new Set<string>();
|
|
51
|
+
while (inspect(path)?.isSymbolicLink()) {
|
|
52
|
+
if (visited.has(path) || visited.size >= MAX_LEAF_LINKS) refuse("an account catalog has a cyclic or excessive link chain.");
|
|
53
|
+
visited.add(path);
|
|
54
|
+
try { path = resolve(dirname(path), readlinkSync(path)); } catch {
|
|
55
|
+
return refuse("an account catalog link could not be inspected.");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (!inspect(dirname(path), true)?.isDirectory()) return null;
|
|
59
|
+
return join(canonical(dirname(path)), basename(path));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function readProfiles(root: string): AsideProfile[] {
|
|
63
|
+
const rootStat = inspect(root);
|
|
64
|
+
if (!rootStat || rootStat.isSymbolicLink() || !rootStat.isDirectory()) {
|
|
65
|
+
refuse("the configured root is missing or is not a safe directory.");
|
|
66
|
+
}
|
|
67
|
+
const manifest = join(root, "accounts.json");
|
|
68
|
+
const manifestStat = inspect(manifest);
|
|
69
|
+
if (!manifestStat || manifestStat.isSymbolicLink() || !manifestStat.isFile()
|
|
70
|
+
|| manifestStat.size > MAX_MANIFEST_BYTES) {
|
|
71
|
+
refuse("the account manifest is missing, unreadable or unsafe. Launch Aside to create it.");
|
|
72
|
+
}
|
|
73
|
+
let parsed: unknown;
|
|
74
|
+
try { parsed = JSON.parse(readFileSync(manifest, "utf8")); } catch {
|
|
75
|
+
return refuse("the account manifest is not readable JSON.");
|
|
76
|
+
}
|
|
77
|
+
if (!object(parsed) || !isId(parsed.currentAccountId)) {
|
|
78
|
+
refuse("the account manifest has no valid current account ID.");
|
|
79
|
+
}
|
|
80
|
+
const currentId = parsed.currentAccountId;
|
|
81
|
+
const accounts: unknown = Object.hasOwn(parsed, "accounts") ? parsed.accounts : [{ id: currentId }];
|
|
82
|
+
if (!Array.isArray(accounts) || accounts.length === 0 || accounts.length > MAX_PROFILES) {
|
|
83
|
+
refuse("the account manifest must contain between 1 and 128 accounts.");
|
|
84
|
+
}
|
|
85
|
+
const ids = new Set<number>();
|
|
86
|
+
const profiles = accounts.map((account: unknown): AsideProfile => {
|
|
87
|
+
if (!object(account) || !isId(account.id) || ids.has(account.id)) {
|
|
88
|
+
return refuse("the account manifest contains an invalid or duplicate account ID.");
|
|
89
|
+
}
|
|
90
|
+
const current = account.id === currentId;
|
|
91
|
+
if (Object.hasOwn(account, "current") && account.current !== current) {
|
|
92
|
+
refuse("the account manifest has inconsistent current account metadata.");
|
|
93
|
+
}
|
|
94
|
+
ids.add(account.id);
|
|
95
|
+
const detectDir = join(root, "u", String(account.id));
|
|
96
|
+
return {
|
|
97
|
+
id: account.id,
|
|
98
|
+
...(typeof account.name === "string" ? { name: account.name } : {}),
|
|
99
|
+
current, root, detectDir, configPath: join(detectDir, "models.json"),
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
if (!ids.has(currentId)) refuse("the current account is not registered in the account manifest.");
|
|
103
|
+
return profiles;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Enumerate account catalogs; browser bindings and session data are never projected. */
|
|
107
|
+
export function listAsideProfiles(env: NodeJS.ProcessEnv = process.env, home: string = homedir()): AsideProfile[] {
|
|
108
|
+
const root = asideHomeDir(env, home);
|
|
109
|
+
if (!isAbsolute(root)) refuse("the configured root must be absolute.");
|
|
110
|
+
return readProfiles(root);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
type DirectoryIdentity = { path: string; dev: number; ino: number };
|
|
114
|
+
type Boundary = Array<DirectoryIdentity | null>;
|
|
115
|
+
|
|
116
|
+
function sameIdentity(a: Pick<Stats, "dev" | "ino">, b: Pick<Stats, "dev" | "ino">): boolean {
|
|
117
|
+
return a.dev === b.dev && a.ino === b.ino;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function validatePaths(profile: AsideProfile): void {
|
|
121
|
+
if (!isId(profile.id) || !isAbsolute(profile.root) || resolve(profile.root) !== profile.root
|
|
122
|
+
|| profile.detectDir !== join(profile.root, "u", String(profile.id))
|
|
123
|
+
|| profile.configPath !== join(profile.detectDir, "models.json")) {
|
|
124
|
+
refuse("the selected account paths are invalid.");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function registeredProfiles(profile: AsideProfile, profiles?: AsideProfile[]): AsideProfile[] {
|
|
129
|
+
validatePaths(profile);
|
|
130
|
+
const registered = profiles ?? readProfiles(profile.root);
|
|
131
|
+
if (registered.length === 0 || registered.length > MAX_PROFILES) refuse("the account list is invalid.");
|
|
132
|
+
const ids = new Set<number>();
|
|
133
|
+
for (const peer of registered) {
|
|
134
|
+
validatePaths(peer);
|
|
135
|
+
if (peer.root !== profile.root || ids.has(peer.id)) refuse("the account list has conflicting paths.");
|
|
136
|
+
ids.add(peer.id);
|
|
137
|
+
}
|
|
138
|
+
if (!ids.has(profile.id)) refuse("the selected account is not registered.");
|
|
139
|
+
return registered;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function boundary(profile: AsideProfile, profiles: AsideProfile[], mutation: boolean): Boundary {
|
|
143
|
+
const directories = [profile.root, join(profile.root, "u"), profile.detectDir];
|
|
144
|
+
const identities: Boundary = [];
|
|
145
|
+
let parent: string | undefined;
|
|
146
|
+
let absent = false;
|
|
147
|
+
for (const [index, directory] of directories.entries()) {
|
|
148
|
+
const stats = absent ? null : inspect(directory);
|
|
149
|
+
if (!stats) {
|
|
150
|
+
if (index === 0 || mutation) refuse("the account directory is not installed; it will not be created.");
|
|
151
|
+
absent = true;
|
|
152
|
+
identities.push(null);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) refuse("an account directory is a link or is unsafe.");
|
|
156
|
+
const path = canonical(directory);
|
|
157
|
+
// Aliases ABOVE the chosen root (notably macOS /var) are valid.
|
|
158
|
+
const child = index === 1 ? "u" : String(profile.id);
|
|
159
|
+
if (parent && path !== join(parent, child)) refuse("an account directory resolves outside its boundary.");
|
|
160
|
+
identities.push({ path, dev: stats.dev, ino: stats.ino });
|
|
161
|
+
parent = path;
|
|
162
|
+
}
|
|
163
|
+
if (absent) return identities;
|
|
164
|
+
const leaf = inspect(profile.configPath);
|
|
165
|
+
if (leaf && (leaf.isSymbolicLink() || !leaf.isFile() || leaf.nlink > 1)) {
|
|
166
|
+
refuse("the model catalog is a link, shared file or non-regular file.");
|
|
167
|
+
}
|
|
168
|
+
if (leaf && canonical(profile.configPath) !== join(parent!, "models.json")) {
|
|
169
|
+
refuse("the model catalog resolves outside its account directory.");
|
|
170
|
+
}
|
|
171
|
+
const account = identities[2]!;
|
|
172
|
+
for (const peer of profiles) {
|
|
173
|
+
if (peer.id === profile.id) continue;
|
|
174
|
+
// Follow peers only for identity comparison, never for content or writes.
|
|
175
|
+
// This also detects a sibling symlink pointing BACK at this safe target.
|
|
176
|
+
const peerDirectory = inspect(peer.detectDir, true);
|
|
177
|
+
if (peerDirectory && sameIdentity(account, peerDirectory)) refuse("account directories share a target.");
|
|
178
|
+
if (!peerDirectory?.isDirectory()) continue;
|
|
179
|
+
if (inspect(peer.configPath)?.isSymbolicLink()
|
|
180
|
+
&& leafDestination(peer.configPath) === join(parent!, "models.json")) {
|
|
181
|
+
refuse("account catalogs share a target.");
|
|
182
|
+
}
|
|
183
|
+
const peerLeaf = inspect(peer.configPath, true);
|
|
184
|
+
if (leaf && peerLeaf && sameIdentity(leaf, peerLeaf)) refuse("account catalogs share a target.");
|
|
185
|
+
}
|
|
186
|
+
return identities;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Missing account directories are readable as not installed, but never writable. */
|
|
190
|
+
export function assertAsideProfileBoundary(profile: AsideProfile, profiles?: AsideProfile[], mutation = false): void {
|
|
191
|
+
boundary(profile, registeredProfiles(profile, profiles), mutation);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Pin directories for one status/write operation, retaining the caller's IO and store.
|
|
196
|
+
* Rechecks complement atomic writes; they do not defeat a hostile same-user process
|
|
197
|
+
* racing every filesystem syscall. Leaf inodes may change during our atomic writes.
|
|
198
|
+
*/
|
|
199
|
+
export function guardAsideProfileIO(profile: AsideProfile, io: IntegrationIO, profiles?: AsideProfile[]): IntegrationIO {
|
|
200
|
+
const selected = { ...profile };
|
|
201
|
+
const registered = registeredProfiles(selected, profiles).map(peer => ({ ...peer }));
|
|
202
|
+
const captured = boundary(selected, registered, false);
|
|
203
|
+
function check(path: string, directory: boolean, mutation: boolean): void {
|
|
204
|
+
if (path !== (directory ? selected.detectDir : selected.configPath)) {
|
|
205
|
+
refuse("IO attempted to access a different account path.");
|
|
206
|
+
}
|
|
207
|
+
const current = boundary(selected, registered, mutation);
|
|
208
|
+
if (current.some((item, index) => {
|
|
209
|
+
const prior = captured[index];
|
|
210
|
+
return item === null || prior == null ? item !== prior : item.path !== prior.path || !sameIdentity(item, prior);
|
|
211
|
+
})) refuse("the account directory changed after the operation began.");
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
readText: path => { check(path, false, false); return io.readText(path); },
|
|
215
|
+
statKind: path => { check(path, path === selected.detectDir, false); return io.statKind(path); },
|
|
216
|
+
writeText: (path, text) => { check(path, false, true); io.writeText(path, text); },
|
|
217
|
+
removeFile: path => { check(path, false, true); io.removeFile(path); },
|
|
218
|
+
mkdirp: path => { check(path, true, true); io.mkdirp(path); },
|
|
219
|
+
now: () => io.now(),
|
|
220
|
+
appendJournal: entry => io.appendJournal(entry),
|
|
221
|
+
putRecord: record => io.putRecord(record),
|
|
222
|
+
dropRecord: clientId => io.dropRecord(clientId),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import {
|
|
3
|
-
atomicWriteFile,
|
|
4
3
|
deleteConfigTopLevelKey,
|
|
5
4
|
getConfigPath,
|
|
6
5
|
saveConfigPreservingClaudeCode,
|
|
@@ -107,13 +106,10 @@ function restoreRuntimeConfig(target: OcxConfig, snapshot: OcxConfig): void {
|
|
|
107
106
|
Object.assign(target, snapshot);
|
|
108
107
|
}
|
|
109
108
|
|
|
110
|
-
function
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
} catch (error) {
|
|
114
|
-
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
109
|
+
function assertPersistedConfigUnchanged(configPath: string, previousBytes: Buffer): void {
|
|
110
|
+
if (!readFileSync(configPath).equals(previousBytes)) {
|
|
111
|
+
throw new CodexAccountDeleteRollbackError();
|
|
115
112
|
}
|
|
116
|
-
atomicWriteFile(configPath, previousBytes);
|
|
117
113
|
}
|
|
118
114
|
|
|
119
115
|
/**
|
|
@@ -133,7 +129,7 @@ export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string):
|
|
|
133
129
|
const previousConfig = structuredClone(runtimeConfig);
|
|
134
130
|
const configPath = getConfigPath();
|
|
135
131
|
const hasPersistedConfig = existsSync(configPath);
|
|
136
|
-
const previousPersistedConfig = hasPersistedConfig ? readFileSync(configPath
|
|
132
|
+
const previousPersistedConfig = hasPersistedConfig ? readFileSync(configPath) : undefined;
|
|
137
133
|
const hadStoredAccount = (runtimeConfig.codexAccounts ?? [])
|
|
138
134
|
.some(account => !account.isMain && account.id === accountId);
|
|
139
135
|
const hadVisiblePickerBinding = hadStoredAccount
|
|
@@ -162,7 +158,7 @@ export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string):
|
|
|
162
158
|
} catch (error) {
|
|
163
159
|
restoreRuntimeConfig(runtimeConfig, previousConfig);
|
|
164
160
|
try {
|
|
165
|
-
|
|
161
|
+
assertPersistedConfigUnchanged(configPath, previousPersistedConfig);
|
|
166
162
|
} catch {
|
|
167
163
|
throw new CodexAccountDeleteRollbackError();
|
|
168
164
|
}
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -105,6 +105,7 @@ import {
|
|
|
105
105
|
type MainAccountInfo,
|
|
106
106
|
} from "./main-account-cache";
|
|
107
107
|
export { clearMainAccountInfoCache } from "./main-account-cache";
|
|
108
|
+
import type { CodexQuotaRefreshOutcome } from "./quota-refresh-outcome";
|
|
108
109
|
import { getMainAccountHardLockStatus, type MainAccountHardLockStatus } from "./main-account-hard-lock";
|
|
109
110
|
import { observeMainReserveRevocation } from "./reserve-availability";
|
|
110
111
|
import { maskEmail } from "../lib/privacy";
|
|
@@ -777,6 +778,10 @@ async function readMainAuthErrorCode(resp: Response): Promise<unknown> {
|
|
|
777
778
|
|
|
778
779
|
interface MainAccountInfoFetchResult {
|
|
779
780
|
info: MainAccountInfo;
|
|
781
|
+
/** Ephemeral result of this attempt, omitted when no WHAM request was made. */
|
|
782
|
+
quotaRefresh?: CodexQuotaRefreshOutcome;
|
|
783
|
+
/** Internal dispatch fence for diagnostics only; never copied into a public DTO or cache. */
|
|
784
|
+
quotaRefreshGeneration?: number;
|
|
780
785
|
/** Whether this attempt safely inspected the physical native-main credential. */
|
|
781
786
|
credentialChecked: boolean;
|
|
782
787
|
/** Meaningful only when credentialChecked is true. */
|
|
@@ -792,12 +797,16 @@ interface MainAccountInfoFetchResult {
|
|
|
792
797
|
export interface MainAccountInfoSnapshot {
|
|
793
798
|
info: MainAccountInfo;
|
|
794
799
|
mainIdentityGeneration: number;
|
|
800
|
+
quotaRefresh?: CodexQuotaRefreshOutcome;
|
|
795
801
|
}
|
|
796
802
|
|
|
797
803
|
export async function fetchMainAccountInfoSnapshot(forceRefresh = false): Promise<MainAccountInfoSnapshot> {
|
|
798
804
|
const result = await fetchMainAccountInfoAttempt(forceRefresh, 1);
|
|
799
805
|
return {
|
|
800
806
|
info: result.info,
|
|
807
|
+
...(result.quotaRefresh && result.quotaRefreshGeneration !== undefined
|
|
808
|
+
&& isMainAccountIdentityGenerationLive(result.quotaRefreshGeneration)
|
|
809
|
+
? { quotaRefresh: result.quotaRefresh } : {}),
|
|
801
810
|
mainIdentityGeneration: result.identityGeneration ?? captureMainAccountIdentityGeneration(),
|
|
802
811
|
};
|
|
803
812
|
}
|
|
@@ -900,34 +909,55 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
900
909
|
? observeMainQuotaCredential(tokens.access_token, tokens.account_id)
|
|
901
910
|
: undefined;
|
|
902
911
|
const mainQuotaCredentialGeneration = getMainQuotaCredentialGeneration();
|
|
912
|
+
// Keep diagnostics separate from authentication and freshness policy. Never serialize errors.
|
|
913
|
+
const quotaSignal = AbortSignal.timeout(WHAM_REQUEST_TIMEOUT_MS);
|
|
914
|
+
let quotaPhase: "request" | "body" | "decode" | "publish" = "request";
|
|
915
|
+
let quotaRefreshGeneration = captureMainAccountIdentityGeneration();
|
|
903
916
|
try {
|
|
904
917
|
const resp = await fetch("https://chatgpt.com/backend-api/wham/usage", {
|
|
905
918
|
headers: { Authorization: `Bearer ${tokens.access_token}`, "ChatGPT-Account-Id": tokens.account_id },
|
|
906
|
-
signal:
|
|
919
|
+
signal: quotaSignal,
|
|
907
920
|
});
|
|
921
|
+
quotaPhase = "publish";
|
|
908
922
|
if (!resp.ok) {
|
|
909
923
|
const terminalAuthFailure = await isTerminalMainAuthResponse(resp, isMainAccountTokenVerifiablyLive());
|
|
910
924
|
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
|
|
911
925
|
if (retried) return retried;
|
|
912
926
|
if (terminalAuthFailure) {
|
|
927
|
+
// Account for this attempt's own synchronous invalidation, never prior external drift.
|
|
928
|
+
const diagnosticStillLive = isMainAccountIdentityGenerationLive(quotaRefreshGeneration);
|
|
913
929
|
clearMainAccountInfoCache();
|
|
930
|
+
if (diagnosticStillLive) quotaRefreshGeneration = captureMainAccountIdentityGeneration();
|
|
914
931
|
markAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID, writerGeneration);
|
|
915
932
|
}
|
|
916
|
-
return {
|
|
933
|
+
return {
|
|
934
|
+
info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true,
|
|
935
|
+
quotaRefresh: { status: "http_error", httpStatus: resp.status },
|
|
936
|
+
quotaRefreshGeneration,
|
|
937
|
+
};
|
|
917
938
|
}
|
|
939
|
+
quotaPhase = "body";
|
|
918
940
|
const data = (await resp.json()) as WhamUsageResponse;
|
|
941
|
+
quotaPhase = "publish";
|
|
919
942
|
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
|
|
920
943
|
if (retried) return retried;
|
|
944
|
+
quotaPhase = "decode";
|
|
945
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
946
|
+
throw new Error("Invalid WHAM usage object");
|
|
947
|
+
}
|
|
948
|
+
quotaPhase = "publish";
|
|
921
949
|
// A delayed response from a replaced bearer cannot revoke a newer Reserve grant,
|
|
922
950
|
// even in the same workspace or after an A→B→A credential transition.
|
|
923
951
|
if (mainQuotaCredentialGeneration === getMainQuotaCredentialGeneration()
|
|
924
952
|
&& matchesMainQuotaCredential(tokens.access_token, tokens.account_id)) {
|
|
925
953
|
observeMainReserveRevocation(data, mainQuotaWriter);
|
|
926
954
|
}
|
|
955
|
+
quotaPhase = "decode";
|
|
927
956
|
const plan = nonEmptyPlan(data.plan_type) ?? nonEmptyPlan(cached?.plan) ?? nonEmptyPlan(getMainAccountPlan());
|
|
928
957
|
const usage = { ...data, ...(plan ? { plan_type: plan } : {}) };
|
|
929
958
|
const quota = parseUsageQuota(usage);
|
|
930
959
|
const policyQuota = parseMainPolicyUsageQuota(usage);
|
|
960
|
+
quotaPhase = "publish";
|
|
931
961
|
const freshResetCredits = quota?.resetCredits;
|
|
932
962
|
// Tag the count with the identity it was read from, so a later response that omits the
|
|
933
963
|
// summary can restore the badge without ever crossing an account boundary.
|
|
@@ -957,14 +987,26 @@ async function fetchMainAccountInfoWhileOwned(
|
|
|
957
987
|
}
|
|
958
988
|
return {
|
|
959
989
|
info: result,
|
|
990
|
+
quotaRefresh: { status: quota ? "ok" : "not_reported" },
|
|
991
|
+
quotaRefreshGeneration,
|
|
960
992
|
credentialChecked: true,
|
|
961
993
|
hasCredential: true,
|
|
962
994
|
...(quota ? { freshQuota: quota } : {}),
|
|
963
995
|
...(freshResetCredits !== undefined ? { freshResetCredits } : {}),
|
|
964
996
|
};
|
|
965
|
-
} catch {
|
|
997
|
+
} catch (error) {
|
|
966
998
|
const retried = await retryMainAccountInfoIfIdentityChanged(requestAccountId, retriesRemaining, nativeMainLease, explicitRefresh);
|
|
967
|
-
|
|
999
|
+
if (retried) return retried;
|
|
1000
|
+
let status: CodexQuotaRefreshOutcome["status"] = "internal_error";
|
|
1001
|
+
if ((quotaPhase === "request" || quotaPhase === "body") && quotaSignal.aborted) status = "timeout";
|
|
1002
|
+
else if (quotaPhase === "request") status = "network_error";
|
|
1003
|
+
else if (quotaPhase === "body") status = error instanceof SyntaxError ? "invalid_response" : "network_error";
|
|
1004
|
+
else if (quotaPhase === "decode") status = "invalid_response";
|
|
1005
|
+
return {
|
|
1006
|
+
info: EMPTY_MAIN_ACCOUNT_INFO, credentialChecked: true, hasCredential: true,
|
|
1007
|
+
quotaRefresh: { status },
|
|
1008
|
+
quotaRefreshGeneration,
|
|
1009
|
+
};
|
|
968
1010
|
}
|
|
969
1011
|
}
|
|
970
1012
|
|
|
@@ -1061,6 +1103,7 @@ export interface CodexAuthAccountDto {
|
|
|
1061
1103
|
healthSummary: string;
|
|
1062
1104
|
healthAction?: string;
|
|
1063
1105
|
quotaProbeSkipped?: true;
|
|
1106
|
+
quotaRefresh?: CodexQuotaRefreshOutcome;
|
|
1064
1107
|
mainAccountHardLock?: MainAccountHardLockStatus;
|
|
1065
1108
|
}
|
|
1066
1109
|
|
|
@@ -1769,6 +1812,9 @@ export async function listCodexAuthAccountsSnapshot(
|
|
|
1769
1812
|
id: MAIN_CODEX_ACCOUNT_ID,
|
|
1770
1813
|
email: maskEmail(mainInfo.email) ?? "Codex App login",
|
|
1771
1814
|
plan: mainInfo.plan,
|
|
1815
|
+
...(mainSnapshotLive && mainResult.quotaRefresh && mainResult.quotaRefreshGeneration !== undefined
|
|
1816
|
+
&& isMainAccountIdentityGenerationLive(mainResult.quotaRefreshGeneration)
|
|
1817
|
+
? { quotaRefresh: mainResult.quotaRefresh } : {}),
|
|
1772
1818
|
logLabel: "main",
|
|
1773
1819
|
isMain: true,
|
|
1774
1820
|
paused: isCodexAccountPaused(runtimeConfig, MAIN_CODEX_ACCOUNT_ID),
|
|
@@ -299,8 +299,6 @@ function narrowToLimits(raw: number | undefined, slug: string, input: NativeCont
|
|
|
299
299
|
return overlay !== undefined && cap !== undefined ? Math.min(window, cap) : window;
|
|
300
300
|
}
|
|
301
301
|
const narrowed = overlay === undefined ? raw : Math.min(raw, overlay);
|
|
302
|
-
// 922k is the GPT-5.6 1M opt-in, not a request to shrink gpt-5.4's 1M window.
|
|
303
|
-
if (cap === NATIVE_GPT56_MAX_INPUT_TOKENS) return narrowed;
|
|
304
302
|
return applyProviderContextCap(narrowed, cap) ?? narrowed;
|
|
305
303
|
}
|
|
306
304
|
|
|
@@ -1506,11 +1506,14 @@ async function fetchProviderModelsWithAuth(
|
|
|
1506
1506
|
&& prov.googleMode === "vertex"
|
|
1507
1507
|
&& (prov.models?.length ?? 0) === 0
|
|
1508
1508
|
&& Boolean(prov.defaultModel);
|
|
1509
|
-
|
|
1509
|
+
const seedStaticDefault = prov.liveModels === false
|
|
1510
|
+
&& (prov.models?.length ?? 0) === 0
|
|
1511
|
+
&& Boolean(prov.defaultModel);
|
|
1512
|
+
// Ordered dedupe union: implicit default seed, then `models`, then `retainModels`. `configured` is the
|
|
1510
1513
|
// single seed for the static path, the degraded fallback, drop diagnostics, and provider hints,
|
|
1511
1514
|
// so a retain-only id must enter here or it never exists to be retained (#1690).
|
|
1512
1515
|
const configuredIds = [...new Set([
|
|
1513
|
-
...(seedVertexDefault && prov.defaultModel ? [prov.defaultModel] : []),
|
|
1516
|
+
...((seedVertexDefault || seedStaticDefault) && prov.defaultModel ? [prov.defaultModel] : []),
|
|
1514
1517
|
...(prov.models ?? []),
|
|
1515
1518
|
...(prov.retainModels ?? []),
|
|
1516
1519
|
])];
|
|
@@ -1563,9 +1566,8 @@ async function fetchProviderModelsWithAuth(
|
|
|
1563
1566
|
: resolveAuth.resolve(name, prov));
|
|
1564
1567
|
const apiKey = auth.apiKey;
|
|
1565
1568
|
// A configured default is a real callable selector and must remain discoverable when a
|
|
1566
|
-
// compatible provider's live /models request fails (issue #308).
|
|
1567
|
-
//
|
|
1568
|
-
// rows, while a failed live discovery may degrade to the default selector.
|
|
1569
|
+
// compatible provider's live /models request fails (issue #308). Static providers already seed
|
|
1570
|
+
// their default selector above when no explicit model list exists.
|
|
1569
1571
|
const failedDiscoveryConfigured = configured.length > 0 || !prov.defaultModel || prov.adapter !== "anthropic"
|
|
1570
1572
|
? configured
|
|
1571
1573
|
: [{
|