@bitkyc08/opencodex 2.48.0 → 2.49.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 +8 -5
- package/SPONSORS.md +1 -1
- package/assets/sponsors/orcarouter.png +0 -0
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/index-BoBRSehJ.css +1 -0
- package/gui/dist/assets/index-BtyONQrZ.js +115 -0
- package/gui/dist/index.html +2 -2
- package/gui/dist/provider-icons/packycode.svg +19 -0
- package/gui/dist/provider-icons/qoder.svg +5 -0
- package/package.json +5 -3
- package/src/adapters/anthropic.ts +31 -16
- package/src/adapters/codebuddy/adapter.ts +85 -0
- package/src/adapters/codebuddy/profiles.ts +52 -0
- package/src/adapters/coding-agent/profile.ts +100 -0
- package/src/adapters/coding-agent/protocol.ts +463 -0
- package/src/adapters/coding-agent/turn.ts +353 -0
- package/src/adapters/google.ts +15 -11
- package/src/adapters/mimo-free.ts +3 -0
- package/src/adapters/openai-chat.ts +2 -2
- package/src/adapters/openai-responses.ts +18 -11
- package/src/adapters/qoder/adapter.ts +70 -0
- package/src/adapters/qoder/live-models.ts +89 -0
- package/src/adapters/qoder/profiles.ts +36 -0
- package/src/adapters/registry.ts +12 -0
- package/src/adapters/responses-tool-schema.ts +113 -8
- package/src/cli/dispatch.ts +2 -0
- package/src/cli/doctor.ts +40 -0
- package/src/cli/effort.ts +24 -8
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +24 -1
- package/src/cli/provider-runtime.ts +2 -1
- package/src/cli/registry.ts +2 -2
- package/src/cli/system-command.ts +10 -3
- package/src/cli/usage-report.ts +9 -5
- package/src/codex/account-lifecycle.ts +35 -2
- package/src/codex/auth-api.ts +6 -3
- package/src/codex/auth-collision.ts +12 -2
- package/src/codex/auth-context.ts +75 -7
- package/src/codex/catalog/provider-fetch.ts +73 -9
- package/src/codex/inject.ts +127 -30
- package/src/codex/internal/catalog-writer.ts +3 -0
- package/src/codex/journal.ts +61 -12
- package/src/codex/model-cache.ts +11 -4
- package/src/codex/native-profile-startup.ts +72 -5
- package/src/codex/native-profile-store.ts +2 -2
- package/src/codex/ocx-compaction-history.ts +226 -0
- package/src/codex/project-config-warnings.ts +3 -1
- package/src/codex/quota.ts +18 -8
- package/src/codex/reserve-availability.ts +21 -5
- package/src/codex/runtime.ts +45 -1
- package/src/codex/sync.ts +5 -0
- package/src/config.ts +1 -0
- package/src/generated/compatibility-version.json +126 -74
- package/src/images/loop.ts +1 -0
- package/src/images/xai-video-client.ts +2 -0
- package/src/integrations/registry.ts +1 -0
- package/src/lib/process-control.ts +52 -8
- package/src/lib/upstream-retry.ts +1 -0
- package/src/oauth/chatgpt.ts +83 -0
- package/src/oauth/xai.ts +151 -8
- package/src/providers/api-key-selection-capture.ts +10 -0
- package/src/providers/api-key-selection.ts +2 -7
- package/src/providers/caller-authorization.ts +36 -0
- package/src/providers/codebuddy-models.ts +184 -0
- package/src/providers/derive.ts +5 -0
- package/src/providers/free-directory.ts +26 -2
- package/src/providers/openai-sidecar.ts +35 -11
- package/src/providers/qoder-models.ts +25 -0
- package/src/providers/registry.ts +139 -0
- package/src/responses/compaction.ts +4 -0
- package/src/responses/task-input.ts +21 -1
- package/src/router.ts +1 -1
- package/src/server/chat-completions.ts +26 -9
- package/src/server/claude-messages.ts +12 -2
- package/src/server/images.ts +1 -0
- package/src/server/management/account-selection-stream.ts +13 -4
- package/src/server/management/config-routes.ts +24 -5
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/native-integration-routes.ts +2 -1
- package/src/server/management/provider-routes.ts +24 -0
- package/src/server/management-api.ts +14 -0
- package/src/server/port-reclaim.ts +11 -26
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/collaboration.ts +4 -9
- package/src/server/responses/core.ts +141 -19
- package/src/server/responses/fetch-helpers.ts +18 -7
- package/src/server/responses/policy-fallback.ts +12 -0
- package/src/service.ts +36 -2
- package/src/storage/cleanup.ts +77 -45
- package/src/types/config.ts +7 -1
- package/src/usage/log.ts +24 -0
- package/src/vision/anthropic-describe.ts +1 -0
- package/src/web-search/anthropic-executor.ts +1 -0
- package/src/web-search/loop.ts +1 -0
- package/src/web-search/progress-stream.ts +4 -0
- package/gui/dist/assets/index-B5r7LNHN.js +0 -115
- package/gui/dist/assets/index-D5SiRo8X.css +0 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { clearCodingAgentBinaryCache, resolveProfileByBaseUrl, type CodingAgentProviderProfile } from "../coding-agent/profile";
|
|
2
|
+
|
|
3
|
+
/** Official Qoder CLI profile. Region variants are separate profiles and credentials. */
|
|
4
|
+
export interface QoderProfile extends CodingAgentProviderProfile {
|
|
5
|
+
family: "qoder";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const QODER_GLOBAL_PROFILE: QoderProfile = {
|
|
9
|
+
providerId: "qoder",
|
|
10
|
+
family: "qoder",
|
|
11
|
+
region: "global",
|
|
12
|
+
label: "Qoder",
|
|
13
|
+
canonicalBaseUrl: "https://qoder.com",
|
|
14
|
+
binaryCandidates: ["qoder", "qodercli"],
|
|
15
|
+
tokenEnv: "QODER_PERSONAL_ACCESS_TOKEN",
|
|
16
|
+
installHint: "npm install -g @qoder-ai/qodercli",
|
|
17
|
+
documentationUrl: "https://docs.qoder.com/cli/authentication",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export const QODER_CN_PROFILE: QoderProfile = {
|
|
21
|
+
providerId: "qoder-cn",
|
|
22
|
+
family: "qoder",
|
|
23
|
+
region: "cn",
|
|
24
|
+
label: "Qoder CN",
|
|
25
|
+
canonicalBaseUrl: "https://qoder.cn",
|
|
26
|
+
binaryCandidates: ["qodercn", "qoderclicn"],
|
|
27
|
+
tokenEnv: "QODERCN_PERSONAL_ACCESS_TOKEN",
|
|
28
|
+
installHint: "npm install -g @qodercn-ai/qoderclicn",
|
|
29
|
+
documentationUrl: "https://docs.qoder.cn/en/cli/authentication",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const QODER_PROFILES: readonly QoderProfile[] = [QODER_GLOBAL_PROFILE, QODER_CN_PROFILE];
|
|
33
|
+
export function resolveQoderProfile(baseUrl: string | undefined): QoderProfile | undefined {
|
|
34
|
+
return resolveProfileByBaseUrl(QODER_PROFILES, baseUrl) as QoderProfile | undefined;
|
|
35
|
+
}
|
|
36
|
+
export const clearQoderBinaryCache = clearCodingAgentBinaryCache;
|
package/src/adapters/registry.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { createAnthropicAdapter } from "./anthropic";
|
|
|
2
2
|
import { createAzureAdapter } from "./azure";
|
|
3
3
|
import type { ProviderAdapter } from "./base";
|
|
4
4
|
import { withClinePassDeepSeekV4ToolReplayCompatibility } from "./cline-pass-deepseek-v4-tool-replay";
|
|
5
|
+
import { createCodeBuddyAdapter } from "./codebuddy/adapter";
|
|
6
|
+
import { createQoderAdapter } from "./qoder/adapter";
|
|
5
7
|
import { createCommandCodeAdapter } from "./command-code";
|
|
6
8
|
import { createCursorAdapter } from "./cursor";
|
|
7
9
|
import { createGoogleAdapter } from "./google";
|
|
@@ -20,6 +22,7 @@ export interface AdapterFactoryContext {
|
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
export type AdapterWire =
|
|
25
|
+
| "codebuddy"
|
|
23
26
|
| "command-code"
|
|
24
27
|
| "openai-chat"
|
|
25
28
|
| "ollama-native"
|
|
@@ -53,6 +56,11 @@ type InheritedAdapterDefinition = {
|
|
|
53
56
|
type AdapterDefinition = DirectAdapterDefinition | InheritedAdapterDefinition;
|
|
54
57
|
|
|
55
58
|
export const ADAPTER_REGISTRY = {
|
|
59
|
+
codebuddy: {
|
|
60
|
+
wire: "codebuddy",
|
|
61
|
+
mutation: "codex-owned",
|
|
62
|
+
create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createCodeBuddyAdapter(provider),
|
|
63
|
+
},
|
|
56
64
|
"command-code": {
|
|
57
65
|
wire: "command-code",
|
|
58
66
|
mutation: "codex-owned",
|
|
@@ -108,6 +116,10 @@ export const ADAPTER_REGISTRY = {
|
|
|
108
116
|
contractParent: "openai-chat",
|
|
109
117
|
create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createMimoFreeAdapter(provider),
|
|
110
118
|
},
|
|
119
|
+
qoder: {
|
|
120
|
+
contractParent: "codebuddy",
|
|
121
|
+
create: (provider: OcxProviderConfig, _context: AdapterFactoryContext) => createQoderAdapter(provider),
|
|
122
|
+
},
|
|
111
123
|
} as const satisfies Record<string, AdapterDefinition>;
|
|
112
124
|
|
|
113
125
|
export type AdapterId = keyof typeof ADAPTER_REGISTRY;
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
const ENCRYPTED_MARKER_NAME_BAG_KEYS = new Set([
|
|
1
|
+
// Keys whose *children's names* are caller-chosen rather than schema keywords, and keys whose
|
|
2
|
+
// values are literal payloads rather than schemas. Shared by both strippers below: each one has
|
|
3
|
+
// to tell "the keyword `x`" apart from "a property someone named `x`".
|
|
4
|
+
const SCHEMA_NAME_BAG_KEYS = new Set([
|
|
6
5
|
"properties",
|
|
7
6
|
"patternProperties",
|
|
8
7
|
"$defs",
|
|
@@ -11,9 +10,21 @@ const ENCRYPTED_MARKER_NAME_BAG_KEYS = new Set([
|
|
|
11
10
|
"dependentSchemas",
|
|
12
11
|
"dependentRequired",
|
|
13
12
|
]);
|
|
14
|
-
const
|
|
13
|
+
const SCHEMA_LITERAL_VALUE_KEYS = new Set(["const", "default", "enum", "examples"]);
|
|
14
|
+
|
|
15
|
+
// These subtrees depend on property evaluation, polarity, branch selection or references.
|
|
16
|
+
// Preserve their complete argument contract rather than guessing whether a local relaxation
|
|
17
|
+
// remains a relaxation in the containing schema. The destination reports unsupported regexes.
|
|
18
|
+
const PRESERVED_PATTERN_SUBTREES = new Set([
|
|
19
|
+
"patternProperties", "not", "oneOf", "if", "contains", "$defs", "definitions",
|
|
20
|
+
]);
|
|
15
21
|
|
|
16
22
|
/**
|
|
23
|
+
* Codex multi-agent v2 stamps a Responses-only `encrypted: true` marker on collaboration tool
|
|
24
|
+
* schemas (openai/codex 5f4d06ef; issue #85). It is an annotation for the ChatGPT backend only,
|
|
25
|
+
* so translated provider schemas must drop it without removing properties or definitions
|
|
26
|
+
* literally named `encrypted`.
|
|
27
|
+
*
|
|
17
28
|
* The schema is caller-supplied, so its nesting depth is attacker-influenced. Native recursion
|
|
18
29
|
* would turn a deep schema into a stack overflow that takes down the request path, so this walks
|
|
19
30
|
* an explicit stack instead: depth costs heap, which is bounded and recoverable.
|
|
@@ -52,11 +63,11 @@ export function stripResponsesOnlyEncryptedMarker(node: unknown, inNameBag = fal
|
|
|
52
63
|
// Inside a name bag every key is a caller-chosen name, so `encrypted` here is data.
|
|
53
64
|
stack.push({ node: value, inNameBag: false, assign: v => { out[key] = v; } });
|
|
54
65
|
} else if (key !== "encrypted") {
|
|
55
|
-
if (
|
|
66
|
+
if (SCHEMA_LITERAL_VALUE_KEYS.has(key)) {
|
|
56
67
|
// Literal payloads are values, not schemas: an `encrypted` key inside them is data.
|
|
57
68
|
out[key] = value;
|
|
58
69
|
} else {
|
|
59
|
-
const childInNameBag =
|
|
70
|
+
const childInNameBag = SCHEMA_NAME_BAG_KEYS.has(key);
|
|
60
71
|
stack.push({ node: value, inNameBag: childInNameBag, assign: v => { out[key] = v; } });
|
|
61
72
|
}
|
|
62
73
|
}
|
|
@@ -65,3 +76,97 @@ export function stripResponsesOnlyEncryptedMarker(node: unknown, inNameBag = fal
|
|
|
65
76
|
|
|
66
77
|
return result;
|
|
67
78
|
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* `\p{…}` is an escape only when the backslash introducing it is itself unescaped: in `\\p{2}`
|
|
82
|
+
* the pair is a literal backslash and the `p{2}` that follows is an ordinary quantified `p`,
|
|
83
|
+
* which Python compiles fine. Scanning for the raw substring would misread that as a property
|
|
84
|
+
* escape and discard a working pattern.
|
|
85
|
+
*/
|
|
86
|
+
function usesUnicodePropertyEscape(pattern: string): boolean {
|
|
87
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
88
|
+
if (pattern[i] !== "\\") continue;
|
|
89
|
+
const next = pattern[i + 1];
|
|
90
|
+
if (next === "\\") {
|
|
91
|
+
i++;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if ((next === "p" || next === "P") && pattern[i + 2] === "{") return true;
|
|
95
|
+
}
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Remove unsupported Unicode property escapes from scalar `pattern` constraints in ordinary
|
|
101
|
+
* positive schema positions. This keeps built-in Artifact tools usable on Python-re backends;
|
|
102
|
+
* the omitted constraint is not enforced by this proxy and tools must validate their inputs.
|
|
103
|
+
*
|
|
104
|
+
* Regex-keyed objects are preserved. Removing a matcher can lose evaluated-property annotations
|
|
105
|
+
* needed by an ancestor's unevaluatedProperties, even when the local object appears open.
|
|
106
|
+
* Negation, exclusive alternatives, conditions, contains and reusable definitions are also
|
|
107
|
+
* preserved: loosening a nested constraint can instead reject an input in those contexts.
|
|
108
|
+
* Unsupported patterns there remain the destination's validation responsibility.
|
|
109
|
+
*
|
|
110
|
+
* Returns `node` itself when nothing was dropped. Uses an explicit stack for caller-controlled
|
|
111
|
+
* nesting depth; the separate Responses-only encrypted-marker normalization is unchanged.
|
|
112
|
+
*/
|
|
113
|
+
export function stripUnicodePropertyPatterns(node: unknown, inNameBag = false): unknown {
|
|
114
|
+
type Assign = (value: unknown) => void;
|
|
115
|
+
interface Frame { node: unknown; inNameBag: boolean; assign: Assign }
|
|
116
|
+
|
|
117
|
+
let result: unknown;
|
|
118
|
+
let dropped = 0;
|
|
119
|
+
const stack: Frame[] = [{ node, inNameBag, assign: value => { result = value; } }];
|
|
120
|
+
|
|
121
|
+
while (stack.length > 0) {
|
|
122
|
+
const frame = stack.pop()!;
|
|
123
|
+
const current = frame.node;
|
|
124
|
+
|
|
125
|
+
if (Array.isArray(current)) {
|
|
126
|
+
const out: unknown[] = new Array(current.length);
|
|
127
|
+
frame.assign(out);
|
|
128
|
+
// Array items are schemas in their own right, never a name bag.
|
|
129
|
+
for (let i = current.length - 1; i >= 0; i--) {
|
|
130
|
+
stack.push({ node: current[i], inNameBag: false, assign: value => { out[i] = value; } });
|
|
131
|
+
}
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (!current || typeof current !== "object") {
|
|
135
|
+
frame.assign(current);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// A schema name may be `__proto__`; a null-prototype record keeps it as data.
|
|
140
|
+
const out: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
141
|
+
frame.assign(out);
|
|
142
|
+
|
|
143
|
+
for (const [key, value] of Object.entries(current as Record<string, unknown>)) {
|
|
144
|
+
if (frame.inNameBag) {
|
|
145
|
+
// Inside a name bag every key is a caller-chosen name, so `pattern` here is a property
|
|
146
|
+
// name; its value is still a schema and is walked as one.
|
|
147
|
+
stack.push({ node: value, inNameBag: false, assign: v => { out[key] = v; } });
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
if (PRESERVED_PATTERN_SUBTREES.has(key)) {
|
|
151
|
+
out[key] = value;
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (key === "pattern" && typeof value === "string" && usesUnicodePropertyEscape(value)) {
|
|
155
|
+
dropped++;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (SCHEMA_LITERAL_VALUE_KEYS.has(key)) {
|
|
159
|
+
// Literal payloads are values, not schemas: a `pattern` key inside them is data.
|
|
160
|
+
out[key] = value;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
stack.push({
|
|
164
|
+
node: value,
|
|
165
|
+
inNameBag: SCHEMA_NAME_BAG_KEYS.has(key),
|
|
166
|
+
assign: v => { out[key] = v; },
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return dropped === 0 ? node : result;
|
|
172
|
+
}
|
package/src/cli/dispatch.ts
CHANGED
|
@@ -25,6 +25,7 @@ import { afterCatalogWriteHandleAppServers } from "../codex/app-server-processes
|
|
|
25
25
|
import { normalizeUpdateChannel, runGuiUpdateWorker } from "../update/job";
|
|
26
26
|
import { isJsonOption, takeFlag } from "./runtime-api";
|
|
27
27
|
import type { ClientConnectionState } from "../client/state";
|
|
28
|
+
import { OCX_NATIVE_REPLAY_RECOVERY_NOTE } from "../responses/compaction";
|
|
28
29
|
|
|
29
30
|
export interface CliDispatchDeps {
|
|
30
31
|
args: string[];
|
|
@@ -199,6 +200,7 @@ const commandRunners: Record<string, CommandRunner> = {
|
|
|
199
200
|
}
|
|
200
201
|
if (r.success) {
|
|
201
202
|
console.log("Codex integration is OFF and plain `codex` now runs natively. Switch back with: ocx restore back");
|
|
203
|
+
console.log(`Note: ${OCX_NATIVE_REPLAY_RECOVERY_NOTE}`);
|
|
202
204
|
} else {
|
|
203
205
|
console.error("Plain `codex` was not fully restored. Inspect $CODEX_HOME/config.toml before using native Codex.");
|
|
204
206
|
}
|
package/src/cli/doctor.ts
CHANGED
|
@@ -45,6 +45,9 @@ import {
|
|
|
45
45
|
probeCodexCoordinatorNamespace,
|
|
46
46
|
resolveEffectiveUserIdentity,
|
|
47
47
|
} from "../codex/user-identity";
|
|
48
|
+
import { isCanonicalOpenAiForwardProvider } from "../providers/openai-tiers-destination";
|
|
49
|
+
import type { OcxProviderConfig } from "../types/provider";
|
|
50
|
+
import { routedProviderConfig } from "../router";
|
|
48
51
|
import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings";
|
|
49
52
|
import {
|
|
50
53
|
collectLegacyCodexConfigKeyDiagnostics,
|
|
@@ -1000,6 +1003,41 @@ export function proxyDownRestartHint(input: {
|
|
|
1000
1003
|
return `The ocx proxy is not running. ${uncleanExit}Codex/Claude clients pinned to 127.0.0.1:${input.port} fail with errors like "error sending request for url (http://127.0.0.1:${input.port}/v1/responses)". ${restart}`;
|
|
1001
1004
|
}
|
|
1002
1005
|
|
|
1006
|
+
/** Explain the expected channel and latency trade-off for native ChatGPT routing. */
|
|
1007
|
+
export function chatgptPublicEndpointHint(
|
|
1008
|
+
providers: Record<string, unknown> | undefined,
|
|
1009
|
+
): string | null {
|
|
1010
|
+
const openai = providers?.openai;
|
|
1011
|
+
if (!openai || typeof openai !== "object") {
|
|
1012
|
+
return null;
|
|
1013
|
+
}
|
|
1014
|
+
// A disabled row never routes, so it must not be described as the route in use.
|
|
1015
|
+
const configured = openai as OcxProviderConfig;
|
|
1016
|
+
if (configured.disabled === true) {
|
|
1017
|
+
return null;
|
|
1018
|
+
}
|
|
1019
|
+
// Classify the destination the router resolves, not the raw config text. Two things follow
|
|
1020
|
+
// from the registry entry for the built-in `openai` id: a row that omits `authMode` still
|
|
1021
|
+
// forwards, and a row carrying some other `baseUrl` has it discarded in favour of the
|
|
1022
|
+
// canonical ChatGPT endpoint. Both keep using the public endpoint, so both want this hint;
|
|
1023
|
+
// reading the raw row would have suppressed the first and misjudged the second.
|
|
1024
|
+
//
|
|
1025
|
+
// `routedProviderConfig` throws for an unresolved URL only when the registry entry allows a
|
|
1026
|
+
// baseUrl override, which this entry does not, so no input reaches that path today. The guard
|
|
1027
|
+
// is here because doctor is read-only diagnostics: a later registry change must not turn a
|
|
1028
|
+
// diagnostic into a crash.
|
|
1029
|
+
let routed: OcxProviderConfig;
|
|
1030
|
+
try {
|
|
1031
|
+
routed = routedProviderConfig("openai", configured);
|
|
1032
|
+
} catch {
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
if (!isCanonicalOpenAiForwardProvider(routed)) {
|
|
1036
|
+
return null;
|
|
1037
|
+
}
|
|
1038
|
+
return "ChatGPT-family requests use the public ChatGPT endpoint through this proxy, in both Pool and Direct modes. Eligible streaming turns dial the ChatGPT websocket transport (the same responses_websockets lane Codex CLI defaults to) and fall back to SSE over HTTP when a turn is not eligible - an unsupported Bun runtime, an oversized create frame, or a proxy route that cannot carry the socket - and local provider pacing can hold a request before it is dispatched at all. This hint classifies configuration only and measures nothing, so upstream queueing is one possible contributor to a slow first output: compare actual transport, pacing, network, and provider observations before concluding. service_tier=priority is a request preference: this backend can echo service_tier \"default\" even on turns it scheduled as priority (#2558), so the echoed response tier in request logs stays an observation with confirmation \"assumed\" and cannot confirm or deny the granted tier.";
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1003
1041
|
export async function runDoctor(args: string[] = []): Promise<void> {
|
|
1004
1042
|
if (args.includes("--fix-codex-runtime")) {
|
|
1005
1043
|
const resolved = resolveCodexRuntime();
|
|
@@ -1330,6 +1368,8 @@ export async function runDoctor(args: string[] = []): Promise<void> {
|
|
|
1330
1368
|
|
|
1331
1369
|
// Hints, not fixes.
|
|
1332
1370
|
const hints: string[] = [];
|
|
1371
|
+
const chatgptHint = chatgptPublicEndpointHint(doctorConfig.providers);
|
|
1372
|
+
if (chatgptHint) hints.push(chatgptHint);
|
|
1333
1373
|
const proxyDown = proxyDownRestartHint({
|
|
1334
1374
|
proxyRunning: Boolean(live),
|
|
1335
1375
|
port: live?.port ?? doctorConfig.port ?? 10100,
|
package/src/cli/effort.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { loadConfig, saveConfig } from "../config";
|
|
|
2
2
|
import {
|
|
3
3
|
CODEX_REASONING_LEVELS,
|
|
4
4
|
configuredReasoningEfforts,
|
|
5
|
+
isCodexReasoningEffort,
|
|
5
6
|
isDeclaredReasoningEffort,
|
|
6
7
|
mapReasoningEffort,
|
|
7
8
|
reasoningEffortMapFor,
|
|
@@ -21,7 +22,7 @@ import {
|
|
|
21
22
|
|
|
22
23
|
export const EFFORT_USAGE = `Usage:
|
|
23
24
|
ocx effort [status] [--json]
|
|
24
|
-
ocx effort <low|medium|high|xhigh|max|ultra
|
|
25
|
+
ocx effort <low|medium|high|xhigh|max|ultra|-> [--json]
|
|
25
26
|
ocx effort set [--main <level|->] [--subagent <level|->] [--injection <level|->] [--json]
|
|
26
27
|
ocx effort clear [--json]
|
|
27
28
|
ocx effort model <provider/model|model> [--json]
|
|
@@ -33,13 +34,18 @@ function clearable(value: string | undefined): string | null | undefined {
|
|
|
33
34
|
return value === "-" ? null : value;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
function validateEffortLevel(
|
|
37
|
+
function validateEffortLevel(
|
|
38
|
+
level: string | null | undefined,
|
|
39
|
+
label: string,
|
|
40
|
+
kind: "cap" | "injection",
|
|
41
|
+
): string | null | undefined {
|
|
37
42
|
if (level === undefined || level === null) return level;
|
|
38
43
|
const trimmed = level.trim();
|
|
39
44
|
if (trimmed === "-" || trimmed === "") return null;
|
|
40
|
-
|
|
45
|
+
const valid = kind === "cap" ? isCodexReasoningEffort(trimmed) : isDeclaredReasoningEffort(trimmed);
|
|
46
|
+
if (!valid) {
|
|
41
47
|
throw new CliUsageError(
|
|
42
|
-
`unknown reasoning effort "${trimmed}" for ${label} (allowed: ${CODEX_REASONING_LEVELS.map(l => l.effort).join(", ")}, none, minimal, -)`,
|
|
48
|
+
`unknown reasoning effort "${trimmed}" for ${label} (allowed: ${CODEX_REASONING_LEVELS.map(l => l.effort).join(", ")}${kind === "injection" ? ", none, minimal" : ""}, -)`,
|
|
43
49
|
EFFORT_USAGE,
|
|
44
50
|
);
|
|
45
51
|
}
|
|
@@ -114,6 +120,15 @@ async function status(wantsJson: boolean, deps: RuntimeApiDeps): Promise<void> {
|
|
|
114
120
|
data = getOfflineStatus();
|
|
115
121
|
}
|
|
116
122
|
|
|
123
|
+
// Report the stored/runtime value exactly as the enforcement layer evaluates it.
|
|
124
|
+
// An ignored subagent field does not disable a valid main cap on that child.
|
|
125
|
+
const warnings = ([ ["effortCap", "--main"], ["subagentEffortCap", "--subagent"] ] as const)
|
|
126
|
+
.flatMap(([key, flag]) => {
|
|
127
|
+
const value = data[key];
|
|
128
|
+
if (value === null || isCodexReasoningEffort(value)) return [];
|
|
129
|
+
return [`${key}=${JSON.stringify(value)} is invalid and is not applied. Use: ocx effort set ${flag} <${CODEX_REASONING_LEVELS.map(l => l.effort).join("|")}|->.`];
|
|
130
|
+
});
|
|
131
|
+
|
|
117
132
|
const lines = [
|
|
118
133
|
`Reasoning effort status (${data.source === "runtime" ? "live proxy" : "offline config"}):`,
|
|
119
134
|
` Main agent effort cap: ${data.effortCap ?? "(unset — no cap)"}`,
|
|
@@ -122,9 +137,10 @@ async function status(wantsJson: boolean, deps: RuntimeApiDeps): Promise<void> {
|
|
|
122
137
|
"",
|
|
123
138
|
"Supported Codex reasoning effort ladder:",
|
|
124
139
|
...CODEX_REASONING_LEVELS.map(l => ` - ${l.effort.padEnd(8)} ${l.description}`),
|
|
140
|
+
...(warnings.length ? ["", "Warnings:", ...warnings.map(warning => ` ${warning}`)] : []),
|
|
125
141
|
];
|
|
126
142
|
|
|
127
|
-
printData(data, wantsJson, lines);
|
|
143
|
+
printData({ ...data, warnings }, wantsJson, lines);
|
|
128
144
|
}
|
|
129
145
|
|
|
130
146
|
async function setEffort(
|
|
@@ -136,9 +152,9 @@ async function setEffort(
|
|
|
136
152
|
wantsJson: boolean,
|
|
137
153
|
deps: RuntimeApiDeps,
|
|
138
154
|
): Promise<void> {
|
|
139
|
-
const validatedMain = validateEffortLevel(options.main, "--main");
|
|
140
|
-
const validatedSubagent = validateEffortLevel(options.subagent, "--subagent");
|
|
141
|
-
const validatedInjection = validateEffortLevel(options.injection, "--injection");
|
|
155
|
+
const validatedMain = validateEffortLevel(options.main, "--main", "cap");
|
|
156
|
+
const validatedSubagent = validateEffortLevel(options.subagent, "--subagent", "cap");
|
|
157
|
+
const validatedInjection = validateEffortLevel(options.injection, "--injection", "injection");
|
|
142
158
|
|
|
143
159
|
if (validatedMain === undefined && validatedSubagent === undefined && validatedInjection === undefined) {
|
|
144
160
|
throw new CliUsageError("at least one effort option (--main, --subagent, or --injection) is required", EFFORT_USAGE);
|
package/src/cli/help.ts
CHANGED
|
@@ -33,6 +33,8 @@ Usage:
|
|
|
33
33
|
ocx restore back Re-point codex at the running proxy (undo restore)
|
|
34
34
|
ocx recover-history --legacy-openai --yes
|
|
35
35
|
Force all user-message opencodex rows to OpenAI (legacy recovery)
|
|
36
|
+
ocx recover-history --ocx-compaction <thread-id> --yes
|
|
37
|
+
Back up and make one ocx1-compacted thread replayable by native Codex
|
|
36
38
|
ocx uninstall Remove service/shim/config and restore native Codex (alias: remove)
|
|
37
39
|
ocx service [sub] Run as a background service (default: install/update/start)
|
|
38
40
|
ocx codex-shim <sub> Auto-start proxy when \`codex\` launches (install|status|uninstall|remove)
|
package/src/cli/index.ts
CHANGED
|
@@ -1442,8 +1442,31 @@ async function handleStatus() {
|
|
|
1442
1442
|
}
|
|
1443
1443
|
|
|
1444
1444
|
async function handleRecoverHistory() {
|
|
1445
|
+
if (args[1] === "--ocx-compaction") {
|
|
1446
|
+
const threadId = args[2];
|
|
1447
|
+
if (args.length !== 4 || !threadId || args[3] !== "--yes") {
|
|
1448
|
+
console.error("Usage: ocx recover-history --ocx-compaction <thread-id> --yes");
|
|
1449
|
+
console.error("This rewrites one rollout after saving a private byte-for-byte backup. Close that Codex thread before retrying.");
|
|
1450
|
+
process.exit(1);
|
|
1451
|
+
}
|
|
1452
|
+
console.error("WARNING: this converts OpenCodeX-owned ocx1 compaction state into a plain summary for native Codex replay.");
|
|
1453
|
+
try {
|
|
1454
|
+
const { recoverOcxCompactionHistory } = await import("../codex/ocx-compaction-history");
|
|
1455
|
+
const result = recoverOcxCompactionHistory({ threadId });
|
|
1456
|
+
if (result.replaced === 0) {
|
|
1457
|
+
console.log(`Thread ${threadId} has no repairable ocx1 compaction history; no files changed.`);
|
|
1458
|
+
return;
|
|
1459
|
+
}
|
|
1460
|
+
console.log(`Recovered ${result.replaced} ocx1 compaction item(s) in thread ${threadId}.`);
|
|
1461
|
+
console.log(`Backup: ${result.backupPath}`);
|
|
1462
|
+
return;
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
console.error(`Recovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1465
|
+
process.exit(1);
|
|
1466
|
+
}
|
|
1467
|
+
}
|
|
1445
1468
|
if (args[1] !== "--legacy-openai") {
|
|
1446
|
-
console.error("Usage: ocx recover-history --legacy-openai --yes");
|
|
1469
|
+
console.error("Usage: ocx recover-history (--legacy-openai | --ocx-compaction <thread-id>) --yes");
|
|
1447
1470
|
console.error("This force-relabels every user-message opencodex row to OpenAI, including legitimate dedicated-provider history. Back up first and use it only for pre-backup legacy recovery.");
|
|
1448
1471
|
process.exit(1);
|
|
1449
1472
|
}
|
|
@@ -204,7 +204,8 @@ async function presets(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
204
204
|
const rows = Array.isArray(result) ? result : result.providers ?? [];
|
|
205
205
|
printData(result, wantsJson, rows.map(row => {
|
|
206
206
|
const record = row as Record<string, unknown>;
|
|
207
|
-
|
|
207
|
+
const sponsor = record.sponsor ? ` (sponsor: ${String(record.sponsor)})` : "";
|
|
208
|
+
return `${String(record.id ?? record.name ?? "?")} ${String(record.label ?? record.adapter ?? "")}${sponsor}`.trimEnd();
|
|
208
209
|
}));
|
|
209
210
|
}
|
|
210
211
|
|
package/src/cli/registry.ts
CHANGED
|
@@ -36,8 +36,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
|
|
|
36
36
|
},
|
|
37
37
|
{
|
|
38
38
|
name: "recover-history",
|
|
39
|
-
usage: "ocx recover-history --legacy-openai --yes",
|
|
40
|
-
summary: "
|
|
39
|
+
usage: "ocx recover-history (--legacy-openai | --ocx-compaction <thread-id>) --yes",
|
|
40
|
+
summary: "Recover legacy provider metadata or one OpenCodeX-compacted thread for native replay.",
|
|
41
41
|
},
|
|
42
42
|
{
|
|
43
43
|
name: "uninstall",
|
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
const USAGE = `Usage:
|
|
15
15
|
ocx system [status] [--json]
|
|
16
16
|
ocx system settings [--auto-start <on|off>] [--stream-mode <auto|legacy-tee|eager-relay>]
|
|
17
|
-
[--desktop-authless <on|off>] [--json]
|
|
17
|
+
[--desktop-authless <on|off>] [--client-compaction <on|off>] [--json]
|
|
18
18
|
ocx system startup <health|install-service|install-shim> [--json]
|
|
19
19
|
ocx system diagnostics [--json]
|
|
20
20
|
ocx system sync [--json]
|
|
@@ -23,7 +23,11 @@ const USAGE = `Usage:
|
|
|
23
23
|
ocx system codex-cli-update check [--json]
|
|
24
24
|
ocx system update check [--channel <latest|preview>] [--json]
|
|
25
25
|
ocx system update run [--channel <latest|preview>] [--restart <on|off>] --yes [--json]
|
|
26
|
-
ocx system update status <job-id> [--json]
|
|
26
|
+
ocx system update status <job-id> [--json]
|
|
27
|
+
|
|
28
|
+
--client-compaction favors native replay portability for future compactions while
|
|
29
|
+
keeping OpenCodeX routing active; the configured provider may process summaries
|
|
30
|
+
and consume its quota.`;
|
|
27
31
|
|
|
28
32
|
async function status(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
29
33
|
const args = [...argv];
|
|
@@ -44,8 +48,10 @@ async function settings(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
44
48
|
const autoStart = takeBooleanOption(args, "--auto-start");
|
|
45
49
|
const streamMode = takeOption(args, "--stream-mode");
|
|
46
50
|
const desktopAuthless = takeBooleanOption(args, "--desktop-authless");
|
|
51
|
+
const clientCompaction = takeBooleanOption(args, "--client-compaction");
|
|
47
52
|
rejectArgs(args, USAGE);
|
|
48
|
-
if (autoStart === undefined && streamMode === undefined
|
|
53
|
+
if (autoStart === undefined && streamMode === undefined
|
|
54
|
+
&& desktopAuthless === undefined && clientCompaction === undefined) {
|
|
49
55
|
const result = await runtimeRequest("/api/settings", {}, deps);
|
|
50
56
|
printData(result, wantsJson, summaryLines(result));
|
|
51
57
|
return;
|
|
@@ -54,6 +60,7 @@ async function settings(argv: string[], deps: RuntimeApiDeps): Promise<void> {
|
|
|
54
60
|
...(autoStart !== undefined ? { codexAutoStart: autoStart } : {}),
|
|
55
61
|
...(streamMode !== undefined ? { streamMode } : {}),
|
|
56
62
|
...(desktopAuthless !== undefined ? { codexDesktopAuthless: desktopAuthless } : {}),
|
|
63
|
+
...(clientCompaction !== undefined ? { codexClientCompaction: clientCompaction } : {}),
|
|
57
64
|
};
|
|
58
65
|
const result = await runtimeRequest("/api/settings", { method: "PUT", body: JSON.stringify(body) }, deps);
|
|
59
66
|
printData(result, wantsJson, ["System settings updated."]);
|
package/src/cli/usage-report.ts
CHANGED
|
@@ -59,8 +59,11 @@ interface UsageReportInput {
|
|
|
59
59
|
|
|
60
60
|
const MAX_MODEL_ROWS = 10;
|
|
61
61
|
|
|
62
|
-
function terminalText(value:
|
|
63
|
-
|
|
62
|
+
function terminalText(value: unknown): string {
|
|
63
|
+
const text = typeof value === "string" ? value
|
|
64
|
+
: value === null || value === undefined ? ""
|
|
65
|
+
: typeof value === "number" || typeof value === "boolean" ? String(value) : "[invalid]";
|
|
66
|
+
return text.replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/g, character => {
|
|
64
67
|
const code = character.charCodeAt(0);
|
|
65
68
|
return code <= 0x7f
|
|
66
69
|
? `\\x${code.toString(16).padStart(2, "0")}`
|
|
@@ -69,7 +72,8 @@ function terminalText(value: string): string {
|
|
|
69
72
|
}
|
|
70
73
|
|
|
71
74
|
function count(value: number | undefined): string {
|
|
72
|
-
|
|
75
|
+
if (value === undefined || value === null) return "0";
|
|
76
|
+
return typeof value === "number" && Number.isFinite(value) ? value.toLocaleString("en-US") : "—";
|
|
73
77
|
}
|
|
74
78
|
|
|
75
79
|
/**
|
|
@@ -111,7 +115,7 @@ export function formatUsageReport(data: UsageReportInput): string[] {
|
|
|
111
115
|
.filter(Boolean).join(" and ");
|
|
112
116
|
lines.push(`No usage recorded for ${terminalText(what)} in this range.`);
|
|
113
117
|
lines.push("Check the spelling against `ocx usage --json`, or widen --range.");
|
|
114
|
-
return lines;
|
|
118
|
+
return lines.map(terminalText);
|
|
115
119
|
}
|
|
116
120
|
|
|
117
121
|
const tokenSplit = [
|
|
@@ -185,5 +189,5 @@ export function formatUsageReport(data: UsageReportInput): string[] {
|
|
|
185
189
|
|
|
186
190
|
lines.push("");
|
|
187
191
|
lines.push("Not a billing receipt. Subscription usage or provider credits may apply instead.");
|
|
188
|
-
return lines;
|
|
192
|
+
return lines.map(terminalText);
|
|
189
193
|
}
|
|
@@ -7,12 +7,13 @@ import {
|
|
|
7
7
|
} from "../config";
|
|
8
8
|
import { removeCodexAccountCredential } from "./account-store";
|
|
9
9
|
import { clearAccountNeedsReauth } from "./account-runtime-state";
|
|
10
|
-
import { getMainChatgptAccountId } from "./auth-collision";
|
|
10
|
+
import { getMainChatgptAccountId, readCodexTokensResult } from "./auth-collision";
|
|
11
11
|
import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
|
|
12
12
|
import { clearAccountQuota } from "./quota";
|
|
13
13
|
import { clearCodexUpstreamHealthForAccount, clearThreadAccountMapForAccount } from "./routing";
|
|
14
14
|
import { invalidateCodexWebSocketsForAccount } from "./websocket-registry";
|
|
15
|
-
import { clearMainAccountCredentialPresence, clearMainAccountInfoCache, observeMainQuotaIdentity } from "./main-account-cache";
|
|
15
|
+
import { clearMainAccountCredentialPresence, clearMainAccountInfoCache, observeMainQuotaCredential, observeMainQuotaIdentity } from "./main-account-cache";
|
|
16
|
+
import { extractAccountIdClaims } from "../oauth/chatgpt";
|
|
16
17
|
import { forgetCodexAccountPause } from "./account-pause";
|
|
17
18
|
import { clearCodexAccountPin, forgetCodexAccountPriority } from "./account-priority";
|
|
18
19
|
import { forgetCodexQuotaAutoRefreshAccount } from "./quota-auto-refresh-state";
|
|
@@ -75,6 +76,38 @@ export function reconcileMainCodexAccountRuntimeState(): boolean {
|
|
|
75
76
|
return true;
|
|
76
77
|
}
|
|
77
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Rebuild the memory-only policy binding from a startup-owned, recovered auth path.
|
|
81
|
+
* The caller holds the native owner and exclusive claim; an incoming bearer is never evidence.
|
|
82
|
+
* A failed read creates no binding and cannot revoke a prior verified observation or its block.
|
|
83
|
+
* Only a valid replacement observation or confirmed account transition supersedes that evidence.
|
|
84
|
+
*/
|
|
85
|
+
export function initializeMainAccountPolicyBinding(authPath: string): boolean {
|
|
86
|
+
// Startup observes the pinned owned path inside the exclusive claim: bound the read so a
|
|
87
|
+
// replaced non-regular or oversized file cannot stall startup inside that claim.
|
|
88
|
+
const result = readCodexTokensResult(authPath, { bounded: true });
|
|
89
|
+
if (result.status !== "ok") return false;
|
|
90
|
+
const { tokens } = result;
|
|
91
|
+
if (typeof tokens.access_token !== "string" || !tokens.access_token
|
|
92
|
+
|| typeof tokens.account_id !== "string" || !tokens.account_id) return false;
|
|
93
|
+
if (tokens.id_token != null && typeof tokens.id_token !== "string") return false;
|
|
94
|
+
const accountId = tokens.account_id;
|
|
95
|
+
// An owned file may contain an opaque bearer, but every decoded identity must agree —
|
|
96
|
+
// including the two account-id encodings within a single token.
|
|
97
|
+
const idTokenClaims = extractAccountIdClaims(tokens.id_token);
|
|
98
|
+
const accessTokenClaims = extractAccountIdClaims(tokens.access_token);
|
|
99
|
+
if (idTokenClaims.conflict || accessTokenClaims.conflict) return false;
|
|
100
|
+
const idTokenAccountId = idTokenClaims.accountId;
|
|
101
|
+
const accessTokenAccountId = accessTokenClaims.accountId;
|
|
102
|
+
if ((idTokenAccountId !== undefined && idTokenAccountId !== accountId)
|
|
103
|
+
|| (accessTokenAccountId !== undefined && accessTokenAccountId !== accountId)) return false;
|
|
104
|
+
const previousAccountId = observedMainChatgptAccountId;
|
|
105
|
+
observedMainChatgptAccountId = accountId;
|
|
106
|
+
if (previousAccountId !== undefined && previousAccountId !== accountId) purgeMainCodexAccountRuntimeState();
|
|
107
|
+
observeMainQuotaIdentity(accountId);
|
|
108
|
+
return observeMainQuotaCredential(tokens.access_token, accountId) !== undefined;
|
|
109
|
+
}
|
|
110
|
+
|
|
78
111
|
/**
|
|
79
112
|
* Apply a transaction-confirmed physical native-login change without waiting for
|
|
80
113
|
* a later auth.json observation. The caller owns credential commit/rollback.
|
package/src/codex/auth-api.ts
CHANGED
|
@@ -241,12 +241,15 @@ function codexAccountPersistenceConflict(
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
/**
|
|
244
|
-
* The exact
|
|
244
|
+
* The exact labels `parseUsageQuota` emits for the Codex Spark windows (quota.ts).
|
|
245
245
|
* Matching on the label rather than on "is a custom window" is load-bearing: the same array
|
|
246
246
|
* carries Cursor's First-party models / API usage, Anthropic's Fable / Opus / Sonnet,
|
|
247
247
|
* Antigravity's Gem / Cla, Kimi's subscription credits and a dozen dynamic provider meters.
|
|
248
248
|
*/
|
|
249
|
-
const
|
|
249
|
+
const CODEX_SPARK_WINDOW_LABELS = new Set([
|
|
250
|
+
"GPT-5.3-Codex-Spark 5h",
|
|
251
|
+
"GPT-5.3-Codex-Spark Weekly",
|
|
252
|
+
]);
|
|
250
253
|
|
|
251
254
|
/**
|
|
252
255
|
* Drop the Spark window unless the operator asked for it (default hidden).
|
|
@@ -264,7 +267,7 @@ export function withSparkVisibility<T extends Omit<StoredAccountQuota, "updatedA
|
|
|
264
267
|
): T {
|
|
265
268
|
if (!quota?.customWindows?.length) return quota;
|
|
266
269
|
if (loadConfig().showCodexSparkQuota === true) return quota;
|
|
267
|
-
const kept = quota.customWindows.filter(window => window.label
|
|
270
|
+
const kept = quota.customWindows.filter(window => !CODEX_SPARK_WINDOW_LABELS.has(window.label));
|
|
268
271
|
if (kept.length === quota.customWindows.length) return quota;
|
|
269
272
|
// An empty list is dropped rather than serialized: an absent field and an empty array should
|
|
270
273
|
// not be two different ways of saying "no custom windows" on the wire.
|
|
@@ -6,6 +6,7 @@ import { resolveCodexHomeDir } from "./home";
|
|
|
6
6
|
import { extractAccountId } from "../oauth/chatgpt";
|
|
7
7
|
import { isSelectableCodexPoolAccount } from "./account-id";
|
|
8
8
|
import { codexPlanKey } from "./plan";
|
|
9
|
+
import { MAX_AUTH_BYTES, readBounded } from "./native-profile-store";
|
|
9
10
|
|
|
10
11
|
export interface CodexTokens {
|
|
11
12
|
access_token: string;
|
|
@@ -31,12 +32,21 @@ function hasErrnoCode(error: unknown, code: string): boolean {
|
|
|
31
32
|
/**
|
|
32
33
|
* Reads the Codex CLI credential file and classifies the outcome. Reads once instead of doing an
|
|
33
34
|
* `existsSync` pre-check, so a file replaced between check and read cannot be misread as absent.
|
|
35
|
+
* An already-owned lifecycle may supply its pinned auth path instead of resolving ambient home.
|
|
36
|
+
* `bounded` opts into the native-profile bounded reader (regular file, size-capped, no-follow,
|
|
37
|
+
* non-blocking) for startup observation paths that run inside the owner claim; bounded violations
|
|
38
|
+
* classify as `unreadable`. Legacy callers keep the unbounded read.
|
|
34
39
|
* Never returns or logs the raw error or any token material.
|
|
35
40
|
*/
|
|
36
|
-
export function readCodexTokensResult(
|
|
41
|
+
export function readCodexTokensResult(
|
|
42
|
+
authPath = join(resolveCodexHomeDir(), "auth.json"),
|
|
43
|
+
options?: { bounded?: boolean },
|
|
44
|
+
): CodexTokenReadResult {
|
|
37
45
|
let raw: string;
|
|
38
46
|
try {
|
|
39
|
-
raw =
|
|
47
|
+
raw = options?.bounded === true
|
|
48
|
+
? readBounded(authPath, MAX_AUTH_BYTES).toString("utf-8")
|
|
49
|
+
: readFileSync(authPath, "utf-8");
|
|
40
50
|
} catch (error) {
|
|
41
51
|
return { status: hasErrnoCode(error, "ENOENT") ? "missing" : "unreadable" };
|
|
42
52
|
}
|