@ai-sdk/harness-codex 1.0.0-canary.2 → 1.0.0-canary.4
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/dist/bridge/index.mjs +0 -9
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +115 -78
- package/dist/index.js.map +1 -1
- package/package.json +4 -6
- package/src/bridge/index.ts +0 -12
- package/src/codex-bridge-protocol.ts +0 -9
- package/src/codex-harness.ts +154 -85
|
@@ -20,15 +20,6 @@ export const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
|
|
|
20
20
|
instructions: z.string().optional(),
|
|
21
21
|
reasoningEffort: z.enum(['low', 'medium', 'high']).optional(),
|
|
22
22
|
webSearch: z.boolean().optional(),
|
|
23
|
-
skills: z
|
|
24
|
-
.array(
|
|
25
|
-
z.object({
|
|
26
|
-
name: z.string(),
|
|
27
|
-
description: z.string(),
|
|
28
|
-
content: z.string(),
|
|
29
|
-
}),
|
|
30
|
-
)
|
|
31
|
-
.optional(),
|
|
32
23
|
// Resume signal. When supplied, the bridge calls
|
|
33
24
|
// `codex.resumeThread(resumeThreadId, …)` instead of starting a fresh thread.
|
|
34
25
|
// The host sources the id from lifecycle state `data` cached from a prior
|
package/src/codex-harness.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
3
4
|
import { fileURLToPath } from 'node:url';
|
|
4
5
|
import {
|
|
5
6
|
commonTool,
|
|
@@ -19,16 +20,20 @@ import {
|
|
|
19
20
|
type HarnessV1Skill,
|
|
20
21
|
type HarnessV1StreamPart,
|
|
21
22
|
} from '@ai-sdk/harness';
|
|
22
|
-
import { classifyDiskLog, SandboxChannel } from '@ai-sdk/harness/utils';
|
|
23
23
|
import {
|
|
24
|
-
|
|
24
|
+
classifyDiskLog,
|
|
25
|
+
markBridgeStarting,
|
|
26
|
+
SandboxChannel,
|
|
27
|
+
waitForBridgeReady,
|
|
28
|
+
} from '@ai-sdk/harness/utils';
|
|
29
|
+
import {
|
|
25
30
|
type Experimental_SandboxProcess,
|
|
31
|
+
type Experimental_SandboxSession,
|
|
26
32
|
} from '@ai-sdk/provider-utils';
|
|
27
33
|
import { WebSocket } from 'ws';
|
|
28
34
|
import { z } from 'zod';
|
|
29
35
|
import { resolveCodexEnv, type CodexAuthOptions } from './codex-auth';
|
|
30
36
|
import {
|
|
31
|
-
bridgeReadySchema,
|
|
32
37
|
outboundMessageSchema,
|
|
33
38
|
type InboundMessage,
|
|
34
39
|
type OutboundMessage,
|
|
@@ -37,6 +42,11 @@ import {
|
|
|
37
42
|
type CodexChannel = SandboxChannel<OutboundMessage, InboundMessage>;
|
|
38
43
|
type CodexRespawnStrategy = 'replay' | 'rerun';
|
|
39
44
|
|
|
45
|
+
type WriteSkillsResult = {
|
|
46
|
+
readonly homeDir: string;
|
|
47
|
+
readonly codexHomeDir: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
40
50
|
/*
|
|
41
51
|
* The model the adapter pins when the consumer configures none. The Codex SDK
|
|
42
52
|
* does not report the model it resolves to at runtime (no model field on any
|
|
@@ -258,7 +268,6 @@ export function createCodex(
|
|
|
258
268
|
channel: attachChannel,
|
|
259
269
|
// The live bridge was spawned by another process; no process handle.
|
|
260
270
|
proc: undefined,
|
|
261
|
-
skills: startOpts.skills,
|
|
262
271
|
model: settings.model ?? DEFAULT_CODEX_MODEL,
|
|
263
272
|
reasoningEffort: settings.reasoningEffort,
|
|
264
273
|
webSearch: settings.webSearch,
|
|
@@ -302,10 +311,24 @@ export function createCodex(
|
|
|
302
311
|
|
|
303
312
|
const port = resolveBridgePort(sandboxSession, settings.port);
|
|
304
313
|
const token = randomBytes(32).toString('hex');
|
|
314
|
+
const codexSkillSetup =
|
|
315
|
+
startOpts.skills && startOpts.skills.length > 0
|
|
316
|
+
? await writeSkills({
|
|
317
|
+
sandbox: session,
|
|
318
|
+
skills: startOpts.skills,
|
|
319
|
+
abortSignal: startOpts.abortSignal,
|
|
320
|
+
})
|
|
321
|
+
: undefined;
|
|
305
322
|
const env = {
|
|
306
323
|
...resolveCodexEnv(settings.auth),
|
|
307
324
|
BRIDGE_CHANNEL_TOKEN: token,
|
|
308
325
|
BRIDGE_WS_PORT: String(port),
|
|
326
|
+
...(codexSkillSetup
|
|
327
|
+
? {
|
|
328
|
+
HOME: codexSkillSetup.homeDir,
|
|
329
|
+
CODEX_HOME: codexSkillSetup.codexHomeDir,
|
|
330
|
+
}
|
|
331
|
+
: {}),
|
|
309
332
|
...(respawnStrategy === 'replay'
|
|
310
333
|
? { BRIDGE_REPLAY_FROM_DISK: '1' }
|
|
311
334
|
: {}),
|
|
@@ -318,6 +341,13 @@ export function createCodex(
|
|
|
318
341
|
});
|
|
319
342
|
}
|
|
320
343
|
|
|
344
|
+
await markBridgeStarting({
|
|
345
|
+
sandbox: session,
|
|
346
|
+
bridgeStateDir,
|
|
347
|
+
bridgeType: 'codex',
|
|
348
|
+
abortSignal: startOpts.abortSignal,
|
|
349
|
+
});
|
|
350
|
+
|
|
321
351
|
const proc = await session.spawn({
|
|
322
352
|
command: `node ${BOOTSTRAP_DIR}/bridge.mjs --workdir ${workDir} --bridge-state-dir ${bridgeStateDir} --bootstrap-dir ${BOOTSTRAP_DIR}`,
|
|
323
353
|
env,
|
|
@@ -326,9 +356,27 @@ export function createCodex(
|
|
|
326
356
|
|
|
327
357
|
const { port: boundPort } = await waitForBridgeReady({
|
|
328
358
|
proc,
|
|
359
|
+
sandbox: session,
|
|
360
|
+
bridgeStateDir,
|
|
361
|
+
bridgeType: 'codex',
|
|
329
362
|
timeoutMs,
|
|
330
363
|
abortSignal: startOpts.abortSignal,
|
|
364
|
+
createTimeoutError: () =>
|
|
365
|
+
new Error('codex bridge did not become ready in time.'),
|
|
366
|
+
createExitError: () =>
|
|
367
|
+
new Error('codex bridge exited before becoming ready.'),
|
|
331
368
|
});
|
|
369
|
+
void drainRest(proc.stdout);
|
|
370
|
+
/*
|
|
371
|
+
* Bridge stderr is the only diagnostic channel for what happens
|
|
372
|
+
* inside the sandbox once the bridge is running (uncaught
|
|
373
|
+
* exceptions, Codex SDK errors, network failures). Forward it
|
|
374
|
+
* line-by-line to the host console so a mid-turn bridge crash can
|
|
375
|
+
* be inspected from `pnpm dev` logs without redeploying. The
|
|
376
|
+
* bridge itself writes nothing to stderr in steady state, so this
|
|
377
|
+
* is silent on the happy path.
|
|
378
|
+
*/
|
|
379
|
+
void forwardBridgeStderr(proc.stderr);
|
|
332
380
|
|
|
333
381
|
const wsUrl =
|
|
334
382
|
(await sandboxSession.getPortUrl({
|
|
@@ -355,7 +403,6 @@ export function createCodex(
|
|
|
355
403
|
sessionId: startOpts.sessionId,
|
|
356
404
|
channel,
|
|
357
405
|
proc,
|
|
358
|
-
skills: startOpts.skills,
|
|
359
406
|
model: settings.model ?? DEFAULT_CODEX_MODEL,
|
|
360
407
|
reasoningEffort: settings.reasoningEffort,
|
|
361
408
|
webSearch: settings.webSearch,
|
|
@@ -405,86 +452,119 @@ async function readBridgeAsset(name: string): Promise<string> {
|
|
|
405
452
|
throw lastErr ?? new Error(`bridge asset not found: ${name}`);
|
|
406
453
|
}
|
|
407
454
|
|
|
408
|
-
async function
|
|
409
|
-
|
|
410
|
-
|
|
455
|
+
async function writeSkills({
|
|
456
|
+
sandbox,
|
|
457
|
+
skills,
|
|
411
458
|
abortSignal,
|
|
412
459
|
}: {
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
abortSignal
|
|
416
|
-
}): Promise<
|
|
417
|
-
const
|
|
460
|
+
sandbox: Experimental_SandboxSession;
|
|
461
|
+
skills: ReadonlyArray<HarnessV1Skill>;
|
|
462
|
+
abortSignal?: AbortSignal;
|
|
463
|
+
}): Promise<WriteSkillsResult> {
|
|
464
|
+
for (const skill of skills) {
|
|
465
|
+
safeCodexSkillName(skill.name);
|
|
466
|
+
for (const file of skill.files ?? []) {
|
|
467
|
+
safeCodexSkillFilePath({
|
|
468
|
+
skillName: skill.name,
|
|
469
|
+
filePath: file.path,
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}
|
|
418
473
|
|
|
419
|
-
const
|
|
474
|
+
const homeDir = await resolveSandboxHomeDir({ sandbox, abortSignal });
|
|
475
|
+
const codexHomeDir = path.posix.join(homeDir, '.codex');
|
|
476
|
+
await sandbox.run({
|
|
477
|
+
command: `mkdir -p ${shellQuote(codexHomeDir)}`,
|
|
478
|
+
abortSignal,
|
|
479
|
+
});
|
|
420
480
|
|
|
421
|
-
const
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
text: line,
|
|
449
|
-
schema: bridgeReadySchema,
|
|
450
|
-
});
|
|
451
|
-
if (parsed.success) return { port: parsed.value.port };
|
|
452
|
-
}
|
|
481
|
+
const rootDir = path.posix.join(homeDir, '.agents', 'skills');
|
|
482
|
+
await sandbox.run({
|
|
483
|
+
command: `mkdir -p ${shellQuote(rootDir)}`,
|
|
484
|
+
abortSignal,
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
for (const skill of skills) {
|
|
488
|
+
const name = safeCodexSkillName(skill.name);
|
|
489
|
+
const skillDir = path.posix.join(rootDir, name);
|
|
490
|
+
const content = `---\nname: ${skill.name}\ndescription: ${skill.description}\n---\n\n${skill.content}`;
|
|
491
|
+
|
|
492
|
+
await sandbox.writeTextFile({
|
|
493
|
+
path: path.posix.join(skillDir, 'SKILL.md'),
|
|
494
|
+
content,
|
|
495
|
+
abortSignal,
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
for (const file of skill.files ?? []) {
|
|
499
|
+
const filePath = safeCodexSkillFilePath({
|
|
500
|
+
skillName: skill.name,
|
|
501
|
+
filePath: file.path,
|
|
502
|
+
});
|
|
503
|
+
await sandbox.writeTextFile({
|
|
504
|
+
path: path.posix.join(skillDir, filePath),
|
|
505
|
+
content: file.content,
|
|
506
|
+
abortSignal,
|
|
507
|
+
});
|
|
453
508
|
}
|
|
454
|
-
} finally {
|
|
455
|
-
reader.releaseLock();
|
|
456
|
-
void drainRest(proc.stdout);
|
|
457
|
-
/*
|
|
458
|
-
* Bridge stderr is the only diagnostic channel for what happens
|
|
459
|
-
* inside the sandbox once the bridge is running (uncaught
|
|
460
|
-
* exceptions, Codex SDK errors, network failures). Forward it
|
|
461
|
-
* line-by-line to the host console so a mid-turn bridge crash can
|
|
462
|
-
* be inspected from `pnpm dev` logs without redeploying. The
|
|
463
|
-
* bridge itself writes nothing to stderr in steady state, so this
|
|
464
|
-
* is silent on the happy path.
|
|
465
|
-
*/
|
|
466
|
-
void forwardBridgeStderr(proc.stderr);
|
|
467
509
|
}
|
|
468
|
-
}
|
|
469
510
|
|
|
470
|
-
function lineDecoder() {
|
|
471
|
-
let buffer = '';
|
|
472
511
|
return {
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const lines: string[] = [];
|
|
476
|
-
let nl: number;
|
|
477
|
-
while ((nl = buffer.indexOf('\n')) !== -1) {
|
|
478
|
-
const raw = buffer.slice(0, nl);
|
|
479
|
-
buffer = buffer.slice(nl + 1);
|
|
480
|
-
const line = raw.replace(/\r$/, '').trim();
|
|
481
|
-
if (line.length > 0) lines.push(line);
|
|
482
|
-
}
|
|
483
|
-
return lines;
|
|
484
|
-
},
|
|
512
|
+
homeDir,
|
|
513
|
+
codexHomeDir,
|
|
485
514
|
};
|
|
486
515
|
}
|
|
487
516
|
|
|
517
|
+
async function resolveSandboxHomeDir({
|
|
518
|
+
sandbox,
|
|
519
|
+
abortSignal,
|
|
520
|
+
}: {
|
|
521
|
+
sandbox: Experimental_SandboxSession;
|
|
522
|
+
abortSignal?: AbortSignal;
|
|
523
|
+
}): Promise<string> {
|
|
524
|
+
const result = await sandbox.run({
|
|
525
|
+
command: 'printf "%s" "$HOME"',
|
|
526
|
+
abortSignal,
|
|
527
|
+
});
|
|
528
|
+
const homeDir = result.stdout.trim();
|
|
529
|
+
if (result.exitCode !== 0 || !homeDir || !path.posix.isAbsolute(homeDir)) {
|
|
530
|
+
throw new Error(
|
|
531
|
+
`Unable to resolve sandbox HOME directory: ${result.stderr || result.stdout}`,
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
return homeDir;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function safeCodexSkillName(name: string): string {
|
|
538
|
+
if (!/^[A-Za-z0-9._-]+$/.test(name) || name === '.' || name === '..') {
|
|
539
|
+
throw new Error(`Invalid Codex skill name: ${name}`);
|
|
540
|
+
}
|
|
541
|
+
return name;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function safeCodexSkillFilePath({
|
|
545
|
+
skillName,
|
|
546
|
+
filePath,
|
|
547
|
+
}: {
|
|
548
|
+
skillName: string;
|
|
549
|
+
filePath: string;
|
|
550
|
+
}): string {
|
|
551
|
+
const normalized = path.posix.normalize(filePath);
|
|
552
|
+
if (
|
|
553
|
+
normalized === '.' ||
|
|
554
|
+
normalized.startsWith('../') ||
|
|
555
|
+
path.posix.isAbsolute(normalized)
|
|
556
|
+
) {
|
|
557
|
+
throw new Error(
|
|
558
|
+
`Invalid Codex skill file path for ${skillName}: ${filePath}`,
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
return normalized;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function shellQuote(value: string): string {
|
|
565
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
566
|
+
}
|
|
567
|
+
|
|
488
568
|
async function forwardBridgeStderr(
|
|
489
569
|
stream: ReadableStream<Uint8Array>,
|
|
490
570
|
): Promise<void> {
|
|
@@ -536,7 +616,6 @@ function createSession({
|
|
|
536
616
|
sessionId,
|
|
537
617
|
channel,
|
|
538
618
|
proc,
|
|
539
|
-
skills,
|
|
540
619
|
model,
|
|
541
620
|
reasoningEffort,
|
|
542
621
|
webSearch,
|
|
@@ -554,7 +633,6 @@ function createSession({
|
|
|
554
633
|
channel: CodexChannel;
|
|
555
634
|
/** Undefined on `attach` — the live bridge was spawned by another process. */
|
|
556
635
|
proc: Experimental_SandboxProcess | undefined;
|
|
557
|
-
skills: ReadonlyArray<HarnessV1Skill> | undefined;
|
|
558
636
|
model: string | undefined;
|
|
559
637
|
reasoningEffort: 'low' | 'medium' | 'high' | undefined;
|
|
560
638
|
webSearch: boolean | undefined;
|
|
@@ -758,15 +836,6 @@ function createSession({
|
|
|
758
836
|
reasoningEffort,
|
|
759
837
|
webSearch,
|
|
760
838
|
...(permissionMode ? { permissionMode } : {}),
|
|
761
|
-
...(skills && skills.length > 0
|
|
762
|
-
? {
|
|
763
|
-
skills: skills.map(s => ({
|
|
764
|
-
name: s.name,
|
|
765
|
-
description: s.description,
|
|
766
|
-
content: s.content,
|
|
767
|
-
})),
|
|
768
|
-
}
|
|
769
|
-
: {}),
|
|
770
839
|
...(pendingResumeThreadId
|
|
771
840
|
? { resumeThreadId: pendingResumeThreadId }
|
|
772
841
|
: {}),
|