@claudexor/harness-codex 1.0.0 → 2.1.2
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/dist/attachments.d.ts +4 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +11 -0
- package/dist/attachments.js.map +1 -0
- package/dist/auth.d.ts +59 -0
- package/dist/auth.d.ts.map +1 -0
- package/dist/auth.js +154 -0
- package/dist/auth.js.map +1 -0
- package/dist/index.d.ts +26 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +379 -286
- package/dist/index.js.map +1 -1
- package/dist/parse.d.ts +12 -1
- package/dist/parse.d.ts.map +1 -1
- package/dist/parse.js +112 -16
- package/dist/parse.js.map +1 -1
- package/dist/pricing.d.ts.map +1 -1
- package/dist/pricing.js +4 -1
- package/dist/pricing.js.map +1 -1
- package/dist/profile.d.ts +25 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +178 -0
- package/dist/profile.js.map +1 -0
- package/dist/smoke.d.ts +5 -0
- package/dist/smoke.d.ts.map +1 -0
- package/dist/smoke.js +59 -0
- package/dist/smoke.js.map +1 -0
- package/dist/transcript.d.ts +4 -7
- package/dist/transcript.d.ts.map +1 -1
- package/dist/transcript.js +38 -21
- package/dist/transcript.js.map +1 -1
- package/package.json +8 -5
package/dist/index.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { codexTranscriptModel, codexTranscriptRateLimits } from "./transcript.js";
|
|
3
|
-
import { homedir, tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { ConformanceReport as ConformanceReportSchema, HarnessManifest as HarnessManifestSchema } from "@claudexor/schema";
|
|
6
|
-
import { HarnessUnavailableError, normalizeEffort, playwrightMcpArgs, providerScrubEnv, resolveNpxBin, runCapture, runCliHarness } from "@claudexor/core";
|
|
7
3
|
import { resolveSecret } from "@claudexor/secrets";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { ConformanceReport as ConformanceReportSchema, HarnessManifest as HarnessManifestSchema, } from "@claudexor/schema";
|
|
7
|
+
import { abortSignalFromSpec, browserMcpCommand, HarnessUnavailableError, normalizeEffort, providerScrubEnv, resolveHarnessBinary, runCapture, runCliHarness, selectStrictAuthRoute, selectedAuthAvailable, selectedAuthReady, shouldVerifyApiKey, } from "@claudexor/core";
|
|
8
8
|
import { CLAUDEXOR_VERSION, nowIso, redactSecrets } from "@claudexor/util";
|
|
9
9
|
import { parseCodexEvent } from "./parse.js";
|
|
10
|
+
import { probeCodexCredentialProfile, resolveCodexProfileRoute } from "./profile.js";
|
|
11
|
+
import { smokeIsolatedApiKey } from "./smoke.js";
|
|
12
|
+
export { canonicalCodexProfileHome } from "./profile.js";
|
|
10
13
|
import { estimateCodexCostUsd } from "./pricing.js";
|
|
11
|
-
|
|
14
|
+
import { codexImageArgs } from "./attachments.js";
|
|
15
|
+
export const BIN = process.env.CLAUDEXOR_CODEX_BIN || "codex";
|
|
12
16
|
/**
|
|
13
17
|
* Ordered (weakest→strongest) reasoning-effort levels codex's
|
|
14
18
|
* `model_reasoning_effort` config accepts. SINGLE source: the manifest's
|
|
@@ -16,90 +20,41 @@ const BIN = process.env.CLAUDEXOR_CODEX_BIN || "codex";
|
|
|
16
20
|
* `max` hint clamps to `xhigh` (the ceiling) via the shared normalizer.
|
|
17
21
|
*/
|
|
18
22
|
const CODEX_EFFORT_LEVELS = ["low", "medium", "high", "xhigh"];
|
|
23
|
+
/** Exported for focused route-policy tests; runtime uses this exact selector. */
|
|
24
|
+
export const selectCodexRunAuthRoute = selectStrictAuthRoute;
|
|
25
|
+
export { CODEX_FILE_AUTH_ARGS, CODEX_FILE_AUTH_OVERRIDE, codexApiKey, codexAuthModeAt, defaultNativeCodexHome, ensureCodexApiAuth, probeLogin, } from "./auth.js";
|
|
26
|
+
import { CODEX_FILE_AUTH_ARGS, codexApiKey, codexAuthModeAt, defaultNativeCodexHome, ensureCodexApiAuth, hasApiKey, probeLogin, } from "./auth.js";
|
|
19
27
|
/**
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
// The hermetic kill switch is honored inside resolveSecret (single owner).
|
|
26
|
-
const stored = resolveSecret("openai");
|
|
27
|
-
return process.env.CLAUDEXOR_CODEX_API_KEY || process.env.CODEX_API_KEY || process.env.OPENAI_API_KEY || stored || undefined;
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* Seed `api_key` auth into an isolated CODEX_HOME. Codex does not read
|
|
31
|
-
* `OPENAI_API_KEY` from the environment when run against an empty config dir
|
|
32
|
-
* (it requires `auth.json`), so an envelope-scoped CODEX_HOME would otherwise
|
|
33
|
-
* fail with 401 even though a key is available. We write the same file
|
|
34
|
-
* `codex login --with-api-key` produces. No-op when not isolated (use codex's
|
|
35
|
-
* native auth), when no key is available, or when auth already exists.
|
|
36
|
-
*/
|
|
37
|
-
export function ensureCodexApiAuth(env, allowApiKey = true) {
|
|
38
|
-
if (!allowApiKey)
|
|
39
|
-
return;
|
|
40
|
-
const home = env?.["CODEX_HOME"];
|
|
41
|
-
if (!home)
|
|
42
|
-
return;
|
|
43
|
-
const apiKey = codexApiKey();
|
|
44
|
-
if (!apiKey)
|
|
45
|
-
return;
|
|
46
|
-
const authPath = join(home, "auth.json");
|
|
47
|
-
if (existsSync(authPath))
|
|
48
|
-
return;
|
|
49
|
-
try {
|
|
50
|
-
mkdirSync(home, { recursive: true });
|
|
51
|
-
writeFileSync(authPath, JSON.stringify({ auth_mode: "apikey", OPENAI_API_KEY: apiKey }) + "\n", { mode: 0o600 });
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
/* best-effort: codex will surface an auth error if this did not take */
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
/** The user's real codex home (native ChatGPT/subscription session lives here). */
|
|
58
|
-
export function defaultNativeCodexHome() {
|
|
59
|
-
const override = process.env.CLAUDEXOR_CODEX_NATIVE_HOME;
|
|
60
|
-
if (override && override.trim())
|
|
61
|
-
return override;
|
|
62
|
-
return join(homedir(), ".codex");
|
|
63
|
-
}
|
|
64
|
-
/**
|
|
65
|
-
* Seed the user's NATIVE codex session (`auth.json`, ChatGPT/subscription mode)
|
|
66
|
-
* into an isolated CODEX_HOME so a Max/Pro subscriber with NO API key can run
|
|
67
|
-
* inside a Claudexor envelope. This is the "subscription-first must actually
|
|
68
|
-
* work" fix: previously the scoped empty CODEX_HOME hid the native session and
|
|
69
|
-
* the run failed demanding an API key.
|
|
70
|
-
*
|
|
71
|
-
* Copies ONLY if the scoped auth is absent and a native `auth.json` exists; never
|
|
72
|
-
* overwrites (codex refreshes the token in place). Returns true when scoped auth
|
|
73
|
-
* is present afterwards. No-op when not isolated or no native session exists.
|
|
28
|
+
* A TOML basic-string literal for a `-c key=value` override. `developer_instructions`
|
|
29
|
+
* is a documented additive Codex config key (layered as a developer block BEFORE
|
|
30
|
+
* AGENTS.md, not a replacement); passing per-invocation `-c` keeps it isolated
|
|
31
|
+
* to this run (never a shared-config mutation). Instructions may contain quotes
|
|
32
|
+
* and newlines, so they are TOML-escaped.
|
|
74
33
|
*/
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return false;
|
|
34
|
+
function tomlBasicString(value) {
|
|
35
|
+
// TOML basic-string escapes, built by code point so the SOURCE carries no
|
|
36
|
+
// literal control characters: a backslash and quote are escaped, a literal
|
|
37
|
+
// newline/tab/CR become their escapes (a raw newline is invalid in a basic
|
|
38
|
+
// string), other control chars become \uXXXX, and everything else is literal.
|
|
39
|
+
let out = '"';
|
|
40
|
+
for (const ch of value) {
|
|
41
|
+
const code = ch.charCodeAt(0);
|
|
42
|
+
if (ch === "\\")
|
|
43
|
+
out += "\\\\";
|
|
44
|
+
else if (ch === '"')
|
|
45
|
+
out += '\\"';
|
|
46
|
+
else if (code === 10)
|
|
47
|
+
out += "\\n";
|
|
48
|
+
else if (code === 13)
|
|
49
|
+
out += "\\r";
|
|
50
|
+
else if (code === 9)
|
|
51
|
+
out += "\\t";
|
|
52
|
+
else if (code < 32 || code === 127)
|
|
53
|
+
out += "\\u" + code.toString(16).padStart(4, "0");
|
|
54
|
+
else
|
|
55
|
+
out += ch;
|
|
98
56
|
}
|
|
99
|
-
|
|
100
|
-
/** True when a native codex session exists and can be seeded into an envelope. */
|
|
101
|
-
function nativeCodexSeedable() {
|
|
102
|
-
return existsSync(join(defaultNativeCodexHome(), "auth.json"));
|
|
57
|
+
return out + '"';
|
|
103
58
|
}
|
|
104
59
|
function sandboxArgs(access) {
|
|
105
60
|
switch (access) {
|
|
@@ -114,83 +69,61 @@ function sandboxArgs(access) {
|
|
|
114
69
|
return [];
|
|
115
70
|
}
|
|
116
71
|
}
|
|
117
|
-
async function detectVersion() {
|
|
72
|
+
async function detectVersion(abortSignal) {
|
|
118
73
|
try {
|
|
119
|
-
const r = await runCapture(BIN, ["--version"], {
|
|
74
|
+
const r = await runCapture(BIN, ["--version"], {
|
|
75
|
+
timeoutMs: 10_000,
|
|
76
|
+
abortSignal,
|
|
77
|
+
cancelSignal: "SIGTERM",
|
|
78
|
+
cancelKillDelayMs: 0,
|
|
79
|
+
});
|
|
120
80
|
return r.stdout.trim() || `${BIN} (version unknown)`;
|
|
121
81
|
}
|
|
122
82
|
catch {
|
|
123
83
|
return null;
|
|
124
84
|
}
|
|
125
85
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const r = await runCapture(BIN, ["login", "status"], { timeoutMs: 10_000 });
|
|
129
|
-
return r.code === 0;
|
|
130
|
-
}
|
|
131
|
-
catch {
|
|
132
|
-
return false;
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
function hasApiKey() {
|
|
136
|
-
return Boolean(codexApiKey());
|
|
86
|
+
export function redactCodexDoctorDetail(text) {
|
|
87
|
+
return redactSecrets(text).slice(0, 500);
|
|
137
88
|
}
|
|
138
|
-
function
|
|
139
|
-
|
|
140
|
-
|
|
89
|
+
export function codexNativeEnv(base, codexHome) {
|
|
90
|
+
return {
|
|
91
|
+
...(base ?? {}),
|
|
92
|
+
...providerScrubEnv(),
|
|
93
|
+
CODEX_HOME: codexHome ?? defaultNativeCodexHome(),
|
|
94
|
+
};
|
|
141
95
|
}
|
|
142
|
-
|
|
143
|
-
if (
|
|
144
|
-
return {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
cwd: dir,
|
|
151
|
-
env: {
|
|
152
|
-
HOME: dir,
|
|
153
|
-
XDG_CONFIG_HOME: join(dir, ".config"),
|
|
154
|
-
CODEX_HOME: codexHome,
|
|
155
|
-
OPENAI_API_KEY: null,
|
|
156
|
-
CODEX_API_KEY: null,
|
|
157
|
-
CLAUDEXOR_CODEX_API_KEY: null,
|
|
158
|
-
},
|
|
159
|
-
timeoutMs: 25_000,
|
|
160
|
-
});
|
|
161
|
-
const text = `${r.stdout}\n${r.stderr}`;
|
|
162
|
-
if (r.code === 0 && text.includes("\"turn.completed\"") && text.includes("OK")) {
|
|
163
|
-
return { ok: true, detail: "isolated CODEX_HOME smoke passed" };
|
|
164
|
-
}
|
|
165
|
-
return { ok: false, detail: redactCodexDoctorDetail(text || `codex exited with code ${r.code}`) };
|
|
166
|
-
}
|
|
167
|
-
catch (err) {
|
|
168
|
-
return { ok: false, detail: redactCodexDoctorDetail(err instanceof Error ? err.message : String(err)) };
|
|
96
|
+
function codexNativeReadiness(login) {
|
|
97
|
+
if (login.probeError) {
|
|
98
|
+
return {
|
|
99
|
+
source: "native_session",
|
|
100
|
+
availability: "unknown",
|
|
101
|
+
verification: "not_run",
|
|
102
|
+
detail: `login-status probe failed: ${redactCodexDoctorDetail(login.probeError)}`,
|
|
103
|
+
};
|
|
169
104
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
177
|
-
catch {
|
|
178
|
-
/* OS tmp reaper owns the leftovers */
|
|
179
|
-
}
|
|
105
|
+
if (login.method === "chatgpt") {
|
|
106
|
+
return {
|
|
107
|
+
source: "native_session",
|
|
108
|
+
availability: "available",
|
|
109
|
+
verification: "passed",
|
|
110
|
+
detail: "vendor status confirmed a native ChatGPT session in the exact run environment",
|
|
111
|
+
};
|
|
180
112
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
const out = [];
|
|
189
|
-
for (const a of attachments ?? []) {
|
|
190
|
-
if (a.kind === "image")
|
|
191
|
-
out.push("-i", a.path);
|
|
113
|
+
if (login.authed) {
|
|
114
|
+
return {
|
|
115
|
+
source: "native_session",
|
|
116
|
+
availability: "available",
|
|
117
|
+
verification: "failed",
|
|
118
|
+
detail: `Codex is authenticated via ${login.method}, not ChatGPT subscription auth`,
|
|
119
|
+
};
|
|
192
120
|
}
|
|
193
|
-
return
|
|
121
|
+
return {
|
|
122
|
+
source: "native_session",
|
|
123
|
+
availability: "unavailable",
|
|
124
|
+
verification: "not_run",
|
|
125
|
+
detail: "native Codex session is not logged in",
|
|
126
|
+
};
|
|
194
127
|
}
|
|
195
128
|
/**
|
|
196
129
|
* Inject the Playwright browser MCP as stateless `-c mcp_servers.browser.*`
|
|
@@ -204,11 +137,12 @@ function codexImageArgs(attachments) {
|
|
|
204
137
|
export function codexBrowserArgs(browser, externalContextPolicy) {
|
|
205
138
|
if (!browser || externalContextPolicy === "off")
|
|
206
139
|
return [];
|
|
140
|
+
const mcp = browserMcpCommand(browser);
|
|
207
141
|
return [
|
|
208
142
|
"-c",
|
|
209
|
-
`mcp_servers.browser.command=${JSON.stringify(
|
|
143
|
+
`mcp_servers.browser.command=${JSON.stringify(mcp.command)}`,
|
|
210
144
|
"-c",
|
|
211
|
-
`mcp_servers.browser.args=${JSON.stringify(
|
|
145
|
+
`mcp_servers.browser.args=${JSON.stringify(mcp.args)}`,
|
|
212
146
|
"-c",
|
|
213
147
|
"mcp_servers.browser.startup_timeout_sec=90",
|
|
214
148
|
"-c",
|
|
@@ -248,18 +182,26 @@ export function codexExecArgs(spec, opts = {}) {
|
|
|
248
182
|
// effort not tunable -> pass no flag.
|
|
249
183
|
const effort = normalizeEffort(spec.effort_hint, CODEX_EFFORT_LEVELS);
|
|
250
184
|
if (spec.resume_session_id) {
|
|
251
|
-
const args = [
|
|
252
|
-
|
|
185
|
+
const args = [
|
|
186
|
+
"exec",
|
|
187
|
+
"resume",
|
|
188
|
+
spec.resume_session_id,
|
|
189
|
+
"--json",
|
|
190
|
+
...CODEX_FILE_AUTH_ARGS,
|
|
191
|
+
...sandboxConfigArgs(spec.access),
|
|
192
|
+
"--skip-git-repo-check",
|
|
193
|
+
];
|
|
253
194
|
if (opts.outputSchemaPath)
|
|
254
195
|
args.push("--output-schema", opts.outputSchemaPath);
|
|
255
196
|
if (spec.model_hint)
|
|
256
197
|
args.push("-m", spec.model_hint);
|
|
257
198
|
if (effort)
|
|
258
199
|
args.push("-c", `model_reasoning_effort="${effort}"`);
|
|
200
|
+
if (spec.instructions && spec.instructions.trim())
|
|
201
|
+
args.push("-c", `developer_instructions=${tomlBasicString(spec.instructions)}`);
|
|
259
202
|
args.push(...codexWebArgs(spec.external_context_policy ?? "auto"));
|
|
260
203
|
// ALL `-c` config overrides go BEFORE `-i` so the variadic `-i/--image
|
|
261
|
-
// <FILE>...` can't swallow them as image paths
|
|
262
|
-
// positional prompt survives, then the prompt.
|
|
204
|
+
// <FILE>...` can't swallow them as image paths.
|
|
263
205
|
args.push(...codexBrowserArgs(spec.browser, spec.external_context_policy));
|
|
264
206
|
args.push(...nodeReplArgs);
|
|
265
207
|
const imageArgs = codexImageArgs(spec.attachments);
|
|
@@ -273,14 +215,16 @@ export function codexExecArgs(spec, opts = {}) {
|
|
|
273
215
|
args.push(spec.prompt);
|
|
274
216
|
return args;
|
|
275
217
|
}
|
|
276
|
-
const args = ["exec", "--json", ...
|
|
277
|
-
|
|
218
|
+
const args = ["exec", "--json", ...CODEX_FILE_AUTH_ARGS];
|
|
219
|
+
args.push(...sandboxArgs(spec.access), "--skip-git-repo-check");
|
|
278
220
|
if (opts.outputSchemaPath)
|
|
279
221
|
args.push("--output-schema", opts.outputSchemaPath);
|
|
280
222
|
if (spec.model_hint)
|
|
281
223
|
args.push("-m", spec.model_hint);
|
|
282
224
|
if (effort)
|
|
283
225
|
args.push("-c", `model_reasoning_effort="${effort}"`);
|
|
226
|
+
if (spec.instructions && spec.instructions.trim())
|
|
227
|
+
args.push("-c", `developer_instructions=${tomlBasicString(spec.instructions)}`);
|
|
284
228
|
args.push(...codexWebArgs(spec.external_context_policy ?? "auto"));
|
|
285
229
|
// ALL `-c` config overrides BEFORE `-i` (variadic) so they can't be eaten as
|
|
286
230
|
// image paths; then images, then `--`, then the prompt. See resume branch.
|
|
@@ -318,18 +262,29 @@ function codexWebArgs(policy) {
|
|
|
318
262
|
return ["-c", 'web_search="cached"'];
|
|
319
263
|
}
|
|
320
264
|
}
|
|
321
|
-
export function createCodexAdapter() {
|
|
265
|
+
export function createCodexAdapter(deps = {}) {
|
|
266
|
+
const runtime = {
|
|
267
|
+
detectVersion,
|
|
268
|
+
probeLogin,
|
|
269
|
+
hasApiKey,
|
|
270
|
+
codexApiKey,
|
|
271
|
+
resolveProfileSecret: (ref) => resolveSecret(ref),
|
|
272
|
+
smokeIsolatedApiKey,
|
|
273
|
+
runCliHarness,
|
|
274
|
+
...deps,
|
|
275
|
+
};
|
|
322
276
|
return {
|
|
323
277
|
id: "codex",
|
|
324
278
|
async discover() {
|
|
325
|
-
const version = await detectVersion();
|
|
279
|
+
const version = await runtime.detectVersion();
|
|
326
280
|
if (version === null) {
|
|
327
281
|
throw new HarnessUnavailableError("codex CLI not found on PATH (set CLAUDEXOR_CODEX_BIN to override)");
|
|
328
282
|
}
|
|
329
|
-
const apiKey = hasApiKey();
|
|
330
|
-
const
|
|
283
|
+
const apiKey = runtime.hasApiKey();
|
|
284
|
+
const login = await runtime.probeLogin(BIN, { env: codexNativeEnv() });
|
|
285
|
+
const nativeSessionAvailable = login.method === "chatgpt";
|
|
331
286
|
const authModes = [
|
|
332
|
-
...(
|
|
287
|
+
...(nativeSessionAvailable ? ["local_session"] : []),
|
|
333
288
|
...(apiKey ? ["api_key"] : []),
|
|
334
289
|
];
|
|
335
290
|
return HarnessManifestSchema.parse({
|
|
@@ -361,36 +316,48 @@ export function createCodexAdapter() {
|
|
|
361
316
|
// Current + still-API-available ids per the vendor Codex models page,
|
|
362
317
|
// verified against the installed CLI recorded below.
|
|
363
318
|
known_models: [
|
|
319
|
+
"gpt-5.6",
|
|
320
|
+
"gpt-5.6-sol",
|
|
321
|
+
"gpt-5.6-terra",
|
|
322
|
+
"gpt-5.6-luna",
|
|
364
323
|
"gpt-5.5",
|
|
365
324
|
"gpt-5.4",
|
|
366
325
|
"gpt-5.4-mini",
|
|
367
326
|
"gpt-5.3-codex-spark",
|
|
368
|
-
"gpt-5.3-codex",
|
|
369
|
-
"gpt-5.2",
|
|
370
327
|
],
|
|
371
|
-
known_models_verified_against: "0.
|
|
328
|
+
known_models_verified_against: "0.144.1",
|
|
372
329
|
},
|
|
373
330
|
capability_profile: {
|
|
374
331
|
auth: {
|
|
375
|
-
supported_sources: ["native_session", "
|
|
376
|
-
preferred_source:
|
|
332
|
+
supported_sources: ["native_session", "provider_auth_file"],
|
|
333
|
+
preferred_source: nativeSessionAvailable
|
|
334
|
+
? "native_session"
|
|
335
|
+
: apiKey
|
|
336
|
+
? "provider_auth_file"
|
|
337
|
+
: null,
|
|
377
338
|
credential_transports: [
|
|
378
339
|
{ source: "native_session", kind: "config_file", relocatable_by: ["CONFIG_DIR"] },
|
|
379
340
|
{ source: "provider_auth_file", kind: "config_file", relocatable_by: ["CONFIG_DIR"] },
|
|
380
|
-
{ source: "api_key_env", kind: "config_file", relocatable_by: ["CONFIG_DIR"] },
|
|
381
341
|
],
|
|
382
342
|
},
|
|
383
343
|
access_control: { readonly_mechanism: "fs_sandbox" },
|
|
384
|
-
isolation: { supported_containment: ["env_or_file_injection"] },
|
|
385
|
-
|
|
386
|
-
|
|
344
|
+
isolation: { supported_containment: ["host_user_context", "env_or_file_injection"] },
|
|
345
|
+
attachment_inputs: [
|
|
346
|
+
{
|
|
347
|
+
kind: "image",
|
|
348
|
+
mime_types: ["image/png", "image/jpeg", "image/gif", "image/webp"],
|
|
349
|
+
max_bytes: 20 * 1024 * 1024,
|
|
350
|
+
max_count: 20,
|
|
351
|
+
transport: "file_path",
|
|
352
|
+
},
|
|
353
|
+
],
|
|
387
354
|
},
|
|
388
355
|
auth_modes: authModes,
|
|
389
356
|
access_profiles_supported: ["readonly", "workspace_write", "full", "inherit_native"],
|
|
390
357
|
});
|
|
391
358
|
},
|
|
392
359
|
async doctor(_spec) {
|
|
393
|
-
const version = await detectVersion();
|
|
360
|
+
const version = await runtime.detectVersion(_spec.abortSignal);
|
|
394
361
|
if (version === null) {
|
|
395
362
|
return ConformanceReportSchema.parse({
|
|
396
363
|
harness_id: "codex",
|
|
@@ -399,145 +366,240 @@ export function createCodexAdapter() {
|
|
|
399
366
|
reasons: ["codex CLI not found (install Codex or set CLAUDEXOR_CODEX_BIN)"],
|
|
400
367
|
});
|
|
401
368
|
}
|
|
402
|
-
const
|
|
403
|
-
const
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
const
|
|
369
|
+
const requestedSource = _spec.authSource;
|
|
370
|
+
const probeNative = requestedSource === undefined || requestedSource === "native_session";
|
|
371
|
+
const probeApi = requestedSource === undefined || requestedSource === "provider_auth_file";
|
|
372
|
+
const login = probeNative
|
|
373
|
+
? await runtime.probeLogin(BIN, {
|
|
374
|
+
env: codexNativeEnv(_spec.env),
|
|
375
|
+
abortSignal: _spec.abortSignal,
|
|
376
|
+
})
|
|
377
|
+
: { authed: false, method: "logged_out", probeError: null };
|
|
378
|
+
const nativeSource = codexNativeReadiness(login);
|
|
379
|
+
const nativeReady = nativeSource.verification === "passed";
|
|
380
|
+
const apiKey = probeApi && runtime.hasApiKey();
|
|
381
|
+
const preference = requestedSource === "native_session"
|
|
382
|
+
? "subscription"
|
|
383
|
+
: requestedSource === "provider_auth_file"
|
|
384
|
+
? "api_key"
|
|
385
|
+
: (_spec.authPreference ?? "auto");
|
|
386
|
+
const shouldSmokeKey = probeApi && shouldVerifyApiKey({ preference, apiKeyAvailable: apiKey, nativeReady });
|
|
387
|
+
const smoke = shouldSmokeKey
|
|
388
|
+
? await runtime.smokeIsolatedApiKey(_spec.abortSignal)
|
|
389
|
+
: {
|
|
390
|
+
ok: false,
|
|
391
|
+
detail: apiKey
|
|
392
|
+
? "verification not run for the unselected API-key route"
|
|
393
|
+
: "no API key fallback available",
|
|
394
|
+
};
|
|
395
|
+
const ok = selectedAuthReady({ preference, nativeReady, apiKeyReady: smoke.ok });
|
|
396
|
+
const selectedAvailable = selectedAuthAvailable({
|
|
397
|
+
preference,
|
|
398
|
+
nativeAvailable: nativeSource.availability === "available",
|
|
399
|
+
apiKeyAvailable: apiKey,
|
|
400
|
+
});
|
|
401
|
+
const probeUnknown = preference !== "api_key" && nativeSource.availability === "unknown";
|
|
402
|
+
const allIntents = [
|
|
403
|
+
"plan",
|
|
404
|
+
"spec",
|
|
405
|
+
"implement",
|
|
406
|
+
"repair",
|
|
407
|
+
"create_from_scratch",
|
|
408
|
+
"review",
|
|
409
|
+
"verify",
|
|
410
|
+
"synthesize",
|
|
411
|
+
"explain",
|
|
412
|
+
"audit",
|
|
413
|
+
"orchestrate",
|
|
414
|
+
];
|
|
415
|
+
const binPath = resolveHarnessBinary(BIN);
|
|
416
|
+
const apiSource = {
|
|
417
|
+
source: "provider_auth_file",
|
|
418
|
+
availability: apiKey ? "available" : "unavailable",
|
|
419
|
+
verification: smoke.ok ? "passed" : shouldSmokeKey ? "failed" : "not_run",
|
|
420
|
+
detail: smoke.detail,
|
|
421
|
+
};
|
|
422
|
+
const authSources = requestedSource === "native_session"
|
|
423
|
+
? [nativeSource]
|
|
424
|
+
: requestedSource === "provider_auth_file"
|
|
425
|
+
? [apiSource]
|
|
426
|
+
: requestedSource !== undefined
|
|
427
|
+
? [
|
|
428
|
+
{
|
|
429
|
+
source: requestedSource,
|
|
430
|
+
availability: "unavailable",
|
|
431
|
+
verification: "not_run",
|
|
432
|
+
detail: `Codex does not support ${requestedSource}`,
|
|
433
|
+
},
|
|
434
|
+
]
|
|
435
|
+
: [nativeSource, apiSource];
|
|
412
436
|
return ConformanceReportSchema.parse({
|
|
413
437
|
harness_id: "codex",
|
|
414
|
-
status: ok ? "ok" :
|
|
438
|
+
status: ok ? "ok" : selectedAvailable || probeUnknown ? "degraded" : "unavailable",
|
|
415
439
|
checks: [
|
|
416
|
-
{
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
440
|
+
{
|
|
441
|
+
id: "installed",
|
|
442
|
+
status: "pass",
|
|
443
|
+
detail: binPath ? `${version} at ${binPath}` : version,
|
|
444
|
+
},
|
|
445
|
+
...(probeNative
|
|
446
|
+
? [
|
|
447
|
+
{
|
|
448
|
+
id: "native_session",
|
|
449
|
+
status: nativeReady ? "pass" : "fail",
|
|
450
|
+
detail: nativeReady
|
|
451
|
+
? "vendor status confirmed native ChatGPT auth in the exact run environment"
|
|
452
|
+
: login.probeError
|
|
453
|
+
? `login-status probe failed (NOT an auth verdict): ${redactCodexDoctorDetail(login.probeError)}`
|
|
454
|
+
: login.authed
|
|
455
|
+
? `logged in via ${login.method}, not ChatGPT subscription auth`
|
|
456
|
+
: "not logged in (run `codex login`)",
|
|
457
|
+
},
|
|
458
|
+
]
|
|
459
|
+
: []),
|
|
460
|
+
...(probeApi
|
|
461
|
+
? [
|
|
462
|
+
{
|
|
463
|
+
id: "stored_key",
|
|
464
|
+
status: apiKey ? "pass" : "fail",
|
|
465
|
+
detail: apiKey
|
|
466
|
+
? "openai secret/env available (API-key fallback)"
|
|
467
|
+
: "no openai key fallback",
|
|
468
|
+
},
|
|
469
|
+
{
|
|
470
|
+
id: "isolated_api_smoke",
|
|
471
|
+
status: smoke.ok ? "pass" : shouldSmokeKey ? "fail" : "skip",
|
|
472
|
+
detail: smoke.detail,
|
|
473
|
+
},
|
|
474
|
+
]
|
|
475
|
+
: []),
|
|
420
476
|
],
|
|
477
|
+
auth_sources: authSources,
|
|
421
478
|
enabled_intents: ok ? allIntents : [],
|
|
422
479
|
disabled_intents: ok ? [] : allIntents,
|
|
423
480
|
reasons: ok
|
|
424
481
|
? []
|
|
425
|
-
:
|
|
426
|
-
? [
|
|
427
|
-
|
|
482
|
+
: preference === "subscription"
|
|
483
|
+
? [
|
|
484
|
+
login.probeError
|
|
485
|
+
? `Codex native-session probe failed: ${redactCodexDoctorDetail(login.probeError)}`
|
|
486
|
+
: login.authed
|
|
487
|
+
? `Codex is authenticated via ${login.method}, not a ChatGPT subscription session`
|
|
488
|
+
: "Codex subscription route is not ready (run `codex login`)",
|
|
489
|
+
]
|
|
490
|
+
: preference === "api_key"
|
|
491
|
+
? [
|
|
492
|
+
apiKey
|
|
493
|
+
? `isolated Codex API-key smoke failed: ${smoke.detail}`
|
|
494
|
+
: "Codex API-key route is not configured",
|
|
495
|
+
]
|
|
496
|
+
: apiKey
|
|
497
|
+
? [`isolated Codex API-key smoke failed: ${smoke.detail}`]
|
|
498
|
+
: [
|
|
499
|
+
"not authenticated (run `codex login` for native/subscription use, or store an openai API key fallback)",
|
|
500
|
+
],
|
|
428
501
|
});
|
|
429
502
|
},
|
|
430
503
|
run(spec) {
|
|
431
|
-
return runCodex(spec);
|
|
504
|
+
return runCodex(spec, runtime);
|
|
432
505
|
},
|
|
433
506
|
review(spec) {
|
|
434
|
-
return runCodex(spec);
|
|
507
|
+
return runCodex(spec, runtime);
|
|
508
|
+
},
|
|
509
|
+
probeCredentialProfile(profile, abortSignal) {
|
|
510
|
+
return probeCodexCredentialProfile(profile, runtime, abortSignal);
|
|
435
511
|
},
|
|
436
512
|
};
|
|
437
513
|
}
|
|
438
|
-
async function* runCodex(spec) {
|
|
439
|
-
const
|
|
440
|
-
const
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
const
|
|
514
|
+
async function* runCodex(spec, runtime) {
|
|
515
|
+
const profile = spec.credential_profile;
|
|
516
|
+
const authPreference = spec.auth_preference ?? "auto";
|
|
517
|
+
let nativeEnv = codexNativeEnv(spec.env);
|
|
518
|
+
let key;
|
|
519
|
+
let tempCodexHome = null;
|
|
520
|
+
let authRoute;
|
|
521
|
+
if (profile) {
|
|
522
|
+
const resolved = await resolveCodexProfileRoute(profile, spec.env, runtime, abortSignalFromSpec(spec));
|
|
523
|
+
if (resolved.refusal !== null) {
|
|
524
|
+
yield { type: "error", session_id: spec.session_id, ts: nowIso(), error: resolved.refusal };
|
|
525
|
+
yield { type: "completed", session_id: spec.session_id, ts: nowIso() };
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
({ nativeEnv, tempCodexHome } = resolved);
|
|
529
|
+
key = resolved.key ?? undefined;
|
|
530
|
+
authRoute = resolved.route;
|
|
531
|
+
}
|
|
532
|
+
else {
|
|
533
|
+
const nativeLogin = authPreference === "api_key"
|
|
534
|
+
? { authed: false, method: "logged_out", probeError: null }
|
|
535
|
+
: await runtime.probeLogin(BIN, {
|
|
536
|
+
env: nativeEnv,
|
|
537
|
+
abortSignal: abortSignalFromSpec(spec),
|
|
538
|
+
});
|
|
539
|
+
const nativeSessionReady = nativeLogin.method === "chatgpt" && nativeLogin.probeError === null;
|
|
540
|
+
const trySubscription = () => nativeSessionReady;
|
|
541
|
+
const tryApiKey = () => {
|
|
542
|
+
key ??= runtime.codexApiKey();
|
|
458
543
|
if (!key)
|
|
459
544
|
return false;
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
if (
|
|
463
|
-
|
|
464
|
-
|
|
545
|
+
tempCodexHome = mkdtempSync(join(tmpdir(), "claudexor-codex-auth-"));
|
|
546
|
+
ensureCodexApiAuth({ CODEX_HOME: tempCodexHome });
|
|
547
|
+
if (codexAuthModeAt(tempCodexHome) === "api_key")
|
|
548
|
+
return true;
|
|
549
|
+
rmSync(tempCodexHome, { recursive: true, force: true });
|
|
550
|
+
tempCodexHome = null;
|
|
551
|
+
return false;
|
|
465
552
|
};
|
|
466
|
-
|
|
467
|
-
if (
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
};
|
|
553
|
+
authRoute = selectCodexRunAuthRoute(authPreference, trySubscription, tryApiKey);
|
|
554
|
+
if (authRoute === null) {
|
|
555
|
+
const error = authPreference === "subscription"
|
|
556
|
+
? "Codex subscription auth was explicitly requested but vendor status did not confirm a native ChatGPT session (run `codex login`)"
|
|
557
|
+
: authPreference === "api_key"
|
|
558
|
+
? "Codex API-key auth was explicitly requested but no usable OpenAI API key route is ready"
|
|
559
|
+
: "no usable Codex auth: native ChatGPT session is not ready and no OpenAI API key fallback is ready";
|
|
560
|
+
yield { type: "error", session_id: spec.session_id, ts: nowIso(), error };
|
|
474
561
|
yield { type: "completed", session_id: spec.session_id, ts: nowIso() };
|
|
475
562
|
return;
|
|
476
563
|
}
|
|
477
|
-
// An EXPLICIT auth preference that could not be honored is disclosed as a
|
|
478
|
-
// typed marker; the orchestrator lifts it into route.fallback.auth_switched.
|
|
479
|
-
const preferred = preferApi ? "api_key" : "subscription";
|
|
480
|
-
if (spec.auth_preference !== "auto" && authRoute && authRoute !== preferred) {
|
|
481
|
-
yield {
|
|
482
|
-
type: "message",
|
|
483
|
-
session_id: spec.session_id,
|
|
484
|
-
ts: nowIso(),
|
|
485
|
-
text: `[auth] ${preferred} route unavailable; fell back to ${authRoute}`,
|
|
486
|
-
payload: { auth_switched: true, from_auth_mode: preferred === "subscription" ? "local_session" : "api_key", to_auth_mode: authRoute === "subscription" ? "local_session" : "api_key" },
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
564
|
}
|
|
490
|
-
//
|
|
491
|
-
//
|
|
492
|
-
|
|
493
|
-
const env = {
|
|
494
|
-
...spec.env,
|
|
495
|
-
...providerScrubEnv(),
|
|
496
|
-
};
|
|
497
|
-
// Non-envelope run (no scoped CODEX_HOME): an explicit `api_key` preference is
|
|
498
|
-
// HONORED even when natively logged in (a private temp CODEX_HOME seeded with
|
|
499
|
-
// the key; codex ignores OPENAI_API_KEY without an auth.json) — and a missing
|
|
500
|
-
// key falls back to the native session with a typed disclosure. Without a
|
|
501
|
-
// preference, the key route is only the no-native fallback.
|
|
502
|
-
let tempCodexHome = null;
|
|
503
|
-
if (!scopedHome && key && (preferApi || !nativeAuthed)) {
|
|
504
|
-
tempCodexHome = mkdtempSync(join(tmpdir(), "claudexor-codex-auth-"));
|
|
505
|
-
env["CODEX_HOME"] = tempCodexHome;
|
|
506
|
-
ensureCodexApiAuth({ CODEX_HOME: tempCodexHome });
|
|
507
|
-
// Same disclosure as the envelope path: an explicit subscription preference
|
|
508
|
-
// that lands on the billed key route is never silent.
|
|
509
|
-
if (spec.auth_preference === "subscription") {
|
|
510
|
-
yield {
|
|
511
|
-
type: "message",
|
|
512
|
-
session_id: spec.session_id,
|
|
513
|
-
ts: nowIso(),
|
|
514
|
-
text: "[auth] subscription route unavailable (not logged in); fell back to api_key",
|
|
515
|
-
payload: { auth_switched: true, from_auth_mode: "local_session", to_auth_mode: "api_key" },
|
|
516
|
-
};
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
else if (!scopedHome && preferApi && !key && nativeAuthed) {
|
|
565
|
+
// Auto is subscription-first. Selecting its API-key fallback is a paid-route
|
|
566
|
+
// switch and must remain typed/visible; explicit routes never fall back.
|
|
567
|
+
if (!profile && authPreference === "auto" && authRoute === "api_key") {
|
|
520
568
|
yield {
|
|
521
569
|
type: "message",
|
|
522
570
|
session_id: spec.session_id,
|
|
523
571
|
ts: nowIso(),
|
|
524
|
-
text: "[auth]
|
|
525
|
-
payload: {
|
|
572
|
+
text: "[auth] native subscription route unavailable; auto selected api_key",
|
|
573
|
+
payload: {
|
|
574
|
+
auth_switched: true,
|
|
575
|
+
from_auth_mode: "local_session",
|
|
576
|
+
to_auth_mode: "api_key",
|
|
577
|
+
reason: "readiness_preferred",
|
|
578
|
+
},
|
|
526
579
|
};
|
|
527
580
|
}
|
|
581
|
+
// Native auth uses the vendor-owned CODEX_HOME (which may resolve credentials
|
|
582
|
+
// through a config file or OS keychain); API auth uses an isolated generated
|
|
583
|
+
// auth file. Neither route inherits provider env credentials or redirects.
|
|
584
|
+
const env = authRoute === "subscription" ? nativeEnv : { ...spec.env, ...providerScrubEnv() };
|
|
585
|
+
// Non-envelope API-key routes use a private CODEX_HOME because Codex ignores
|
|
586
|
+
// OPENAI_API_KEY against its normal auth store. `tryApiKey` created and
|
|
587
|
+
// verified this file before route selection returned api_key.
|
|
588
|
+
if (authRoute === "api_key" && tempCodexHome) {
|
|
589
|
+
env["CODEX_HOME"] = tempCodexHome;
|
|
590
|
+
}
|
|
528
591
|
// Disable Codex.app's headless-incompatible node_repl MCP, but ONLY when the
|
|
529
592
|
// config codex will actually load (the resolved CODEX_HOME, else ~/.codex)
|
|
530
593
|
// already defines it — never create a transport-less partial entry on a scoped
|
|
531
594
|
// home (that broke codex startup, the "invalid transport" regression).
|
|
532
|
-
// Structured output: codex takes a FILE path
|
|
533
|
-
//
|
|
534
|
-
//
|
|
535
|
-
// tmp dir instead — the capability must not silently vanish on that route.
|
|
595
|
+
// Structured output: codex takes a FILE path. API routes may use their
|
|
596
|
+
// isolated CODEX_HOME; native routes must never write helper files into the
|
|
597
|
+
// vendor-owned native home, so they use a private temp directory.
|
|
536
598
|
let outputSchemaPath = null;
|
|
537
599
|
let tempSchemaDir = null;
|
|
538
600
|
if (spec.output_schema !== undefined && spec.output_schema !== null) {
|
|
539
601
|
try {
|
|
540
|
-
let dir = env["CODEX_HOME"];
|
|
602
|
+
let dir = authRoute === "subscription" ? undefined : env["CODEX_HOME"];
|
|
541
603
|
if (!dir) {
|
|
542
604
|
tempSchemaDir = mkdtempSync(join(tmpdir(), "claudexor-codex-schema-"));
|
|
543
605
|
dir = tempSchemaDir;
|
|
@@ -545,11 +607,22 @@ async function* runCodex(spec) {
|
|
|
545
607
|
outputSchemaPath = join(dir, `claudexor-output-schema-${spec.session_id}.json`);
|
|
546
608
|
writeFileSync(outputSchemaPath, JSON.stringify(spec.output_schema));
|
|
547
609
|
}
|
|
548
|
-
catch {
|
|
549
|
-
|
|
610
|
+
catch (err) {
|
|
611
|
+
// FAIL-CLOSED (Quiz-6a): output_schema is a contract — running the
|
|
612
|
+
// child UNCONSTRAINED because a local schema file failed to write would
|
|
613
|
+
// silently drop it. Fail loudly; the caller retries or reroutes.
|
|
614
|
+
throw new Error(`codex output-schema file could not be written (${err instanceof Error ? err.message : String(err)}); refusing to run unconstrained`);
|
|
550
615
|
}
|
|
551
616
|
}
|
|
552
|
-
const args = codexExecArgs(spec, {
|
|
617
|
+
const args = codexExecArgs(spec, {
|
|
618
|
+
suppressNodeRepl: codexConfigHasNodeRepl(env["CODEX_HOME"]),
|
|
619
|
+
outputSchemaPath,
|
|
620
|
+
});
|
|
621
|
+
// Route evidence: the auth mode this child ACTUALLY runs under, read from
|
|
622
|
+
// the same auth.json codex loads (typed `auth_mode` field — chatgpt vs
|
|
623
|
+
// apikey). Disclosed on the started event; quota attribution consumes it.
|
|
624
|
+
const credentialRoute = authRoute === "subscription" ? "vendor_native" : "managed_api_key";
|
|
625
|
+
const credentialSource = authRoute === "subscription" ? "native_session" : "api_key_env";
|
|
553
626
|
// Codex reports tokens but no $cost; estimate it from the (hint/configured)
|
|
554
627
|
// model so the budget ledger does not see every codex run as free.
|
|
555
628
|
const model = spec.model_hint ?? process.env.CLAUDEXOR_CODEX_MODEL ?? null;
|
|
@@ -557,8 +630,9 @@ async function* runCodex(spec) {
|
|
|
557
630
|
// codex recorded in its own rollout transcript; cache that one read.
|
|
558
631
|
let codexThreadId;
|
|
559
632
|
let transcriptModel;
|
|
633
|
+
const parseState = {}; // per-run finality (see CodexParseState)
|
|
560
634
|
try {
|
|
561
|
-
yield* runCliHarness({
|
|
635
|
+
yield* runtime.runCliHarness({
|
|
562
636
|
bin: BIN,
|
|
563
637
|
args,
|
|
564
638
|
spec,
|
|
@@ -570,7 +644,7 @@ async function* runCodex(spec) {
|
|
|
570
644
|
const raw = obj;
|
|
571
645
|
if (raw?.type === "thread.started" && typeof raw.thread_id === "string")
|
|
572
646
|
codexThreadId = raw.thread_id;
|
|
573
|
-
const out = parseCodexEvent(obj, sessionId);
|
|
647
|
+
const out = parseCodexEvent(obj, sessionId, parseState);
|
|
574
648
|
if (out === null)
|
|
575
649
|
return null;
|
|
576
650
|
for (const ev of out) {
|
|
@@ -578,14 +652,24 @@ async function* runCodex(spec) {
|
|
|
578
652
|
// exists to catch silent fallback, so an unobserved model must stay
|
|
579
653
|
// unobserved. Record the requested model for diagnostics only.
|
|
580
654
|
if (ev.type === "started" && spec.model_hint && !ev.observed_model) {
|
|
581
|
-
ev.payload = {
|
|
655
|
+
ev.payload = {
|
|
656
|
+
...(ev.payload ?? {}),
|
|
657
|
+
requested_model: spec.model_hint,
|
|
658
|
+
observed_model_source: "unobserved",
|
|
659
|
+
};
|
|
582
660
|
}
|
|
661
|
+
// The route is fixed before spawn; attach it to every event so a
|
|
662
|
+
// later usage/quota record remains independently attributable.
|
|
663
|
+
ev.credential_route = credentialRoute;
|
|
664
|
+
ev.credential_source = credentialSource;
|
|
665
|
+
if (profile)
|
|
666
|
+
ev.credential_profile_id = profile.profile_id;
|
|
583
667
|
// codex's --json stream never carries the model, but the CLI
|
|
584
668
|
// records it in its own session rollout. Try to recover it as soon as
|
|
585
669
|
// the rollout's turn_context appears, then attach the transcript-sourced
|
|
586
670
|
// observation to the next normalized event. This keeps route proof from
|
|
587
671
|
// depending on reaching the final usage event under slow reviewer runs.
|
|
588
|
-
if (!ev.observed_model) {
|
|
672
|
+
if (!ev.observed_model && spec.evidence_policy !== "stream_only") {
|
|
589
673
|
transcriptModel ??= codexTranscriptModel(env["CODEX_HOME"], codexThreadId) ?? undefined;
|
|
590
674
|
if (transcriptModel) {
|
|
591
675
|
ev.observed_model = transcriptModel;
|
|
@@ -596,7 +680,10 @@ async function* runCodex(spec) {
|
|
|
596
680
|
// deletes on exit, so the native session it created is gone next turn.
|
|
597
681
|
// Strip its id from the event so it never poisons the thread resume map
|
|
598
682
|
// (a later `codex exec resume <ghost>` would deterministically fail).
|
|
599
|
-
if (ev.type === "started" &&
|
|
683
|
+
if (ev.type === "started" &&
|
|
684
|
+
tempCodexHome &&
|
|
685
|
+
ev.payload &&
|
|
686
|
+
"native_session_id" in ev.payload) {
|
|
600
687
|
const { native_session_id: _dropped, ...rest } = ev.payload;
|
|
601
688
|
ev.payload = { ...rest, resume_disabled: "ephemeral_codex_home" };
|
|
602
689
|
}
|
|
@@ -609,11 +696,17 @@ async function* runCodex(spec) {
|
|
|
609
696
|
}
|
|
610
697
|
// Quota headroom: attach codex's own rate-window record to the usage event
|
|
611
698
|
// (fresh read per usage — the rollout accretes as the turn ends).
|
|
612
|
-
if (ev.type === "usage" && !ev.quota) {
|
|
699
|
+
if (ev.type === "usage" && !ev.quota && spec.evidence_policy !== "stream_only") {
|
|
613
700
|
const rl = codexTranscriptRateLimits(env["CODEX_HOME"], codexThreadId);
|
|
614
701
|
if (rl)
|
|
615
702
|
ev.quota = rl;
|
|
616
703
|
}
|
|
704
|
+
if (ev.quota && profile && ev.quota.subject_id == null) {
|
|
705
|
+
ev.quota = { ...ev.quota, subject_id: profile.profile_id };
|
|
706
|
+
}
|
|
707
|
+
// A profiled run's quota windows belong to THE PROFILE's account
|
|
708
|
+
// (round-17 #2): the rollout record carries no subject, and an
|
|
709
|
+
// unstamped null would register as the engine-default subject.
|
|
617
710
|
}
|
|
618
711
|
return out;
|
|
619
712
|
},
|