@ai-sdk/harness-codex 1.0.73 → 1.0.74

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.74" : "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,
@@ -655,6 +736,7 @@ function createSession({
655
736
  model,
656
737
  reasoningEffort,
657
738
  webSearch,
739
+ ...codexConfig == null ? {} : { codexConfig },
658
740
  ...mcpServers == null ? {} : { mcpServers },
659
741
  ...permissionMode ? { permissionMode } : {},
660
742
  ...pendingResumeThreadId ? { resumeThreadId: pendingResumeThreadId } : {},
@@ -692,6 +774,7 @@ function createSession({
692
774
  model,
693
775
  reasoningEffort,
694
776
  webSearch,
777
+ ...codexConfig == null ? {} : { codexConfig },
695
778
  ...mcpServers == null ? {} : { mcpServers },
696
779
  ...permissionMode ? { permissionMode } : {},
697
780
  ...threadId ? { resumeThreadId: threadId } : {},