@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/CHANGELOG.md +22 -0
- package/README.md +10 -1
- package/dist/bridge/index.mjs +17 -21
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +8 -1
- package/dist/index.js +153 -56
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/index.ts +5 -0
- package/src/codex-auth.ts +52 -2
- package/src/codex-bootstrap.ts +60 -0
- package/src/codex-bridge-protocol.ts +1 -0
- package/src/codex-harness.ts +123 -75
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 {
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
|
333
|
+
const endpoint2 = await sandboxSession.getPortEndpoint({
|
|
257
334
|
port: coords.port,
|
|
258
335
|
protocol: "ws"
|
|
259
|
-
})
|
|
336
|
+
});
|
|
337
|
+
const attachEndpoint = withBridgeToken({
|
|
338
|
+
endpoint: endpoint2,
|
|
339
|
+
token: coords.token
|
|
340
|
+
});
|
|
260
341
|
const attachChannel = new SandboxChannel({
|
|
261
|
-
connect: () => openWebSocket(
|
|
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
|
-
...
|
|
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
|
|
450
|
+
const endpoint = await sandboxSession.getPortEndpoint({
|
|
369
451
|
port: boundPort,
|
|
370
452
|
protocol: "ws"
|
|
371
|
-
})
|
|
453
|
+
});
|
|
454
|
+
const bridgeEndpoint = withBridgeToken({ endpoint, token });
|
|
372
455
|
const channel = new SandboxChannel({
|
|
373
|
-
connect: () => openWebSocket(
|
|
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(
|
|
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 } : {},
|