@workbench-ai/agent-driver-openai-codex 0.0.44

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/index.js ADDED
@@ -0,0 +1,1634 @@
1
+ import { spawn } from "node:child_process";
2
+ import { promises as fs } from "node:fs";
3
+ import path from "node:path";
4
+ import readline from "node:readline";
5
+ import { createCliHarnessManifest, defineHarnessProvider, applyNormalizedHarnessActivity, buildManagedHarnessEnv, createHarnessSession, createPendingHarnessTurn, codexHarnessEffortValues, ensureDir, getManagedHarnessHomePath, HarnessTraceBuilder, nowIso, persistStageSessionWorkspace, prepareStageSessionWorkspace, runHarnessPrepareCommand, resolveHarnessConfiguredEffort, resolveHarnessConfiguredModel, resolveRuntimeEnv, terminateProcess, buildCanonicalToolCall, } from "@workbench-ai/agent-driver";
6
+ import { z } from "zod";
7
+ import { codexHarnessPackageVersion } from "./package-version.js";
8
+ import { projectCodexGlobalSkills, } from "./global-skills.js";
9
+ import { projectCodexIntegrations, } from "./integrations.js";
10
+ const CodexSecretRefAuthSchema = z
11
+ .object({
12
+ strategy: z.literal("secret_ref"),
13
+ ref: z.string().min(1),
14
+ })
15
+ .strict();
16
+ const CodexProfilePathAuthSchema = z
17
+ .object({
18
+ strategy: z.literal("profile_path"),
19
+ path: z.string().min(1),
20
+ })
21
+ .strict();
22
+ export const CodexHarnessAuthSchema = z.discriminatedUnion("strategy", [
23
+ CodexSecretRefAuthSchema,
24
+ CodexProfilePathAuthSchema,
25
+ ]);
26
+ const CodexAzureModelProviderSchema = z
27
+ .object({
28
+ id: z.literal("azure"),
29
+ base_url: z.string().trim().min(1),
30
+ query_params: z
31
+ .record(z.string().trim().min(1), z.string().trim().min(1))
32
+ .optional(),
33
+ })
34
+ .strict();
35
+ const CodexHarnessSandboxModeSchema = z.enum([
36
+ "read-only",
37
+ "workspace-write",
38
+ "danger-full-access",
39
+ ]);
40
+ export const CodexHarnessConfigSchema = z
41
+ .object({
42
+ sandbox_mode: CodexHarnessSandboxModeSchema.optional(),
43
+ model_provider: CodexAzureModelProviderSchema.optional(),
44
+ })
45
+ .strict();
46
+ function requireCanonicalAzureBaseUrl(baseUrl) {
47
+ const parsed = new URL(baseUrl.trim());
48
+ const trimmedPath = parsed.pathname.replace(/\/+$/u, "");
49
+ if (trimmedPath !== "/openai/v1") {
50
+ throw new Error("Codex Azure model_provider.base_url must be an absolute URL ending in /openai/v1.");
51
+ }
52
+ parsed.pathname = trimmedPath;
53
+ return parsed.toString().replace(/\/$/u, "");
54
+ }
55
+ export function createCodexHarnessDefinition(options = {}) {
56
+ return {
57
+ id: "openai/codex",
58
+ displayName: "OpenAI Codex",
59
+ auth: CodexHarnessAuthSchema,
60
+ config: CodexHarnessConfigSchema,
61
+ defaults: {
62
+ auth: {
63
+ strategy: "secret_ref",
64
+ ref: "OPENAI_API_KEY",
65
+ },
66
+ config: {},
67
+ },
68
+ capabilities: {
69
+ supports_resume: true,
70
+ supports_interrupt: true,
71
+ required_runtime_capabilities: ["shell_execution", "dotenv_secrets"],
72
+ },
73
+ supportedWorkspaceModes: ["managed", "project"],
74
+ async checkReadiness(args) {
75
+ await CodexHarnessAdapter.ensureAuthReady(args.plan, args.repoRoot, args.runtimeHome);
76
+ CodexHarnessAdapter.validateConfiguredEffort(args.plan);
77
+ return {
78
+ availability_errors: [],
79
+ };
80
+ },
81
+ create() {
82
+ return new CodexHarnessAdapter(options.executable?.trim() || "codex");
83
+ },
84
+ };
85
+ }
86
+ export const codexHarnessDefinition = createCodexHarnessDefinition();
87
+ export const codexHarnessManifest = createCliHarnessManifest(codexHarnessDefinition);
88
+ export const codexHarnessProvider = codexHarness();
89
+ export { listCodexIntegrations, projectCodexIntegrations, updateCodexIntegrations, } from "./integrations.js";
90
+ export { projectCodexGlobalSkills, syncCodexGlobalSkills, } from "./global-skills.js";
91
+ export { codexWorkbenchProviderAuth, } from "./workbench-auth.js";
92
+ export function codexHarness(options = {}) {
93
+ const definition = createCodexHarnessDefinition(options);
94
+ return defineHarnessProvider({
95
+ manifest: createCliHarnessManifest(definition),
96
+ schemas: {
97
+ auth: definition.auth,
98
+ config: definition.config,
99
+ },
100
+ checkReadiness: definition.checkReadiness,
101
+ create: definition.create,
102
+ });
103
+ }
104
+ const codexReplayMethods = new Set([
105
+ "turn/started",
106
+ "turn/completed",
107
+ "item/started",
108
+ "item/completed",
109
+ "item/agentMessage/delta",
110
+ "thread/tokenUsage/updated",
111
+ "error",
112
+ ]);
113
+ export const codexTraceReplayer = {
114
+ harnessId: codexHarnessManifest.id,
115
+ parseRawReplayEntries(entries) {
116
+ const replayEntries = parseCodexTraceReplayEntries(entries, (entry) => {
117
+ if (entry.source !== "notification" ||
118
+ typeof entry.at !== "string" ||
119
+ typeof entry.method !== "string" ||
120
+ !("payload" in entry)) {
121
+ return null;
122
+ }
123
+ return {
124
+ at: entry.at,
125
+ method: entry.method,
126
+ payload: entry.payload ?? {},
127
+ };
128
+ });
129
+ return replayEntries.length === 0 ? null : { entries: replayEntries };
130
+ },
131
+ parseHarnessReplayEntries(entries) {
132
+ const replayEntries = parseCodexTraceReplayEntries(entries, (entry) => {
133
+ if (typeof entry.at !== "string" ||
134
+ typeof entry.name !== "string" ||
135
+ entry.name.startsWith("claude/") ||
136
+ !("payload" in entry)) {
137
+ return null;
138
+ }
139
+ return {
140
+ at: entry.at,
141
+ method: entry.name,
142
+ payload: entry.payload ?? {},
143
+ };
144
+ });
145
+ return replayEntries.length === 0 ? null : { entries: replayEntries };
146
+ },
147
+ async buildTraceBundle(args) {
148
+ const trace = new HarnessTraceBuilder({
149
+ attemptNumber: args.artifact.attempt_number,
150
+ stageId: args.artifact.stage_id,
151
+ stageRunIndex: args.artifact.run_index,
152
+ stageSpanId: args.stageSpanId,
153
+ });
154
+ const promptAttributes = promptAttributesFromSpan(args.oldTurnSpan);
155
+ const fakeSession = buildSyntheticHarnessSession(args, codexHarnessManifest.id);
156
+ for (const entry of args.source.entries) {
157
+ if (!isJsonObject(entry.payload)) {
158
+ continue;
159
+ }
160
+ const normalized = normalizeCodexNotification(fakeSession, {
161
+ jsonrpc: "2.0",
162
+ method: entry.method,
163
+ params: entry.payload,
164
+ }, entry.at);
165
+ for (const activity of normalized.activities) {
166
+ if (activity.type === "turn.started" &&
167
+ Object.keys(promptAttributes).length > 0) {
168
+ applyNormalizedHarnessActivity(trace, {
169
+ ...activity,
170
+ attributes: {
171
+ ...(activity.attributes ?? {}),
172
+ ...promptAttributes,
173
+ },
174
+ });
175
+ continue;
176
+ }
177
+ applyNormalizedHarnessActivity(trace, activity);
178
+ }
179
+ }
180
+ return trace.buildBundle(await args.readFinalOutput(), args.endedAt);
181
+ },
182
+ };
183
+ const DEFAULT_CODEX_RPC_RESPONSE_TIMEOUT_MS = 60_000;
184
+ const azureSecretRefOnlyError = "Codex Azure model_provider currently supports only secret_ref auth.";
185
+ export class CodexHarnessAdapter {
186
+ executable;
187
+ manifest = codexHarnessManifest;
188
+ constructor(executable) {
189
+ this.executable = executable;
190
+ }
191
+ static getManagedCodexHomeDir(stageSessionPath) {
192
+ return path.join(getManagedHarnessHomePath(stageSessionPath), ".codex");
193
+ }
194
+ static getConfigPath(codexHomeDir) {
195
+ return path.join(codexHomeDir, "config.toml");
196
+ }
197
+ static getAuthPath(codexHomeDir) {
198
+ return path.join(codexHomeDir, "auth.json");
199
+ }
200
+ static getManagedWorkspaceIgnoreEntries(plan) {
201
+ void plan;
202
+ return [];
203
+ }
204
+ static classifyStderrForTrace(text) {
205
+ const lines = text
206
+ .split(/\r?\n/u)
207
+ .map((line) => line.trim())
208
+ .filter((line) => line.length > 0);
209
+ if (lines.length === 0) {
210
+ return "empty";
211
+ }
212
+ let sawWarning = false;
213
+ for (const line of lines) {
214
+ const severityMatch = line.match(/\b(ERROR|FATAL|PANIC|WARN(?:ING)?|INFO|DEBUG|TRACE)\b/i);
215
+ const severity = severityMatch?.[1]?.toUpperCase();
216
+ if (!severity) {
217
+ return "error";
218
+ }
219
+ if (severity === "ERROR" ||
220
+ severity === "FATAL" ||
221
+ severity === "PANIC") {
222
+ return "error";
223
+ }
224
+ sawWarning = true;
225
+ }
226
+ return sawWarning ? "warning" : "error";
227
+ }
228
+ static isNativeCaCertificateError(text) {
229
+ return /\bno native root CA certificates found\b/iu.test(text);
230
+ }
231
+ static nativeCaCertificateErrorMessage(original) {
232
+ const message = "Codex could not verify TLS certificates because the runtime image has no native root CA certificates. Install ca-certificates in environment/Dockerfile.";
233
+ const originalMessage = original.trim();
234
+ if (originalMessage.length === 0 ||
235
+ CodexHarnessAdapter.isNativeCaCertificateError(originalMessage)) {
236
+ return message;
237
+ }
238
+ return `${message} Original error: ${originalMessage}`;
239
+ }
240
+ static requireApiKey(plan, repoRoot, runtimeHome) {
241
+ return CodexHarnessAdapter.resolveApiKeyAuth(plan, repoRoot, runtimeHome)
242
+ .apiKey;
243
+ }
244
+ static resolveApiKeyAuth(plan, repoRoot, runtimeHome) {
245
+ const auth = CodexHarnessAdapter.getHarnessAuth(plan);
246
+ if (auth.strategy !== "secret_ref") {
247
+ throw new Error("Codex secret_ref auth is required for API key access.");
248
+ }
249
+ const resolved = resolveRuntimeEnv(auth.ref, repoRoot, { runtimeHome });
250
+ const apiKey = resolved.value?.trim();
251
+ if (!apiKey) {
252
+ const location = resolved.envPath ?? "the environment";
253
+ throw new Error(`${auth.ref} must be set in ${location} before running Codex sessions.`);
254
+ }
255
+ return {
256
+ secretEnvName: auth.ref,
257
+ apiKey,
258
+ };
259
+ }
260
+ static resolveProviderSelection(plan) {
261
+ const auth = CodexHarnessAdapter.getHarnessAuth(plan);
262
+ const configuredProvider = CodexHarnessAdapter.getHarnessConfig(plan).model_provider;
263
+ if (configuredProvider) {
264
+ if (auth.strategy !== "secret_ref") {
265
+ throw new Error(azureSecretRefOnlyError);
266
+ }
267
+ return {
268
+ auth,
269
+ provider: {
270
+ id: "azure",
271
+ baseUrl: requireCanonicalAzureBaseUrl(configuredProvider.base_url),
272
+ queryParams: { ...(configuredProvider.query_params ?? {}) },
273
+ },
274
+ };
275
+ }
276
+ return {
277
+ auth,
278
+ provider: {
279
+ id: "openai",
280
+ },
281
+ };
282
+ }
283
+ static resolveProfileAuth(plan, repoRoot) {
284
+ const auth = CodexHarnessAdapter.getHarnessAuth(plan);
285
+ if (auth.strategy !== "profile_path") {
286
+ throw new Error("Codex profile_path auth is required for profile auth.");
287
+ }
288
+ const sourceRoot = path.resolve(repoRoot, auth.path);
289
+ return { sourceRoot };
290
+ }
291
+ static async ensureAuthReady(plan, repoRoot, runtimeHome) {
292
+ const { auth } = CodexHarnessAdapter.resolveProviderSelection(plan);
293
+ if (auth.strategy === "secret_ref") {
294
+ CodexHarnessAdapter.resolveApiKeyAuth(plan, repoRoot, runtimeHome);
295
+ return;
296
+ }
297
+ const { sourceRoot } = CodexHarnessAdapter.resolveProfileAuth(plan, repoRoot);
298
+ const authPath = path.join(sourceRoot, ".codex", "auth.json");
299
+ await fs.access(authPath);
300
+ }
301
+ static validateConfiguredEffort(plan) {
302
+ const harness = CodexHarnessAdapter.getHarness(plan);
303
+ const effort = resolveHarnessConfiguredEffort(harness, codexHarnessEffortValues);
304
+ if (harness.effort && !effort) {
305
+ throw new Error(`Unsupported Codex effort "${harness.effort}". Expected one of ${codexHarnessEffortValues.join(", ")}.`);
306
+ }
307
+ }
308
+ static buildCodexEnv(paths, parentEnv = process.env, options = {}) {
309
+ const nestedStageHome = CodexHarnessAdapter.looksLikeNestedStageHome(paths.homeDir);
310
+ const env = buildManagedHarnessEnv(parentEnv, {
311
+ HOME: paths.homeDir,
312
+ CODEX_HOME: paths.codexHomeDir,
313
+ });
314
+ if (nestedStageHome && typeof env.PATH === "string") {
315
+ env.PATH = CodexHarnessAdapter.sanitizeNestedStagePath(env.PATH);
316
+ }
317
+ CodexHarnessAdapter.applyNestedDarwinProxyBypass(env, parentEnv, options.platform ?? process.platform, { nestedStageHome });
318
+ return env;
319
+ }
320
+ static applyNestedDarwinProxyBypass(env, parentEnv, platform = process.platform, options = {}) {
321
+ if (platform !== "darwin") {
322
+ return;
323
+ }
324
+ if (!options.nestedStageHome &&
325
+ !CodexHarnessAdapter.looksLikeNestedStageFlowHome(parentEnv.FLOW_HOME)) {
326
+ return;
327
+ }
328
+ if (CodexHarnessAdapter.hasExplicitProxyConfig(parentEnv) ||
329
+ CodexHarnessAdapter.hasExplicitProxyConfig(env)) {
330
+ return;
331
+ }
332
+ // Nested stage-session launches can inherit a Codex-managed shell context where
333
+ // macOS system proxy discovery fails during app-server bootstrap. An explicit
334
+ // env-level bypass keeps the child Codex process on direct network paths.
335
+ const disabledProxyUrl = "http://127.0.0.1:9";
336
+ env.HTTP_PROXY = disabledProxyUrl;
337
+ env.HTTPS_PROXY = disabledProxyUrl;
338
+ env.ALL_PROXY = disabledProxyUrl;
339
+ env.http_proxy = disabledProxyUrl;
340
+ env.https_proxy = disabledProxyUrl;
341
+ env.all_proxy = disabledProxyUrl;
342
+ env.NO_PROXY = "*";
343
+ env.no_proxy = "*";
344
+ }
345
+ static looksLikeNestedStageFlowHome(value) {
346
+ if (typeof value !== "string") {
347
+ return false;
348
+ }
349
+ return /[\\/]\.flow[\\/]executions[\\/][^\\/]+[\\/]stage-sessions[\\/][^\\/]+[\\/]home[\\/]\.flow$/u.test(value.trim());
350
+ }
351
+ static looksLikeNestedStageHome(value) {
352
+ if (typeof value !== "string") {
353
+ return false;
354
+ }
355
+ return /[\\/]\.flow[\\/]executions[\\/][^\\/]+[\\/]stage-sessions[\\/][^\\/]+[\\/]home$/u.test(value.trim());
356
+ }
357
+ static sanitizeNestedStagePath(pathValue) {
358
+ const seen = new Set();
359
+ return pathValue
360
+ .split(path.delimiter)
361
+ .map((entry) => entry.trim())
362
+ .filter((entry) => {
363
+ if (entry.length === 0) {
364
+ return false;
365
+ }
366
+ if (/[\\/]\.codex[\\/]tmp[\\/]arg0[\\/]/u.test(entry)) {
367
+ return false;
368
+ }
369
+ if (seen.has(entry)) {
370
+ return false;
371
+ }
372
+ seen.add(entry);
373
+ return true;
374
+ })
375
+ .join(path.delimiter);
376
+ }
377
+ static isFatalBootstrapStderr(text) {
378
+ return (/thread 'main'.*panicked at/iu.test(text) ||
379
+ /Attempted to create a NULL object/iu.test(text));
380
+ }
381
+ static resolveBootstrapAttemptLimit(homeDir, platform = process.platform) {
382
+ if (platform === "darwin" &&
383
+ CodexHarnessAdapter.looksLikeNestedStageHome(homeDir)) {
384
+ return 3;
385
+ }
386
+ return 1;
387
+ }
388
+ static shouldRetryBootstrapError(args) {
389
+ if (args.attempt >= args.maxAttempts) {
390
+ return false;
391
+ }
392
+ if ((args.platform ?? process.platform) !== "darwin" ||
393
+ !CodexHarnessAdapter.looksLikeNestedStageHome(args.homeDir)) {
394
+ return false;
395
+ }
396
+ return (/Timed out waiting for codex app-server response to initialize/iu.test(args.error.message) || CodexHarnessAdapter.isFatalBootstrapStderr(args.error.message));
397
+ }
398
+ static buildManagedAppServerLaunchSpec(args) {
399
+ if ((args.platform ?? process.platform) === "darwin" &&
400
+ CodexHarnessAdapter.looksLikeNestedStageHome(args.homeDir) &&
401
+ args.targetUserId != null &&
402
+ args.targetUserId >= 0) {
403
+ return {
404
+ command: "launchctl",
405
+ args: ["asuser", String(args.targetUserId), "sh", "-lc", args.command],
406
+ };
407
+ }
408
+ return {
409
+ command: "sh",
410
+ args: ["-lc", args.command],
411
+ };
412
+ }
413
+ static async resolveManagedLaunchUserId(workspacePath, homeDir) {
414
+ for (const targetPath of [workspacePath, homeDir]) {
415
+ try {
416
+ const stat = await fs.stat(targetPath);
417
+ if (typeof stat.uid === "number" &&
418
+ Number.isInteger(stat.uid) &&
419
+ stat.uid >= 0) {
420
+ return stat.uid;
421
+ }
422
+ }
423
+ catch {
424
+ // Fall back to the current process uid if filesystem ownership is unavailable.
425
+ }
426
+ }
427
+ const getuid = process.getuid;
428
+ if (typeof getuid === "function") {
429
+ return getuid.call(process);
430
+ }
431
+ return null;
432
+ }
433
+ static hasExplicitProxyConfig(env) {
434
+ return [
435
+ "HTTP_PROXY",
436
+ "HTTPS_PROXY",
437
+ "ALL_PROXY",
438
+ "NO_PROXY",
439
+ "http_proxy",
440
+ "https_proxy",
441
+ "all_proxy",
442
+ "no_proxy",
443
+ ].some((key) => typeof env[key] === "string" && env[key].trim().length > 0);
444
+ }
445
+ static resolveSandboxMode(plan) {
446
+ return (CodexHarnessAdapter.getHarnessConfig(plan).sandbox_mode ??
447
+ "workspace-write");
448
+ }
449
+ static async resolveTrustedProjectPaths(workspacePath) {
450
+ const paths = [
451
+ workspacePath,
452
+ ...CodexHarnessAdapter.deriveTrustedProjectPathAliases(workspacePath),
453
+ ];
454
+ const realpath = await fs.realpath(workspacePath).catch(() => null);
455
+ if (realpath) {
456
+ paths.push(realpath, ...CodexHarnessAdapter.deriveTrustedProjectPathAliases(realpath));
457
+ }
458
+ return [...new Set(paths.map((value) => value.trim()).filter(Boolean))];
459
+ }
460
+ static deriveTrustedProjectPathAliases(workspacePath) {
461
+ const trimmed = workspacePath.trim();
462
+ if (!trimmed) {
463
+ return [];
464
+ }
465
+ const aliases = [];
466
+ if (trimmed.startsWith("/var/") || trimmed.startsWith("/tmp/")) {
467
+ aliases.push(`/private${trimmed}`);
468
+ }
469
+ if (trimmed.startsWith("/private/var/") ||
470
+ trimmed.startsWith("/private/tmp/")) {
471
+ aliases.push(trimmed.slice("/private".length));
472
+ }
473
+ return aliases;
474
+ }
475
+ static async ensureHomeConfig(args) {
476
+ const selection = CodexHarnessAdapter.resolveProviderSelection(args.plan);
477
+ await ensureDir(args.codexHomeDir);
478
+ const uniqueTrustedPaths = [
479
+ ...new Set((args.trustedProjectPaths ?? [])
480
+ .map((value) => value.trim())
481
+ .filter(Boolean)),
482
+ ];
483
+ const lines = [
484
+ 'forced_login_method = "api"',
485
+ 'cli_auth_credentials_store = "file"',
486
+ "",
487
+ ];
488
+ if (selection.provider.id === "azure") {
489
+ const authRef = selection.auth.strategy === "secret_ref" ? selection.auth.ref : null;
490
+ if (!authRef) {
491
+ throw new Error(azureSecretRefOnlyError);
492
+ }
493
+ lines.push('model_provider = "azure"');
494
+ lines.push("");
495
+ lines.push("[model_providers.azure]");
496
+ lines.push('name = "Azure OpenAI"');
497
+ lines.push(`base_url = ${quoteTomlString(selection.provider.baseUrl)}`);
498
+ lines.push(`env_key = ${quoteTomlString(authRef)}`);
499
+ lines.push('wire_api = "responses"');
500
+ lines.push("");
501
+ const queryParams = Object.entries(selection.provider.queryParams).sort(([left], [right]) => left.localeCompare(right));
502
+ if (queryParams.length > 0) {
503
+ lines.push("[model_providers.azure.query_params]");
504
+ for (const [key, value] of queryParams) {
505
+ lines.push(`${quoteTomlString(key)} = ${quoteTomlString(value)}`);
506
+ }
507
+ lines.push("");
508
+ }
509
+ }
510
+ for (const projectPath of uniqueTrustedPaths) {
511
+ lines.push(`[projects.${quoteTomlString(projectPath)}]`);
512
+ lines.push('trust_level = "trusted"');
513
+ lines.push("");
514
+ }
515
+ await fs.writeFile(CodexHarnessAdapter.getConfigPath(args.codexHomeDir), lines.join("\n"), "utf8");
516
+ }
517
+ static getAppServerCommand(command) {
518
+ const trimmed = command.trim();
519
+ return /\bapp-server\b/u.test(trimmed) ? trimmed : `${trimmed} app-server`;
520
+ }
521
+ static getLoginCommand(command) {
522
+ const appServerCommand = CodexHarnessAdapter.getAppServerCommand(command);
523
+ const derived = appServerCommand.replace(/\bapp-server\b[\s\S]*$/u, "login --with-api-key");
524
+ return derived === appServerCommand
525
+ ? "codex login --with-api-key"
526
+ : derived;
527
+ }
528
+ static async hasSeededApiKeyAuth(apiKey, codexHomeDir) {
529
+ try {
530
+ const authJson = JSON.parse(await fs.readFile(CodexHarnessAdapter.getAuthPath(codexHomeDir), "utf8"));
531
+ return (authJson.auth_mode === "apikey" && authJson.OPENAI_API_KEY === apiKey);
532
+ }
533
+ catch {
534
+ return false;
535
+ }
536
+ }
537
+ static async ensureManagedHomeAuth(plan, codexHomeDir, childEnv, command, repoRoot, trustedProjectPaths = [], runtimeHome) {
538
+ const { auth, provider } = CodexHarnessAdapter.resolveProviderSelection(plan);
539
+ if (auth.strategy === "profile_path") {
540
+ const { sourceRoot } = CodexHarnessAdapter.resolveProfileAuth(plan, repoRoot);
541
+ await ensureDir(codexHomeDir);
542
+ await fs.copyFile(path.join(sourceRoot, ".codex", "auth.json"), CodexHarnessAdapter.getAuthPath(codexHomeDir));
543
+ await CodexHarnessAdapter.ensureHomeConfig({
544
+ plan,
545
+ codexHomeDir,
546
+ trustedProjectPaths,
547
+ });
548
+ return;
549
+ }
550
+ const apiKeyAuth = CodexHarnessAdapter.resolveApiKeyAuth(plan, repoRoot, runtimeHome);
551
+ if (provider.id === "azure") {
552
+ childEnv[apiKeyAuth.secretEnvName] = apiKeyAuth.apiKey;
553
+ }
554
+ await CodexHarnessAdapter.ensureHomeConfig({
555
+ plan,
556
+ codexHomeDir,
557
+ trustedProjectPaths,
558
+ });
559
+ if (provider.id === "azure") {
560
+ return;
561
+ }
562
+ if (await CodexHarnessAdapter.hasSeededApiKeyAuth(apiKeyAuth.apiKey, codexHomeDir)) {
563
+ return;
564
+ }
565
+ const loginCommand = CodexHarnessAdapter.getLoginCommand(command);
566
+ await new Promise((resolve, reject) => {
567
+ const child = spawn("sh", ["-lc", loginCommand], {
568
+ cwd: repoRoot,
569
+ env: childEnv,
570
+ stdio: "pipe",
571
+ });
572
+ let stdout = "";
573
+ let stderr = "";
574
+ child.stdout.setEncoding("utf8");
575
+ child.stderr.setEncoding("utf8");
576
+ child.stdout.on("data", (chunk) => {
577
+ stdout += chunk.toString();
578
+ });
579
+ child.stderr.on("data", (chunk) => {
580
+ stderr += chunk.toString();
581
+ });
582
+ child.on("error", (error) => {
583
+ reject(error);
584
+ });
585
+ child.on("exit", (code, signal) => {
586
+ if (code === 0) {
587
+ resolve();
588
+ return;
589
+ }
590
+ reject(new Error(`codex login failed with code ${code ?? "null"} signal ${signal ?? "null"}: ${(stderr || stdout).trim()}`));
591
+ });
592
+ child.stdin.end(apiKeyAuth.apiKey);
593
+ });
594
+ await CodexHarnessAdapter.ensureHomeConfig({
595
+ plan,
596
+ codexHomeDir,
597
+ trustedProjectPaths,
598
+ });
599
+ }
600
+ static async prepareManagedCodexHome(args) {
601
+ const managedHomeDir = getManagedHarnessHomePath(args.stageSessionPath);
602
+ const managedCodexHomeDir = CodexHarnessAdapter.getManagedCodexHomeDir(args.stageSessionPath);
603
+ const childEnv = CodexHarnessAdapter.buildCodexEnv({
604
+ homeDir: managedHomeDir,
605
+ codexHomeDir: managedCodexHomeDir,
606
+ }, args.parentEnv ?? process.env);
607
+ const trustedProjectPaths = await CodexHarnessAdapter.resolveTrustedProjectPaths(args.workspacePath);
608
+ await CodexHarnessAdapter.ensureManagedHomeAuth(args.plan, managedCodexHomeDir, childEnv, args.command, args.repoRoot, trustedProjectPaths, args.runtimeHome);
609
+ const sourceHomeDir = resolveAmbientHomeDir(args.parentEnv);
610
+ const sourceCodexHomeDir = resolveAmbientCodexHomeDir(args.parentEnv, sourceHomeDir);
611
+ if (sourceHomeDir && sourceCodexHomeDir) {
612
+ await projectCodexGlobalSkills({
613
+ sourceHomeDir,
614
+ targetHomeDir: managedHomeDir,
615
+ targetCodexHomeDir: managedCodexHomeDir,
616
+ });
617
+ await projectCodexIntegrations({
618
+ sourceCodexHomeDir,
619
+ targetCodexHomeDir: managedCodexHomeDir,
620
+ });
621
+ }
622
+ return {
623
+ managedHomeDir,
624
+ trustedProjectPaths,
625
+ childEnv,
626
+ };
627
+ }
628
+ getManagedWorkspaceIgnoreEntries(plan) {
629
+ return CodexHarnessAdapter.getManagedWorkspaceIgnoreEntries(plan);
630
+ }
631
+ async startSession(args) {
632
+ const harness = CodexHarnessAdapter.getHarness(args.plan);
633
+ const sandboxMode = CodexHarnessAdapter.resolveSandboxMode(args.plan);
634
+ const configuredModel = resolveHarnessConfiguredModel(harness);
635
+ const command = CodexHarnessAdapter.getAppServerCommand(this.executable);
636
+ const preparedWorkspace = await prepareStageSessionWorkspace({
637
+ workspaceMode: args.plan.workspace.mode,
638
+ workspacePath: args.workspacePath,
639
+ stageSessionPath: args.stageSessionPath,
640
+ excludedTopLevelEntries: [".agents", ".codex"],
641
+ });
642
+ const { workspacePath, attemptWorkspacePath, sessionWorkspacePath } = preparedWorkspace;
643
+ const { managedHomeDir, childEnv } = await CodexHarnessAdapter.prepareManagedCodexHome({
644
+ plan: args.plan,
645
+ workspacePath,
646
+ stageSessionPath: args.stageSessionPath,
647
+ repoRoot: args.repoRoot,
648
+ command: this.executable,
649
+ runtimeHome: args.runtimeHome,
650
+ parentEnv: process.env,
651
+ });
652
+ await runHarnessPrepareCommand({
653
+ plan: args.plan,
654
+ workspacePath,
655
+ stageSessionPath: args.stageSessionPath,
656
+ childEnv,
657
+ });
658
+ const launchUserId = await CodexHarnessAdapter.resolveManagedLaunchUserId(workspacePath, managedHomeDir);
659
+ const launchSpec = CodexHarnessAdapter.buildManagedAppServerLaunchSpec({
660
+ command,
661
+ homeDir: managedHomeDir,
662
+ targetUserId: launchUserId,
663
+ });
664
+ const maxBootstrapAttempts = CodexHarnessAdapter.resolveBootstrapAttemptLimit(managedHomeDir);
665
+ let lastBootstrapError = null;
666
+ for (let bootstrapAttempt = 1; bootstrapAttempt <= maxBootstrapAttempts; bootstrapAttempt += 1) {
667
+ const child = spawn(launchSpec.command, launchSpec.args, {
668
+ cwd: workspacePath,
669
+ env: childEnv,
670
+ stdio: "pipe",
671
+ });
672
+ child.stdout.setEncoding("utf8");
673
+ child.stderr.setEncoding("utf8");
674
+ const reader = readline.createInterface({
675
+ input: child.stdout,
676
+ crlfDelay: Infinity,
677
+ });
678
+ const session = createHarnessSession({
679
+ harnessId: this.manifest.id,
680
+ attemptNumber: args.attemptNumber,
681
+ stageId: args.stageId,
682
+ stageRunIndex: args.stageRunIndex,
683
+ harnessSession: args.sessionMode === "resume" ? (args.persistedSession ?? {}) : {},
684
+ });
685
+ const context = {
686
+ adapter: this,
687
+ ownerStageId: args.ownerStageId,
688
+ session,
689
+ state: {
690
+ attemptWorkspacePath,
691
+ sessionWorkspacePath,
692
+ childEnv,
693
+ process: child,
694
+ reader,
695
+ nextId: 1,
696
+ threadId: null,
697
+ turnId: null,
698
+ pendingResponses: new Map(),
699
+ pendingTurn: null,
700
+ preTurnStderrLines: [],
701
+ nativeCaCertificateError: null,
702
+ },
703
+ };
704
+ reader.on("line", (line) => {
705
+ this.handleLine(context, line);
706
+ });
707
+ child.stderr.on("data", (chunk) => {
708
+ this.handleStderr(context, chunk.toString());
709
+ });
710
+ child.on("error", (error) => {
711
+ this.rejectPendingResponses(context, this.withBootstrapStderr(context, error));
712
+ this.rejectPendingTurn(context, error);
713
+ });
714
+ child.on("exit", (code, signal) => {
715
+ const error = this.withBootstrapStderr(context, new Error(`codex app-server exited early with code ${code ?? "null"} signal ${signal ?? "null"}`));
716
+ this.rejectPendingResponses(context, error);
717
+ if (context.state.pendingTurn) {
718
+ this.rejectPendingTurn(context, error);
719
+ }
720
+ });
721
+ try {
722
+ await this.request(context, "initialize", {
723
+ clientInfo: {
724
+ name: "flow",
725
+ version: codexHarnessPackageVersion,
726
+ },
727
+ capabilities: {
728
+ experimentalApi: true,
729
+ },
730
+ });
731
+ this.notify(context, "initialized", undefined);
732
+ const persistedThreadId = typeof args.persistedSession?.thread_id === "string" &&
733
+ args.persistedSession.thread_id.trim().length > 0
734
+ ? args.persistedSession.thread_id
735
+ : null;
736
+ if (args.sessionMode === "resume" && persistedThreadId) {
737
+ const resumeResult = (await this.request(context, "thread/resume", {
738
+ threadId: persistedThreadId,
739
+ cwd: workspacePath,
740
+ approvalPolicy: "never",
741
+ sandbox: sandboxMode,
742
+ config: {},
743
+ ...(configuredModel ? { model: configuredModel } : {}),
744
+ }));
745
+ context.state.threadId = resumeResult.thread?.id ?? persistedThreadId;
746
+ }
747
+ else {
748
+ // Fresh stages still create a resumable lineage so a downstream `session: previous` stage can adopt it.
749
+ const threadResult = (await this.request(context, "thread/start", {
750
+ cwd: workspacePath,
751
+ approvalPolicy: "never",
752
+ sandbox: sandboxMode,
753
+ config: {},
754
+ ephemeral: false,
755
+ experimentalRawEvents: false,
756
+ persistExtendedHistory: true,
757
+ ...(configuredModel ? { model: configuredModel } : {}),
758
+ }));
759
+ context.state.threadId = threadResult.thread.id;
760
+ }
761
+ if (!context.state.threadId) {
762
+ throw new Error("codex session did not yield a thread id");
763
+ }
764
+ context.session.harness_session.thread_id = context.state.threadId;
765
+ return context;
766
+ }
767
+ catch (error) {
768
+ const bootstrapError = error instanceof Error ? error : new Error(String(error));
769
+ lastBootstrapError = bootstrapError;
770
+ await terminateProcess(child, 1_000, 1_000).catch(() => undefined);
771
+ if (!CodexHarnessAdapter.shouldRetryBootstrapError({
772
+ error: bootstrapError,
773
+ attempt: bootstrapAttempt,
774
+ maxAttempts: maxBootstrapAttempts,
775
+ homeDir: managedHomeDir,
776
+ })) {
777
+ throw bootstrapError;
778
+ }
779
+ await new Promise((resolve) => setTimeout(resolve, 250));
780
+ }
781
+ }
782
+ throw lastBootstrapError ?? new Error("codex bootstrap failed");
783
+ }
784
+ async startTurn(context, args) {
785
+ const harness = CodexHarnessAdapter.getHarness(args.plan);
786
+ const configuredModel = resolveHarnessConfiguredModel(harness);
787
+ const configuredEffort = resolveHarnessConfiguredEffort(harness, codexHarnessEffortValues);
788
+ const pendingTurn = createPendingHarnessTurn({
789
+ session: context.session,
790
+ eventsFile: args.eventsFile,
791
+ rawEventsFile: args.rawEventsFile,
792
+ stageSpanId: args.stageSpanId,
793
+ promptText: args.prompt,
794
+ turnTimeoutMs: harness.turn_timeout_ms,
795
+ stallTimeoutMs: harness.stall_timeout_ms,
796
+ onTimeout: (message) => {
797
+ this.rejectPendingTurn(context, new Error(message));
798
+ void this.closeSession(context);
799
+ },
800
+ livePersistence: args.livePersistence,
801
+ });
802
+ context.state.pendingTurn = pendingTurn;
803
+ context.state.nativeCaCertificateError = null;
804
+ pendingTurn.controller.record({
805
+ normalized: {
806
+ type: "turn.started",
807
+ at: nowIso(),
808
+ provider: codexHarnessManifest.id,
809
+ model: configuredModel ?? null,
810
+ sessionId: context.state.threadId,
811
+ },
812
+ });
813
+ try {
814
+ const response = (await this.request(context, "turn/start", {
815
+ threadId: context.state.threadId,
816
+ input: [
817
+ {
818
+ type: "text",
819
+ text: args.prompt,
820
+ text_elements: [],
821
+ },
822
+ ],
823
+ cwd: undefined,
824
+ approvalPolicy: "never",
825
+ sandboxPolicy: undefined,
826
+ collaborationMode: null,
827
+ ...(configuredModel ? { model: configuredModel } : {}),
828
+ ...(configuredEffort ? { effort: configuredEffort } : {}),
829
+ }));
830
+ context.state.turnId = response.turn.id;
831
+ context.session.harness_session.turn_id = response.turn.id;
832
+ }
833
+ catch (error) {
834
+ this.rejectPendingTurn(context, error instanceof Error ? error : new Error(String(error)));
835
+ throw error;
836
+ }
837
+ return await pendingTurn.result;
838
+ }
839
+ async interruptTurn(context) {
840
+ if (context.state.threadId && context.state.turnId) {
841
+ await this.request(context, "turn/interrupt", {
842
+ threadId: context.state.threadId,
843
+ turnId: context.state.turnId,
844
+ }).catch(() => undefined);
845
+ }
846
+ }
847
+ async closeSession(context, cancelConfig) {
848
+ const gracefulTimeoutMs = cancelConfig?.graceful_timeout_ms ?? 1_000;
849
+ const hardKillTimeoutMs = cancelConfig?.hard_kill_timeout_ms ?? 1_000;
850
+ await terminateProcess(context.state.process, gracefulTimeoutMs, hardKillTimeoutMs);
851
+ context.state.reader.close();
852
+ await persistStageSessionWorkspace({
853
+ sessionWorkspacePath: context.state.sessionWorkspacePath,
854
+ attemptWorkspacePath: context.state.attemptWorkspacePath,
855
+ excludedTopLevelEntries: [".agents", ".codex"],
856
+ });
857
+ }
858
+ static getHarness(plan) {
859
+ const harness = plan.harness;
860
+ if (!harness) {
861
+ throw new Error(`Expected ${codexHarnessManifest.id} harness, received no harness configuration`);
862
+ }
863
+ if (harness.id !== codexHarnessManifest.id) {
864
+ throw new Error(`Expected ${codexHarnessManifest.id} harness, received ${harness.id}`);
865
+ }
866
+ return harness;
867
+ }
868
+ static getHarnessAuth(plan) {
869
+ return CodexHarnessAuthSchema.parse(CodexHarnessAdapter.getHarness(plan).auth);
870
+ }
871
+ static getHarnessConfig(plan) {
872
+ return CodexHarnessConfigSchema.parse(CodexHarnessAdapter.getHarness(plan).config);
873
+ }
874
+ handleStderr(context, text) {
875
+ this.recordNativeCaCertificateError(context, text);
876
+ const pendingTurn = context.state.pendingTurn;
877
+ if (!pendingTurn) {
878
+ this.capturePreTurnStderr(context, text);
879
+ if (context.state.pendingResponses.size > 0 &&
880
+ CodexHarnessAdapter.isFatalBootstrapStderr(text)) {
881
+ const error = this.withBootstrapStderr(context, new Error("codex app-server emitted fatal bootstrap stderr"));
882
+ this.rejectPendingResponses(context, error);
883
+ void terminateProcess(context.state.process, 250, 250);
884
+ }
885
+ return;
886
+ }
887
+ const at = nowIso();
888
+ const severity = CodexHarnessAdapter.classifyStderrForTrace(text);
889
+ pendingTurn.controller.record({
890
+ rawEnvelope: {
891
+ at,
892
+ source: "stderr",
893
+ text,
894
+ },
895
+ harnessEvent: {
896
+ at,
897
+ attempt_number: context.session.attempt_number,
898
+ stage_id: context.session.stage_id,
899
+ stage_run_index: context.session.stage_run_index,
900
+ phase: severity === "error" ? "error" : "session",
901
+ name: "stderr",
902
+ payload: {
903
+ text,
904
+ severity,
905
+ },
906
+ },
907
+ normalized: severity === "error"
908
+ ? {
909
+ type: "error",
910
+ at,
911
+ message: text.trim() || "stderr",
912
+ attributes: {
913
+ stream: "stderr",
914
+ },
915
+ }
916
+ : null,
917
+ });
918
+ }
919
+ handleLine(context, line) {
920
+ if (!line.trim()) {
921
+ return;
922
+ }
923
+ let message;
924
+ try {
925
+ message = JSON.parse(line);
926
+ }
927
+ catch {
928
+ const error = this.withBootstrapStderr(context, new Error(`Failed to parse app-server output: ${line}`));
929
+ this.rejectPendingResponses(context, error);
930
+ this.rejectPendingTurn(context, error);
931
+ return;
932
+ }
933
+ if ("id" in message && ("result" in message || "error" in message)) {
934
+ const pending = context.state.pendingResponses.get(Number(message.id));
935
+ if (!pending) {
936
+ return;
937
+ }
938
+ context.state.pendingResponses.delete(Number(message.id));
939
+ if ("error" in message && message.error) {
940
+ pending.reject(new Error(message.error.message));
941
+ }
942
+ else {
943
+ pending.resolve(message.result);
944
+ }
945
+ return;
946
+ }
947
+ if ("id" in message && "method" in message && !("result" in message)) {
948
+ const pendingTurn = context.state.pendingTurn;
949
+ if (pendingTurn) {
950
+ const at = nowIso();
951
+ pendingTurn.controller.record({
952
+ rawEnvelope: {
953
+ at,
954
+ source: "interactive_request",
955
+ method: message.method,
956
+ payload: "params" in message ? (message.params ?? {}) : {},
957
+ },
958
+ harnessEvent: {
959
+ at,
960
+ attempt_number: context.session.attempt_number,
961
+ stage_id: context.session.stage_id,
962
+ stage_run_index: context.session.stage_run_index,
963
+ phase: "error",
964
+ name: "interactive_request",
965
+ payload: {
966
+ method: message.method,
967
+ },
968
+ },
969
+ normalized: {
970
+ type: "error",
971
+ at,
972
+ message: `Interactive server request received: ${message.method}`,
973
+ attributes: {
974
+ method: message.method,
975
+ },
976
+ },
977
+ });
978
+ }
979
+ this.rejectPendingTurn(context, new Error(`Interactive server request received: ${message.method}`));
980
+ this.rejectPendingResponses(context, new Error(`Interactive server request received: ${message.method}`));
981
+ void this.closeSession(context);
982
+ return;
983
+ }
984
+ if (!("method" in message)) {
985
+ return;
986
+ }
987
+ const pendingTurn = context.state.pendingTurn;
988
+ const notification = message;
989
+ if (!pendingTurn) {
990
+ return;
991
+ }
992
+ this.recordNativeCaCertificateError(context, JSON.stringify(notification.params ?? {}));
993
+ const at = nowIso();
994
+ context.session.last_event_at = at;
995
+ const { harnessEvent, activities } = normalizeCodexNotification(context.session, notification, at);
996
+ pendingTurn.controller.record({
997
+ rawEnvelope: {
998
+ at,
999
+ source: "notification",
1000
+ method: notification.method,
1001
+ payload: notification.params ?? {},
1002
+ },
1003
+ harnessEvent,
1004
+ normalized: activities,
1005
+ });
1006
+ if (notification.method !== "turn/completed") {
1007
+ return;
1008
+ }
1009
+ const turnStatus = notification.params.turn;
1010
+ if (turnStatus?.status === "failed") {
1011
+ this.rejectPendingTurn(context, new Error(turnStatus.error?.message ?? "turn failed"));
1012
+ return;
1013
+ }
1014
+ if (turnStatus?.status === "interrupted") {
1015
+ this.rejectPendingTurn(context, new Error("turn interrupted"));
1016
+ return;
1017
+ }
1018
+ const completedTurn = context.state.pendingTurn;
1019
+ if (!completedTurn) {
1020
+ return;
1021
+ }
1022
+ context.state.pendingTurn = null;
1023
+ completedTurn.resolve(completedTurn.controller.succeed({ endedAt: at }));
1024
+ }
1025
+ async request(context, method, params) {
1026
+ if (context.state.process.exitCode != null ||
1027
+ context.state.process.stdin.destroyed) {
1028
+ throw new Error(`codex app-server is not running while sending ${method}`);
1029
+ }
1030
+ const id = context.state.nextId++;
1031
+ const payload = {
1032
+ jsonrpc: "2.0",
1033
+ id,
1034
+ method,
1035
+ params,
1036
+ };
1037
+ const timeoutMs = this.getRpcResponseTimeoutMs();
1038
+ return await new Promise((resolve, reject) => {
1039
+ const timeout = setTimeout(() => {
1040
+ context.state.pendingResponses.delete(id);
1041
+ reject(this.withBootstrapStderr(context, new Error(`Timed out waiting for codex app-server response to ${method}`)));
1042
+ void terminateProcess(context.state.process, 1_000, 1_000);
1043
+ }, timeoutMs);
1044
+ context.state.pendingResponses.set(id, {
1045
+ resolve: (value) => {
1046
+ clearTimeout(timeout);
1047
+ resolve(value);
1048
+ },
1049
+ reject: (error) => {
1050
+ clearTimeout(timeout);
1051
+ reject(error);
1052
+ },
1053
+ });
1054
+ context.state.process.stdin.write(`${JSON.stringify(payload)}\n`, (error) => {
1055
+ if (!error) {
1056
+ return;
1057
+ }
1058
+ clearTimeout(timeout);
1059
+ context.state.pendingResponses.delete(id);
1060
+ reject(this.withBootstrapStderr(context, error));
1061
+ });
1062
+ });
1063
+ }
1064
+ notify(context, method, params) {
1065
+ const payload = {
1066
+ jsonrpc: "2.0",
1067
+ method,
1068
+ ...(params === undefined ? {} : { params }),
1069
+ };
1070
+ context.state.process.stdin.write(`${JSON.stringify(payload)}\n`);
1071
+ }
1072
+ rejectPendingTurn(context, error) {
1073
+ const pendingTurn = context.state.pendingTurn;
1074
+ if (!pendingTurn) {
1075
+ return;
1076
+ }
1077
+ context.state.pendingTurn = null;
1078
+ pendingTurn.controller.dispose();
1079
+ pendingTurn.reject(this.withNativeCaCertificateError(context, error));
1080
+ }
1081
+ recordNativeCaCertificateError(context, text) {
1082
+ if (CodexHarnessAdapter.isNativeCaCertificateError(text)) {
1083
+ context.state.nativeCaCertificateError = text.trim();
1084
+ }
1085
+ }
1086
+ withNativeCaCertificateError(context, error) {
1087
+ const combined = [
1088
+ error.message,
1089
+ context.state.nativeCaCertificateError ?? "",
1090
+ ].join("\n");
1091
+ if (!CodexHarnessAdapter.isNativeCaCertificateError(combined)) {
1092
+ return error;
1093
+ }
1094
+ return new Error(CodexHarnessAdapter.nativeCaCertificateErrorMessage(error.message));
1095
+ }
1096
+ rejectPendingResponses(context, error) {
1097
+ if (context.state.pendingResponses.size === 0) {
1098
+ return;
1099
+ }
1100
+ const pending = [...context.state.pendingResponses.values()];
1101
+ context.state.pendingResponses.clear();
1102
+ for (const response of pending) {
1103
+ response.reject(error);
1104
+ }
1105
+ }
1106
+ capturePreTurnStderr(context, text) {
1107
+ const lines = text
1108
+ .split(/\r?\n/u)
1109
+ .map((line) => line.trim())
1110
+ .filter(Boolean);
1111
+ if (lines.length === 0) {
1112
+ return;
1113
+ }
1114
+ context.state.preTurnStderrLines.push(...lines);
1115
+ if (context.state.preTurnStderrLines.length > 20) {
1116
+ context.state.preTurnStderrLines.splice(0, context.state.preTurnStderrLines.length - 20);
1117
+ }
1118
+ }
1119
+ withBootstrapStderr(context, error) {
1120
+ if (context.state.pendingTurn) {
1121
+ return error;
1122
+ }
1123
+ if (context.state.preTurnStderrLines.length === 0) {
1124
+ return error;
1125
+ }
1126
+ return new Error(`${error.message}. codex stderr: ${context.state.preTurnStderrLines.join(" | ")}`);
1127
+ }
1128
+ getRpcResponseTimeoutMs() {
1129
+ const configured = Number(process.env.FLOW_CODEX_RPC_RESPONSE_TIMEOUT_MS ?? "");
1130
+ if (Number.isFinite(configured) && configured > 0) {
1131
+ return configured;
1132
+ }
1133
+ return DEFAULT_CODEX_RPC_RESPONSE_TIMEOUT_MS;
1134
+ }
1135
+ }
1136
+ function quoteTomlString(value) {
1137
+ return JSON.stringify(value);
1138
+ }
1139
+ export function normalizeCodexNotification(session, notification, at = nowIso()) {
1140
+ const payload = (notification.params ?? {});
1141
+ const harnessEvent = normalizeCodexHarnessEvent(session, notification.method, payload, at);
1142
+ const activities = normalizeCodexActivities(notification, at);
1143
+ return {
1144
+ harnessEvent,
1145
+ activities,
1146
+ };
1147
+ }
1148
+ function normalizeCodexHarnessEvent(session, method, payload, at) {
1149
+ if (method === "thread/started" || method === "thread/status/changed") {
1150
+ return createCodexHarnessEvent(session, at, "session", method, payload);
1151
+ }
1152
+ if (method.startsWith("turn/")) {
1153
+ return createCodexHarnessEvent(session, at, "turn", method, payload);
1154
+ }
1155
+ if (method.startsWith("item/tool") || method.startsWith("item/mcpToolCall")) {
1156
+ return createCodexHarnessEvent(session, at, "tool", method, payload);
1157
+ }
1158
+ if (method.startsWith("item/")) {
1159
+ return createCodexHarnessEvent(session, at, "item", method, payload);
1160
+ }
1161
+ if (method === "thread/tokenUsage/updated") {
1162
+ return createCodexHarnessEvent(session, at, "usage", method, payload);
1163
+ }
1164
+ if (method === "error") {
1165
+ return createCodexHarnessEvent(session, at, "error", method, payload);
1166
+ }
1167
+ if (method.startsWith("codex/event/")) {
1168
+ const phase = payload.msg &&
1169
+ typeof payload.msg === "object" &&
1170
+ "type" in payload.msg &&
1171
+ payload.msg.type === "token_count"
1172
+ ? "usage"
1173
+ : "item";
1174
+ return createCodexHarnessEvent(session, at, phase, method, payload);
1175
+ }
1176
+ return null;
1177
+ }
1178
+ function createCodexHarnessEvent(session, at, phase, name, payload) {
1179
+ return {
1180
+ at,
1181
+ attempt_number: session.attempt_number,
1182
+ stage_id: session.stage_id,
1183
+ stage_run_index: session.stage_run_index,
1184
+ phase,
1185
+ name,
1186
+ payload,
1187
+ };
1188
+ }
1189
+ function normalizeCodexActivities(notification, at) {
1190
+ const payload = (notification.params ?? {});
1191
+ if (notification.method === "turn/started") {
1192
+ return [
1193
+ {
1194
+ type: "turn.started",
1195
+ at,
1196
+ provider: codexHarnessManifest.id,
1197
+ sessionId: readPayloadString(payload, [["threadId"], ["thread_id"]]),
1198
+ operationId: readPayloadString(payload, [
1199
+ ["turn", "id"],
1200
+ ["turnId"],
1201
+ ["turn_id"],
1202
+ ]),
1203
+ },
1204
+ ];
1205
+ }
1206
+ if (notification.method === "turn/completed") {
1207
+ return [
1208
+ {
1209
+ type: "turn.completed",
1210
+ at,
1211
+ provider: codexHarnessManifest.id,
1212
+ sessionId: readPayloadString(payload, [["threadId"], ["thread_id"]]),
1213
+ operationId: readPayloadString(payload, [
1214
+ ["turn", "id"],
1215
+ ["turnId"],
1216
+ ["turn_id"],
1217
+ ]),
1218
+ status: readPayloadString(payload, [["turn", "status"]]),
1219
+ errorMessage: readPayloadString(payload, [
1220
+ ["turn", "error", "message"],
1221
+ ["error", "message"],
1222
+ ]),
1223
+ },
1224
+ ];
1225
+ }
1226
+ if (notification.method === "item/started") {
1227
+ const itemType = readPayloadString(payload, [["item", "type"]]);
1228
+ if (itemType === "agentMessage") {
1229
+ return [
1230
+ {
1231
+ type: "assistant_output.started",
1232
+ at,
1233
+ phase: readPayloadString(payload, [["item", "phase"]]),
1234
+ itemId: readPayloadString(payload, [
1235
+ ["item", "id"],
1236
+ ["itemId"],
1237
+ ["item_id"],
1238
+ ]),
1239
+ },
1240
+ ];
1241
+ }
1242
+ if (isCodexToolItemType(itemType)) {
1243
+ const toolCall = readCodexToolCall(itemType, payload, false);
1244
+ return [
1245
+ {
1246
+ type: "tool.started",
1247
+ at,
1248
+ toolId: readToolId(payload),
1249
+ toolName: toolCall.toolName,
1250
+ attributes: toolCall.attributes,
1251
+ },
1252
+ ];
1253
+ }
1254
+ return [];
1255
+ }
1256
+ if (notification.method === "item/completed") {
1257
+ const itemType = readPayloadString(payload, [["item", "type"]]);
1258
+ if (itemType === "agentMessage") {
1259
+ return [
1260
+ {
1261
+ type: "assistant_output.completed",
1262
+ at,
1263
+ text: readPayloadString(payload, [["item", "text"]]) ?? "",
1264
+ phase: readPayloadString(payload, [["item", "phase"]]),
1265
+ itemId: readPayloadString(payload, [
1266
+ ["item", "id"],
1267
+ ["itemId"],
1268
+ ["item_id"],
1269
+ ]),
1270
+ },
1271
+ ];
1272
+ }
1273
+ if (isCodexToolItemType(itemType)) {
1274
+ const toolCall = readCodexToolCall(itemType, payload, true);
1275
+ return [
1276
+ {
1277
+ type: "tool.completed",
1278
+ at,
1279
+ toolId: readToolId(payload),
1280
+ toolName: toolCall.toolName,
1281
+ attributes: toolCall.attributes,
1282
+ },
1283
+ ];
1284
+ }
1285
+ if (itemType === "fileChange") {
1286
+ const note = readCodexFileChangeNote(payload);
1287
+ return note ? [{ type: "note", at, ...note }] : [];
1288
+ }
1289
+ return [];
1290
+ }
1291
+ if (notification.method === "item/agentMessage/delta") {
1292
+ return [
1293
+ {
1294
+ type: "assistant_output.delta",
1295
+ at,
1296
+ delta: readPayloadString(payload, [["delta"]]) ?? "",
1297
+ phase: readPayloadString(payload, [["item", "phase"]]),
1298
+ itemId: readPayloadString(payload, [
1299
+ ["item", "id"],
1300
+ ["itemId"],
1301
+ ["item_id"],
1302
+ ]),
1303
+ },
1304
+ ];
1305
+ }
1306
+ if (notification.method === "thread/tokenUsage/updated") {
1307
+ const usage = readUsageSnapshot(payload);
1308
+ const cachedInputTokens = usage.cached_input_tokens;
1309
+ const uncachedInputTokens = usage.input_tokens != null && cachedInputTokens != null
1310
+ ? Math.max(usage.input_tokens - cachedInputTokens, 0)
1311
+ : null;
1312
+ return [
1313
+ {
1314
+ type: "usage.updated",
1315
+ at,
1316
+ inputTokens: usage.input_tokens,
1317
+ outputTokens: usage.output_tokens,
1318
+ attributes: {
1319
+ ...(uncachedInputTokens != null ? { uncached_input_tokens: uncachedInputTokens } : {}),
1320
+ ...(cachedInputTokens != null ? { cached_input_tokens: cachedInputTokens } : {}),
1321
+ ...(cachedInputTokens != null ? { cache_read_input_tokens: cachedInputTokens } : {}),
1322
+ ...(usage.reasoning_output_tokens != null ? { reasoning_output_tokens: usage.reasoning_output_tokens } : {}),
1323
+ ...(usage.total_tokens != null ? { total_tokens: usage.total_tokens } : {}),
1324
+ },
1325
+ },
1326
+ ];
1327
+ }
1328
+ if (notification.method === "error") {
1329
+ return [
1330
+ {
1331
+ type: "error",
1332
+ at,
1333
+ message: readPayloadString(payload, [["error", "message"], ["message"]]) ??
1334
+ "Harness error",
1335
+ attributes: payload,
1336
+ },
1337
+ ];
1338
+ }
1339
+ return [];
1340
+ }
1341
+ function isCodexToolItemType(itemType) {
1342
+ return (itemType === "toolCall" ||
1343
+ itemType === "mcpToolCall" ||
1344
+ itemType === "commandExecution" ||
1345
+ itemType === "imageView" ||
1346
+ itemType === "webSearch");
1347
+ }
1348
+ function readCodexToolCall(itemType, payload, includeResultPreview) {
1349
+ const actionType = readPayloadString(payload, [
1350
+ ["item", "action", "type"],
1351
+ ["action", "type"],
1352
+ ]);
1353
+ const actionUrl = readPayloadString(payload, [
1354
+ ["item", "action", "url"],
1355
+ ["action", "url"],
1356
+ ]);
1357
+ const exitCode = readPayloadNumber(payload, [
1358
+ ["item", "exitCode"],
1359
+ ["exitCode"],
1360
+ ["item", "exit_code"],
1361
+ ["exit_code"],
1362
+ ]);
1363
+ const durationMs = readPayloadNumber(payload, [
1364
+ ["item", "durationMs"],
1365
+ ["durationMs"],
1366
+ ["item", "duration_ms"],
1367
+ ["duration_ms"],
1368
+ ]);
1369
+ const previewSource = includeResultPreview
1370
+ ? readPayloadString(payload, [
1371
+ ["item", "aggregatedOutput"],
1372
+ ["aggregatedOutput"],
1373
+ ]) ??
1374
+ readPayloadString(payload, [["item", "result"], ["result"]]) ??
1375
+ JSON.stringify(payload)
1376
+ : null;
1377
+ return buildCanonicalToolCall({
1378
+ canonicalToolName: itemType === "commandExecution"
1379
+ ? "shell"
1380
+ : itemType === "imageView"
1381
+ ? "image"
1382
+ : itemType === "webSearch"
1383
+ ? "web"
1384
+ : null,
1385
+ rawToolName: itemType === "commandExecution" ||
1386
+ itemType === "imageView" ||
1387
+ itemType === "webSearch"
1388
+ ? itemType
1389
+ : readToolName(payload),
1390
+ operation: actionType,
1391
+ command: readPayloadString(payload, [["item", "command"], ["command"]]),
1392
+ cwd: readPayloadString(payload, [["item", "cwd"], ["cwd"]]),
1393
+ query: readPayloadString(payload, [["item", "query"], ["query"]]),
1394
+ path: readPayloadString(payload, [["item", "path"], ["path"]]),
1395
+ url: actionUrl,
1396
+ resultPreview: previewSource ? truncateValue(previewSource, 160) : null,
1397
+ attributes: {
1398
+ ...(actionType ? { action_type: actionType } : {}),
1399
+ ...(actionUrl ? { action_url: actionUrl } : {}),
1400
+ ...(exitCode != null ? { exit_code: exitCode } : {}),
1401
+ ...(durationMs != null ? { duration_ms: durationMs } : {}),
1402
+ },
1403
+ });
1404
+ }
1405
+ function readCodexFileChangeNote(payload) {
1406
+ const rawChanges = readPayloadValue(payload, ["item", "changes"]);
1407
+ if (!Array.isArray(rawChanges) || rawChanges.length === 0) {
1408
+ return null;
1409
+ }
1410
+ const paths = [];
1411
+ const kinds = new Set();
1412
+ for (const entry of rawChanges) {
1413
+ if (!entry || Array.isArray(entry) || typeof entry !== "object") {
1414
+ continue;
1415
+ }
1416
+ const record = entry;
1417
+ const changePath = typeof record.path === "string" ? record.path : null;
1418
+ if (changePath) {
1419
+ paths.push(changePath);
1420
+ }
1421
+ const rawKind = record.kind;
1422
+ if (typeof rawKind === "string" && rawKind.length > 0) {
1423
+ kinds.add(rawKind);
1424
+ continue;
1425
+ }
1426
+ if (rawKind && !Array.isArray(rawKind) && typeof rawKind === "object") {
1427
+ const nestedKind = rawKind.type;
1428
+ if (typeof nestedKind === "string" && nestedKind.length > 0) {
1429
+ kinds.add(nestedKind);
1430
+ }
1431
+ }
1432
+ }
1433
+ const changeCount = paths.length || rawChanges.length;
1434
+ const filePreview = paths
1435
+ .slice(0, 2)
1436
+ .map((currentPath) => path.basename(currentPath))
1437
+ .join(", ");
1438
+ const remainingCount = Math.max(changeCount - Math.min(paths.length, 2), 0);
1439
+ const suffix = filePreview.length > 0
1440
+ ? `${filePreview}${remainingCount > 0 ? `, +${remainingCount} more` : ""}`
1441
+ : null;
1442
+ return {
1443
+ message: `${describeCodexFileChangeVerb(kinds)} ${changeCount} file${changeCount === 1 ? "" : "s"}${suffix ? `: ${suffix}` : ""}`,
1444
+ attributes: {
1445
+ change_count: changeCount,
1446
+ change_kind: kinds.size === 1 ? ([...kinds][0] ?? "mixed") : "mixed",
1447
+ paths: paths.slice(0, 10),
1448
+ },
1449
+ };
1450
+ }
1451
+ function describeCodexFileChangeVerb(kinds) {
1452
+ if (kinds.size === 1) {
1453
+ const kind = [...kinds][0];
1454
+ if (kind === "add") {
1455
+ return "Added";
1456
+ }
1457
+ if (kind === "delete") {
1458
+ return "Deleted";
1459
+ }
1460
+ if (kind === "modify" || kind === "update") {
1461
+ return "Updated";
1462
+ }
1463
+ }
1464
+ return "Changed";
1465
+ }
1466
+ function readPayloadString(payload, paths) {
1467
+ for (const currentPath of paths) {
1468
+ const current = readPayloadValue(payload, currentPath);
1469
+ if (typeof current === "string" && current.length > 0) {
1470
+ return current;
1471
+ }
1472
+ }
1473
+ return null;
1474
+ }
1475
+ function readPayloadNumber(payload, paths) {
1476
+ for (const currentPath of paths) {
1477
+ const current = readPayloadValue(payload, currentPath);
1478
+ if (typeof current === "number" && Number.isFinite(current)) {
1479
+ return current;
1480
+ }
1481
+ }
1482
+ return null;
1483
+ }
1484
+ function readPayloadValue(payload, currentPath) {
1485
+ let current = payload;
1486
+ for (const segment of currentPath) {
1487
+ if (!current || Array.isArray(current) || typeof current !== "object") {
1488
+ return undefined;
1489
+ }
1490
+ current = current[segment];
1491
+ }
1492
+ return current;
1493
+ }
1494
+ function readUsageSnapshot(payload) {
1495
+ return {
1496
+ input_tokens: readPayloadNumber(payload, [
1497
+ ["tokenUsage", "total", "inputTokens"],
1498
+ ["usage", "input_tokens"],
1499
+ ["msg", "info", "total_token_usage", "input_tokens"],
1500
+ ]),
1501
+ output_tokens: readPayloadNumber(payload, [
1502
+ ["tokenUsage", "total", "outputTokens"],
1503
+ ["usage", "output_tokens"],
1504
+ ["msg", "info", "total_token_usage", "output_tokens"],
1505
+ ]),
1506
+ cached_input_tokens: readPayloadNumber(payload, [
1507
+ ["tokenUsage", "total", "cachedInputTokens"],
1508
+ ["usage", "cached_input_tokens"],
1509
+ ["msg", "info", "total_token_usage", "cached_input_tokens"],
1510
+ ]),
1511
+ reasoning_output_tokens: readPayloadNumber(payload, [
1512
+ ["tokenUsage", "total", "reasoningOutputTokens"],
1513
+ ["usage", "reasoning_output_tokens"],
1514
+ ["msg", "info", "total_token_usage", "reasoning_output_tokens"],
1515
+ ]),
1516
+ total_tokens: readPayloadNumber(payload, [
1517
+ ["tokenUsage", "total", "totalTokens"],
1518
+ ["usage", "total_tokens"],
1519
+ ["msg", "info", "total_token_usage", "total_tokens"],
1520
+ ]),
1521
+ };
1522
+ }
1523
+ function readToolId(payload) {
1524
+ return readPayloadString(payload, [
1525
+ ["item", "id"],
1526
+ ["itemId"],
1527
+ ["item_id"],
1528
+ ["toolCall", "id"],
1529
+ ["tool_call", "id"],
1530
+ ["call", "id"],
1531
+ ]);
1532
+ }
1533
+ function readToolName(payload) {
1534
+ return readPayloadString(payload, [
1535
+ ["item", "name"],
1536
+ ["toolName"],
1537
+ ["tool_name"],
1538
+ ["tool", "name"],
1539
+ ["call", "name"],
1540
+ ]);
1541
+ }
1542
+ function truncateValue(input, maxLength) {
1543
+ if (input.length <= maxLength) {
1544
+ return input;
1545
+ }
1546
+ return `${input.slice(0, maxLength - 1)}…`;
1547
+ }
1548
+ function parseCodexTraceReplayEntries(entries, select) {
1549
+ const replayEntries = [];
1550
+ for (const [index, entry] of entries.entries()) {
1551
+ const selected = select(entry);
1552
+ if (!selected || !codexReplayMethods.has(selected.method)) {
1553
+ continue;
1554
+ }
1555
+ replayEntries.push({
1556
+ at: selected.at,
1557
+ method: selected.method,
1558
+ payload: selected.payload,
1559
+ originalIndex: index,
1560
+ });
1561
+ }
1562
+ replayEntries.sort(compareCodexTraceReplayEntries);
1563
+ return replayEntries.map(({ originalIndex: _originalIndex, ...entry }) => entry);
1564
+ }
1565
+ function compareCodexTraceReplayEntries(left, right) {
1566
+ const atCompare = left.at.localeCompare(right.at);
1567
+ if (atCompare !== 0) {
1568
+ return atCompare;
1569
+ }
1570
+ const phaseCompare = codexReplayPhaseRank(left.method) - codexReplayPhaseRank(right.method);
1571
+ if (phaseCompare !== 0) {
1572
+ return phaseCompare;
1573
+ }
1574
+ return left.originalIndex - right.originalIndex;
1575
+ }
1576
+ function codexReplayPhaseRank(method) {
1577
+ switch (method) {
1578
+ case "turn/started":
1579
+ return 0;
1580
+ case "item/started":
1581
+ return 1;
1582
+ case "item/agentMessage/delta":
1583
+ return 2;
1584
+ case "item/completed":
1585
+ return 3;
1586
+ case "thread/tokenUsage/updated":
1587
+ return 4;
1588
+ case "error":
1589
+ return 5;
1590
+ case "turn/completed":
1591
+ return 6;
1592
+ default:
1593
+ return 7;
1594
+ }
1595
+ }
1596
+ function promptAttributesFromSpan(span) {
1597
+ const attributes = {};
1598
+ if (!span?.attributes) {
1599
+ return attributes;
1600
+ }
1601
+ for (const key of ["prompt_text", "prompt_format", "prompt_source"]) {
1602
+ const value = span.attributes[key];
1603
+ if (value != null) {
1604
+ attributes[key] = value;
1605
+ }
1606
+ }
1607
+ return attributes;
1608
+ }
1609
+ function buildSyntheticHarnessSession(args, harnessId) {
1610
+ return {
1611
+ id: `session_reprocess_${args.artifact.attempt_number}_${args.artifact.stage_id}_${args.artifact.run_index}`,
1612
+ harness_id: harnessId,
1613
+ attempt_number: args.artifact.attempt_number,
1614
+ stage_id: args.artifact.stage_id,
1615
+ stage_run_index: args.artifact.run_index,
1616
+ harness_session: {},
1617
+ started_at: new Date(0).toISOString(),
1618
+ last_event_at: null,
1619
+ };
1620
+ }
1621
+ function resolveAmbientHomeDir(parentEnv) {
1622
+ const homeDir = parentEnv?.HOME?.trim() || process.env.HOME?.trim() || "";
1623
+ return homeDir ? path.resolve(homeDir) : null;
1624
+ }
1625
+ function resolveAmbientCodexHomeDir(parentEnv, homeDir) {
1626
+ const codexHomeDir = parentEnv?.CODEX_HOME?.trim() || process.env.CODEX_HOME?.trim() || "";
1627
+ if (codexHomeDir) {
1628
+ return path.resolve(codexHomeDir);
1629
+ }
1630
+ return homeDir ? path.join(homeDir, ".codex") : null;
1631
+ }
1632
+ function isJsonObject(value) {
1633
+ return Boolean(value) && !Array.isArray(value) && typeof value === "object";
1634
+ }