@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.
- package/CHANGELOG.md +21 -0
- package/README.md +10 -1
- package/dist/bridge/index.mjs +1 -0
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +14 -2
- package/dist/index.js +165 -62
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/index.ts +1 -0
- package/src/codex-auth.ts +93 -11
- package/src/codex-bootstrap.ts +60 -0
- package/src/codex-bridge-protocol.ts +1 -0
- package/src/codex-harness.ts +97 -75
- package/src/index.ts +1 -1
package/src/codex-harness.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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(
|
|
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
|
-
...
|
|
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
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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(
|
|
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(
|
|
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';
|