@ai-sdk/harness-codex 1.0.126 → 1.0.129

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,13 +1,9 @@
1
- // Long-running process that runs alongside the `codex` CLI in the sandbox.
2
- // The generic transport — WebSocket server, token auth, single-flight
3
- // reconnect, the in-memory event log + `seq`, resume replay, and the
4
- // lifecycle/meta files — lives in the shared `@ai-sdk/harness/bridge` runtime.
5
- // This file supplies only the Codex-specific turn driver.
6
- //
7
- // Host-defined tools are routed through an HTTP relay bound to
8
- // `127.0.0.1:0`. The bridge writes a CLI shim into the per-session runtime
9
- // directory; the shim POSTs each tool call to the relay, which emits
10
- // `tool-call` to the host and waits for the matching `tool-result`.
1
+ /*
2
+ * This long-running bridge process runs Codex app-server in the sandbox. The
3
+ * shared bridge runtime owns the WebSocket transport, authentication,
4
+ * reconnects, event replay, and lifecycle files; this entry point supplies the
5
+ * Codex-specific turn implementation.
6
+ */
11
7
 
12
8
  import {
13
9
  runBridge,
@@ -15,64 +11,32 @@ import {
15
11
  type BridgeTurn,
16
12
  } from '@ai-sdk/harness/bridge';
17
13
  import type { StartMessage } from '../codex-bridge-protocol';
18
- import { mkdir, writeFile } from 'node:fs/promises';
19
- // Temporary workaround for upstream codex MCP-tool bug — see ./cli-relay.ts
20
- import {
21
- CLI_SHIM_FILENAME,
22
- buildCliShimScript,
23
- parseToolRelayCommands,
24
- } from './cli-relay';
14
+ import { createCodexAppServerRuntime } from './codex-app-server-driver';
25
15
  import { createCodexStepTracker, defaultUsage } from './codex-step-tracker';
26
- import {
27
- createEmitStreamEvent,
28
- type CodexEvent,
29
- } from './create-emit-stream-event';
30
- import { startAuthorizedToolRelay, type ToolRelay } from './tool-relay';
16
+ import { createEmitStreamEvent } from './create-emit-stream-event';
31
17
  import { argv, env as procEnv, stdout } from 'node:process';
32
18
 
33
- /*
34
- * CONSTRAINT — the third-party imports below are NEVER bundled into the
35
- * compiled `bridge/index.mjs`. They are declared `external` in
36
- * tsup.config.ts and resolved at runtime from the node_modules that this
37
- * bridge installs *inside the sandbox* from `src/bridge/package.json` (and
38
- * its pinned `pnpm-lock.yaml`). That bridge package.json — NOT this host
39
- * package — is the single source of truth for these packages and their
40
- * versions; the published `@ai-sdk/harness-codex` package does not provide
41
- * them at runtime.
42
- *
43
- * When adding or changing a third-party import here you MUST keep all three
44
- * in sync, or the bridge will either get the dependency bundled in or fail
45
- * to resolve it in the sandbox:
46
- * 1. the import statement below,
47
- * 2. the `external` array in tsup.config.ts, and
48
- * 3. the dependency entry in `src/bridge/package.json`.
49
- */
50
- import * as codexSdkModule from '@openai/codex-sdk';
51
-
52
19
  const args = parseArgs(argv.slice(2));
53
20
  const workdir = requireArg({ value: args.workdir, name: '--workdir' });
54
21
  const bridgeStateDir = requireArg({
55
22
  value: args.bridgeStateDir,
56
23
  name: '--bridge-state-dir',
57
24
  });
58
- const cliShimDir = requireArg({
59
- value: args.cliShimDir,
60
- name: '--cli-shim-dir',
61
- });
62
25
  const HARNESS_CLIENT_APP = procEnv.AI_SDK_HARNESS_CLIENT_APP;
63
26
 
64
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
65
- const codexSdk = codexSdkModule as any;
66
-
67
- // Codex thread id — survives across turns within this bridge process and is
68
- // returned to the host on `stop` so a future process can resume the thread.
69
27
  const threadState: { id: string | undefined } = { id: undefined };
28
+ const appServer = createCodexAppServerRuntime();
70
29
 
71
30
  await runBridge<StartMessage>({
72
31
  bridgeType: 'codex',
73
32
  bridgeStateDir,
74
33
  onStart: runTurn,
75
- onStop: () => (threadState.id ? { threadId: threadState.id } : {}),
34
+ onStop: async () => {
35
+ const data = threadState.id ? { threadId: threadState.id } : {};
36
+ await appServer.close();
37
+ return data;
38
+ },
39
+ onDestroy: () => appServer.close(),
76
40
  });
77
41
 
78
42
  type Emit = (msg: Record<string, unknown>) => void;
@@ -80,9 +44,6 @@ type Emit = (msg: Record<string, unknown>) => void;
80
44
  async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
81
45
  const emit: Emit = msg => turn.emit(msg as BridgeEvent);
82
46
 
83
- // Cross-process resume: the host carries the threadId we returned on stop.
84
- // Seed `threadState.id` so the codex SDK call below takes the `resumeThread`
85
- // branch.
86
47
  if (start.restartThread) {
87
48
  threadState.id = undefined;
88
49
  } else if (
@@ -92,36 +53,50 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
92
53
  threadState.id = start.resumeThreadId;
93
54
  }
94
55
 
95
- /*
96
- * Known limitation: codex CLI does not reliably surface MCP tools to the
97
- * model in `codex exec --experimental-json` mode (the path the
98
- * `@openai/codex-sdk` uses). Some versions do not register MCP tools at all;
99
- * others expose the tool names but pass empty arguments.
100
- *
101
- * Until that's fixed, host tools are made available to the model via a
102
- * separate CLI-relay workaround (see `./cli-relay.ts`). Writing the shim
103
- * here, adding matching prompt guidance in the host adapter, and filtering
104
- * the shim command below implement the workaround and can be removed once the
105
- * upstream bug is fixed.
106
- */
107
- let relay: ToolRelay | undefined;
108
- let cliShimPath: string | undefined;
109
- if (start.tools && start.tools.length > 0) {
110
- cliShimPath = `${cliShimDir}/${CLI_SHIM_FILENAME}`;
111
- relay = await startToolRelay({
112
- tools: start.tools,
56
+ const runtime = resolveCodexRuntime({ start });
57
+ let turnUsage: Record<string, unknown> = defaultUsage();
58
+ const stepTracker = createCodexStepTracker({ send: emit });
59
+ const emitStreamEvent = createEmitStreamEvent({
60
+ send: emit,
61
+ stepTracker,
62
+ setTurnUsage: usage => (turnUsage = usage),
63
+ setThreadId: threadId => (threadState.id = threadId),
64
+ emitWarning: turn.emitWarning,
65
+ emitError: turn.emitError,
66
+ });
67
+
68
+ try {
69
+ await appServer.runTurn({
70
+ start,
71
+ turn,
113
72
  emit,
114
- requestToolResult: turn.requestToolResult,
73
+ workdir,
74
+ threadId: threadState.id,
75
+ codexModel: runtime.codexModel,
76
+ codexConfig: runtime.codexConfig,
77
+ stepTracker,
78
+ emitStreamEvent,
115
79
  });
116
- // Temporary workaround for upstream codex MCP-tool bug — see ./cli-relay.ts
117
- await mkdir(cliShimDir, { recursive: true });
118
- await writeFile(
119
- cliShimPath,
120
- buildCliShimScript({ relayPort: relay.port }),
121
- 'utf8',
122
- );
80
+ } catch (error) {
81
+ if (!turn.abortSignal.aborted) {
82
+ turn.emitError({ error, message: 'codex turn failed' });
83
+ }
84
+ return;
123
85
  }
124
86
 
87
+ emit({
88
+ type: 'finish',
89
+ finishReason: { unified: 'stop', raw: 'stop' },
90
+ totalUsage: turnUsage,
91
+ });
92
+ }
93
+
94
+ type CodexRuntime = {
95
+ codexConfig: Record<string, unknown>;
96
+ codexModel: string | undefined;
97
+ };
98
+
99
+ function resolveCodexRuntime({ start }: { start: StartMessage }): CodexRuntime {
125
100
  const codexConfig: Record<string, unknown> = {
126
101
  ...start.codexConfig,
127
102
  developer_instructions: [
@@ -140,10 +115,17 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
140
115
  'AI Gateway auth was selected but AI_GATEWAY_BASE_URL is missing from the Codex bridge environment.',
141
116
  );
142
117
  }
118
+ /*
119
+ * The standalone app-server deliberately ignores CODEX_API_KEY for its
120
+ * built-in OpenAI provider. A custom provider with an explicit env_key keeps
121
+ * direct credentials process-local.
122
+ */
143
123
  const apiBaseUrl = hasGatewayAuth
144
124
  ? gatewayBaseUrl
145
125
  : (procEnv.OPENAI_BASE_URL ??
146
- (start.headers != null ? 'https://api.openai.com/v1' : undefined));
126
+ (procEnv.CODEX_API_KEY != null || start.headers != null
127
+ ? 'https://api.openai.com/v1'
128
+ : undefined));
147
129
  const codexModel =
148
130
  start.model && hasGatewayAuth && !start.model.includes('/')
149
131
  ? `openai/${start.model}`
@@ -186,133 +168,22 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
186
168
  if (start.mcpServers != null) {
187
169
  codexConfig.mcp_servers = start.mcpServers;
188
170
  }
189
- const usesConfiguredModelProvider =
190
- typeof codexConfig.model_provider === 'string';
191
-
192
- const codex = new codexSdk.Codex({
193
- ...(procEnv.CODEX_API_KEY ? { apiKey: procEnv.CODEX_API_KEY } : {}),
194
- ...(!usesConfiguredModelProvider && apiBaseUrl
195
- ? { baseUrl: apiBaseUrl }
196
- : {}),
197
- env: Object.fromEntries(
198
- Object.entries(procEnv).filter(
199
- (entry): entry is [string, string] => typeof entry[1] === 'string',
200
- ),
201
- ),
202
- ...(Object.keys(codexConfig).length > 0 ? { config: codexConfig } : {}),
203
- });
204
-
205
- const threadOptions = {
206
- ...(codexModel ? { model: codexModel } : {}),
207
- sandboxMode: 'danger-full-access',
208
- approvalPolicy: 'never',
209
- workingDirectory: workdir,
210
- skipGitRepoCheck: true,
211
- ...(start.reasoningEffort
212
- ? { modelReasoningEffort: start.reasoningEffort }
213
- : {}),
214
- webSearchMode: start.webSearch ? 'live' : 'disabled',
215
- };
216
- const thread = threadState.id
217
- ? codex.resumeThread(threadState.id, threadOptions)
218
- : codex.startThread(threadOptions);
219
-
220
- emit({ type: 'stream-start' });
221
-
222
- const userMessage = start.prompt;
223
- let turnUsage: Record<string, unknown> | undefined;
224
- const stepTracker = createCodexStepTracker({ send: emit });
225
- const emitStreamEvent = createEmitStreamEvent({
226
- send: emit,
227
- stepTracker,
228
- setTurnUsage: usage => (turnUsage = usage),
229
- setThreadId: threadId => (threadState.id = threadId),
230
- emitWarning: turn.emitWarning,
231
- emitError: turn.emitError,
232
- });
233
-
234
- try {
235
- const { events } = await thread.runStreamed(userMessage, {
236
- signal: turn.abortSignal,
237
- ...(start.responseFormat?.type === 'json' &&
238
- start.responseFormat.schema != null
239
- ? { outputSchema: start.responseFormat.schema }
240
- : {}),
241
- });
242
- for await (const event of events as AsyncIterable<CodexEvent>) {
243
- if (turn.abortSignal.aborted) break;
244
- // Temporary workaround for upstream codex MCP-tool bug — see ./cli-relay.ts
245
- if (cliShimPath && event.item?.type === 'command_execution') {
246
- const relayCalls =
247
- typeof event.item.command === 'string'
248
- ? parseToolRelayCommands({
249
- command: event.item.command,
250
- cliShimPath,
251
- })
252
- : undefined;
253
- if (event.type === 'item.started' && relay && relayCalls) {
254
- for (const relayCall of relayCalls) {
255
- relay.authorizeToolCall(relayCall);
256
- }
257
- }
258
- if (relayCalls) {
259
- stepTracker.observeEvent({ event, itemId: event.item.id });
260
- continue;
261
- }
262
- }
263
- emitStreamEvent(event);
264
- }
265
- } catch (err) {
266
- turn.emitError({ error: err, message: 'codex turn failed' });
267
- return;
268
- } finally {
269
- relay?.close();
270
- }
271
-
272
- emit({
273
- type: 'finish',
274
- finishReason: { unified: 'stop', raw: 'stop' },
275
- totalUsage: turnUsage ?? defaultUsage(),
276
- });
277
- }
278
-
279
- /**
280
- * Tool relay — HTTP server on 127.0.0.1:0. The CLI shim invoked by Codex POSTs
281
- * each tool invocation here; the relay forwards the call to the host (via the
282
- * shared runtime's `emit`), awaits the matching `tool-result` (via
283
- * `requestToolResult`), and responds with `{ result }`.
284
- */
285
- async function startToolRelay({
286
- tools,
287
- emit,
288
- requestToolResult,
289
- }: {
290
- tools: ReadonlyArray<{ name: string }>;
291
- emit: Emit;
292
- requestToolResult: (
293
- toolCallId: string,
294
- ) => Promise<{ output: unknown; isError?: boolean }>;
295
- }): Promise<ToolRelay> {
296
- return startAuthorizedToolRelay({ tools, emit, requestToolResult });
171
+ return { codexConfig, codexModel };
297
172
  }
298
173
 
299
174
  function parseArgs(args: string[]): {
300
175
  workdir?: string;
301
176
  bridgeStateDir?: string;
302
- cliShimDir?: string;
303
177
  } {
304
178
  const out: {
305
179
  workdir?: string;
306
180
  bridgeStateDir?: string;
307
- cliShimDir?: string;
308
181
  } = {};
309
182
  for (let i = 0; i < args.length; i++) {
310
183
  if (args[i] === '--workdir' && i + 1 < args.length) {
311
184
  out.workdir = args[++i];
312
185
  } else if (args[i] === '--bridge-state-dir' && i + 1 < args.length) {
313
186
  out.bridgeStateDir = args[++i];
314
- } else if (args[i] === '--cli-shim-dir' && i + 1 < args.length) {
315
- out.cliShimDir = args[++i];
316
187
  }
317
188
  }
318
189
  return out;
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "dependencies": {
7
- "@openai/codex-sdk": "0.155.0",
7
+ "@openai/codex": "0.155.0",
8
8
  "ws": "8.21.0"
9
9
  }
10
10
  }
@@ -8,7 +8,7 @@ importers:
8
8
 
9
9
  .:
10
10
  dependencies:
11
- '@openai/codex-sdk':
11
+ '@openai/codex':
12
12
  specifier: 0.155.0
13
13
  version: 0.155.0
14
14
  ws:
@@ -17,10 +17,6 @@ importers:
17
17
 
18
18
  packages:
19
19
 
20
- '@openai/codex-sdk@0.155.0':
21
- resolution: {integrity: sha512-oD/7iCZnXqDlrnoWWpGkZExrUStpAau1koyUO/fSW3WwkqewRf+MGj3OS41Buw7gNwj0oM6ilI9RHqP5QQWJaQ==}
22
- engines: {node: '>=18'}
23
-
24
20
  '@openai/codex@0.155.0':
25
21
  resolution: {integrity: sha512-35a85Hbwy9WXkDTJumLjTcmMgpR7BMdrTloWBVGjoA+FBCh7jb3+cYp2W+3U4klqGAjAskoIvQZv7/eedmCjHA==}
26
22
  engines: {node: '>=16'}
@@ -76,10 +72,6 @@ packages:
76
72
 
77
73
  snapshots:
78
74
 
79
- '@openai/codex-sdk@0.155.0':
80
- dependencies:
81
- '@openai/codex': 0.155.0
82
-
83
75
  '@openai/codex@0.155.0':
84
76
  optionalDependencies:
85
77
  '@openai/codex-darwin-arm64': '@openai/codex@0.155.0-darwin-arm64'
@@ -23,10 +23,10 @@ export const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
23
23
  codexConfig: z.record(z.string(), z.unknown()).optional(),
24
24
  mcpServers: z.record(z.string(), z.unknown()).optional(),
25
25
  headers: z.record(z.string(), z.string()).optional(),
26
- // Resume signal. When supplied, the bridge calls
27
- // `codex.resumeThread(resumeThreadId, …)` instead of starting a fresh thread.
28
- // The host sources the id from lifecycle state `data` cached from a prior
29
- // `agent.detach`.
26
+ /*
27
+ * The host carries this app-server thread ID in lifecycle state. Supplying it
28
+ * makes the bridge call `thread/resume` instead of starting a fresh thread.
29
+ */
30
30
  resumeThreadId: z.string().optional(),
31
31
  restartThread: z.boolean().optional(),
32
32
  });