@ai-sdk/harness-codex 1.0.94 → 1.0.95

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,4 +1,4 @@
1
- import { randomBytes } from 'node:crypto';
1
+ import { createHash, randomBytes } from 'node:crypto';
2
2
  import path from 'node:path';
3
3
  import {
4
4
  commonTool,
@@ -36,6 +36,7 @@ import {
36
36
  warnCredentialBrokeringUnavailable,
37
37
  waitForBridgeReady,
38
38
  writeSkills as writeHarnessSkills,
39
+ type WriteSkillsResult,
39
40
  } from '@ai-sdk/harness/utils';
40
41
  import {
41
42
  type Experimental_SandboxProcess,
@@ -66,11 +67,6 @@ import { VERSION } from './version';
66
67
  type CodexChannel = SandboxChannel<OutboundMessage, InboundMessage>;
67
68
  type CodexRespawnStrategy = 'replay' | 'rerun';
68
69
 
69
- type WriteSkillsResult = {
70
- readonly homeDir: string;
71
- readonly codexHomeDir: string;
72
- };
73
-
74
70
  /*
75
71
  * This intentionally is not the latest Codex model. Newer GPT-5.6 models use
76
72
  * Responses Lite, which does not expose their code-mode tools as callable
@@ -110,6 +106,8 @@ export type CodexHarnessSettings = {
110
106
  /**
111
107
  * OpenAI model id the underlying `codex` CLI should use. Leaving this unset
112
108
  * pins the adapter default (`DEFAULT_CODEX_MODEL`).
109
+ *
110
+ * @deprecated Use `model` on `HarnessAgent` instead.
113
111
  */
114
112
  readonly model?: string;
115
113
  /**
@@ -189,6 +187,7 @@ const codexBridgeCoordsSchema = z.object({
189
187
  */
190
188
  const codexResumeStateSchema = z.object({
191
189
  threadId: z.string().optional(),
190
+ turnConfigurationFingerprint: z.string().optional(),
192
191
  bridge: codexBridgeCoordsSchema.optional(),
193
192
  sandboxCredentialEnvironment: z.record(z.string(), z.string()).optional(),
194
193
  });
@@ -206,6 +205,7 @@ export function createCodex(
206
205
  lifecycleStateSchema: codexResumeStateSchema,
207
206
  getBootstrap: getCodexBootstrap,
208
207
  doStart: async startOpts => {
208
+ const model = startOpts.model ?? settings.model ?? DEFAULT_CODEX_MODEL;
209
209
  if (startOpts.builtinToolFiltering != null) {
210
210
  throw new HarnessCapabilityUnsupportedError({
211
211
  message:
@@ -251,6 +251,7 @@ export function createCodex(
251
251
  isResume && typeof lifecycleState?.data === 'object'
252
252
  ? (lifecycleState.data as {
253
253
  threadId?: unknown;
254
+ turnConfigurationFingerprint?: unknown;
254
255
  bridge?: CodexBridgeCoords;
255
256
  sandboxCredentialEnvironment?: Record<string, string>;
256
257
  })
@@ -260,6 +261,10 @@ export function createCodex(
260
261
  typeof resumeThreadId === 'string' && resumeThreadId.length > 0
261
262
  ? resumeThreadId
262
263
  : undefined;
264
+ const turnConfigurationFingerprint =
265
+ typeof resumeData?.turnConfigurationFingerprint === 'string'
266
+ ? resumeData.turnConfigurationFingerprint
267
+ : undefined;
263
268
  const coords = resumeData?.bridge;
264
269
  const authenticationMode = resolveCodexAuthenticationMode(settings.auth);
265
270
  const resolvedAuthEnvironment = resolveCodexEnv(settings.auth);
@@ -315,6 +320,10 @@ export function createCodex(
315
320
  );
316
321
 
317
322
  const workDir = startOpts.sessionWorkDir;
323
+ const sandboxHomeDir = await resolveSandboxHomeDir({
324
+ sandbox: toolSafeSandboxSession,
325
+ abortSignal: startOpts.abortSignal,
326
+ });
318
327
  const sessionDataDir = `${defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`;
319
328
  const bridgeStateDir = `${sessionDataDir}/bridge`;
320
329
  const cliShimDir = `${sessionDataDir}/codex`;
@@ -372,7 +381,7 @@ export function createCodex(
372
381
  cliShimPath,
373
382
  // The live bridge was spawned by another process; no process handle.
374
383
  proc: undefined,
375
- model: settings.model ?? DEFAULT_CODEX_MODEL,
384
+ model,
376
385
  reasoningEffort: settings.reasoningEffort,
377
386
  webSearch: settings.webSearch,
378
387
  codexConfig: settings.codexConfig,
@@ -387,6 +396,9 @@ export function createCodex(
387
396
  sandboxCredentialEnvironment,
388
397
  debug: startOpts.observability?.debug,
389
398
  permissionMode: startOpts.permissionMode,
399
+ sandbox: toolSafeSandboxSession,
400
+ sandboxHomeDir,
401
+ turnConfigurationFingerprint,
390
402
  });
391
403
  } catch {
392
404
  // Bridge no longer reachable — recover by respawning below.
@@ -432,25 +444,11 @@ export function createCodex(
432
444
  CODEX_CREDENTIAL_ENVIRONMENT_VARIABLES,
433
445
  credentialForwarding: settings.credentialForwarding,
434
446
  });
435
- const codexSkillSetup =
436
- startOpts.skills && startOpts.skills.length > 0
437
- ? await writeCodexSkills({
438
- sandbox: toolSafeSandboxSession,
439
- skills: startOpts.skills,
440
- abortSignal: startOpts.abortSignal,
441
- })
442
- : undefined;
443
447
  const env = {
444
448
  ...forwardedAuthEnvironment,
445
449
  AI_SDK_HARNESS_CLIENT_APP: CODEX_CLIENT_APP,
446
450
  BRIDGE_CHANNEL_TOKEN: token,
447
451
  BRIDGE_WS_PORT: String(port),
448
- ...(codexSkillSetup
449
- ? {
450
- HOME: codexSkillSetup.homeDir,
451
- CODEX_HOME: codexSkillSetup.codexHomeDir,
452
- }
453
- : {}),
454
452
  ...(respawnStrategy === 'replay'
455
453
  ? { BRIDGE_REPLAY_FROM_DISK: '1' }
456
454
  : {}),
@@ -537,7 +535,7 @@ export function createCodex(
537
535
  channel,
538
536
  cliShimPath,
539
537
  proc,
540
- model: settings.model ?? DEFAULT_CODEX_MODEL,
538
+ model,
541
539
  reasoningEffort: settings.reasoningEffort,
542
540
  webSearch: settings.webSearch,
543
541
  codexConfig: settings.codexConfig,
@@ -552,6 +550,9 @@ export function createCodex(
552
550
  sandboxCredentialEnvironment,
553
551
  debug: startOpts.observability?.debug,
554
552
  permissionMode: startOpts.permissionMode,
553
+ sandbox: toolSafeSandboxSession,
554
+ sandboxHomeDir,
555
+ turnConfigurationFingerprint,
555
556
  });
556
557
  },
557
558
  };
@@ -624,22 +625,17 @@ async function resolveBridgeEndpoint({
624
625
 
625
626
  async function writeCodexSkills({
626
627
  sandbox,
628
+ sandboxHomeDir,
627
629
  skills,
628
630
  abortSignal,
629
631
  }: {
630
632
  sandbox: SandboxSession;
633
+ sandboxHomeDir: string;
631
634
  skills: ReadonlyArray<HarnessV1Skill>;
632
635
  abortSignal?: AbortSignal;
633
636
  }): Promise<WriteSkillsResult> {
634
- const homeDir = await resolveSandboxHomeDir({ sandbox, abortSignal });
635
- const codexHomeDir = path.posix.join(homeDir, '.codex');
636
- await sandbox.run({
637
- command: `mkdir -p ${shellQuote(codexHomeDir)}`,
638
- abortSignal,
639
- });
640
-
641
- const rootDir = path.posix.join(homeDir, '.agents', 'skills');
642
- await writeHarnessSkills({
637
+ const rootDir = path.posix.join(sandboxHomeDir, '.agents', 'skills');
638
+ return writeHarnessSkills({
643
639
  sandbox,
644
640
  rootDir,
645
641
  skills,
@@ -648,11 +644,6 @@ async function writeCodexSkills({
648
644
  invalidSkillFilePathMessage: ({ skillName, filePath }) =>
649
645
  `Invalid Codex skill file path for ${skillName}: ${filePath}`,
650
646
  });
651
-
652
- return {
653
- homeDir,
654
- codexHomeDir,
655
- };
656
647
  }
657
648
 
658
649
  function openWebSocket({
@@ -708,6 +699,9 @@ function createSession({
708
699
  sandboxCredentialEnvironment,
709
700
  debug,
710
701
  permissionMode,
702
+ sandbox,
703
+ sandboxHomeDir,
704
+ turnConfigurationFingerprint,
711
705
  }: {
712
706
  sessionId: string;
713
707
  channel: CodexChannel;
@@ -729,6 +723,9 @@ function createSession({
729
723
  sandboxCredentialEnvironment: Record<string, string> | undefined;
730
724
  debug: HarnessV1DebugConfig | undefined;
731
725
  permissionMode: HarnessV1PermissionMode | undefined;
726
+ sandbox: SandboxSession;
727
+ sandboxHomeDir: string;
728
+ turnConfigurationFingerprint: string | undefined;
732
729
  }): HarnessV1Session {
733
730
  let stopped = false;
734
731
  let stopPromise: Promise<void> | undefined;
@@ -755,10 +752,55 @@ function createSession({
755
752
  * can include a thread id even before this process has run a turn.
756
753
  */
757
754
  let latestThreadId = resumeThreadId;
755
+ let latestTurnConfigurationFingerprint = turnConfigurationFingerprint;
758
756
  channel.on('bridge-thread', msg => {
759
757
  latestThreadId = msg.threadId;
760
758
  });
761
759
 
760
+ const synchronizeTurnConfiguration = async ({
761
+ skills,
762
+ instructions,
763
+ tools,
764
+ abortSignal,
765
+ }: {
766
+ skills: ReadonlyArray<HarnessV1Skill>;
767
+ instructions: string | undefined;
768
+ tools: ReadonlyArray<{
769
+ name: string;
770
+ description?: string;
771
+ inputSchema: unknown;
772
+ }>;
773
+ abortSignal?: AbortSignal;
774
+ }): Promise<{ restartThread: boolean }> => {
775
+ const skillsResult = await writeCodexSkills({
776
+ sandbox,
777
+ sandboxHomeDir,
778
+ skills,
779
+ abortSignal,
780
+ });
781
+ const nextFingerprint = fingerprintCodexTurnConfiguration({
782
+ instructions,
783
+ tools,
784
+ });
785
+ const restartThread =
786
+ latestThreadId != null &&
787
+ (skillsResult.changed ||
788
+ (latestTurnConfigurationFingerprint != null &&
789
+ latestTurnConfigurationFingerprint !== nextFingerprint));
790
+ latestTurnConfigurationFingerprint = nextFingerprint;
791
+ if (restartThread) {
792
+ /*
793
+ * `codex exec resume` retains the native thread's original developer
794
+ * instructions and skill catalog. A fresh native thread is therefore
795
+ * required for replacement semantics. Host-tool guidance is framed
796
+ * again because that guidance also belongs to the new native thread.
797
+ */
798
+ initialPromptGuidanceApplied = false;
799
+ pendingResumeThreadId = undefined;
800
+ }
801
+ return { restartThread };
802
+ };
803
+
762
804
  /*
763
805
  * Wire the channel into one turn's worth of events and return the control
764
806
  * surface. Shared by `doPromptTurn` (which sends a `start` afterwards) and
@@ -928,16 +970,22 @@ function createSession({
928
970
  harnessId: 'codex',
929
971
  });
930
972
  }
931
- const turn = wireTurn({
932
- emit: promptOpts.emit,
933
- abortSignal: promptOpts.abortSignal,
934
- });
935
-
936
973
  const tools = (promptOpts.tools ?? []).map(t => ({
937
974
  name: t.name,
938
975
  description: t.description,
939
976
  inputSchema: t.inputSchema,
940
977
  }));
978
+ const { restartThread } = await synchronizeTurnConfiguration({
979
+ skills: promptOpts.skills,
980
+ instructions: promptOpts.instructions,
981
+ tools,
982
+ abortSignal: promptOpts.abortSignal,
983
+ });
984
+ const turn = wireTurn({
985
+ emit: promptOpts.emit,
986
+ abortSignal: promptOpts.abortSignal,
987
+ });
988
+
941
989
  let promptText = extractUserText(promptOpts.prompt);
942
990
  if (!initialPromptGuidanceApplied) {
943
991
  promptText = frameInitialPromptGuidance({
@@ -972,6 +1020,7 @@ function createSession({
972
1020
  ...(pendingResumeThreadId
973
1021
  ? { resumeThreadId: pendingResumeThreadId }
974
1022
  : {}),
1023
+ ...(restartThread ? { restartThread: true } : {}),
975
1024
  ...(debug ? { debug } : {}),
976
1025
  };
977
1026
  pendingResumeThreadId = undefined;
@@ -990,6 +1039,17 @@ function createSession({
990
1039
  harnessId: 'codex',
991
1040
  });
992
1041
  }
1042
+ const tools = (continueOpts.tools ?? []).map(t => ({
1043
+ name: t.name,
1044
+ description: t.description,
1045
+ inputSchema: t.inputSchema,
1046
+ }));
1047
+ const { restartThread } = await synchronizeTurnConfiguration({
1048
+ skills: continueOpts.skills,
1049
+ instructions: continueOpts.instructions,
1050
+ tools,
1051
+ abortSignal: continueOpts.abortSignal,
1052
+ });
993
1053
  const turn = wireTurn({
994
1054
  emit: continueOpts.emit,
995
1055
  abortSignal: continueOpts.abortSignal,
@@ -1021,11 +1081,7 @@ function createSession({
1021
1081
  * empty text block trips the Anthropic API's `cache_control` rule).
1022
1082
  */
1023
1083
  prompt: 'Continue.',
1024
- tools: (continueOpts.tools ?? []).map(t => ({
1025
- name: t.name,
1026
- description: t.description,
1027
- inputSchema: t.inputSchema,
1028
- })),
1084
+ tools,
1029
1085
  ...(continueOpts.responseFormat == null
1030
1086
  ? {}
1031
1087
  : { responseFormat: continueOpts.responseFormat }),
@@ -1039,6 +1095,7 @@ function createSession({
1039
1095
  ...(mcpServers == null ? {} : { mcpServers }),
1040
1096
  ...(permissionMode ? { permissionMode } : {}),
1041
1097
  ...(threadId ? { resumeThreadId: threadId } : {}),
1098
+ ...(restartThread ? { restartThread: true } : {}),
1042
1099
  ...(debug ? { debug } : {}),
1043
1100
  }),
1044
1101
  );
@@ -1074,6 +1131,12 @@ function createSession({
1074
1131
  specificationVersion: 'harness-v1',
1075
1132
  data: {
1076
1133
  ...(latestThreadId ? { threadId: latestThreadId } : {}),
1134
+ ...(latestTurnConfigurationFingerprint
1135
+ ? {
1136
+ turnConfigurationFingerprint:
1137
+ latestTurnConfigurationFingerprint,
1138
+ }
1139
+ : {}),
1077
1140
  ...(sandboxCredentialEnvironment == null
1078
1141
  ? {}
1079
1142
  : { sandboxCredentialEnvironment }),
@@ -1195,6 +1258,12 @@ function createSession({
1195
1258
  specificationVersion: 'harness-v1',
1196
1259
  data: {
1197
1260
  ...lifecycleData,
1261
+ ...(latestTurnConfigurationFingerprint
1262
+ ? {
1263
+ turnConfigurationFingerprint:
1264
+ latestTurnConfigurationFingerprint,
1265
+ }
1266
+ : {}),
1198
1267
  ...(sandboxCredentialEnvironment == null
1199
1268
  ? {}
1200
1269
  : { sandboxCredentialEnvironment }),
@@ -1224,6 +1293,12 @@ function createSession({
1224
1293
  specificationVersion: 'harness-v1',
1225
1294
  data: {
1226
1295
  ...(latestThreadId ? { threadId: latestThreadId } : {}),
1296
+ ...(latestTurnConfigurationFingerprint
1297
+ ? {
1298
+ turnConfigurationFingerprint:
1299
+ latestTurnConfigurationFingerprint,
1300
+ }
1301
+ : {}),
1227
1302
  ...(sandboxCredentialEnvironment == null
1228
1303
  ? {}
1229
1304
  : { sandboxCredentialEnvironment }),
@@ -1240,6 +1315,22 @@ function createSession({
1240
1315
  };
1241
1316
  }
1242
1317
 
1318
+ function fingerprintCodexTurnConfiguration({
1319
+ instructions,
1320
+ tools,
1321
+ }: {
1322
+ instructions: string | undefined;
1323
+ tools: ReadonlyArray<{
1324
+ name: string;
1325
+ description?: string;
1326
+ inputSchema: unknown;
1327
+ }>;
1328
+ }): string {
1329
+ return createHash('sha256')
1330
+ .update(JSON.stringify({ instructions: instructions ?? null, tools }))
1331
+ .digest('hex');
1332
+ }
1333
+
1243
1334
  /*
1244
1335
  * Frame host-tool relay guidance and the user's text so Codex treats the
1245
1336
  * prepended block as operating guidance rather than user prose. Applied only