@ai-sdk/harness-codex 1.0.10 → 1.0.12
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 +13 -0
- package/dist/bridge/host-tool-mcp.mjs +1 -3
- package/dist/bridge/host-tool-mcp.mjs.map +1 -1
- package/dist/bridge/index.mjs +333 -89
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.js +76 -10
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/bridge/cli-relay.ts +125 -41
- package/src/bridge/host-tool-mcp.ts +1 -3
- package/src/bridge/index.ts +141 -101
- package/src/bridge/tool-relay-auth.ts +195 -0
- package/src/codex-bridge-protocol.ts +0 -1
- package/src/codex-harness.ts +102 -20
package/src/codex-harness.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
type InboundMessage,
|
|
39
39
|
type OutboundMessage,
|
|
40
40
|
} from './codex-bridge-protocol';
|
|
41
|
+
import { CLI_SHIM_FILENAME } from './bridge/cli-relay';
|
|
41
42
|
|
|
42
43
|
type CodexChannel = SandboxChannel<OutboundMessage, InboundMessage>;
|
|
43
44
|
type CodexRespawnStrategy = 'replay' | 'rerun';
|
|
@@ -115,12 +116,11 @@ const CODEX_BUILTIN_TOOLS = {
|
|
|
115
116
|
* reinstall the CLI and bridge files on any fresh sandbox from the recipe.
|
|
116
117
|
* Persistence comes from the sandbox provider's snapshot, not the path.
|
|
117
118
|
*
|
|
118
|
-
* The session work dir (`startOpts.sessionWorkDir`)
|
|
119
|
-
*
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
* stop -> snapshot -> resume cycles.
|
|
119
|
+
* The session work dir (`startOpts.sessionWorkDir`) lives under the sandbox's
|
|
120
|
+
* default working directory — the provider's persistent mount — so any files
|
|
121
|
+
* the agent edits survive both detach -> attach and stop -> snapshot -> resume
|
|
122
|
+
* cycles. Harness infra derived from `sandboxSession.defaultWorkingDirectory`
|
|
123
|
+
* lives under `.agent-runs`, outside the agent workdir.
|
|
124
124
|
*/
|
|
125
125
|
const BOOTSTRAP_DIR = '/tmp/harness/codex';
|
|
126
126
|
|
|
@@ -225,6 +225,8 @@ export function createCodex(
|
|
|
225
225
|
const workDir = startOpts.sessionWorkDir;
|
|
226
226
|
const sessionDataDir = `${sandboxSession.defaultWorkingDirectory}/.agent-runs/${startOpts.sessionId}`;
|
|
227
227
|
const bridgeStateDir = `${sessionDataDir}/bridge`;
|
|
228
|
+
const cliShimDir = `${sessionDataDir}/codex`;
|
|
229
|
+
const cliShimPath = `${cliShimDir}/${CLI_SHIM_FILENAME}`;
|
|
228
230
|
const timeoutMs = settings.startupTimeoutMs ?? 120_000;
|
|
229
231
|
|
|
230
232
|
// Normalize each forwarded bridge diagnostics frame into the general
|
|
@@ -266,6 +268,7 @@ export function createCodex(
|
|
|
266
268
|
return createSession({
|
|
267
269
|
sessionId: startOpts.sessionId,
|
|
268
270
|
channel: attachChannel,
|
|
271
|
+
cliShimPath,
|
|
269
272
|
// The live bridge was spawned by another process; no process handle.
|
|
270
273
|
proc: undefined,
|
|
271
274
|
model: settings.model ?? DEFAULT_CODEX_MODEL,
|
|
@@ -349,7 +352,7 @@ export function createCodex(
|
|
|
349
352
|
});
|
|
350
353
|
|
|
351
354
|
const proc = await session.spawn({
|
|
352
|
-
command: `node ${BOOTSTRAP_DIR}/bridge.mjs --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)} --bootstrap-dir ${shellQuote(BOOTSTRAP_DIR)}`,
|
|
355
|
+
command: `node ${BOOTSTRAP_DIR}/bridge.mjs --workdir ${shellQuote(workDir)} --bridge-state-dir ${shellQuote(bridgeStateDir)} --bootstrap-dir ${shellQuote(BOOTSTRAP_DIR)} --cli-shim-dir ${shellQuote(cliShimDir)}`,
|
|
353
356
|
env,
|
|
354
357
|
abortSignal: startOpts.abortSignal,
|
|
355
358
|
});
|
|
@@ -402,6 +405,7 @@ export function createCodex(
|
|
|
402
405
|
return createSession({
|
|
403
406
|
sessionId: startOpts.sessionId,
|
|
404
407
|
channel,
|
|
408
|
+
cliShimPath,
|
|
405
409
|
proc,
|
|
406
410
|
model: settings.model ?? DEFAULT_CODEX_MODEL,
|
|
407
411
|
reasoningEffort: settings.reasoningEffort,
|
|
@@ -615,6 +619,7 @@ function openWebSocket(url: string): Promise<WebSocket> {
|
|
|
615
619
|
function createSession({
|
|
616
620
|
sessionId,
|
|
617
621
|
channel,
|
|
622
|
+
cliShimPath,
|
|
618
623
|
proc,
|
|
619
624
|
model,
|
|
620
625
|
reasoningEffort,
|
|
@@ -631,6 +636,7 @@ function createSession({
|
|
|
631
636
|
}: {
|
|
632
637
|
sessionId: string;
|
|
633
638
|
channel: CodexChannel;
|
|
639
|
+
cliShimPath: string;
|
|
634
640
|
/** Undefined on `attach` — the live bridge was spawned by another process. */
|
|
635
641
|
proc: Experimental_SandboxProcess | undefined;
|
|
636
642
|
model: string | undefined;
|
|
@@ -658,10 +664,10 @@ function createSession({
|
|
|
658
664
|
? resumeThreadId
|
|
659
665
|
: undefined;
|
|
660
666
|
/*
|
|
661
|
-
*
|
|
662
|
-
* only. A resumed session (attach/replay/rerun) already carried
|
|
663
|
-
* original first message (preserved in the persisted thread), so it
|
|
664
|
-
* "applied".
|
|
667
|
+
* Initial prompt guidance is prepended to the first user message of a fresh
|
|
668
|
+
* session only. A resumed session (attach/replay/rerun) already carried it
|
|
669
|
+
* in its original first message (preserved in the persisted thread), so it
|
|
670
|
+
* starts "applied".
|
|
665
671
|
*/
|
|
666
672
|
let instructionsApplied = isResume;
|
|
667
673
|
|
|
@@ -842,19 +848,34 @@ function createSession({
|
|
|
842
848
|
abortSignal: promptOpts.abortSignal,
|
|
843
849
|
});
|
|
844
850
|
|
|
845
|
-
const
|
|
846
|
-
|
|
851
|
+
const tools = (promptOpts.tools ?? []).map(t => ({
|
|
852
|
+
name: t.name,
|
|
853
|
+
description: t.description,
|
|
854
|
+
inputSchema: t.inputSchema,
|
|
855
|
+
}));
|
|
856
|
+
let promptText = extractUserText(promptOpts.prompt);
|
|
857
|
+
if (!instructionsApplied) {
|
|
858
|
+
const instructions =
|
|
859
|
+
(promptOpts.instructions ? promptOpts.instructions + '\n\n' : '') +
|
|
860
|
+
'Only respond with your `final` message once you have fully addressed the user request.';
|
|
861
|
+
promptText = frameInitialPromptGuidance({
|
|
862
|
+
instructions,
|
|
863
|
+
toolUsageBlock:
|
|
864
|
+
tools.length > 0
|
|
865
|
+
? composeToolUsageInstructions({
|
|
866
|
+
tools,
|
|
867
|
+
cliShimPath,
|
|
868
|
+
})
|
|
869
|
+
: undefined,
|
|
870
|
+
userText: promptText,
|
|
871
|
+
});
|
|
872
|
+
}
|
|
847
873
|
instructionsApplied = true;
|
|
848
874
|
|
|
849
875
|
const startMessage = {
|
|
850
876
|
type: 'start' as const,
|
|
851
|
-
prompt:
|
|
852
|
-
|
|
853
|
-
tools: (promptOpts.tools ?? []).map(t => ({
|
|
854
|
-
name: t.name,
|
|
855
|
-
description: t.description,
|
|
856
|
-
inputSchema: t.inputSchema,
|
|
857
|
-
})),
|
|
877
|
+
prompt: promptText,
|
|
878
|
+
tools,
|
|
858
879
|
model,
|
|
859
880
|
reasoningEffort,
|
|
860
881
|
webSearch,
|
|
@@ -1098,6 +1119,67 @@ function createSession({
|
|
|
1098
1119
|
};
|
|
1099
1120
|
}
|
|
1100
1121
|
|
|
1122
|
+
/*
|
|
1123
|
+
* Frame session instructions, host-tool relay guidance, and the user's text so
|
|
1124
|
+
* Codex treats the prepended blocks as operating guidance rather than user
|
|
1125
|
+
* prose. Applied only to the first user message of a fresh session.
|
|
1126
|
+
*/
|
|
1127
|
+
function frameInitialPromptGuidance({
|
|
1128
|
+
instructions,
|
|
1129
|
+
toolUsageBlock,
|
|
1130
|
+
userText,
|
|
1131
|
+
}: {
|
|
1132
|
+
instructions: string | undefined;
|
|
1133
|
+
toolUsageBlock: string | undefined;
|
|
1134
|
+
userText: string;
|
|
1135
|
+
}): string {
|
|
1136
|
+
const blocks: string[] = [];
|
|
1137
|
+
if (instructions) {
|
|
1138
|
+
blocks.push(
|
|
1139
|
+
'<session-instructions>\n' +
|
|
1140
|
+
'The block below is operating guidance from the system, not a message from the user — follow it, but do not mention it or attribute it to the user.\n\n' +
|
|
1141
|
+
`${instructions}\n` +
|
|
1142
|
+
'</session-instructions>',
|
|
1143
|
+
);
|
|
1144
|
+
}
|
|
1145
|
+
if (toolUsageBlock) blocks.push(toolUsageBlock);
|
|
1146
|
+
if (blocks.length === 0) return userText;
|
|
1147
|
+
return `${blocks.join('\n\n')}\n\n<user-message>\n${userText}\n</user-message>`;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
function composeToolUsageInstructions({
|
|
1151
|
+
tools,
|
|
1152
|
+
cliShimPath,
|
|
1153
|
+
}: {
|
|
1154
|
+
tools: ReadonlyArray<{
|
|
1155
|
+
name: string;
|
|
1156
|
+
description?: string;
|
|
1157
|
+
inputSchema?: unknown;
|
|
1158
|
+
}>;
|
|
1159
|
+
cliShimPath: string;
|
|
1160
|
+
}): string {
|
|
1161
|
+
const lines: string[] = [
|
|
1162
|
+
'<host-tool-instructions>',
|
|
1163
|
+
'You have access to the following host-provided tools. To use one, run the following command via your built-in `bash` tool:',
|
|
1164
|
+
'',
|
|
1165
|
+
` node ${cliShimPath} <toolName> '<jsonInput>'`,
|
|
1166
|
+
'',
|
|
1167
|
+
'The script prints the JSON result to stdout. Do not invent another way to call these tools — only this CLI invocation will work. Pass the JSON input as a single-quoted argument.',
|
|
1168
|
+
'For every user request that depends on a host-provided tool, run a separate CLI invocation for each needed tool call in the current turn before answering. Do not reuse previous tool results, and do not say you used a host tool unless the command has completed in the current turn.',
|
|
1169
|
+
'',
|
|
1170
|
+
];
|
|
1171
|
+
for (const toolSpec of tools) {
|
|
1172
|
+
lines.push(
|
|
1173
|
+
`- **${toolSpec.name}**${toolSpec.description ? ': ' + toolSpec.description : ''}`,
|
|
1174
|
+
);
|
|
1175
|
+
lines.push(
|
|
1176
|
+
` - Input schema: \`${JSON.stringify(toolSpec.inputSchema ?? {})}\``,
|
|
1177
|
+
);
|
|
1178
|
+
}
|
|
1179
|
+
lines.push('</host-tool-instructions>');
|
|
1180
|
+
return lines.join('\n');
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1101
1183
|
/*
|
|
1102
1184
|
* Reduce a `HarnessV1Prompt` to the plain user text the bridge forwards
|
|
1103
1185
|
* to the Codex SDK. File and image parts on the message are not yet
|