@ai-sdk/harness-codex 1.0.72 → 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.
@@ -1,18 +1,16 @@
1
1
  import { randomBytes } from 'node:crypto';
2
- import { readFile } from 'node:fs/promises';
3
2
  import path from 'node:path';
4
- import { fileURLToPath } from 'node:url';
5
3
  import {
6
4
  commonTool,
7
5
  HarnessCapabilityUnsupportedError,
8
6
  harnessV1DiagnosticFromBridgeFrame,
9
7
  type HarnessV1,
10
- type HarnessV1Bootstrap,
11
8
  type HarnessV1DebugConfig,
12
9
  type HarnessV1BuiltinTool,
13
10
  type HarnessV1ContinueTurnState,
14
11
  type HarnessV1Prompt,
15
12
  type HarnessV1PromptControl,
13
+ type HarnessV1PortEndpoint,
16
14
  type HarnessV1ResumeSessionState,
17
15
  type HarnessV1NetworkSandboxSession,
18
16
  type HarnessV1PermissionMode,
@@ -27,9 +25,11 @@ import {
27
25
  drainBridgeProcessStream,
28
26
  forwardBridgeProcessStream,
29
27
  markBridgeStarting,
28
+ maskSandboxCredentials,
30
29
  resolveSandboxHomeDir,
31
30
  SandboxChannel,
32
31
  shellQuote,
32
+ warnCredentialBrokeringUnavailable,
33
33
  waitForBridgeReady,
34
34
  writeSkills as writeHarnessSkills,
35
35
  } from '@ai-sdk/harness/utils';
@@ -39,7 +39,18 @@ import {
39
39
  } from '@ai-sdk/provider-utils';
40
40
  import { WebSocket } from 'ws';
41
41
  import { z } from 'zod/v4';
42
- import { resolveCodexEnv, type CodexAuthOptions } from './codex-auth';
42
+ import {
43
+ CODEX_BOOTSTRAP_DIR as BOOTSTRAP_DIR,
44
+ getCodexBootstrap,
45
+ } from './codex-bootstrap';
46
+ import {
47
+ CODEX_CREDENTIAL_ENVIRONMENT_VARIABLES,
48
+ createCodexRequestTransformations,
49
+ DEFAULT_OPENAI_BASE_URL,
50
+ resolveCodexAuthenticationMode,
51
+ resolveCodexEnv,
52
+ type CodexAuthOptions,
53
+ } from './codex-auth';
43
54
  import {
44
55
  outboundMessageSchema,
45
56
  type InboundMessage,
@@ -75,6 +86,12 @@ const CODEX_CLIENT_APP = `ai-sdk/harness-codex/${VERSION}`;
75
86
 
76
87
  export type CodexHarnessSettings = {
77
88
  readonly auth?: CodexAuthOptions;
89
+ /**
90
+ * Additional configuration passed through to Codex as-is. Codex config keys
91
+ * typically use snake_case and must be provided in that form. Values managed
92
+ * by this adapter take precedence over conflicting entries.
93
+ */
94
+ readonly codexConfig?: Record<string, unknown>;
78
95
  /**
79
96
  * MCP server definitions keyed by server name. Each definition uses the
80
97
  * underlying runtime's native MCP server configuration format.
@@ -135,19 +152,6 @@ const CODEX_BUILTIN_TOOLS = {
135
152
  }),
136
153
  } as const satisfies Record<string, HarnessV1BuiltinTool<any, any>>;
137
154
 
138
- /*
139
- * Bootstrap is derived state stored under the sandbox's default working
140
- * directory so snapshot-capable providers can preserve the installed CLI,
141
- * bridge, and recipe marker without requiring root filesystem access.
142
- *
143
- * The session work dir (`startOpts.sessionWorkDir`) lives under the sandbox's
144
- * default working directory — the provider's persistent mount — so any files
145
- * the agent edits survive both detach -> attach and stop -> snapshot -> resume
146
- * cycles. Harness infra derived from `sandboxSession.defaultWorkingDirectory`
147
- * lives under `.agent-runs`, outside the agent workdir.
148
- */
149
- const BOOTSTRAP_DIR = '.harness-bootstrap/codex';
150
-
151
155
  /**
152
156
  * Live bridge coordinates returned by `doDetach()` and `doSuspendTurn()`. A
153
157
  * future process uses them to reopen a socket to the still-running bridge
@@ -178,37 +182,13 @@ type CodexBridgeCoords = z.infer<typeof codexBridgeCoordsSchema>;
178
182
  export function createCodex(
179
183
  settings: CodexHarnessSettings = {},
180
184
  ): HarnessV1<typeof CODEX_BUILTIN_TOOLS> {
181
- let cachedBootstrap: HarnessV1Bootstrap | undefined;
182
-
183
185
  return {
184
186
  specificationVersion: 'harness-v1',
185
187
  harnessId: 'codex',
186
188
  builtinTools: CODEX_BUILTIN_TOOLS,
187
189
  supportsBuiltinToolApprovals: false,
188
190
  lifecycleStateSchema: codexResumeStateSchema,
189
- getBootstrap: async () => {
190
- if (cachedBootstrap != null) return cachedBootstrap;
191
- const [pkg, lock, bridge] = await Promise.all([
192
- readBridgeAsset('package.json'),
193
- readBridgeAsset('pnpm-lock.yaml'),
194
- readBridgeAsset('index.mjs'),
195
- ]);
196
- cachedBootstrap = {
197
- harnessId: 'codex',
198
- bootstrapDir: BOOTSTRAP_DIR,
199
- files: [
200
- { path: `${BOOTSTRAP_DIR}/package.json`, content: pkg },
201
- { path: `${BOOTSTRAP_DIR}/pnpm-lock.yaml`, content: lock },
202
- { path: `${BOOTSTRAP_DIR}/bridge.mjs`, content: bridge },
203
- ],
204
- commands: [
205
- {
206
- command: 'pnpm install --frozen-lockfile --store-dir .pnpm-store',
207
- },
208
- ],
209
- };
210
- return cachedBootstrap;
211
- },
191
+ getBootstrap: getCodexBootstrap,
212
192
  doStart: async startOpts => {
213
193
  if (startOpts.builtinToolFiltering != null) {
214
194
  throw new HarnessCapabilityUnsupportedError({
@@ -228,6 +208,40 @@ export function createCodex(
228
208
  });
229
209
  }
230
210
  const sandboxSession = startOpts.sandboxSession;
211
+ const authenticationMode = resolveCodexAuthenticationMode(settings.auth);
212
+ const resolvedAuthEnvironment = resolveCodexEnv(settings.auth);
213
+ let sandboxAuthEnvironment = resolvedAuthEnvironment;
214
+ if (sandboxSession.addRequestTransformations != null) {
215
+ const requestTransformations = createCodexRequestTransformations(
216
+ resolvedAuthEnvironment,
217
+ authenticationMode,
218
+ );
219
+ if (requestTransformations.length > 0) {
220
+ await sandboxSession.addRequestTransformations(
221
+ requestTransformations,
222
+ );
223
+ }
224
+ sandboxAuthEnvironment = maskSandboxCredentials({
225
+ environment: resolvedAuthEnvironment,
226
+ credentialEnvironmentVariables:
227
+ CODEX_CREDENTIAL_ENVIRONMENT_VARIABLES,
228
+ });
229
+ if (
230
+ requestTransformations.length > 0 &&
231
+ authenticationMode === 'direct' &&
232
+ resolvedAuthEnvironment.OPENAI_BASE_URL == null
233
+ ) {
234
+ /*
235
+ * Vercel Sandbox request transformations apply only to HTTP traffic.
236
+ * Materializing Codex's standard OpenAI URL makes the bridge select
237
+ * its custom provider, where WebSockets are disabled, while keeping
238
+ * the non-brokered path on Codex's built-in OpenAI provider.
239
+ */
240
+ sandboxAuthEnvironment.OPENAI_BASE_URL = DEFAULT_OPENAI_BASE_URL;
241
+ }
242
+ } else {
243
+ warnCredentialBrokeringUnavailable();
244
+ }
231
245
  const session = sandboxSession.restricted();
232
246
  const sandboxId = sandboxSession.id;
233
247
  const bootstrapDir = path.posix.resolve(
@@ -286,13 +300,16 @@ export function createCodex(
286
300
  */
287
301
  if (coords) {
288
302
  try {
289
- const attachUrl =
290
- (await sandboxSession.getPortUrl({
291
- port: coords.port,
292
- protocol: 'ws',
293
- })) + `?agent_bridge_token=${encodeURIComponent(coords.token)}`;
303
+ const endpoint = await sandboxSession.getPortEndpoint({
304
+ port: coords.port,
305
+ protocol: 'ws',
306
+ });
307
+ const attachEndpoint = withBridgeToken({
308
+ endpoint,
309
+ token: coords.token,
310
+ });
294
311
  const attachChannel: CodexChannel = new SandboxChannel({
295
- connect: () => openWebSocket(attachUrl),
312
+ connect: () => openWebSocket(attachEndpoint),
296
313
  outboundSchema: outboundMessageSchema,
297
314
  initialLastSeenEventId: coords.lastSeenEventId,
298
315
  onDiagnostic,
@@ -308,6 +325,7 @@ export function createCodex(
308
325
  model: settings.model ?? DEFAULT_CODEX_MODEL,
309
326
  reasoningEffort: settings.reasoningEffort,
310
327
  webSearch: settings.webSearch,
328
+ codexConfig: settings.codexConfig,
311
329
  mcpServers: settings.mcpServers,
312
330
  resumeThreadId: resumeThreadIdString,
313
331
  isResume: true,
@@ -361,7 +379,7 @@ export function createCodex(
361
379
  })
362
380
  : undefined;
363
381
  const env = {
364
- ...resolveCodexEnv(settings.auth),
382
+ ...sandboxAuthEnvironment,
365
383
  AI_SDK_HARNESS_CLIENT_APP: CODEX_CLIENT_APP,
366
384
  BRIDGE_CHANNEL_TOKEN: token,
367
385
  BRIDGE_WS_PORT: String(port),
@@ -429,14 +447,14 @@ export function createCodex(
429
447
  });
430
448
  void drainBridgeProcessStream(proc.stdout);
431
449
 
432
- const wsUrl =
433
- (await sandboxSession.getPortUrl({
434
- port: boundPort,
435
- protocol: 'ws',
436
- })) + `?agent_bridge_token=${encodeURIComponent(token)}`;
450
+ const endpoint = await sandboxSession.getPortEndpoint({
451
+ port: boundPort,
452
+ protocol: 'ws',
453
+ });
454
+ const bridgeEndpoint = withBridgeToken({ endpoint, token });
437
455
 
438
456
  const channel: CodexChannel = new SandboxChannel({
439
- connect: () => openWebSocket(wsUrl),
457
+ connect: () => openWebSocket(bridgeEndpoint),
440
458
  outboundSchema: outboundMessageSchema,
441
459
  onDiagnostic,
442
460
  onBridgeError,
@@ -459,6 +477,7 @@ export function createCodex(
459
477
  model: settings.model ?? DEFAULT_CODEX_MODEL,
460
478
  reasoningEffort: settings.reasoningEffort,
461
479
  webSearch: settings.webSearch,
480
+ codexConfig: settings.codexConfig,
462
481
  mcpServers: settings.mcpServers,
463
482
  resumeThreadId: resumeThreadIdString,
464
483
  isResume: respawnStrategy !== undefined,
@@ -488,24 +507,6 @@ function resolveBridgePort(
488
507
  });
489
508
  }
490
509
 
491
- async function readBridgeAsset(name: string): Promise<string> {
492
- const candidates = [
493
- new URL(`./bridge/${name}`, import.meta.url),
494
- new URL(`../bridge/${name}`, import.meta.url),
495
- ];
496
- let lastErr: unknown;
497
- for (const url of candidates) {
498
- try {
499
- return await readFile(fileURLToPath(url), 'utf8');
500
- } catch (err) {
501
- const code = (err as NodeJS.ErrnoException).code;
502
- if (code !== 'ENOENT') throw err;
503
- lastErr = err;
504
- }
505
- }
506
- throw lastErr ?? new Error(`bridge asset not found: ${name}`);
507
- }
508
-
509
510
  async function writeCodexSkills({
510
511
  sandbox,
511
512
  skills,
@@ -539,9 +540,14 @@ async function writeCodexSkills({
539
540
  };
540
541
  }
541
542
 
542
- function openWebSocket(url: string): Promise<WebSocket> {
543
+ function openWebSocket({
544
+ url,
545
+ headers,
546
+ }: HarnessV1PortEndpoint): Promise<WebSocket> {
543
547
  return new Promise((resolve, reject) => {
544
- const ws = new WebSocket(url);
548
+ const ws = new WebSocket(url, {
549
+ headers: headers == null ? undefined : { ...headers },
550
+ });
545
551
  const onOpen = () => {
546
552
  ws.off('error', onError);
547
553
  resolve(ws);
@@ -555,6 +561,18 @@ function openWebSocket(url: string): Promise<WebSocket> {
555
561
  });
556
562
  }
557
563
 
564
+ function withBridgeToken({
565
+ endpoint,
566
+ token,
567
+ }: {
568
+ endpoint: HarnessV1PortEndpoint;
569
+ token: string;
570
+ }): HarnessV1PortEndpoint {
571
+ const bridgeUrl = new URL(endpoint.url);
572
+ bridgeUrl.searchParams.set('agent_bridge_token', token);
573
+ return { ...endpoint, url: bridgeUrl.toString() };
574
+ }
575
+
558
576
  function createSession({
559
577
  sessionId,
560
578
  channel,
@@ -563,6 +581,7 @@ function createSession({
563
581
  model,
564
582
  reasoningEffort,
565
583
  webSearch,
584
+ codexConfig,
566
585
  mcpServers,
567
586
  resumeThreadId,
568
587
  isResume,
@@ -582,6 +601,7 @@ function createSession({
582
601
  model: string | undefined;
583
602
  reasoningEffort: 'low' | 'medium' | 'high' | undefined;
584
603
  webSearch: boolean | undefined;
604
+ codexConfig: Record<string, unknown> | undefined;
585
605
  mcpServers: Record<string, unknown> | undefined;
586
606
  resumeThreadId: string | undefined;
587
607
  isResume: boolean;
@@ -819,6 +839,7 @@ function createSession({
819
839
  model,
820
840
  reasoningEffort,
821
841
  webSearch,
842
+ ...(codexConfig == null ? {} : { codexConfig }),
822
843
  ...(mcpServers == null ? {} : { mcpServers }),
823
844
  ...(permissionMode ? { permissionMode } : {}),
824
845
  ...(pendingResumeThreadId
@@ -874,6 +895,7 @@ function createSession({
874
895
  model,
875
896
  reasoningEffort,
876
897
  webSearch,
898
+ ...(codexConfig == null ? {} : { codexConfig }),
877
899
  ...(mcpServers == null ? {} : { mcpServers }),
878
900
  ...(permissionMode ? { permissionMode } : {}),
879
901
  ...(threadId ? { resumeThreadId: threadId } : {}),
package/src/index.ts CHANGED
@@ -10,4 +10,4 @@ export const codex = createCodex();
10
10
  export { createCodex } from './codex-harness';
11
11
  export { VERSION } from './version';
12
12
  export type { CodexHarnessSettings } from './codex-harness';
13
- export type { CodexAuthOptions } from './codex-auth';
13
+ export type { CodexAuthenticationMode, CodexAuthOptions } from './codex-auth';