@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.
- package/CHANGELOG.md +26 -0
- package/README.md +12 -13
- package/dist/bridge/index.mjs +1248 -484
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/bridge/package.json +1 -1
- package/dist/bridge/pnpm-lock.yaml +1 -9
- package/dist/index.d.ts +1399 -0
- package/dist/index.js +46 -73
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/bridge/codex-app-server-client.ts +320 -0
- package/src/bridge/codex-app-server-driver.ts +617 -0
- package/src/bridge/codex-step-tracker.ts +2 -0
- package/src/bridge/codex-tool-filtering-hook.ts +148 -0
- package/src/bridge/codex-tool-filtering.ts +42 -0
- package/src/bridge/create-app-server-event-handler.ts +531 -0
- package/src/bridge/create-emit-stream-event.ts +27 -2
- package/src/bridge/index.ts +64 -193
- package/src/bridge/package.json +1 -1
- package/src/bridge/pnpm-lock.yaml +1 -9
- package/src/codex-bridge-protocol.ts +4 -4
- package/src/codex-harness.ts +56 -117
- package/src/bridge/cli-relay.ts +0 -268
- package/src/bridge/tool-relay-auth.ts +0 -156
- package/src/bridge/tool-relay.ts +0 -119
package/src/bridge/index.ts
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 {
|
|
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: () =>
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
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
|
-
|
|
73
|
+
workdir,
|
|
74
|
+
threadId: threadState.id,
|
|
75
|
+
codexModel: runtime.codexModel,
|
|
76
|
+
codexConfig: runtime.codexConfig,
|
|
77
|
+
stepTracker,
|
|
78
|
+
emitStreamEvent,
|
|
115
79
|
});
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
(
|
|
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
|
-
|
|
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;
|
package/src/bridge/package.json
CHANGED
|
@@ -8,7 +8,7 @@ importers:
|
|
|
8
8
|
|
|
9
9
|
.:
|
|
10
10
|
dependencies:
|
|
11
|
-
'@openai/codex
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
});
|