@ai-sdk/harness-codex 0.0.0-6b196531-20260710185421

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,899 @@
1
+ // src/codex-harness.ts
2
+ import { randomBytes } from "crypto";
3
+ import { readFile } from "fs/promises";
4
+ import path from "path";
5
+ import { fileURLToPath } from "url";
6
+ import {
7
+ commonTool,
8
+ HarnessCapabilityUnsupportedError,
9
+ harnessV1DiagnosticFromBridgeFrame
10
+ } from "@ai-sdk/harness";
11
+ import {
12
+ classifyDiskLog,
13
+ createBridgeErrorHandler,
14
+ createBridgeStartupError,
15
+ drainBridgeProcessStream,
16
+ forwardBridgeProcessStream,
17
+ markBridgeStarting,
18
+ resolveSandboxHomeDir,
19
+ SandboxChannel,
20
+ shellQuote,
21
+ waitForBridgeReady,
22
+ writeSkills as writeHarnessSkills
23
+ } from "@ai-sdk/harness/utils";
24
+ import { WebSocket } from "ws";
25
+ import { z as z2 } from "zod/v4";
26
+
27
+ // src/codex-auth.ts
28
+ import { getAiGatewayAuthFromEnv } from "@ai-sdk/harness/utils";
29
+ function resolveCodexEnv(auth, processEnv = process.env) {
30
+ if (auth?.openaiCompatible) {
31
+ return pickOpenAICompatible(auth.openaiCompatible, processEnv);
32
+ }
33
+ if (auth?.openai) {
34
+ return pickOpenAI({ explicit: auth.openai, processEnv });
35
+ }
36
+ const gatewayAuthFromEnv = getAiGatewayAuthFromEnv({
37
+ env: processEnv
38
+ });
39
+ if (auth?.gateway) {
40
+ return pickGateway({
41
+ explicit: auth.gateway,
42
+ gatewayAuthFromEnv
43
+ });
44
+ }
45
+ if (gatewayAuthFromEnv.apiKey) {
46
+ return pickGateway({
47
+ explicit: {},
48
+ gatewayAuthFromEnv
49
+ });
50
+ }
51
+ return pickOpenAI({ processEnv });
52
+ }
53
+ function pickOpenAICompatible(explicit, processEnv) {
54
+ const env = {};
55
+ const apiKey = explicit.apiKey ?? processEnv.OPENAI_API_KEY ?? processEnv.CODEX_API_KEY;
56
+ if (apiKey) env.CODEX_API_KEY = apiKey;
57
+ if (explicit.baseUrl) env.OPENAI_BASE_URL = explicit.baseUrl;
58
+ if (explicit.modelProviderName)
59
+ env.CODEX_MODEL_PROVIDER_NAME = explicit.modelProviderName;
60
+ if (explicit.queryParamsJson)
61
+ env.OPENAI_QUERY_PARAMS_JSON = explicit.queryParamsJson;
62
+ return env;
63
+ }
64
+ function pickOpenAI({
65
+ explicit,
66
+ processEnv
67
+ }) {
68
+ const env = {};
69
+ const apiKey = explicit?.apiKey ?? processEnv.OPENAI_API_KEY ?? processEnv.CODEX_API_KEY;
70
+ if (apiKey) env.CODEX_API_KEY = apiKey;
71
+ const baseUrl = explicit?.baseUrl ?? processEnv.OPENAI_BASE_URL;
72
+ if (baseUrl) env.OPENAI_BASE_URL = baseUrl;
73
+ const organization = explicit?.organization ?? processEnv.OPENAI_ORGANIZATION;
74
+ if (organization) env.OPENAI_ORGANIZATION = organization;
75
+ const project = explicit?.project ?? processEnv.OPENAI_PROJECT;
76
+ if (project) env.OPENAI_PROJECT = project;
77
+ return env;
78
+ }
79
+ function pickGateway({
80
+ explicit,
81
+ gatewayAuthFromEnv
82
+ }) {
83
+ const apiKey = explicit.apiKey ?? gatewayAuthFromEnv.apiKey;
84
+ const baseUrl = toCodexGatewayBaseUrl(
85
+ explicit.baseUrl ?? gatewayAuthFromEnv.baseUrl
86
+ );
87
+ const env = {};
88
+ if (apiKey) {
89
+ env.AI_GATEWAY_API_KEY = apiKey;
90
+ env.CODEX_API_KEY = apiKey;
91
+ }
92
+ env.AI_GATEWAY_BASE_URL = baseUrl;
93
+ return env;
94
+ }
95
+ function toCodexGatewayBaseUrl(baseUrl) {
96
+ const trimmed = baseUrl.replace(/\/+$/, "");
97
+ return trimmed.endsWith("/v1") ? trimmed : `${trimmed}/v1`;
98
+ }
99
+
100
+ // src/codex-bridge-protocol.ts
101
+ import {
102
+ harnessV1BridgeInboundCommandSchemas,
103
+ harnessV1BridgeOutboundMessageSchema,
104
+ harnessV1BridgeReadySchema,
105
+ harnessV1BridgeStartBaseSchema
106
+ } from "@ai-sdk/harness";
107
+ import { z } from "zod/v4";
108
+ var outboundMessageSchema = harnessV1BridgeOutboundMessageSchema;
109
+ var startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
110
+ reasoningEffort: z.enum(["low", "medium", "high"]).optional(),
111
+ webSearch: z.boolean().optional(),
112
+ // Resume signal. When supplied, the bridge calls
113
+ // `codex.resumeThread(resumeThreadId, …)` instead of starting a fresh thread.
114
+ // The host sources the id from lifecycle state `data` cached from a prior
115
+ // `agent.detach`.
116
+ resumeThreadId: z.string().optional()
117
+ });
118
+ var inboundMessageSchema = z.discriminatedUnion("type", [
119
+ startMessageSchema,
120
+ ...harnessV1BridgeInboundCommandSchemas
121
+ ]);
122
+
123
+ // src/bridge/cli-relay.ts
124
+ var CLI_SHIM_FILENAME = "harness-tool.mjs";
125
+
126
+ // src/version.ts
127
+ var VERSION = true ? "0.0.0-6b196531-20260710185421" : "0.0.0-test";
128
+
129
+ // src/codex-harness.ts
130
+ var DEFAULT_CODEX_MODEL = "gpt-5.3-codex";
131
+ var CODEX_CLIENT_APP = `ai-sdk/harness-codex/${VERSION}`;
132
+ var CODEX_BUILTIN_TOOLS = {
133
+ bash: commonTool("bash", {
134
+ nativeName: "shell",
135
+ toolUseKind: "bash",
136
+ description: "Execute a shell command",
137
+ inputSchema: z2.object({ command: z2.string() })
138
+ }),
139
+ webSearch: commonTool("webSearch", {
140
+ nativeName: "web_search",
141
+ toolUseKind: "readonly",
142
+ description: "Search the web",
143
+ inputSchema: z2.object({ query: z2.string() })
144
+ })
145
+ };
146
+ var BOOTSTRAP_DIR = "/tmp/harness/codex";
147
+ var codexBridgeCoordsSchema = z2.object({
148
+ port: z2.number(),
149
+ token: z2.string(),
150
+ lastSeenEventId: z2.number(),
151
+ sandboxId: z2.string().optional()
152
+ });
153
+ var codexResumeStateSchema = z2.object({
154
+ threadId: z2.string().optional(),
155
+ bridge: codexBridgeCoordsSchema.optional()
156
+ });
157
+ function createCodex(settings = {}) {
158
+ let cachedBootstrap;
159
+ return {
160
+ specificationVersion: "harness-v1",
161
+ harnessId: "codex",
162
+ builtinTools: CODEX_BUILTIN_TOOLS,
163
+ supportsBuiltinToolApprovals: false,
164
+ lifecycleStateSchema: codexResumeStateSchema,
165
+ getBootstrap: async () => {
166
+ if (cachedBootstrap != null) return cachedBootstrap;
167
+ const [pkg, lock, bridge, hostToolMcp] = await Promise.all([
168
+ readBridgeAsset("package.json"),
169
+ readBridgeAsset("pnpm-lock.yaml"),
170
+ readBridgeAsset("index.mjs"),
171
+ readBridgeAsset("host-tool-mcp.mjs")
172
+ ]);
173
+ cachedBootstrap = {
174
+ harnessId: "codex",
175
+ bootstrapDir: BOOTSTRAP_DIR,
176
+ files: [
177
+ { path: `${BOOTSTRAP_DIR}/package.json`, content: pkg },
178
+ { path: `${BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
179
+ { path: `${BOOTSTRAP_DIR}/bridge.mjs`, content: bridge },
180
+ {
181
+ path: `${BOOTSTRAP_DIR}/host-tool-mcp.mjs`,
182
+ content: hostToolMcp
183
+ }
184
+ ],
185
+ commands: [
186
+ { command: `mkdir -p ${BOOTSTRAP_DIR}` },
187
+ {
188
+ command: `pnpm --dir ${BOOTSTRAP_DIR} install --frozen-lockfile --store-dir ${BOOTSTRAP_DIR}/.pnpm-store`
189
+ }
190
+ ]
191
+ };
192
+ return cachedBootstrap;
193
+ },
194
+ doStart: async (startOpts) => {
195
+ if (startOpts.builtinToolFiltering != null) {
196
+ throw new HarnessCapabilityUnsupportedError({
197
+ message: "Harness 'codex' does not support built-in tool filtering controls.",
198
+ harnessId: "codex"
199
+ });
200
+ }
201
+ if (startOpts.permissionMode != null && startOpts.permissionMode !== "allow-all") {
202
+ throw new HarnessCapabilityUnsupportedError({
203
+ message: "Harness 'codex' does not support built-in tool approval requests; use permissionMode: 'allow-all'.",
204
+ harnessId: "codex"
205
+ });
206
+ }
207
+ const sandboxSession = startOpts.sandboxSession;
208
+ const session = sandboxSession.restricted();
209
+ const sandboxId = sandboxSession.id;
210
+ const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
211
+ const isResume = lifecycleState != null;
212
+ const isContinue = startOpts.continueFrom != null;
213
+ const resumeData = isResume && typeof lifecycleState?.data === "object" ? lifecycleState.data : void 0;
214
+ const resumeThreadId = resumeData?.threadId;
215
+ const resumeThreadIdString = typeof resumeThreadId === "string" && resumeThreadId.length > 0 ? resumeThreadId : void 0;
216
+ const coords = resumeData?.bridge;
217
+ const workDir = startOpts.sessionWorkDir;
218
+ const sessionDataDir = `${sandboxSession.defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`;
219
+ const bridgeStateDir = `${sessionDataDir}/bridge`;
220
+ const cliShimDir = `${sessionDataDir}/codex`;
221
+ const cliShimPath = `${cliShimDir}/${CLI_SHIM_FILENAME}`;
222
+ const timeoutMs = settings.startupTimeoutMs ?? 12e4;
223
+ const report = startOpts.observability?.report;
224
+ const onDiagnostic = report ? (frame) => report(
225
+ harnessV1DiagnosticFromBridgeFrame(frame, {
226
+ sessionId: startOpts.sessionId,
227
+ timestamp: Date.now()
228
+ })
229
+ ) : void 0;
230
+ const onBridgeError = createBridgeErrorHandler({
231
+ harnessId: "codex",
232
+ sessionId: startOpts.sessionId
233
+ });
234
+ if (coords) {
235
+ try {
236
+ const attachUrl = await sandboxSession.getPortUrl({
237
+ port: coords.port,
238
+ protocol: "ws"
239
+ }) + `?agent_bridge_token=${encodeURIComponent(coords.token)}`;
240
+ const attachChannel = new SandboxChannel({
241
+ connect: () => openWebSocket(attachUrl),
242
+ outboundSchema: outboundMessageSchema,
243
+ initialLastSeenEventId: coords.lastSeenEventId,
244
+ onDiagnostic,
245
+ onBridgeError
246
+ });
247
+ await attachChannel.open(isContinue ? { resume: true } : void 0);
248
+ return createSession({
249
+ sessionId: startOpts.sessionId,
250
+ channel: attachChannel,
251
+ cliShimPath,
252
+ // The live bridge was spawned by another process; no process handle.
253
+ proc: void 0,
254
+ model: settings.model ?? DEFAULT_CODEX_MODEL,
255
+ reasoningEffort: settings.reasoningEffort,
256
+ webSearch: settings.webSearch,
257
+ resumeThreadId: resumeThreadIdString,
258
+ isResume: true,
259
+ seedResumeThreadOnFirstPrompt: false,
260
+ rerunContinue: false,
261
+ bridgePort: coords.port,
262
+ bridgeToken: coords.token,
263
+ sandboxId,
264
+ debug: startOpts.observability?.debug,
265
+ permissionMode: startOpts.permissionMode
266
+ });
267
+ } catch {
268
+ }
269
+ }
270
+ let respawnStrategy = isResume ? "rerun" : void 0;
271
+ if (coords && isContinue) {
272
+ const logRaw = await Promise.resolve(
273
+ session.readTextFile({
274
+ path: `${bridgeStateDir}/event-log.ndjson`,
275
+ abortSignal: startOpts.abortSignal
276
+ })
277
+ ).catch(() => null);
278
+ if (await classifyDiskLog(logRaw) === "replay") {
279
+ respawnStrategy = "replay";
280
+ }
281
+ }
282
+ const port = resolveBridgePort(sandboxSession, settings.port);
283
+ const token = randomBytes(32).toString("hex");
284
+ const codexSkillSetup = startOpts.skills && startOpts.skills.length > 0 ? await writeCodexSkills({
285
+ sandbox: session,
286
+ skills: startOpts.skills,
287
+ abortSignal: startOpts.abortSignal
288
+ }) : void 0;
289
+ const env = {
290
+ ...resolveCodexEnv(settings.auth),
291
+ AI_SDK_HARNESS_CLIENT_APP: CODEX_CLIENT_APP,
292
+ BRIDGE_CHANNEL_TOKEN: token,
293
+ BRIDGE_WS_PORT: String(port),
294
+ ...codexSkillSetup ? {
295
+ HOME: codexSkillSetup.homeDir,
296
+ CODEX_HOME: codexSkillSetup.codexHomeDir
297
+ } : {},
298
+ ...respawnStrategy === "replay" ? { BRIDGE_REPLAY_FROM_DISK: "1" } : {}
299
+ };
300
+ if (respawnStrategy === void 0) {
301
+ await session.run({
302
+ command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)}`,
303
+ abortSignal: startOpts.abortSignal
304
+ });
305
+ }
306
+ await markBridgeStarting({
307
+ sandbox: session,
308
+ bridgeStateDir,
309
+ bridgeType: "codex",
310
+ abortSignal: startOpts.abortSignal
311
+ });
312
+ const proc = await session.spawn({
313
+ command: `node ${BOOTSTRAP_DIR}/bridge.mjs --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)} --bootstrap-dir ${shellQuote(BOOTSTRAP_DIR)} --cli-shim-dir ${shellQuote(cliShimDir)}`,
314
+ env,
315
+ abortSignal: startOpts.abortSignal
316
+ });
317
+ const stderrTail = [];
318
+ const bridgeStderrDone = forwardBridgeProcessStream({
319
+ stream: proc.stderr,
320
+ streamName: "stderr",
321
+ source: "codex",
322
+ collectTail: stderrTail
323
+ });
324
+ const { port: boundPort } = await waitForBridgeReady({
325
+ proc,
326
+ sandbox: session,
327
+ bridgeStateDir,
328
+ bridgeType: "codex",
329
+ timeoutMs,
330
+ abortSignal: startOpts.abortSignal,
331
+ createTimeoutError: ({ proc: proc2, stdoutTail }) => createBridgeStartupError({
332
+ message: "codex bridge did not become ready in time.",
333
+ proc: proc2,
334
+ stdoutTail,
335
+ stderrTail,
336
+ stderrDone: bridgeStderrDone
337
+ }),
338
+ createExitError: ({ proc: proc2, stdoutTail }) => createBridgeStartupError({
339
+ message: "codex bridge exited before becoming ready.",
340
+ proc: proc2,
341
+ stdoutTail,
342
+ stderrTail,
343
+ stderrDone: bridgeStderrDone
344
+ })
345
+ });
346
+ void drainBridgeProcessStream(proc.stdout);
347
+ const wsUrl = await sandboxSession.getPortUrl({
348
+ port: boundPort,
349
+ protocol: "ws"
350
+ }) + `?agent_bridge_token=${encodeURIComponent(token)}`;
351
+ const channel = new SandboxChannel({
352
+ connect: () => openWebSocket(wsUrl),
353
+ outboundSchema: outboundMessageSchema,
354
+ onDiagnostic,
355
+ onBridgeError,
356
+ // In replay mode the respawned bridge reloaded the finished turn from
357
+ // disk; seed the cursor and resume so it streams the tail (incl.
358
+ // `finish`).
359
+ ...respawnStrategy === "replay" ? { initialLastSeenEventId: coords?.lastSeenEventId ?? 0 } : {}
360
+ });
361
+ await channel.open(
362
+ respawnStrategy === "replay" ? { resume: true } : void 0
363
+ );
364
+ return createSession({
365
+ sessionId: startOpts.sessionId,
366
+ channel,
367
+ cliShimPath,
368
+ proc,
369
+ model: settings.model ?? DEFAULT_CODEX_MODEL,
370
+ reasoningEffort: settings.reasoningEffort,
371
+ webSearch: settings.webSearch,
372
+ resumeThreadId: resumeThreadIdString,
373
+ isResume: respawnStrategy !== void 0,
374
+ seedResumeThreadOnFirstPrompt: respawnStrategy !== void 0,
375
+ rerunContinue: respawnStrategy === "rerun",
376
+ bridgePort: boundPort,
377
+ bridgeToken: token,
378
+ sandboxId,
379
+ debug: startOpts.observability?.debug,
380
+ permissionMode: startOpts.permissionMode
381
+ });
382
+ }
383
+ };
384
+ }
385
+ function resolveBridgePort(sandboxSession, override) {
386
+ if (override !== void 0) return override;
387
+ if (sandboxSession.ports.length > 0) return sandboxSession.ports[0];
388
+ throw new HarnessCapabilityUnsupportedError({
389
+ harnessId: "codex",
390
+ message: "The codex harness needs a TCP port exposed by the sandbox. Create the sandbox with `ports: [<port>]` or pass `createCodex({ port })`."
391
+ });
392
+ }
393
+ async function readBridgeAsset(name) {
394
+ const candidates = [
395
+ new URL(`./bridge/${name}`, import.meta.url),
396
+ new URL(`../bridge/${name}`, import.meta.url)
397
+ ];
398
+ let lastErr;
399
+ for (const url of candidates) {
400
+ try {
401
+ return await readFile(fileURLToPath(url), "utf8");
402
+ } catch (err) {
403
+ const code = err.code;
404
+ if (code !== "ENOENT") throw err;
405
+ lastErr = err;
406
+ }
407
+ }
408
+ throw lastErr ?? new Error(`bridge asset not found: ${name}`);
409
+ }
410
+ async function writeCodexSkills({
411
+ sandbox,
412
+ skills,
413
+ abortSignal
414
+ }) {
415
+ const homeDir = await resolveSandboxHomeDir({ sandbox, abortSignal });
416
+ const codexHomeDir = path.posix.join(homeDir, ".codex");
417
+ await sandbox.run({
418
+ command: `mkdir -p ${shellQuote(codexHomeDir)}`,
419
+ abortSignal
420
+ });
421
+ const rootDir = path.posix.join(homeDir, ".agents", "skills");
422
+ await writeHarnessSkills({
423
+ sandbox,
424
+ rootDir,
425
+ skills,
426
+ abortSignal,
427
+ invalidSkillNameMessage: ({ name }) => `Invalid Codex skill name: ${name}`,
428
+ invalidSkillFilePathMessage: ({ skillName, filePath }) => `Invalid Codex skill file path for ${skillName}: ${filePath}`
429
+ });
430
+ return {
431
+ homeDir,
432
+ codexHomeDir
433
+ };
434
+ }
435
+ function openWebSocket(url) {
436
+ return new Promise((resolve, reject) => {
437
+ const ws = new WebSocket(url);
438
+ const onOpen = () => {
439
+ ws.off("error", onError);
440
+ resolve(ws);
441
+ };
442
+ const onError = (err) => {
443
+ ws.off("open", onOpen);
444
+ reject(err);
445
+ };
446
+ ws.once("open", onOpen);
447
+ ws.once("error", onError);
448
+ });
449
+ }
450
+ function createSession({
451
+ sessionId,
452
+ channel,
453
+ cliShimPath,
454
+ proc,
455
+ model,
456
+ reasoningEffort,
457
+ webSearch,
458
+ resumeThreadId,
459
+ isResume,
460
+ seedResumeThreadOnFirstPrompt,
461
+ rerunContinue,
462
+ bridgePort,
463
+ bridgeToken,
464
+ sandboxId,
465
+ debug,
466
+ permissionMode
467
+ }) {
468
+ let stopped = false;
469
+ let stopPromise;
470
+ let pendingResumeThreadId = seedResumeThreadOnFirstPrompt ? resumeThreadId : void 0;
471
+ let instructionsApplied = isResume;
472
+ let latestThreadId = resumeThreadId;
473
+ channel.on("bridge-thread", (msg) => {
474
+ latestThreadId = msg.threadId;
475
+ });
476
+ const wireTurn = (turnOpts) => {
477
+ let pendingResolve;
478
+ let pendingReject;
479
+ const done = new Promise((resolve, reject) => {
480
+ pendingResolve = resolve;
481
+ pendingReject = reject;
482
+ });
483
+ const unsubs = [];
484
+ const forward = (event) => {
485
+ try {
486
+ turnOpts.emit(event);
487
+ } catch {
488
+ }
489
+ };
490
+ const eventTypes = [
491
+ "stream-start",
492
+ "text-start",
493
+ "text-delta",
494
+ "text-end",
495
+ "reasoning-start",
496
+ "reasoning-delta",
497
+ "reasoning-end",
498
+ "tool-call",
499
+ "tool-approval-request",
500
+ "tool-result",
501
+ "file-change",
502
+ "finish-step",
503
+ "raw"
504
+ ];
505
+ let isSettled = false;
506
+ const settleSuccess = () => {
507
+ if (isSettled) return;
508
+ isSettled = true;
509
+ for (const u of unsubs) u();
510
+ pendingResolve();
511
+ };
512
+ const settleError = (err) => {
513
+ if (isSettled) return;
514
+ isSettled = true;
515
+ for (const u of unsubs) u();
516
+ pendingReject(err);
517
+ };
518
+ for (const type of eventTypes) {
519
+ unsubs.push(
520
+ channel.on(type, (msg) => {
521
+ forward(msg);
522
+ })
523
+ );
524
+ }
525
+ unsubs.push(
526
+ channel.on("finish", (msg) => {
527
+ forward(msg);
528
+ settleSuccess();
529
+ })
530
+ );
531
+ unsubs.push(
532
+ channel.on("error", (msg) => {
533
+ forward(msg);
534
+ settleError(msg.error);
535
+ })
536
+ );
537
+ const onClose = (_code, reason) => {
538
+ if (isSettled) return;
539
+ if (reason === "suspended") {
540
+ settleSuccess();
541
+ return;
542
+ }
543
+ settleError(new Error("codex bridge closed before the turn finished."));
544
+ };
545
+ channel.onClose(onClose);
546
+ const onAbort = () => {
547
+ if (isSettled) return;
548
+ try {
549
+ channel.send({ type: "abort" });
550
+ } catch {
551
+ }
552
+ settleError(
553
+ turnOpts.abortSignal?.reason ?? new DOMException("Aborted", "AbortError")
554
+ );
555
+ };
556
+ if (turnOpts.abortSignal) {
557
+ if (turnOpts.abortSignal.aborted) {
558
+ onAbort();
559
+ } else {
560
+ turnOpts.abortSignal.addEventListener("abort", onAbort, {
561
+ once: true
562
+ });
563
+ }
564
+ }
565
+ const control = {
566
+ submitToolResult: async (input) => {
567
+ channel.send({
568
+ type: "tool-result",
569
+ toolCallId: input.toolCallId,
570
+ output: input.output,
571
+ isError: input.isError
572
+ });
573
+ },
574
+ submitToolApproval: async (input) => {
575
+ channel.send({
576
+ type: "tool-approval-response",
577
+ approvalId: input.approvalId,
578
+ approved: input.approved,
579
+ reason: input.reason
580
+ });
581
+ },
582
+ submitUserMessage: async (text) => {
583
+ channel.send({ type: "user-message", text });
584
+ },
585
+ done
586
+ };
587
+ return {
588
+ control,
589
+ sendStart: (send) => {
590
+ const timer = setTimeout(() => {
591
+ if (isSettled) return;
592
+ try {
593
+ send();
594
+ } catch (err) {
595
+ settleError(err);
596
+ }
597
+ }, 0);
598
+ timer.unref?.();
599
+ }
600
+ };
601
+ };
602
+ return {
603
+ sessionId,
604
+ isResume,
605
+ modelId: model,
606
+ doPromptTurn: async (promptOpts) => {
607
+ const turn = wireTurn({
608
+ emit: promptOpts.emit,
609
+ abortSignal: promptOpts.abortSignal
610
+ });
611
+ const tools = (promptOpts.tools ?? []).map((t) => ({
612
+ name: t.name,
613
+ description: t.description,
614
+ inputSchema: t.inputSchema
615
+ }));
616
+ let promptText = extractUserText(promptOpts.prompt);
617
+ if (!instructionsApplied) {
618
+ const instructions = (promptOpts.instructions ? promptOpts.instructions + "\n\n" : "") + "Only respond with your `final` message once you have fully addressed the user request.";
619
+ promptText = frameInitialPromptGuidance({
620
+ instructions,
621
+ toolUsageBlock: tools.length > 0 ? composeToolUsageInstructions({
622
+ tools,
623
+ cliShimPath
624
+ }) : void 0,
625
+ userText: promptText
626
+ });
627
+ }
628
+ instructionsApplied = true;
629
+ const startMessage = {
630
+ type: "start",
631
+ prompt: promptText,
632
+ tools,
633
+ model,
634
+ reasoningEffort,
635
+ webSearch,
636
+ ...permissionMode ? { permissionMode } : {},
637
+ ...pendingResumeThreadId ? { resumeThreadId: pendingResumeThreadId } : {},
638
+ ...debug ? { debug } : {}
639
+ };
640
+ pendingResumeThreadId = void 0;
641
+ turn.sendStart(() => channel.send(startMessage));
642
+ return turn.control;
643
+ },
644
+ doContinueTurn: async (continueOpts) => {
645
+ const turn = wireTurn({
646
+ emit: continueOpts.emit,
647
+ abortSignal: continueOpts.abortSignal
648
+ });
649
+ if (rerunContinue) {
650
+ const threadId = pendingResumeThreadId ?? latestThreadId;
651
+ pendingResumeThreadId = void 0;
652
+ turn.sendStart(
653
+ () => channel.send({
654
+ type: "start",
655
+ /*
656
+ * A continuation nudge rather than an empty prompt: `resumeThreadId`
657
+ * rehydrates the prior thread, and this is the new user turn that
658
+ * drives it forward. Keeping it non-empty avoids handing the runtime
659
+ * an empty user message (and mirrors the claude-code adapter, where an
660
+ * empty text block trips the Anthropic API's `cache_control` rule).
661
+ */
662
+ prompt: "Continue.",
663
+ tools: (continueOpts.tools ?? []).map((t) => ({
664
+ name: t.name,
665
+ description: t.description,
666
+ inputSchema: t.inputSchema
667
+ })),
668
+ model,
669
+ reasoningEffort,
670
+ webSearch,
671
+ ...permissionMode ? { permissionMode } : {},
672
+ ...threadId ? { resumeThreadId: threadId } : {},
673
+ ...debug ? { debug } : {}
674
+ })
675
+ );
676
+ }
677
+ return turn.control;
678
+ },
679
+ doCompact: async () => {
680
+ throw new HarnessCapabilityUnsupportedError({
681
+ message: "Harness 'codex' does not support manual compaction; Codex auto-compacts its context internally.",
682
+ harnessId: "codex"
683
+ });
684
+ },
685
+ doDetach: async () => {
686
+ if (stopped) {
687
+ throw new Error(
688
+ `codex session ${sessionId} is already stopped; cannot detach.`
689
+ );
690
+ }
691
+ stopped = true;
692
+ const lastSeenEventId = await channel.suspend();
693
+ const payload = {
694
+ type: "resume-session",
695
+ harnessId: "codex",
696
+ specificationVersion: "harness-v1",
697
+ data: {
698
+ ...latestThreadId ? { threadId: latestThreadId } : {},
699
+ bridge: {
700
+ port: bridgePort,
701
+ token: bridgeToken,
702
+ lastSeenEventId,
703
+ sandboxId
704
+ }
705
+ }
706
+ };
707
+ return payload;
708
+ },
709
+ doDestroy: async () => {
710
+ if (stopped) return stopPromise;
711
+ stopped = true;
712
+ stopPromise = (async () => {
713
+ channel.beginClose();
714
+ try {
715
+ if (!channel.isClosed()) {
716
+ channel.send({ type: "shutdown" });
717
+ }
718
+ } catch {
719
+ }
720
+ let stopTimer;
721
+ try {
722
+ if (proc) {
723
+ await Promise.race([
724
+ proc.wait(),
725
+ new Promise((resolve) => {
726
+ stopTimer = setTimeout(resolve, 5e3);
727
+ stopTimer.unref?.();
728
+ })
729
+ ]);
730
+ }
731
+ } finally {
732
+ if (stopTimer) clearTimeout(stopTimer);
733
+ try {
734
+ await proc?.kill();
735
+ } catch {
736
+ }
737
+ channel.close();
738
+ }
739
+ })();
740
+ return stopPromise;
741
+ },
742
+ doStop: async () => {
743
+ if (stopped) {
744
+ throw new Error(
745
+ `codex session ${sessionId} is already stopped; cannot stop.`
746
+ );
747
+ }
748
+ stopped = true;
749
+ channel.beginClose();
750
+ const data = channel.isClosed() ? {} : await new Promise((resolve, reject) => {
751
+ const timer = setTimeout(() => {
752
+ unsub();
753
+ reject(
754
+ new Error(
755
+ `codex session ${sessionId} did not reply to detach within 5s.`
756
+ )
757
+ );
758
+ }, 5e3);
759
+ timer.unref?.();
760
+ const unsub = channel.on("bridge-detach", (msg) => {
761
+ clearTimeout(timer);
762
+ unsub();
763
+ resolve(msg.data);
764
+ });
765
+ try {
766
+ channel.send({ type: "detach" });
767
+ } catch (err) {
768
+ clearTimeout(timer);
769
+ unsub();
770
+ reject(err);
771
+ }
772
+ });
773
+ let stopTimer;
774
+ try {
775
+ if (proc) {
776
+ await Promise.race([
777
+ proc.wait(),
778
+ new Promise((resolve) => {
779
+ stopTimer = setTimeout(resolve, 5e3);
780
+ stopTimer.unref?.();
781
+ })
782
+ ]);
783
+ }
784
+ } finally {
785
+ if (stopTimer) clearTimeout(stopTimer);
786
+ try {
787
+ await proc?.kill();
788
+ } catch {
789
+ }
790
+ channel.close();
791
+ }
792
+ const payload = {
793
+ type: "resume-session",
794
+ harnessId: "codex",
795
+ specificationVersion: "harness-v1",
796
+ data: data ?? {}
797
+ };
798
+ return payload;
799
+ },
800
+ doSuspendTurn: async () => {
801
+ if (stopped) {
802
+ throw new Error(
803
+ `codex session ${sessionId} is stopped; cannot suspend.`
804
+ );
805
+ }
806
+ stopped = true;
807
+ await channel.interrupt();
808
+ const lastSeenEventId = await channel.suspend();
809
+ const payload = {
810
+ type: "continue-turn",
811
+ harnessId: "codex",
812
+ specificationVersion: "harness-v1",
813
+ data: {
814
+ ...latestThreadId ? { threadId: latestThreadId } : {},
815
+ bridge: {
816
+ port: bridgePort,
817
+ token: bridgeToken,
818
+ lastSeenEventId,
819
+ sandboxId
820
+ }
821
+ }
822
+ };
823
+ return payload;
824
+ }
825
+ };
826
+ }
827
+ function frameInitialPromptGuidance({
828
+ instructions,
829
+ toolUsageBlock,
830
+ userText
831
+ }) {
832
+ const blocks = [];
833
+ if (instructions) {
834
+ blocks.push(
835
+ `<session-instructions>
836
+ The block below is operating guidance from the system, not a message from the user \u2014 follow it, but do not mention it or attribute it to the user.
837
+
838
+ ${instructions}
839
+ </session-instructions>`
840
+ );
841
+ }
842
+ if (toolUsageBlock) blocks.push(toolUsageBlock);
843
+ if (blocks.length === 0) return userText;
844
+ return `${blocks.join("\n\n")}
845
+
846
+ <user-message>
847
+ ${userText}
848
+ </user-message>`;
849
+ }
850
+ function composeToolUsageInstructions({
851
+ tools,
852
+ cliShimPath
853
+ }) {
854
+ const lines = [
855
+ "<host-tool-instructions>",
856
+ "You have access to the following host-provided tools. To use one, run the following command via your built-in `bash` tool:",
857
+ "",
858
+ ` node ${cliShimPath} <toolName> '<jsonInput>'`,
859
+ "",
860
+ "The script prints the JSON result to stdout. Do not invent another way to call these tools \u2014 only this CLI invocation will work. Pass the JSON input as a single-quoted argument.",
861
+ "For every user request that depends on a host-provided tool, run a separate CLI invocation for each needed tool call in the current turn before answering. Do not reuse previous tool results, and do not say you used a host tool unless the command has completed in the current turn.",
862
+ ""
863
+ ];
864
+ for (const toolSpec of tools) {
865
+ lines.push(
866
+ `- **${toolSpec.name}**${toolSpec.description ? ": " + toolSpec.description : ""}`
867
+ );
868
+ lines.push(
869
+ ` - Input schema: \`${JSON.stringify(toolSpec.inputSchema ?? {})}\``
870
+ );
871
+ }
872
+ lines.push("</host-tool-instructions>");
873
+ return lines.join("\n");
874
+ }
875
+ function extractUserText(prompt) {
876
+ if (typeof prompt === "string") return prompt;
877
+ const { content } = prompt;
878
+ if (typeof content === "string") return content;
879
+ const parts = [];
880
+ for (const part of content) {
881
+ if (part.type !== "text") {
882
+ throw new HarnessCapabilityUnsupportedError({
883
+ harnessId: "codex",
884
+ message: `The codex harness does not yet support user message parts of type '${part.type}'. Pass a string or a user message whose content contains only text parts.`
885
+ });
886
+ }
887
+ parts.push(part.text);
888
+ }
889
+ return parts.join("\n\n");
890
+ }
891
+
892
+ // src/index.ts
893
+ var codex = createCodex();
894
+ export {
895
+ VERSION,
896
+ codex,
897
+ createCodex
898
+ };
899
+ //# sourceMappingURL=index.js.map