@ai-sdk/harness-codex 0.0.0-6b196531-20260710185421
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 +391 -0
- package/LICENSE +13 -0
- package/README.md +73 -0
- package/dist/bridge/host-tool-mcp.mjs +103 -0
- package/dist/bridge/host-tool-mcp.mjs.map +1 -0
- package/dist/bridge/index.mjs +1297 -0
- package/dist/bridge/index.mjs.map +1 -0
- package/dist/bridge/package.json +12 -0
- package/dist/bridge/pnpm-lock.yaml +879 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +899 -0
- package/dist/index.js.map +1 -0
- package/package.json +77 -0
- package/src/bridge/cli-relay.ts +198 -0
- package/src/bridge/codex-step-tracker.ts +83 -0
- package/src/bridge/host-tool-mcp.ts +160 -0
- package/src/bridge/index.ts +746 -0
- package/src/bridge/package.json +12 -0
- package/src/bridge/pnpm-lock.yaml +879 -0
- package/src/bridge/tool-relay-auth.ts +195 -0
- package/src/codex-auth.ts +119 -0
- package/src/codex-bridge-protocol.ts +38 -0
- package/src/codex-harness.ts +1132 -0
- package/src/index.ts +13 -0
- package/src/version.ts +6 -0
|
@@ -0,0 +1,1297 @@
|
|
|
1
|
+
// ../harness/dist/bridge/index.js
|
|
2
|
+
import { appendFile, mkdir, writeFile } from "fs/promises";
|
|
3
|
+
import { existsSync, readFileSync } from "fs";
|
|
4
|
+
import { env as procEnv, pid, stdout } from "process";
|
|
5
|
+
import { WebSocketServer } from "ws";
|
|
6
|
+
var DEBUG_LEVEL_WEIGHT = {
|
|
7
|
+
error: 0,
|
|
8
|
+
warn: 1,
|
|
9
|
+
info: 2,
|
|
10
|
+
debug: 3,
|
|
11
|
+
trace: 4
|
|
12
|
+
};
|
|
13
|
+
function subsystemMatches(filters, subsystem) {
|
|
14
|
+
if (!filters || filters.length === 0) return true;
|
|
15
|
+
return filters.some(
|
|
16
|
+
(filter) => subsystem === filter || subsystem.startsWith(`${filter}.`)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
function formatBridgeError(err) {
|
|
20
|
+
if (err instanceof Error) {
|
|
21
|
+
return { name: err.name, message: err.message, stack: err.stack };
|
|
22
|
+
}
|
|
23
|
+
if (typeof err === "string") {
|
|
24
|
+
return { message: err };
|
|
25
|
+
}
|
|
26
|
+
if (err !== null && typeof err === "object") {
|
|
27
|
+
try {
|
|
28
|
+
return { message: JSON.stringify(err) };
|
|
29
|
+
} catch {
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return { message: String(err) };
|
|
33
|
+
}
|
|
34
|
+
function parseEnvList(value) {
|
|
35
|
+
if (!value) return void 0;
|
|
36
|
+
const items = value.split(",").map((item) => item.trim()).filter(Boolean);
|
|
37
|
+
return items.length > 0 ? items : void 0;
|
|
38
|
+
}
|
|
39
|
+
var ENV_TRUTHY = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
|
|
40
|
+
var WS_OPEN = 1;
|
|
41
|
+
async function runBridge(options) {
|
|
42
|
+
const { bridgeType, bridgeStateDir: bridgeStateDir2, onStart, onDetach } = options;
|
|
43
|
+
const expectedToken = options.token ?? procEnv.BRIDGE_CHANNEL_TOKEN ?? "";
|
|
44
|
+
const bridgeWsPort = options.port ?? parseInt(procEnv.BRIDGE_WS_PORT ?? "0", 10);
|
|
45
|
+
const bridgeMetaPath = `${bridgeStateDir2}/bridge-meta.json`;
|
|
46
|
+
const startConfigPath = `${bridgeStateDir2}/start-config.json`;
|
|
47
|
+
const rerunStartConfigPath = `${bridgeStateDir2}/rerun-start-config.json`;
|
|
48
|
+
const eventLogPath = `${bridgeStateDir2}/event-log.ndjson`;
|
|
49
|
+
try {
|
|
50
|
+
await mkdir(bridgeStateDir2, { recursive: true });
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
let currentBoundPort = 0;
|
|
54
|
+
let currentTurnState = "init";
|
|
55
|
+
let activeSocket;
|
|
56
|
+
let isFirstTurn = true;
|
|
57
|
+
let turnAbort;
|
|
58
|
+
let currentUserMessages;
|
|
59
|
+
let currentInterruptHandler;
|
|
60
|
+
let debugConfig;
|
|
61
|
+
let consoleCaptureInstalled = false;
|
|
62
|
+
const envDebugEnabled = ENV_TRUTHY.has(
|
|
63
|
+
(procEnv.HARNESS_DEBUG ?? "").toLowerCase()
|
|
64
|
+
);
|
|
65
|
+
let seqCounter = 0;
|
|
66
|
+
let eventLog = [];
|
|
67
|
+
let diskBuffer = "";
|
|
68
|
+
let flushPromise = null;
|
|
69
|
+
const flushEventsToDisk = async () => {
|
|
70
|
+
while (diskBuffer.length > 0) {
|
|
71
|
+
const buf = diskBuffer;
|
|
72
|
+
diskBuffer = "";
|
|
73
|
+
await appendFile(eventLogPath, buf).catch(() => {
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
const scheduleEventFlush = () => {
|
|
78
|
+
if (flushPromise) return;
|
|
79
|
+
flushPromise = new Promise((resolve) => {
|
|
80
|
+
setImmediate(() => {
|
|
81
|
+
void flushEventsToDisk().finally(resolve);
|
|
82
|
+
});
|
|
83
|
+
}).finally(() => {
|
|
84
|
+
flushPromise = null;
|
|
85
|
+
if (diskBuffer.length > 0) {
|
|
86
|
+
scheduleEventFlush();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
const flushPendingEventsToDisk = async () => {
|
|
91
|
+
if (diskBuffer.length > 0 && !flushPromise) {
|
|
92
|
+
scheduleEventFlush();
|
|
93
|
+
}
|
|
94
|
+
let inFlight = flushPromise;
|
|
95
|
+
while (inFlight) {
|
|
96
|
+
await inFlight;
|
|
97
|
+
inFlight = flushPromise;
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
const replayFromDisk = procEnv.BRIDGE_REPLAY_FROM_DISK === "1";
|
|
101
|
+
if (replayFromDisk && existsSync(eventLogPath)) {
|
|
102
|
+
try {
|
|
103
|
+
const lines = readFileSync(eventLogPath, "utf8").split("\n").map((line) => line.trim()).filter(Boolean);
|
|
104
|
+
eventLog = lines.map((line) => ({
|
|
105
|
+
seq: JSON.parse(line).seq,
|
|
106
|
+
line
|
|
107
|
+
}));
|
|
108
|
+
seqCounter = eventLog.at(-1)?.seq ?? 0;
|
|
109
|
+
} catch {
|
|
110
|
+
eventLog = [];
|
|
111
|
+
seqCounter = 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
const pendingToolResults = /* @__PURE__ */ new Map();
|
|
115
|
+
const pendingToolApprovals = /* @__PURE__ */ new Map();
|
|
116
|
+
const writeBridgeMeta = async (state) => {
|
|
117
|
+
try {
|
|
118
|
+
await writeFile(
|
|
119
|
+
bridgeMetaPath,
|
|
120
|
+
JSON.stringify({
|
|
121
|
+
type: bridgeType,
|
|
122
|
+
port: currentBoundPort,
|
|
123
|
+
state,
|
|
124
|
+
pid
|
|
125
|
+
})
|
|
126
|
+
);
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
const writeStartConfig = async (start) => {
|
|
131
|
+
try {
|
|
132
|
+
const serialized = JSON.stringify(start);
|
|
133
|
+
await writeFile(startConfigPath, serialized);
|
|
134
|
+
if (!existsSync(rerunStartConfigPath)) {
|
|
135
|
+
await writeFile(rerunStartConfigPath, serialized);
|
|
136
|
+
}
|
|
137
|
+
} catch {
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const sendControl = (msg) => {
|
|
141
|
+
if (activeSocket?.readyState === WS_OPEN) {
|
|
142
|
+
try {
|
|
143
|
+
activeSocket.send(JSON.stringify(msg));
|
|
144
|
+
} catch {
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const emit = (event) => {
|
|
149
|
+
const seq = ++seqCounter;
|
|
150
|
+
const line = JSON.stringify({ ...event, seq });
|
|
151
|
+
eventLog.push({ seq, line });
|
|
152
|
+
diskBuffer += `${line}
|
|
153
|
+
`;
|
|
154
|
+
scheduleEventFlush();
|
|
155
|
+
if (activeSocket?.readyState === WS_OPEN) {
|
|
156
|
+
try {
|
|
157
|
+
activeSocket.send(line);
|
|
158
|
+
} catch {
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
const replay = (ws, afterSeq) => {
|
|
163
|
+
for (const entry of eventLog) {
|
|
164
|
+
if (entry.seq > afterSeq && ws.readyState === WS_OPEN) {
|
|
165
|
+
ws.send(entry.line);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
const shouldEmitDebugEvent = (level, subsystem) => {
|
|
170
|
+
if (!debugConfig?.enabled) return false;
|
|
171
|
+
const threshold = debugConfig.level ?? "debug";
|
|
172
|
+
if (DEBUG_LEVEL_WEIGHT[level] > DEBUG_LEVEL_WEIGHT[threshold]) return false;
|
|
173
|
+
return subsystemMatches(debugConfig.subsystems, subsystem);
|
|
174
|
+
};
|
|
175
|
+
const rawStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
176
|
+
const rawStderrWrite = process.stderr.write.bind(process.stderr);
|
|
177
|
+
const writeErrorToStderr = (input) => {
|
|
178
|
+
try {
|
|
179
|
+
const formatted = formatBridgeError(input.error);
|
|
180
|
+
rawStderrWrite(
|
|
181
|
+
`[harness:${bridgeType}:error] ${input.message}: ${formatted.message}
|
|
182
|
+
`
|
|
183
|
+
);
|
|
184
|
+
if (formatted.stack) {
|
|
185
|
+
rawStderrWrite(`${formatted.stack}
|
|
186
|
+
`);
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
const emitWarning = (input) => {
|
|
192
|
+
try {
|
|
193
|
+
for (const line of input.message.split("\n")) {
|
|
194
|
+
if (line.trim().length > 0) {
|
|
195
|
+
rawStderrWrite(`[harness:${bridgeType}:warn] ${line}
|
|
196
|
+
`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
const emitError = (input) => {
|
|
203
|
+
writeErrorToStderr({
|
|
204
|
+
message: input.message ?? "bridge error",
|
|
205
|
+
error: input.error
|
|
206
|
+
});
|
|
207
|
+
emit({ type: "error", error: serialiseError(input.error) });
|
|
208
|
+
};
|
|
209
|
+
const installConsoleCapture = () => {
|
|
210
|
+
if (consoleCaptureInstalled) return;
|
|
211
|
+
consoleCaptureInstalled = true;
|
|
212
|
+
const buffers = {
|
|
213
|
+
stdout: "",
|
|
214
|
+
stderr: ""
|
|
215
|
+
};
|
|
216
|
+
const patch = (stream, raw) => (chunk, encoding, cb) => {
|
|
217
|
+
if (debugConfig?.enabled) {
|
|
218
|
+
try {
|
|
219
|
+
const enc = typeof encoding === "string" ? encoding : "utf8";
|
|
220
|
+
const text = typeof chunk === "string" ? chunk : Buffer.from(chunk).toString(
|
|
221
|
+
enc
|
|
222
|
+
);
|
|
223
|
+
const combined = buffers[stream] + text.replace(/\r\n/g, "\n");
|
|
224
|
+
const parts = combined.split("\n");
|
|
225
|
+
buffers[stream] = parts.pop() ?? "";
|
|
226
|
+
for (const line of parts) {
|
|
227
|
+
const trimmed = line.replace(/\s+$/, "");
|
|
228
|
+
if (trimmed) {
|
|
229
|
+
emit({
|
|
230
|
+
type: "sandbox-log",
|
|
231
|
+
source: bridgeType,
|
|
232
|
+
stream,
|
|
233
|
+
line: trimmed
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} catch {
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return raw(
|
|
241
|
+
chunk,
|
|
242
|
+
encoding,
|
|
243
|
+
cb
|
|
244
|
+
);
|
|
245
|
+
};
|
|
246
|
+
process.stdout.write = patch(
|
|
247
|
+
"stdout",
|
|
248
|
+
rawStdoutWrite
|
|
249
|
+
);
|
|
250
|
+
process.stderr.write = patch(
|
|
251
|
+
"stderr",
|
|
252
|
+
rawStderrWrite
|
|
253
|
+
);
|
|
254
|
+
};
|
|
255
|
+
const handleInbound = async (msg, ws) => {
|
|
256
|
+
switch (msg.type) {
|
|
257
|
+
case "start": {
|
|
258
|
+
const firstTurn = isFirstTurn;
|
|
259
|
+
isFirstTurn = false;
|
|
260
|
+
eventLog = [];
|
|
261
|
+
diskBuffer = "";
|
|
262
|
+
void writeFile(eventLogPath, "").catch(() => {
|
|
263
|
+
});
|
|
264
|
+
turnAbort = new AbortController();
|
|
265
|
+
currentTurnState = "running";
|
|
266
|
+
currentInterruptHandler = void 0;
|
|
267
|
+
void writeStartConfig(msg);
|
|
268
|
+
void writeBridgeMeta("running");
|
|
269
|
+
const startDebug = msg.debug;
|
|
270
|
+
debugConfig = {
|
|
271
|
+
enabled: startDebug?.enabled ?? envDebugEnabled,
|
|
272
|
+
level: startDebug?.level ?? procEnv.HARNESS_DEBUG_LEVEL,
|
|
273
|
+
subsystems: startDebug?.subsystems ?? parseEnvList(procEnv.HARNESS_DEBUG_SUBSYSTEMS)
|
|
274
|
+
};
|
|
275
|
+
if (debugConfig.enabled) {
|
|
276
|
+
installConsoleCapture();
|
|
277
|
+
}
|
|
278
|
+
const turn = {
|
|
279
|
+
emit,
|
|
280
|
+
requestToolResult: (toolCallId) => new Promise((resolve) => {
|
|
281
|
+
pendingToolResults.set(toolCallId, resolve);
|
|
282
|
+
}),
|
|
283
|
+
requestToolApproval: (approvalId) => new Promise((resolve) => {
|
|
284
|
+
pendingToolApprovals.set(approvalId, resolve);
|
|
285
|
+
}),
|
|
286
|
+
pendingUserMessages: [],
|
|
287
|
+
abortSignal: turnAbort.signal,
|
|
288
|
+
onInterrupt: (handler) => {
|
|
289
|
+
currentInterruptHandler = handler;
|
|
290
|
+
},
|
|
291
|
+
firstTurn,
|
|
292
|
+
bridgeLog: (input) => {
|
|
293
|
+
const level = input.level ?? "debug";
|
|
294
|
+
if (!shouldEmitDebugEvent(level, input.subsystem)) return;
|
|
295
|
+
emit({
|
|
296
|
+
type: "debug-event",
|
|
297
|
+
level,
|
|
298
|
+
subsystem: input.subsystem,
|
|
299
|
+
message: input.message,
|
|
300
|
+
...input.attrs ? { attrs: input.attrs } : {},
|
|
301
|
+
...input.error !== void 0 ? { error: formatBridgeError(input.error) } : {}
|
|
302
|
+
});
|
|
303
|
+
},
|
|
304
|
+
emitWarning,
|
|
305
|
+
emitError
|
|
306
|
+
};
|
|
307
|
+
currentUserMessages = turn.pendingUserMessages;
|
|
308
|
+
try {
|
|
309
|
+
await onStart(msg, turn);
|
|
310
|
+
} catch (err) {
|
|
311
|
+
emitError({ error: err, message: "bridge turn failed" });
|
|
312
|
+
} finally {
|
|
313
|
+
currentInterruptHandler = void 0;
|
|
314
|
+
currentTurnState = "waiting";
|
|
315
|
+
void writeBridgeMeta("waiting");
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
case "tool-result": {
|
|
320
|
+
const resolver = pendingToolResults.get(msg.toolCallId);
|
|
321
|
+
if (resolver) {
|
|
322
|
+
pendingToolResults.delete(msg.toolCallId);
|
|
323
|
+
resolver({ output: msg.output, isError: msg.isError });
|
|
324
|
+
}
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
case "tool-approval-response": {
|
|
328
|
+
const resolver = pendingToolApprovals.get(msg.approvalId);
|
|
329
|
+
if (resolver) {
|
|
330
|
+
pendingToolApprovals.delete(msg.approvalId);
|
|
331
|
+
resolver({ approved: msg.approved, reason: msg.reason });
|
|
332
|
+
}
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
case "user-message":
|
|
336
|
+
currentUserMessages?.push(msg.text);
|
|
337
|
+
return;
|
|
338
|
+
case "abort":
|
|
339
|
+
turnAbort?.abort();
|
|
340
|
+
return;
|
|
341
|
+
case "interrupt":
|
|
342
|
+
try {
|
|
343
|
+
if (pendingToolResults.size === 0 && pendingToolApprovals.size === 0) {
|
|
344
|
+
if (currentInterruptHandler) {
|
|
345
|
+
await currentInterruptHandler();
|
|
346
|
+
} else {
|
|
347
|
+
turnAbort?.abort();
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
sendControl({ type: "bridge-interrupted", ok: true });
|
|
351
|
+
} catch (err) {
|
|
352
|
+
sendControl({
|
|
353
|
+
type: "bridge-interrupted",
|
|
354
|
+
ok: false,
|
|
355
|
+
error: serialiseError(err)
|
|
356
|
+
});
|
|
357
|
+
}
|
|
358
|
+
return;
|
|
359
|
+
case "resume":
|
|
360
|
+
replay(ws, msg.lastSeenEventId);
|
|
361
|
+
return;
|
|
362
|
+
case "shutdown":
|
|
363
|
+
currentTurnState = "done";
|
|
364
|
+
void writeBridgeMeta("done");
|
|
365
|
+
drainThenExit(ws, 1e3, "shutdown");
|
|
366
|
+
return;
|
|
367
|
+
case "detach": {
|
|
368
|
+
currentTurnState = "done";
|
|
369
|
+
void writeBridgeMeta("done");
|
|
370
|
+
const data = await onDetach?.() ?? {};
|
|
371
|
+
sendControl({ type: "bridge-detach", data });
|
|
372
|
+
drainThenExit(ws, 1e3, "detach");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
void writeBridgeMeta("init");
|
|
378
|
+
const wss = new WebSocketServer({ port: bridgeWsPort, host: "0.0.0.0" });
|
|
379
|
+
const exit = () => {
|
|
380
|
+
if (options.onExit) {
|
|
381
|
+
options.onExit();
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
wss.close(() => process.exit(0));
|
|
385
|
+
setTimeout(() => process.exit(0), 1e3).unref();
|
|
386
|
+
};
|
|
387
|
+
const drainThenExit = (ws, code, reason) => {
|
|
388
|
+
const start = Date.now();
|
|
389
|
+
const tick = () => {
|
|
390
|
+
const drained = ws.bufferedAmount === 0 || ws.readyState !== WS_OPEN;
|
|
391
|
+
if (drained || Date.now() - start >= 5e3) {
|
|
392
|
+
void flushPendingEventsToDisk().finally(() => {
|
|
393
|
+
try {
|
|
394
|
+
ws.close(code, reason);
|
|
395
|
+
} finally {
|
|
396
|
+
exit();
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
setTimeout(tick, 10).unref();
|
|
402
|
+
};
|
|
403
|
+
tick();
|
|
404
|
+
};
|
|
405
|
+
wss.on("listening", () => {
|
|
406
|
+
const addr = wss.address();
|
|
407
|
+
currentBoundPort = typeof addr === "object" && addr ? addr.port : 0;
|
|
408
|
+
currentTurnState = "waiting";
|
|
409
|
+
void writeBridgeMeta("waiting");
|
|
410
|
+
stdout.write(
|
|
411
|
+
JSON.stringify({
|
|
412
|
+
type: "bridge-ready",
|
|
413
|
+
port: currentBoundPort
|
|
414
|
+
}) + "\n"
|
|
415
|
+
);
|
|
416
|
+
options.onListening?.(currentBoundPort);
|
|
417
|
+
});
|
|
418
|
+
wss.on("connection", (ws, req) => {
|
|
419
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
420
|
+
if (url.searchParams.get("agent_bridge_token") !== expectedToken) {
|
|
421
|
+
ws.close(1008, "unauthorized");
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
activeSocket = ws;
|
|
425
|
+
sendControl({
|
|
426
|
+
type: "bridge-hello",
|
|
427
|
+
state: currentTurnState,
|
|
428
|
+
lastSeq: seqCounter
|
|
429
|
+
});
|
|
430
|
+
ws.on("message", (raw) => {
|
|
431
|
+
let parsed;
|
|
432
|
+
try {
|
|
433
|
+
const text = typeof raw === "string" ? raw : Buffer.from(raw).toString("utf8");
|
|
434
|
+
parsed = JSON.parse(text);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
sendControl({
|
|
437
|
+
type: "error",
|
|
438
|
+
error: `protocol parse error: ${err.message}`
|
|
439
|
+
});
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
void handleInbound(parsed, ws);
|
|
443
|
+
});
|
|
444
|
+
ws.on("close", () => {
|
|
445
|
+
if (activeSocket === ws) {
|
|
446
|
+
activeSocket = void 0;
|
|
447
|
+
}
|
|
448
|
+
});
|
|
449
|
+
ws.on("error", () => {
|
|
450
|
+
});
|
|
451
|
+
});
|
|
452
|
+
process.on("uncaughtException", (err) => {
|
|
453
|
+
emitError({ error: err, message: "uncaught exception" });
|
|
454
|
+
});
|
|
455
|
+
process.on("unhandledRejection", (err) => {
|
|
456
|
+
emitError({ error: err, message: "unhandled rejection" });
|
|
457
|
+
});
|
|
458
|
+
await new Promise((resolve, reject) => {
|
|
459
|
+
if (wss.address() != null) {
|
|
460
|
+
resolve();
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
wss.once("listening", resolve);
|
|
464
|
+
wss.once("error", reject);
|
|
465
|
+
});
|
|
466
|
+
return {
|
|
467
|
+
port: currentBoundPort,
|
|
468
|
+
close: () => new Promise((resolve) => {
|
|
469
|
+
wss.close(() => resolve());
|
|
470
|
+
})
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
function serialiseError(err) {
|
|
474
|
+
if (err instanceof Error) {
|
|
475
|
+
return { name: err.name, message: err.message, stack: err.stack };
|
|
476
|
+
}
|
|
477
|
+
return err;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// src/bridge/index.ts
|
|
481
|
+
import { randomUUID } from "crypto";
|
|
482
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
483
|
+
import { createServer } from "http";
|
|
484
|
+
|
|
485
|
+
// src/bridge/cli-relay.ts
|
|
486
|
+
var CLI_SHIM_FILENAME = "harness-tool.mjs";
|
|
487
|
+
function buildCliShimScript({
|
|
488
|
+
relayPort
|
|
489
|
+
}) {
|
|
490
|
+
return `#!/usr/bin/env node
|
|
491
|
+
const [toolName, inputJson = '{}'] = process.argv.slice(2);
|
|
492
|
+
if (!toolName) {
|
|
493
|
+
console.error('Usage: harness-tool <tool_name> <json_input>');
|
|
494
|
+
process.exit(64);
|
|
495
|
+
}
|
|
496
|
+
let input;
|
|
497
|
+
try {
|
|
498
|
+
input = JSON.parse(inputJson);
|
|
499
|
+
} catch (error) {
|
|
500
|
+
console.error('Invalid JSON input: ' + (error instanceof Error ? error.message : String(error)));
|
|
501
|
+
process.exit(64);
|
|
502
|
+
}
|
|
503
|
+
const requestId = 'cli-' + Date.now() + '-' + Math.random().toString(16).slice(2);
|
|
504
|
+
const response = await fetch('http://127.0.0.1:${relayPort}', {
|
|
505
|
+
method: 'POST',
|
|
506
|
+
headers: { 'Content-Type': 'application/json' },
|
|
507
|
+
body: JSON.stringify({ requestId, toolName, input }),
|
|
508
|
+
});
|
|
509
|
+
const text = await response.text();
|
|
510
|
+
let payload;
|
|
511
|
+
try {
|
|
512
|
+
payload = text ? JSON.parse(text) : {};
|
|
513
|
+
} catch {
|
|
514
|
+
payload = { error: text };
|
|
515
|
+
}
|
|
516
|
+
if (!response.ok || payload.error) {
|
|
517
|
+
console.error(String(payload.error ?? ('tool relay failed with HTTP ' + response.status)));
|
|
518
|
+
process.exit(1);
|
|
519
|
+
}
|
|
520
|
+
console.log(JSON.stringify(payload.result ?? payload, null, 2));
|
|
521
|
+
`;
|
|
522
|
+
}
|
|
523
|
+
function parseToolRelayCommand({
|
|
524
|
+
command,
|
|
525
|
+
cliShimPath
|
|
526
|
+
}) {
|
|
527
|
+
return parseToolRelayCommandInternal({ command, cliShimPath, depth: 0 });
|
|
528
|
+
}
|
|
529
|
+
function parseToolRelayCommandInternal({
|
|
530
|
+
command,
|
|
531
|
+
cliShimPath,
|
|
532
|
+
depth
|
|
533
|
+
}) {
|
|
534
|
+
const argv2 = parseShellWords(command);
|
|
535
|
+
if (!argv2) return void 0;
|
|
536
|
+
const relayCall = parseDirectToolRelayArgv({ argv: argv2, cliShimPath });
|
|
537
|
+
if (relayCall) return relayCall;
|
|
538
|
+
const innerCommand = extractShellEvalCommand(argv2);
|
|
539
|
+
if (!innerCommand || depth >= 2) return void 0;
|
|
540
|
+
return parseToolRelayCommandInternal({
|
|
541
|
+
command: innerCommand,
|
|
542
|
+
cliShimPath,
|
|
543
|
+
depth: depth + 1
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
function parseDirectToolRelayArgv({
|
|
547
|
+
argv: argv2,
|
|
548
|
+
cliShimPath
|
|
549
|
+
}) {
|
|
550
|
+
if (argv2.length < 3 || argv2.length > 4) return void 0;
|
|
551
|
+
if (argv2[0] !== "node" || argv2[1] !== cliShimPath) return void 0;
|
|
552
|
+
const toolName = argv2[2];
|
|
553
|
+
if (!toolName) return void 0;
|
|
554
|
+
try {
|
|
555
|
+
return { toolName, input: JSON.parse(argv2[3] ?? "{}") };
|
|
556
|
+
} catch {
|
|
557
|
+
return void 0;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
function extractShellEvalCommand(argv2) {
|
|
561
|
+
if (argv2.length !== 3) return void 0;
|
|
562
|
+
const shellName = argv2[0].split("/").at(-1);
|
|
563
|
+
if (shellName !== "bash" && shellName !== "sh" && shellName !== "zsh") {
|
|
564
|
+
return void 0;
|
|
565
|
+
}
|
|
566
|
+
if (argv2[1] !== "-c" && argv2[1] !== "-lc") return void 0;
|
|
567
|
+
return argv2[2];
|
|
568
|
+
}
|
|
569
|
+
function parseShellWords(command) {
|
|
570
|
+
const words = [];
|
|
571
|
+
let current = "";
|
|
572
|
+
let quote;
|
|
573
|
+
let hasCurrent = false;
|
|
574
|
+
const pushCurrent = () => {
|
|
575
|
+
if (!hasCurrent) return;
|
|
576
|
+
words.push(current);
|
|
577
|
+
current = "";
|
|
578
|
+
hasCurrent = false;
|
|
579
|
+
};
|
|
580
|
+
for (let i = 0; i < command.length; i++) {
|
|
581
|
+
const char = command[i];
|
|
582
|
+
if (quote === "'") {
|
|
583
|
+
if (char === "'") {
|
|
584
|
+
quote = void 0;
|
|
585
|
+
} else {
|
|
586
|
+
current += char;
|
|
587
|
+
}
|
|
588
|
+
hasCurrent = true;
|
|
589
|
+
continue;
|
|
590
|
+
}
|
|
591
|
+
if (quote === '"') {
|
|
592
|
+
if (char === '"') {
|
|
593
|
+
quote = void 0;
|
|
594
|
+
} else if (char === "\\" && i + 1 < command.length) {
|
|
595
|
+
current += command[++i];
|
|
596
|
+
} else {
|
|
597
|
+
current += char;
|
|
598
|
+
}
|
|
599
|
+
hasCurrent = true;
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
if (/\s/.test(char)) {
|
|
603
|
+
pushCurrent();
|
|
604
|
+
continue;
|
|
605
|
+
}
|
|
606
|
+
if (char === '"' || char === "'") {
|
|
607
|
+
quote = char;
|
|
608
|
+
hasCurrent = true;
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (char === "\\" && i + 1 < command.length) {
|
|
612
|
+
current += command[++i];
|
|
613
|
+
hasCurrent = true;
|
|
614
|
+
continue;
|
|
615
|
+
}
|
|
616
|
+
if (/[;&|<>()`$]/.test(char)) return void 0;
|
|
617
|
+
current += char;
|
|
618
|
+
hasCurrent = true;
|
|
619
|
+
}
|
|
620
|
+
if (quote) return void 0;
|
|
621
|
+
pushCurrent();
|
|
622
|
+
return words;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// src/bridge/codex-step-tracker.ts
|
|
626
|
+
function createCodexStepTracker(input) {
|
|
627
|
+
let stepOpen = false;
|
|
628
|
+
const pendingToolItemIds = /* @__PURE__ */ new Set();
|
|
629
|
+
const finishStep = () => {
|
|
630
|
+
if (!stepOpen || pendingToolItemIds.size > 0) return;
|
|
631
|
+
input.send({
|
|
632
|
+
type: "finish-step",
|
|
633
|
+
finishReason: { unified: "stop", raw: "stop" },
|
|
634
|
+
usage: defaultUsage(),
|
|
635
|
+
harnessMetadata: { codex: { inferredStep: true } }
|
|
636
|
+
});
|
|
637
|
+
stepOpen = false;
|
|
638
|
+
};
|
|
639
|
+
return {
|
|
640
|
+
observeEvent({ event, itemId }) {
|
|
641
|
+
const item = event.item;
|
|
642
|
+
if (!item || !isStepItem(item)) return;
|
|
643
|
+
stepOpen = true;
|
|
644
|
+
if (isToolStepItem(item)) {
|
|
645
|
+
if (event.type === "item.started" && itemId) {
|
|
646
|
+
pendingToolItemIds.add(itemId);
|
|
647
|
+
} else if (event.type === "item.completed") {
|
|
648
|
+
if (itemId) pendingToolItemIds.delete(itemId);
|
|
649
|
+
finishStep();
|
|
650
|
+
}
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
},
|
|
654
|
+
finishStep
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
function isStepItem(item) {
|
|
658
|
+
return isModelStepItem(item) || isToolStepItem(item);
|
|
659
|
+
}
|
|
660
|
+
function isModelStepItem(item) {
|
|
661
|
+
return item.type === "reasoning" || item.type === "agent_message";
|
|
662
|
+
}
|
|
663
|
+
function isToolStepItem(item) {
|
|
664
|
+
return item.type === "command_execution" || item.type === "mcp_tool_call" || item.type === "web_search" || item.type === "file_change" || item.type === "todo_list";
|
|
665
|
+
}
|
|
666
|
+
function defaultUsage() {
|
|
667
|
+
return {
|
|
668
|
+
inputTokens: { total: 0, noCache: 0, cacheRead: 0, cacheWrite: 0 },
|
|
669
|
+
outputTokens: { total: 0, text: 0 }
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// src/bridge/tool-relay-auth.ts
|
|
674
|
+
import { readdir, readFile, readlink } from "fs/promises";
|
|
675
|
+
var ToolRelayAuthorizer = class {
|
|
676
|
+
constructor({
|
|
677
|
+
ttlMs = 1e4,
|
|
678
|
+
now = Date.now
|
|
679
|
+
} = {}) {
|
|
680
|
+
this.authorizations = [];
|
|
681
|
+
this.ttlMs = ttlMs;
|
|
682
|
+
this.now = now;
|
|
683
|
+
}
|
|
684
|
+
authorizeToolCall(call) {
|
|
685
|
+
this.pruneExpired();
|
|
686
|
+
this.authorizations.push({
|
|
687
|
+
key: toolRelayCallKey(call),
|
|
688
|
+
expiresAt: this.now() + this.ttlMs
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
authorizeAnyToolCall() {
|
|
692
|
+
this.pruneExpired();
|
|
693
|
+
this.authorizations.push({
|
|
694
|
+
expiresAt: this.now() + this.ttlMs
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
consumeToolCall(call) {
|
|
698
|
+
this.pruneExpired();
|
|
699
|
+
const key = toolRelayCallKey(call);
|
|
700
|
+
let index = this.authorizations.findIndex((auth) => auth.key === key);
|
|
701
|
+
if (index === -1) {
|
|
702
|
+
index = this.authorizations.findIndex((auth) => auth.key === void 0);
|
|
703
|
+
}
|
|
704
|
+
if (index === -1) return false;
|
|
705
|
+
this.authorizations.splice(index, 1);
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
pruneExpired() {
|
|
709
|
+
const now = this.now();
|
|
710
|
+
for (let i = this.authorizations.length - 1; i >= 0; i--) {
|
|
711
|
+
if (this.authorizations[i].expiresAt <= now) {
|
|
712
|
+
this.authorizations.splice(i, 1);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
var ToolRelayPendingCalls = class {
|
|
718
|
+
constructor() {
|
|
719
|
+
this.calls = /* @__PURE__ */ new Map();
|
|
720
|
+
}
|
|
721
|
+
begin({
|
|
722
|
+
call,
|
|
723
|
+
run
|
|
724
|
+
}) {
|
|
725
|
+
const key = toolRelayCallKey(call);
|
|
726
|
+
const existing = this.calls.get(key);
|
|
727
|
+
if (existing) return { result: existing, isNew: false };
|
|
728
|
+
const result = run();
|
|
729
|
+
this.calls.set(key, result);
|
|
730
|
+
void result.finally(() => {
|
|
731
|
+
if (this.calls.get(key) === result) {
|
|
732
|
+
this.calls.delete(key);
|
|
733
|
+
}
|
|
734
|
+
}).catch(() => {
|
|
735
|
+
});
|
|
736
|
+
return { result, isNew: true };
|
|
737
|
+
}
|
|
738
|
+
};
|
|
739
|
+
function toolRelayCallKey({ toolName, input }) {
|
|
740
|
+
return `${toolName}\0${canonicalJson(input ?? {})}`;
|
|
741
|
+
}
|
|
742
|
+
function canonicalJson(value) {
|
|
743
|
+
return JSON.stringify(normalizeJsonValue(value));
|
|
744
|
+
}
|
|
745
|
+
function normalizeJsonValue(value) {
|
|
746
|
+
if (Array.isArray(value)) {
|
|
747
|
+
return value.map(normalizeJsonValue);
|
|
748
|
+
}
|
|
749
|
+
if (value && typeof value === "object") {
|
|
750
|
+
return Object.fromEntries(
|
|
751
|
+
Object.entries(value).filter(([, entryValue]) => entryValue !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, entryValue]) => [key, normalizeJsonValue(entryValue)])
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
return value;
|
|
755
|
+
}
|
|
756
|
+
async function isToolRelayRequestFromAllowedProcess({
|
|
757
|
+
socket,
|
|
758
|
+
allowedScriptPaths
|
|
759
|
+
}) {
|
|
760
|
+
if (process.platform !== "linux") return false;
|
|
761
|
+
if (!socket.remotePort || !socket.localPort) return false;
|
|
762
|
+
const inode = await findTcpSocketInode({
|
|
763
|
+
clientPort: socket.remotePort,
|
|
764
|
+
serverPort: socket.localPort
|
|
765
|
+
});
|
|
766
|
+
if (!inode) return false;
|
|
767
|
+
const cmdline = await findProcessCmdlineForSocketInode({ inode });
|
|
768
|
+
return cmdline?.some((arg) => allowedScriptPaths.has(arg)) ?? false;
|
|
769
|
+
}
|
|
770
|
+
async function findTcpSocketInode({
|
|
771
|
+
clientPort,
|
|
772
|
+
serverPort
|
|
773
|
+
}) {
|
|
774
|
+
for (const tablePath of ["/proc/net/tcp", "/proc/net/tcp6"]) {
|
|
775
|
+
const table = await readFile(tablePath, "utf8").catch(() => void 0);
|
|
776
|
+
if (!table) continue;
|
|
777
|
+
for (const line of table.split("\n").slice(1)) {
|
|
778
|
+
const columns = line.trim().split(/\s+/);
|
|
779
|
+
if (columns.length < 10) continue;
|
|
780
|
+
const local = parseProcNetAddress(columns[1]);
|
|
781
|
+
const remote = parseProcNetAddress(columns[2]);
|
|
782
|
+
if (local?.port === clientPort && remote?.port === serverPort && columns[9] !== "0") {
|
|
783
|
+
return columns[9];
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return void 0;
|
|
788
|
+
}
|
|
789
|
+
function parseProcNetAddress(value) {
|
|
790
|
+
const [, portHex] = value.split(":");
|
|
791
|
+
if (!portHex) return void 0;
|
|
792
|
+
return { port: Number.parseInt(portHex, 16) };
|
|
793
|
+
}
|
|
794
|
+
async function findProcessCmdlineForSocketInode({
|
|
795
|
+
inode
|
|
796
|
+
}) {
|
|
797
|
+
const procEntries = await readdir("/proc", { withFileTypes: true }).catch(
|
|
798
|
+
() => []
|
|
799
|
+
);
|
|
800
|
+
for (const entry of procEntries) {
|
|
801
|
+
if (!entry.isDirectory() || !/^\d+$/.test(entry.name)) continue;
|
|
802
|
+
const fdDir = `/proc/${entry.name}/fd`;
|
|
803
|
+
const fds = await readdir(fdDir).catch(() => []);
|
|
804
|
+
for (const fd of fds) {
|
|
805
|
+
const target = await readlink(`${fdDir}/${fd}`).catch(() => void 0);
|
|
806
|
+
if (target !== `socket:[${inode}]`) continue;
|
|
807
|
+
const cmdline = await readFile(`/proc/${entry.name}/cmdline`, "utf8").then((value) => value.split("\0").filter(Boolean)).catch(() => void 0);
|
|
808
|
+
if (cmdline) return cmdline;
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return void 0;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
// src/bridge/index.ts
|
|
815
|
+
import { argv, env as procEnv2, stdout as stdout2 } from "process";
|
|
816
|
+
import * as codexSdkModule from "@openai/codex-sdk";
|
|
817
|
+
var NATIVE_TO_COMMON = {
|
|
818
|
+
shell: "bash",
|
|
819
|
+
web_search: "webSearch"
|
|
820
|
+
};
|
|
821
|
+
function toCommonName(nativeName) {
|
|
822
|
+
return NATIVE_TO_COMMON[nativeName] ?? nativeName;
|
|
823
|
+
}
|
|
824
|
+
var args = parseArgs(argv.slice(2));
|
|
825
|
+
var workdir = requireArg({ value: args.workdir, name: "--workdir" });
|
|
826
|
+
var bridgeStateDir = requireArg({
|
|
827
|
+
value: args.bridgeStateDir,
|
|
828
|
+
name: "--bridge-state-dir"
|
|
829
|
+
});
|
|
830
|
+
var cliShimDir = requireArg({
|
|
831
|
+
value: args.cliShimDir,
|
|
832
|
+
name: "--cli-shim-dir"
|
|
833
|
+
});
|
|
834
|
+
var bootstrapDir = args.bootstrapDir ?? workdir;
|
|
835
|
+
var HARNESS_CLIENT_APP = procEnv2.AI_SDK_HARNESS_CLIENT_APP;
|
|
836
|
+
var codexSdk = codexSdkModule;
|
|
837
|
+
var threadState = { id: void 0 };
|
|
838
|
+
await runBridge({
|
|
839
|
+
bridgeType: "codex",
|
|
840
|
+
bridgeStateDir,
|
|
841
|
+
onStart: runTurn,
|
|
842
|
+
onDetach: () => threadState.id ? { threadId: threadState.id } : {}
|
|
843
|
+
});
|
|
844
|
+
async function runTurn(start, turn) {
|
|
845
|
+
const emit = (msg) => turn.emit(msg);
|
|
846
|
+
if (typeof start.resumeThreadId === "string" && start.resumeThreadId.length > 0) {
|
|
847
|
+
threadState.id = start.resumeThreadId;
|
|
848
|
+
}
|
|
849
|
+
const mcpServers = {};
|
|
850
|
+
let relay;
|
|
851
|
+
let cliShimPath;
|
|
852
|
+
if (start.tools && start.tools.length > 0) {
|
|
853
|
+
cliShimPath = `${cliShimDir}/${CLI_SHIM_FILENAME}`;
|
|
854
|
+
relay = await startToolRelay({
|
|
855
|
+
allowedScriptPaths: [cliShimPath, `${bootstrapDir}/host-tool-mcp.mjs`],
|
|
856
|
+
tools: start.tools,
|
|
857
|
+
emit,
|
|
858
|
+
requestToolResult: turn.requestToolResult
|
|
859
|
+
});
|
|
860
|
+
mcpServers["harness-tools"] = {
|
|
861
|
+
enabled: true,
|
|
862
|
+
command: "node",
|
|
863
|
+
args: [`${bootstrapDir}/host-tool-mcp.mjs`],
|
|
864
|
+
env: {
|
|
865
|
+
TOOL_SCHEMAS: JSON.stringify(
|
|
866
|
+
start.tools.map((t) => ({
|
|
867
|
+
name: t.name,
|
|
868
|
+
description: t.description,
|
|
869
|
+
inputSchema: t.inputSchema
|
|
870
|
+
}))
|
|
871
|
+
),
|
|
872
|
+
TOOL_RELAY_URL: `http://127.0.0.1:${relay.port}`
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
await mkdir2(cliShimDir, { recursive: true });
|
|
876
|
+
await writeFile2(
|
|
877
|
+
cliShimPath,
|
|
878
|
+
buildCliShimScript({ relayPort: relay.port }),
|
|
879
|
+
"utf8"
|
|
880
|
+
);
|
|
881
|
+
}
|
|
882
|
+
const codexConfig = {};
|
|
883
|
+
if (Object.keys(mcpServers).length > 0) codexConfig.mcp_servers = mcpServers;
|
|
884
|
+
const gatewayBaseUrl = procEnv2.AI_GATEWAY_BASE_URL;
|
|
885
|
+
const hasGatewayAuth = Boolean(procEnv2.AI_GATEWAY_API_KEY || gatewayBaseUrl);
|
|
886
|
+
if (hasGatewayAuth && !gatewayBaseUrl) {
|
|
887
|
+
throw new Error(
|
|
888
|
+
"AI Gateway auth was selected but AI_GATEWAY_BASE_URL is missing from the Codex bridge environment."
|
|
889
|
+
);
|
|
890
|
+
}
|
|
891
|
+
const apiBaseUrl = hasGatewayAuth ? gatewayBaseUrl : procEnv2.OPENAI_BASE_URL;
|
|
892
|
+
if (apiBaseUrl) {
|
|
893
|
+
codexConfig.preferred_auth_method = "apikey";
|
|
894
|
+
codexConfig.model_provider = "agent_bridge_openai";
|
|
895
|
+
codexConfig.model_providers = {
|
|
896
|
+
agent_bridge_openai: {
|
|
897
|
+
name: procEnv2.CODEX_MODEL_PROVIDER_NAME || "Agent Bridge OpenAI",
|
|
898
|
+
base_url: apiBaseUrl,
|
|
899
|
+
env_key: "CODEX_API_KEY",
|
|
900
|
+
wire_api: "responses",
|
|
901
|
+
supports_websockets: false,
|
|
902
|
+
...hasGatewayAuth && HARNESS_CLIENT_APP ? {
|
|
903
|
+
http_headers: {
|
|
904
|
+
"User-Agent": HARNESS_CLIENT_APP,
|
|
905
|
+
"x-client-app": HARNESS_CLIENT_APP
|
|
906
|
+
}
|
|
907
|
+
} : {}
|
|
908
|
+
}
|
|
909
|
+
};
|
|
910
|
+
}
|
|
911
|
+
const usesConfiguredModelProvider = typeof codexConfig.model_provider === "string";
|
|
912
|
+
const codex = new codexSdk.Codex({
|
|
913
|
+
...procEnv2.CODEX_API_KEY ? { apiKey: procEnv2.CODEX_API_KEY } : {},
|
|
914
|
+
...!usesConfiguredModelProvider && apiBaseUrl ? { baseUrl: apiBaseUrl } : {},
|
|
915
|
+
env: Object.fromEntries(
|
|
916
|
+
Object.entries(procEnv2).filter(
|
|
917
|
+
(entry) => typeof entry[1] === "string"
|
|
918
|
+
)
|
|
919
|
+
),
|
|
920
|
+
...Object.keys(codexConfig).length > 0 ? { config: codexConfig } : {}
|
|
921
|
+
});
|
|
922
|
+
const threadOptions = {
|
|
923
|
+
...start.model ? { model: start.model } : {},
|
|
924
|
+
sandboxMode: "danger-full-access",
|
|
925
|
+
approvalPolicy: "never",
|
|
926
|
+
workingDirectory: workdir,
|
|
927
|
+
skipGitRepoCheck: true,
|
|
928
|
+
...start.reasoningEffort ? { modelReasoningEffort: start.reasoningEffort } : {},
|
|
929
|
+
webSearchMode: start.webSearch ? "live" : "disabled"
|
|
930
|
+
};
|
|
931
|
+
const thread = threadState.id ? codex.resumeThread(threadState.id, threadOptions) : codex.startThread(threadOptions);
|
|
932
|
+
emit({ type: "stream-start" });
|
|
933
|
+
const userMessage = start.prompt;
|
|
934
|
+
let turnUsage;
|
|
935
|
+
const textByItem = /* @__PURE__ */ new Map();
|
|
936
|
+
const reasoningByItem = /* @__PURE__ */ new Map();
|
|
937
|
+
const stepTracker = createCodexStepTracker({ send: emit });
|
|
938
|
+
try {
|
|
939
|
+
const { events } = await thread.runStreamed(userMessage, {
|
|
940
|
+
signal: turn.abortSignal
|
|
941
|
+
});
|
|
942
|
+
for await (const event of events) {
|
|
943
|
+
if (turn.abortSignal.aborted) break;
|
|
944
|
+
if (event.type === "thread.started" && typeof event.thread_id === "string") {
|
|
945
|
+
threadState.id = event.thread_id;
|
|
946
|
+
emit({ type: "bridge-thread", threadId: event.thread_id });
|
|
947
|
+
}
|
|
948
|
+
if (cliShimPath && event.item?.type === "command_execution") {
|
|
949
|
+
const relayCall = typeof event.item.command === "string" ? parseToolRelayCommand({
|
|
950
|
+
command: event.item.command,
|
|
951
|
+
cliShimPath
|
|
952
|
+
}) : void 0;
|
|
953
|
+
if (event.type === "item.started" && relay) {
|
|
954
|
+
if (relayCall) {
|
|
955
|
+
relay.authorizeToolCall(relayCall);
|
|
956
|
+
} else if (typeof event.item.command !== "string") {
|
|
957
|
+
relay.authorizeAnyToolCall();
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
if (relayCall) {
|
|
961
|
+
stepTracker.observeEvent({ event, itemId: event.item.id });
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
if (relay && isHostMcpToolEvent(event)) {
|
|
966
|
+
stepTracker.observeEvent({ event, itemId: event.item?.id });
|
|
967
|
+
const relayCall = relayCallFromCodexMcpEvent(event);
|
|
968
|
+
if (relayCall) relay.authorizeToolCall(relayCall);
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
translateAndEmit(event, {
|
|
972
|
+
send: emit,
|
|
973
|
+
textByItem,
|
|
974
|
+
reasoningByItem,
|
|
975
|
+
stepTracker,
|
|
976
|
+
setTurnUsage: (u) => turnUsage = u,
|
|
977
|
+
emitWarning: turn.emitWarning,
|
|
978
|
+
emitError: turn.emitError
|
|
979
|
+
});
|
|
980
|
+
}
|
|
981
|
+
} catch (err) {
|
|
982
|
+
turn.emitError({ error: err, message: "codex turn failed" });
|
|
983
|
+
return;
|
|
984
|
+
} finally {
|
|
985
|
+
relay?.close();
|
|
986
|
+
}
|
|
987
|
+
emit({
|
|
988
|
+
type: "finish",
|
|
989
|
+
finishReason: { unified: "stop", raw: "stop" },
|
|
990
|
+
totalUsage: turnUsage ?? defaultUsage()
|
|
991
|
+
});
|
|
992
|
+
void turn.pendingUserMessages;
|
|
993
|
+
}
|
|
994
|
+
function extractMcpToolCallResult(item) {
|
|
995
|
+
if (item.result === void 0 || item.result === null || typeof item.result !== "object") {
|
|
996
|
+
return item.error?.message ? { error: item.error.message } : null;
|
|
997
|
+
}
|
|
998
|
+
const result = item.result;
|
|
999
|
+
if (result.structured_content !== void 0 && result.structured_content !== null) {
|
|
1000
|
+
return result.structured_content;
|
|
1001
|
+
}
|
|
1002
|
+
return result.content ?? null;
|
|
1003
|
+
}
|
|
1004
|
+
function isHostMcpToolEvent(event) {
|
|
1005
|
+
return event.item?.type === "mcp_tool_call" && event.item.server === "harness-tools";
|
|
1006
|
+
}
|
|
1007
|
+
function relayCallFromCodexMcpEvent(event) {
|
|
1008
|
+
if (event.type !== "item.started") return void 0;
|
|
1009
|
+
const toolName = event.item?.tool;
|
|
1010
|
+
if (!toolName) return void 0;
|
|
1011
|
+
return {
|
|
1012
|
+
toolName,
|
|
1013
|
+
input: event.item?.arguments ?? {}
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function translateAndEmit(event, ctx) {
|
|
1017
|
+
if (event.type === "turn.completed") {
|
|
1018
|
+
if (event.usage) ctx.setTurnUsage(mapUsage(event.usage));
|
|
1019
|
+
ctx.stepTracker.finishStep();
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
if (event.type === "turn.failed") {
|
|
1023
|
+
ctx.emitError({
|
|
1024
|
+
error: event.error?.message ?? "codex turn failed",
|
|
1025
|
+
message: "codex turn failed"
|
|
1026
|
+
});
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
if (event.type === "error") {
|
|
1030
|
+
ctx.emitError({
|
|
1031
|
+
error: event.message ?? "codex error",
|
|
1032
|
+
message: "codex stream error"
|
|
1033
|
+
});
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1036
|
+
if (!event.item) return;
|
|
1037
|
+
const item = event.item;
|
|
1038
|
+
const id = item.id ?? randomUUID();
|
|
1039
|
+
const observeStep = () => {
|
|
1040
|
+
ctx.stepTracker.observeEvent({ event, itemId: id });
|
|
1041
|
+
};
|
|
1042
|
+
if (item.type === "agent_message" && typeof item.text === "string") {
|
|
1043
|
+
if (!ctx.textByItem.has(id)) {
|
|
1044
|
+
ctx.send({ type: "text-start", id });
|
|
1045
|
+
ctx.textByItem.set(id, "");
|
|
1046
|
+
}
|
|
1047
|
+
const last = ctx.textByItem.get(id) ?? "";
|
|
1048
|
+
const next = item.text;
|
|
1049
|
+
if (next.length > last.length) {
|
|
1050
|
+
ctx.send({ type: "text-delta", id, delta: next.slice(last.length) });
|
|
1051
|
+
ctx.textByItem.set(id, next);
|
|
1052
|
+
}
|
|
1053
|
+
if (event.type === "item.completed") ctx.send({ type: "text-end", id });
|
|
1054
|
+
observeStep();
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
if (item.type === "reasoning" && typeof item.text === "string") {
|
|
1058
|
+
if (!ctx.reasoningByItem.has(id)) {
|
|
1059
|
+
ctx.send({ type: "reasoning-start", id });
|
|
1060
|
+
ctx.reasoningByItem.set(id, "");
|
|
1061
|
+
}
|
|
1062
|
+
const last = ctx.reasoningByItem.get(id) ?? "";
|
|
1063
|
+
const next = item.text;
|
|
1064
|
+
if (next.length > last.length) {
|
|
1065
|
+
ctx.send({ type: "reasoning-delta", id, delta: next.slice(last.length) });
|
|
1066
|
+
ctx.reasoningByItem.set(id, next);
|
|
1067
|
+
}
|
|
1068
|
+
if (event.type === "item.completed")
|
|
1069
|
+
ctx.send({ type: "reasoning-end", id });
|
|
1070
|
+
observeStep();
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
if (item.type === "command_execution") {
|
|
1074
|
+
const nativeName = "shell";
|
|
1075
|
+
if (event.type === "item.started") {
|
|
1076
|
+
ctx.send({
|
|
1077
|
+
type: "tool-call",
|
|
1078
|
+
toolCallId: id,
|
|
1079
|
+
toolName: toCommonName(nativeName),
|
|
1080
|
+
nativeName,
|
|
1081
|
+
input: JSON.stringify({ command: item.command ?? "" }),
|
|
1082
|
+
providerExecuted: true
|
|
1083
|
+
});
|
|
1084
|
+
} else if (event.type === "item.completed") {
|
|
1085
|
+
ctx.send({
|
|
1086
|
+
type: "tool-result",
|
|
1087
|
+
toolCallId: id,
|
|
1088
|
+
toolName: toCommonName(nativeName),
|
|
1089
|
+
result: {
|
|
1090
|
+
exitCode: item.exit_code ?? null,
|
|
1091
|
+
output: item.aggregated_output ?? "",
|
|
1092
|
+
status: item.status ?? "completed"
|
|
1093
|
+
}
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
observeStep();
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (item.type === "mcp_tool_call") {
|
|
1100
|
+
const isHostTool = item.server === "harness-tools";
|
|
1101
|
+
if (event.type === "item.started") {
|
|
1102
|
+
ctx.send({
|
|
1103
|
+
type: "tool-call",
|
|
1104
|
+
toolCallId: id,
|
|
1105
|
+
toolName: item.tool ?? "unknown",
|
|
1106
|
+
...isHostTool ? {} : { nativeName: item.tool ?? "unknown" },
|
|
1107
|
+
input: JSON.stringify(item.arguments ?? {}),
|
|
1108
|
+
providerExecuted: !isHostTool
|
|
1109
|
+
});
|
|
1110
|
+
} else if (event.type === "item.completed") {
|
|
1111
|
+
ctx.send({
|
|
1112
|
+
type: "tool-result",
|
|
1113
|
+
toolCallId: id,
|
|
1114
|
+
toolName: item.tool ?? "unknown",
|
|
1115
|
+
result: extractMcpToolCallResult(item)
|
|
1116
|
+
});
|
|
1117
|
+
}
|
|
1118
|
+
observeStep();
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
if (item.type === "web_search") {
|
|
1122
|
+
const nativeName = "web_search";
|
|
1123
|
+
if (event.type === "item.started") {
|
|
1124
|
+
ctx.send({
|
|
1125
|
+
type: "tool-call",
|
|
1126
|
+
toolCallId: id,
|
|
1127
|
+
toolName: toCommonName(nativeName),
|
|
1128
|
+
nativeName,
|
|
1129
|
+
input: JSON.stringify({ query: item.query ?? "" }),
|
|
1130
|
+
providerExecuted: true
|
|
1131
|
+
});
|
|
1132
|
+
} else if (event.type === "item.completed") {
|
|
1133
|
+
ctx.send({
|
|
1134
|
+
type: "tool-result",
|
|
1135
|
+
toolCallId: id,
|
|
1136
|
+
toolName: toCommonName(nativeName),
|
|
1137
|
+
result: item.result ?? null
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1140
|
+
observeStep();
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
if (item.type === "file_change" && event.type === "item.completed") {
|
|
1144
|
+
for (const change of item.changes ?? []) {
|
|
1145
|
+
ctx.send({
|
|
1146
|
+
type: "file-change",
|
|
1147
|
+
event: change.kind === "add" ? "create" : change.kind === "delete" ? "delete" : "modify",
|
|
1148
|
+
path: change.path
|
|
1149
|
+
});
|
|
1150
|
+
}
|
|
1151
|
+
observeStep();
|
|
1152
|
+
return;
|
|
1153
|
+
}
|
|
1154
|
+
if (item.type === "error" && event.type === "item.completed") {
|
|
1155
|
+
const message = typeof item.message === "string" && item.message.trim() ? item.message : "codex reported a non-fatal error item";
|
|
1156
|
+
ctx.emitWarning({ message });
|
|
1157
|
+
return;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function mapUsage(usage) {
|
|
1161
|
+
const input = usage.input_tokens ?? 0;
|
|
1162
|
+
const cacheRead = usage.cached_input_tokens ?? 0;
|
|
1163
|
+
return {
|
|
1164
|
+
inputTokens: {
|
|
1165
|
+
total: input,
|
|
1166
|
+
noCache: Math.max(0, input - cacheRead),
|
|
1167
|
+
cacheRead,
|
|
1168
|
+
cacheWrite: 0
|
|
1169
|
+
},
|
|
1170
|
+
outputTokens: {
|
|
1171
|
+
total: usage.output_tokens ?? 0,
|
|
1172
|
+
text: usage.output_tokens ?? 0
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
}
|
|
1176
|
+
async function startToolRelay({
|
|
1177
|
+
allowedScriptPaths,
|
|
1178
|
+
tools,
|
|
1179
|
+
emit,
|
|
1180
|
+
requestToolResult
|
|
1181
|
+
}) {
|
|
1182
|
+
const toolNames = new Set(tools.map((t) => t.name));
|
|
1183
|
+
const allowedScriptPathSet = new Set(allowedScriptPaths);
|
|
1184
|
+
const authorizer = new ToolRelayAuthorizer();
|
|
1185
|
+
const pendingCalls = new ToolRelayPendingCalls();
|
|
1186
|
+
const server = createServer(async (req, res) => {
|
|
1187
|
+
try {
|
|
1188
|
+
if (req.method !== "POST" || req.url !== "/") {
|
|
1189
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1190
|
+
res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
|
|
1191
|
+
return;
|
|
1192
|
+
}
|
|
1193
|
+
const chunks = [];
|
|
1194
|
+
for await (const chunk of req) {
|
|
1195
|
+
chunks.push(chunk);
|
|
1196
|
+
}
|
|
1197
|
+
const body = Buffer.concat(chunks).toString("utf8");
|
|
1198
|
+
const { requestId, toolName, input } = JSON.parse(body);
|
|
1199
|
+
if (!toolNames.has(toolName)) {
|
|
1200
|
+
res.writeHead(403, { "Content-Type": "application/json" });
|
|
1201
|
+
res.end(
|
|
1202
|
+
JSON.stringify({ error: `Tool "${toolName}" is not available` })
|
|
1203
|
+
);
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
const relayCall = { toolName, input };
|
|
1207
|
+
const authorized = authorizer.consumeToolCall(relayCall) || await isToolRelayRequestFromAllowedProcess({
|
|
1208
|
+
socket: req.socket,
|
|
1209
|
+
allowedScriptPaths: allowedScriptPathSet
|
|
1210
|
+
});
|
|
1211
|
+
if (!authorized) {
|
|
1212
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1213
|
+
res.end(JSON.stringify({ error: "unauthorized tool relay request" }));
|
|
1214
|
+
return;
|
|
1215
|
+
}
|
|
1216
|
+
const { result } = pendingCalls.begin({
|
|
1217
|
+
call: relayCall,
|
|
1218
|
+
run: async () => {
|
|
1219
|
+
emit({
|
|
1220
|
+
type: "tool-call",
|
|
1221
|
+
toolCallId: requestId,
|
|
1222
|
+
toolName,
|
|
1223
|
+
input: JSON.stringify(input ?? {}),
|
|
1224
|
+
providerExecuted: false
|
|
1225
|
+
});
|
|
1226
|
+
const toolResult = await requestToolResult(requestId);
|
|
1227
|
+
emit({
|
|
1228
|
+
type: "tool-result",
|
|
1229
|
+
toolCallId: requestId,
|
|
1230
|
+
toolName,
|
|
1231
|
+
result: toolResult.output ?? null,
|
|
1232
|
+
isError: !!toolResult.isError
|
|
1233
|
+
});
|
|
1234
|
+
return toolResult;
|
|
1235
|
+
}
|
|
1236
|
+
});
|
|
1237
|
+
const { output } = await result;
|
|
1238
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1239
|
+
res.end(JSON.stringify({ result: output }));
|
|
1240
|
+
} catch (error) {
|
|
1241
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1242
|
+
res.end(
|
|
1243
|
+
JSON.stringify({
|
|
1244
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1245
|
+
})
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
});
|
|
1249
|
+
await new Promise(
|
|
1250
|
+
(resolve) => server.listen(0, "127.0.0.1", () => resolve())
|
|
1251
|
+
);
|
|
1252
|
+
const address = server.address();
|
|
1253
|
+
if (!address || typeof address === "string") {
|
|
1254
|
+
throw new Error("tool relay did not expose a numeric port");
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
port: address.port,
|
|
1258
|
+
close: () => closeServer(server),
|
|
1259
|
+
authorizeToolCall: (call) => authorizer.authorizeToolCall(call),
|
|
1260
|
+
authorizeAnyToolCall: () => authorizer.authorizeAnyToolCall()
|
|
1261
|
+
};
|
|
1262
|
+
}
|
|
1263
|
+
function closeServer(server) {
|
|
1264
|
+
try {
|
|
1265
|
+
server.close();
|
|
1266
|
+
} catch {
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
function parseArgs(args2) {
|
|
1270
|
+
const out = {};
|
|
1271
|
+
for (let i = 0; i < args2.length; i++) {
|
|
1272
|
+
if (args2[i] === "--workdir" && i + 1 < args2.length) {
|
|
1273
|
+
out.workdir = args2[++i];
|
|
1274
|
+
} else if (args2[i] === "--bridge-state-dir" && i + 1 < args2.length) {
|
|
1275
|
+
out.bridgeStateDir = args2[++i];
|
|
1276
|
+
} else if (args2[i] === "--bootstrap-dir" && i + 1 < args2.length) {
|
|
1277
|
+
out.bootstrapDir = args2[++i];
|
|
1278
|
+
} else if (args2[i] === "--cli-shim-dir" && i + 1 < args2.length) {
|
|
1279
|
+
out.cliShimDir = args2[++i];
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
return out;
|
|
1283
|
+
}
|
|
1284
|
+
function emitFatal(message) {
|
|
1285
|
+
stdout2.write(JSON.stringify({ type: "bridge-fatal", message }) + "\n");
|
|
1286
|
+
process.exit(1);
|
|
1287
|
+
}
|
|
1288
|
+
function requireArg({
|
|
1289
|
+
value,
|
|
1290
|
+
name
|
|
1291
|
+
}) {
|
|
1292
|
+
if (!value) {
|
|
1293
|
+
emitFatal(`Missing ${name} argument.`);
|
|
1294
|
+
}
|
|
1295
|
+
return value;
|
|
1296
|
+
}
|
|
1297
|
+
//# sourceMappingURL=index.mjs.map
|