@ai-sdk/harness-codex 1.0.73 → 1.0.75

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 CHANGED
@@ -1,8 +1,6 @@
1
1
  // src/codex-harness.ts
2
2
  import { randomBytes } from "crypto";
3
- import { readFile } from "fs/promises";
4
3
  import path from "path";
5
- import { fileURLToPath } from "url";
6
4
  import {
7
5
  commonTool,
8
6
  HarnessCapabilityUnsupportedError,
@@ -15,17 +13,83 @@ import {
15
13
  drainBridgeProcessStream,
16
14
  forwardBridgeProcessStream,
17
15
  markBridgeStarting,
16
+ maskSandboxCredentials,
18
17
  resolveSandboxHomeDir,
19
18
  SandboxChannel,
20
19
  shellQuote,
20
+ warnCredentialBrokeringUnavailable,
21
21
  waitForBridgeReady,
22
22
  writeSkills as writeHarnessSkills
23
23
  } from "@ai-sdk/harness/utils";
24
24
  import { WebSocket } from "ws";
25
25
  import { z as z2 } from "zod/v4";
26
26
 
27
+ // src/codex-bootstrap.ts
28
+ import { readFile } from "fs/promises";
29
+ import { fileURLToPath } from "url";
30
+ var CODEX_BOOTSTRAP_DIR = ".harness-bootstrap/codex";
31
+ var cachedBootstrap;
32
+ async function getCodexBootstrap() {
33
+ if (cachedBootstrap != null) return cachedBootstrap;
34
+ const [pkg, lock, bridge] = await Promise.all([
35
+ readBridgeAsset("package.json"),
36
+ readBridgeAsset("pnpm-lock.yaml"),
37
+ readBridgeAsset("index.mjs")
38
+ ]);
39
+ cachedBootstrap = {
40
+ harnessId: "codex",
41
+ bootstrapDir: CODEX_BOOTSTRAP_DIR,
42
+ files: [
43
+ { path: `${CODEX_BOOTSTRAP_DIR}/package.json`, content: pkg },
44
+ { path: `${CODEX_BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
45
+ { path: `${CODEX_BOOTSTRAP_DIR}/bridge.mjs`, content: bridge }
46
+ ],
47
+ commands: [
48
+ {
49
+ command: "pnpm install --frozen-lockfile --store-dir .pnpm-store"
50
+ }
51
+ ]
52
+ };
53
+ return cachedBootstrap;
54
+ }
55
+ async function readBridgeAsset(name) {
56
+ const candidates = [
57
+ new URL(`./bridge/${name}`, import.meta.url),
58
+ new URL(`../bridge/${name}`, import.meta.url)
59
+ ];
60
+ let lastErr;
61
+ for (const url of candidates) {
62
+ try {
63
+ return await readFile(fileURLToPath(url), "utf8");
64
+ } catch (err) {
65
+ const code = err.code;
66
+ if (code !== "ENOENT") throw err;
67
+ lastErr = err;
68
+ }
69
+ }
70
+ throw lastErr ?? new Error(`bridge asset not found: ${name}`);
71
+ }
72
+
27
73
  // src/codex-auth.ts
28
- import { getAiGatewayAuthFromEnv } from "@ai-sdk/harness/utils";
74
+ import {
75
+ createCredentialRequestTransformation,
76
+ getAiGatewayAuthFromEnv
77
+ } from "@ai-sdk/harness/utils";
78
+ var DEFAULT_AI_GATEWAY_BASE_URL = "https://ai-gateway.vercel.sh/v1";
79
+ var DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
80
+ var CODEX_CREDENTIAL_ENVIRONMENT_VARIABLES = [
81
+ "AI_GATEWAY_API_KEY",
82
+ "CODEX_API_KEY"
83
+ ];
84
+ function createCodexRequestTransformations(env, auth) {
85
+ if (!env.CODEX_API_KEY) return [];
86
+ return [
87
+ createCredentialRequestTransformation({
88
+ baseUrl: env.OPENAI_BASE_URL ?? (auth === "ai-gateway" ? DEFAULT_AI_GATEWAY_BASE_URL : DEFAULT_OPENAI_BASE_URL),
89
+ headers: { Authorization: `Bearer ${env.CODEX_API_KEY}` }
90
+ })
91
+ ];
92
+ }
29
93
  function resolveCodexEnv(auth, processEnv = process.env) {
30
94
  const normalizedAuth = normalizeCodexAuthToLegacyAuth(auth);
31
95
  if (normalizedAuth?.openaiCompatible) {
@@ -51,6 +115,18 @@ function resolveCodexEnv(auth, processEnv = process.env) {
51
115
  }
52
116
  return pickOpenAI({ processEnv });
53
117
  }
118
+ function resolveCodexAuthenticationMode(auth, processEnv = process.env) {
119
+ if (typeof auth !== "string" && auth?.openaiCompatible) {
120
+ return "direct";
121
+ }
122
+ if (auth === "direct" || typeof auth !== "string" && auth?.openai) {
123
+ return "direct";
124
+ }
125
+ if (auth === "ai-gateway" || typeof auth !== "string" && auth?.gateway) {
126
+ return "ai-gateway";
127
+ }
128
+ return getAiGatewayAuthFromEnv({ env: processEnv }).apiKey ? "ai-gateway" : "direct";
129
+ }
54
130
  function normalizeCodexAuthToLegacyAuth(auth) {
55
131
  if (auth == null || auth === "auto") {
56
132
  return void 0;
@@ -110,6 +186,7 @@ function pickGateway({
110
186
  env.CODEX_API_KEY = apiKey;
111
187
  }
112
188
  env.AI_GATEWAY_BASE_URL = baseUrl;
189
+ env.OPENAI_BASE_URL = baseUrl;
113
190
  return env;
114
191
  }
115
192
  function toCodexGatewayBaseUrl(baseUrl) {
@@ -130,6 +207,7 @@ var startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
130
207
  instructions: z.string().optional(),
131
208
  reasoningEffort: z.enum(["low", "medium", "high"]).optional(),
132
209
  webSearch: z.boolean().optional(),
210
+ codexConfig: z.record(z.string(), z.unknown()).optional(),
133
211
  mcpServers: z.record(z.string(), z.unknown()).optional(),
134
212
  // Resume signal. When supplied, the bridge calls
135
213
  // `codex.resumeThread(resumeThreadId, …)` instead of starting a fresh thread.
@@ -146,7 +224,7 @@ var inboundMessageSchema = z.discriminatedUnion("type", [
146
224
  var CLI_SHIM_FILENAME = "harness-tool.mjs";
147
225
 
148
226
  // src/version.ts
149
- var VERSION = true ? "1.0.73" : "0.0.0-test";
227
+ var VERSION = true ? "1.0.75" : "0.0.0-test";
150
228
 
151
229
  // src/codex-harness.ts
152
230
  var DEFAULT_CODEX_MODEL = "gpt-5.5";
@@ -165,7 +243,6 @@ var CODEX_BUILTIN_TOOLS = {
165
243
  inputSchema: z2.object({ query: z2.string() })
166
244
  })
167
245
  };
168
- var BOOTSTRAP_DIR = ".harness-bootstrap/codex";
169
246
  var codexBridgeCoordsSchema = z2.object({
170
247
  port: z2.number(),
171
248
  token: z2.string(),
@@ -177,36 +254,13 @@ var codexResumeStateSchema = z2.object({
177
254
  bridge: codexBridgeCoordsSchema.optional()
178
255
  });
179
256
  function createCodex(settings = {}) {
180
- let cachedBootstrap;
181
257
  return {
182
258
  specificationVersion: "harness-v1",
183
259
  harnessId: "codex",
184
260
  builtinTools: CODEX_BUILTIN_TOOLS,
185
261
  supportsBuiltinToolApprovals: false,
186
262
  lifecycleStateSchema: codexResumeStateSchema,
187
- getBootstrap: async () => {
188
- if (cachedBootstrap != null) return cachedBootstrap;
189
- const [pkg, lock, bridge] = await Promise.all([
190
- readBridgeAsset("package.json"),
191
- readBridgeAsset("pnpm-lock.yaml"),
192
- readBridgeAsset("index.mjs")
193
- ]);
194
- cachedBootstrap = {
195
- harnessId: "codex",
196
- bootstrapDir: BOOTSTRAP_DIR,
197
- files: [
198
- { path: `${BOOTSTRAP_DIR}/package.json`, content: pkg },
199
- { path: `${BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
200
- { path: `${BOOTSTRAP_DIR}/bridge.mjs`, content: bridge }
201
- ],
202
- commands: [
203
- {
204
- command: "pnpm install --frozen-lockfile --store-dir .pnpm-store"
205
- }
206
- ]
207
- };
208
- return cachedBootstrap;
209
- },
263
+ getBootstrap: getCodexBootstrap,
210
264
  doStart: async (startOpts) => {
211
265
  if (startOpts.builtinToolFiltering != null) {
212
266
  throw new HarnessCapabilityUnsupportedError({
@@ -221,11 +275,34 @@ function createCodex(settings = {}) {
221
275
  });
222
276
  }
223
277
  const sandboxSession = startOpts.sandboxSession;
278
+ const authenticationMode = resolveCodexAuthenticationMode(settings.auth);
279
+ const resolvedAuthEnvironment = resolveCodexEnv(settings.auth);
280
+ let sandboxAuthEnvironment = resolvedAuthEnvironment;
281
+ if (sandboxSession.addRequestTransformations != null) {
282
+ const requestTransformations = createCodexRequestTransformations(
283
+ resolvedAuthEnvironment,
284
+ authenticationMode
285
+ );
286
+ if (requestTransformations.length > 0) {
287
+ await sandboxSession.addRequestTransformations(
288
+ requestTransformations
289
+ );
290
+ }
291
+ sandboxAuthEnvironment = maskSandboxCredentials({
292
+ environment: resolvedAuthEnvironment,
293
+ credentialEnvironmentVariables: CODEX_CREDENTIAL_ENVIRONMENT_VARIABLES
294
+ });
295
+ if (requestTransformations.length > 0 && authenticationMode === "direct" && resolvedAuthEnvironment.OPENAI_BASE_URL == null) {
296
+ sandboxAuthEnvironment.OPENAI_BASE_URL = DEFAULT_OPENAI_BASE_URL;
297
+ }
298
+ } else {
299
+ warnCredentialBrokeringUnavailable();
300
+ }
224
301
  const session = sandboxSession.restricted();
225
302
  const sandboxId = sandboxSession.id;
226
303
  const bootstrapDir = path.posix.resolve(
227
304
  sandboxSession.defaultWorkingDirectory,
228
- BOOTSTRAP_DIR
305
+ CODEX_BOOTSTRAP_DIR
229
306
  );
230
307
  const lifecycleState = startOpts.continueFrom ?? startOpts.resumeFrom;
231
308
  const isResume = lifecycleState != null;
@@ -253,12 +330,16 @@ function createCodex(settings = {}) {
253
330
  });
254
331
  if (coords) {
255
332
  try {
256
- const attachUrl = await sandboxSession.getPortUrl({
333
+ const endpoint2 = await sandboxSession.getPortEndpoint({
257
334
  port: coords.port,
258
335
  protocol: "ws"
259
- }) + `?agent_bridge_token=${encodeURIComponent(coords.token)}`;
336
+ });
337
+ const attachEndpoint = withBridgeToken({
338
+ endpoint: endpoint2,
339
+ token: coords.token
340
+ });
260
341
  const attachChannel = new SandboxChannel({
261
- connect: () => openWebSocket(attachUrl),
342
+ connect: () => openWebSocket(attachEndpoint),
262
343
  outboundSchema: outboundMessageSchema,
263
344
  initialLastSeenEventId: coords.lastSeenEventId,
264
345
  onDiagnostic,
@@ -274,6 +355,7 @@ function createCodex(settings = {}) {
274
355
  model: settings.model ?? DEFAULT_CODEX_MODEL,
275
356
  reasoningEffort: settings.reasoningEffort,
276
357
  webSearch: settings.webSearch,
358
+ codexConfig: settings.codexConfig,
277
359
  mcpServers: settings.mcpServers,
278
360
  resumeThreadId: resumeThreadIdString,
279
361
  isResume: true,
@@ -308,7 +390,7 @@ function createCodex(settings = {}) {
308
390
  abortSignal: startOpts.abortSignal
309
391
  }) : void 0;
310
392
  const env = {
311
- ...resolveCodexEnv(settings.auth),
393
+ ...sandboxAuthEnvironment,
312
394
  AI_SDK_HARNESS_CLIENT_APP: CODEX_CLIENT_APP,
313
395
  BRIDGE_CHANNEL_TOKEN: token,
314
396
  BRIDGE_WS_PORT: String(port),
@@ -365,12 +447,13 @@ function createCodex(settings = {}) {
365
447
  })
366
448
  });
367
449
  void drainBridgeProcessStream(proc.stdout);
368
- const wsUrl = await sandboxSession.getPortUrl({
450
+ const endpoint = await sandboxSession.getPortEndpoint({
369
451
  port: boundPort,
370
452
  protocol: "ws"
371
- }) + `?agent_bridge_token=${encodeURIComponent(token)}`;
453
+ });
454
+ const bridgeEndpoint = withBridgeToken({ endpoint, token });
372
455
  const channel = new SandboxChannel({
373
- connect: () => openWebSocket(wsUrl),
456
+ connect: () => openWebSocket(bridgeEndpoint),
374
457
  outboundSchema: outboundMessageSchema,
375
458
  onDiagnostic,
376
459
  onBridgeError,
@@ -390,6 +473,7 @@ function createCodex(settings = {}) {
390
473
  model: settings.model ?? DEFAULT_CODEX_MODEL,
391
474
  reasoningEffort: settings.reasoningEffort,
392
475
  webSearch: settings.webSearch,
476
+ codexConfig: settings.codexConfig,
393
477
  mcpServers: settings.mcpServers,
394
478
  resumeThreadId: resumeThreadIdString,
395
479
  isResume: respawnStrategy !== void 0,
@@ -412,23 +496,6 @@ function resolveBridgePort(sandboxSession, override) {
412
496
  message: "The codex harness needs a TCP port exposed by the sandbox. Create the sandbox with `ports: [<port>]` or pass `createCodex({ port })`."
413
497
  });
414
498
  }
415
- async function readBridgeAsset(name) {
416
- const candidates = [
417
- new URL(`./bridge/${name}`, import.meta.url),
418
- new URL(`../bridge/${name}`, import.meta.url)
419
- ];
420
- let lastErr;
421
- for (const url of candidates) {
422
- try {
423
- return await readFile(fileURLToPath(url), "utf8");
424
- } catch (err) {
425
- const code = err.code;
426
- if (code !== "ENOENT") throw err;
427
- lastErr = err;
428
- }
429
- }
430
- throw lastErr ?? new Error(`bridge asset not found: ${name}`);
431
- }
432
499
  async function writeCodexSkills({
433
500
  sandbox,
434
501
  skills,
@@ -454,9 +521,14 @@ async function writeCodexSkills({
454
521
  codexHomeDir
455
522
  };
456
523
  }
457
- function openWebSocket(url) {
524
+ function openWebSocket({
525
+ url,
526
+ headers
527
+ }) {
458
528
  return new Promise((resolve, reject) => {
459
- const ws = new WebSocket(url);
529
+ const ws = new WebSocket(url, {
530
+ headers: headers == null ? void 0 : { ...headers }
531
+ });
460
532
  const onOpen = () => {
461
533
  ws.off("error", onError);
462
534
  resolve(ws);
@@ -469,6 +541,14 @@ function openWebSocket(url) {
469
541
  ws.once("error", onError);
470
542
  });
471
543
  }
544
+ function withBridgeToken({
545
+ endpoint,
546
+ token
547
+ }) {
548
+ const bridgeUrl = new URL(endpoint.url);
549
+ bridgeUrl.searchParams.set("agent_bridge_token", token);
550
+ return { ...endpoint, url: bridgeUrl.toString() };
551
+ }
472
552
  function createSession({
473
553
  sessionId,
474
554
  channel,
@@ -477,6 +557,7 @@ function createSession({
477
557
  model,
478
558
  reasoningEffort,
479
559
  webSearch,
560
+ codexConfig,
480
561
  mcpServers,
481
562
  resumeThreadId,
482
563
  isResume,
@@ -627,6 +708,12 @@ function createSession({
627
708
  isResume,
628
709
  modelId: model,
629
710
  doPromptTurn: async (promptOpts) => {
711
+ if (promptOpts.responseFormat?.type === "json" && promptOpts.responseFormat.schema == null) {
712
+ throw new HarnessCapabilityUnsupportedError({
713
+ message: "Harness 'codex' requires a JSON schema for structured output.",
714
+ harnessId: "codex"
715
+ });
716
+ }
630
717
  const turn = wireTurn({
631
718
  emit: promptOpts.emit,
632
719
  abortSignal: promptOpts.abortSignal
@@ -651,10 +738,12 @@ function createSession({
651
738
  type: "start",
652
739
  prompt: promptText,
653
740
  tools,
741
+ ...promptOpts.responseFormat == null ? {} : { responseFormat: promptOpts.responseFormat },
654
742
  ...promptOpts.instructions ? { instructions: promptOpts.instructions } : {},
655
743
  model,
656
744
  reasoningEffort,
657
745
  webSearch,
746
+ ...codexConfig == null ? {} : { codexConfig },
658
747
  ...mcpServers == null ? {} : { mcpServers },
659
748
  ...permissionMode ? { permissionMode } : {},
660
749
  ...pendingResumeThreadId ? { resumeThreadId: pendingResumeThreadId } : {},
@@ -665,6 +754,12 @@ function createSession({
665
754
  return turn.control;
666
755
  },
667
756
  doContinueTurn: async (continueOpts) => {
757
+ if (continueOpts.responseFormat?.type === "json" && continueOpts.responseFormat.schema == null) {
758
+ throw new HarnessCapabilityUnsupportedError({
759
+ message: "Harness 'codex' requires a JSON schema for structured output.",
760
+ harnessId: "codex"
761
+ });
762
+ }
668
763
  const turn = wireTurn({
669
764
  emit: continueOpts.emit,
670
765
  abortSignal: continueOpts.abortSignal
@@ -688,10 +783,12 @@ function createSession({
688
783
  description: t.description,
689
784
  inputSchema: t.inputSchema
690
785
  })),
786
+ ...continueOpts.responseFormat == null ? {} : { responseFormat: continueOpts.responseFormat },
691
787
  ...continueOpts.instructions ? { instructions: continueOpts.instructions } : {},
692
788
  model,
693
789
  reasoningEffort,
694
790
  webSearch,
791
+ ...codexConfig == null ? {} : { codexConfig },
695
792
  ...mcpServers == null ? {} : { mcpServers },
696
793
  ...permissionMode ? { permissionMode } : {},
697
794
  ...threadId ? { resumeThreadId: threadId } : {},