@runtypelabs/cli 2.59.1 → 2.59.2
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/dist/chunk-0-T9NWjH.js +3899 -0
- package/dist/chunk-0x7UKC1U.js +17 -0
- package/dist/chunk-2ySxXtcg.js +418 -0
- package/dist/chunk-3An_BRHv.js +501 -0
- package/dist/chunk-6uTxgdiY.js +16218 -0
- package/dist/chunk-87nRnNRG.js +1363 -0
- package/dist/chunk-B3wo8uRx.js +322 -0
- package/dist/chunk-BGp3ZdBy.js +560 -0
- package/dist/chunk-BJQv9Elp.js +107 -0
- package/dist/chunk-BOpO6xE-.js +6053 -0
- package/dist/chunk-BQXXCL8B.js +102 -0
- package/dist/chunk-BV-8isOd.js +34 -0
- package/dist/chunk-BVMhjGVe.js +826 -0
- package/dist/chunk-BW2fg46T.js +141 -0
- package/dist/chunk-BWJdY5Ag.js +36 -0
- package/dist/chunk-BYJOrLhh.js +302 -0
- package/dist/chunk-BYab8EVL.js +127 -0
- package/dist/chunk-BfEKvp7h.js +82 -0
- package/dist/chunk-BhgLSfH6.js +1123 -0
- package/dist/chunk-BirUxTtH.js +296 -0
- package/dist/chunk-BmB6UVZ9.js +1559 -0
- package/dist/chunk-BnWhIqW1.js +109 -0
- package/dist/chunk-Bpfnfk0t.js +123 -0
- package/dist/chunk-BrhgdnM3.js +108 -0
- package/dist/chunk-Bt0XAgX5.js +327 -0
- package/dist/chunk-Bu0FJr6S.js +84 -0
- package/dist/chunk-BvEoKpN3.js +255 -0
- package/dist/chunk-BxNJgYbE.js +28 -0
- package/dist/chunk-C1OFhd7F.js +6131 -0
- package/dist/chunk-C7e7WwbU.js +331 -0
- package/dist/chunk-CDTYP6bN.js +558 -0
- package/dist/chunk-CGDQoqDC.js +24 -0
- package/dist/chunk-CJraEBVd.js +127 -0
- package/dist/chunk-CQrNhOtv.js +40 -0
- package/dist/chunk-CW6AzBH6.js +128 -0
- package/dist/chunk-CXgL4w-9.js +317 -0
- package/dist/chunk-CZrqoPyf.js +233 -0
- package/dist/chunk-CcFCyqbB.js +83 -0
- package/dist/chunk-CdaIvO-y.js +485 -0
- package/dist/chunk-CehSXtgK.js +624 -0
- package/dist/chunk-Ciqkop65.js +218 -0
- package/dist/chunk-CoAWdSiE.js +43 -0
- package/dist/chunk-Crb4y7t4.js +830 -0
- package/dist/chunk-CrlqIfmb.js +337 -0
- package/dist/chunk-Cs4pwCQ-.js +324 -0
- package/dist/chunk-CwM_9y01.js +519 -0
- package/dist/chunk-CyRbIJg8.js +440 -0
- package/dist/chunk-D0OHsyMy.js +212 -0
- package/dist/chunk-D3y2bURs.js +1347 -0
- package/dist/chunk-D4_mJk03.js +2338 -0
- package/dist/chunk-D6C8rned.js +578 -0
- package/dist/chunk-D9ZbUnks.js +11 -0
- package/dist/chunk-DEPmLshX.js +834 -0
- package/dist/chunk-DFj1UPB2.js +330 -0
- package/dist/chunk-DGdFdZVf.js +197 -0
- package/dist/chunk-DGv7X9S_.js +63 -0
- package/dist/chunk-DJQ47WYZ.js +664 -0
- package/dist/chunk-DJw3O61j.js +1974 -0
- package/dist/chunk-DK4FbWVK.js +100 -0
- package/dist/chunk-DSRYhUus.js +993 -0
- package/dist/chunk-DajZ9Eg4.js +299 -0
- package/dist/chunk-Df4dunvU.js +195 -0
- package/dist/chunk-DjNTPhfO.js +481 -0
- package/dist/chunk-Dn8eaJuu.js +1042 -0
- package/dist/chunk-DwDWqSxM.js +637 -0
- package/dist/chunk-Dz7cEtxl.js +84 -0
- package/dist/chunk-E_0bgjrS.js +359 -0
- package/dist/chunk-KPYN2vUp.js +776 -0
- package/dist/chunk-KcqPQJBi.js +86 -0
- package/dist/chunk-N9H0JnoG.js +349 -0
- package/dist/chunk-NONFcXkn.js +728 -0
- package/dist/chunk-PrFjcpmH.js +256 -0
- package/dist/chunk-QSfNYiw9.js +11 -0
- package/dist/chunk-R265bzj9.js +226 -0
- package/dist/chunk-X9V_r6jM.js +166 -0
- package/dist/chunk-ZBtPUtyA.js +55 -0
- package/dist/chunk-ZCTfmOs7.js +4939 -0
- package/dist/chunk-h6oavj0t.js +97 -0
- package/dist/chunk-i7DwkvvD.js +42 -0
- package/dist/chunk-mG-4eZ6S.js +203 -0
- package/dist/chunk-o0ygUbCf.js +313 -0
- package/dist/chunk-ucu5lrDZ.js +5715 -0
- package/dist/index.js +11367 -73789
- package/package.json +4 -4
|
@@ -0,0 +1,2338 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { F as getCliVersion } from "./index.js";
|
|
3
|
+
import { t as HARNESS_LIMITS } from "./chunk-0x7UKC1U.js";
|
|
4
|
+
import { createRequire } from "node:module";
|
|
5
|
+
import { constants } from "node:fs";
|
|
6
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
+
import { execFile, spawn } from "node:child_process";
|
|
8
|
+
import { homedir, tmpdir } from "node:os";
|
|
9
|
+
import { promisify } from "node:util";
|
|
10
|
+
import { Readable, Writable } from "node:stream";
|
|
11
|
+
import { access, chmod, copyFile, mkdir, mkdtemp, readFile, realpath, rm, writeFile } from "node:fs/promises";
|
|
12
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
13
|
+
import { Client, StreamableHTTPClientTransport } from "@modelcontextprotocol/client";
|
|
14
|
+
import { RequestError, client, methods, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
15
|
+
// src/lib/local-harness/backends/claude/observations.ts
|
|
16
|
+
const object$2 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
17
|
+
const number = (value) => typeof value === "number" && Number.isFinite(value) && value >= 0;
|
|
18
|
+
const string$1 = (value) => typeof value === "string" ? value : void 0;
|
|
19
|
+
const counters = [
|
|
20
|
+
"inputTokens",
|
|
21
|
+
"outputTokens",
|
|
22
|
+
"cacheReadInputTokens",
|
|
23
|
+
"cacheCreationInputTokens"
|
|
24
|
+
];
|
|
25
|
+
function reportedUsage$1(current, previous) {
|
|
26
|
+
const unknown = {
|
|
27
|
+
costUsd: null,
|
|
28
|
+
completeness: "partial",
|
|
29
|
+
scope: "claude_sdk_query"
|
|
30
|
+
};
|
|
31
|
+
if (!string$1(current.session_id) || !number(current.total_cost_usd) || current.subtype !== "success" && current.total_cost_usd === 0) return unknown;
|
|
32
|
+
if (previous && (previous.session_id !== current.session_id || previous.uuid === current.uuid || !number(previous.total_cost_usd) || current.total_cost_usd < previous.total_cost_usd)) return unknown;
|
|
33
|
+
let complete = true;
|
|
34
|
+
const models = [];
|
|
35
|
+
const nowModels = object$2(current.modelUsage);
|
|
36
|
+
const oldModels = object$2(previous?.modelUsage);
|
|
37
|
+
for (const [model, raw] of Object.entries(nowModels).slice(0, 100)) {
|
|
38
|
+
const now = object$2(raw);
|
|
39
|
+
const before = object$2(oldModels[model]);
|
|
40
|
+
const entry = {
|
|
41
|
+
model: model.slice(0, 200),
|
|
42
|
+
pricingBasis: now.costBasis === "list" || now.costBasis === "managed" ? now.costBasis : "unknown"
|
|
43
|
+
};
|
|
44
|
+
if (string$1(now.provider)) entry.provider = String(now.provider).slice(0, 100);
|
|
45
|
+
if (string$1(now.canonicalModel)) entry.canonicalModel = String(now.canonicalModel).slice(0, 200);
|
|
46
|
+
for (const field of [...counters, "costUSD"]) {
|
|
47
|
+
const amount = now[field];
|
|
48
|
+
const baseline = model in oldModels ? before[field] : 0;
|
|
49
|
+
if (number(amount) && number(baseline) && amount >= baseline && (field === "costUSD" || Number.isInteger(amount) && Number.isInteger(baseline))) entry[field === "costUSD" ? "costUsd" : field] = amount - baseline;
|
|
50
|
+
else complete = false;
|
|
51
|
+
}
|
|
52
|
+
models.push(entry);
|
|
53
|
+
}
|
|
54
|
+
if (Object.keys(nowModels).length > 100 || Object.keys(oldModels).some((model) => !(model in nowModels))) complete = false;
|
|
55
|
+
return {
|
|
56
|
+
costUsd: current.total_cost_usd - (number(previous?.total_cost_usd) ? previous.total_cost_usd : 0),
|
|
57
|
+
completeness: complete ? "complete" : "partial",
|
|
58
|
+
scope: "claude_sdk_query",
|
|
59
|
+
models
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function createObserver$1(emit, servers) {
|
|
63
|
+
const tools = /* @__PURE__ */ new Map();
|
|
64
|
+
const seen = /* @__PURE__ */ new Set();
|
|
65
|
+
return {
|
|
66
|
+
observe(raw) {
|
|
67
|
+
const message = object$2(raw);
|
|
68
|
+
const common = {
|
|
69
|
+
nativeId: string$1(message.uuid),
|
|
70
|
+
nativeSessionId: string$1(message.session_id),
|
|
71
|
+
nativeTurnId: string$1(message.user_message_uuid),
|
|
72
|
+
parentId: string$1(message.parent_tool_use_id)
|
|
73
|
+
};
|
|
74
|
+
emit({
|
|
75
|
+
...common,
|
|
76
|
+
kind: "native",
|
|
77
|
+
native: raw,
|
|
78
|
+
...message.type === "stream_event" ? { fragment: true } : {}
|
|
79
|
+
});
|
|
80
|
+
if (typeof message.uuid === "string") {
|
|
81
|
+
if (seen.has(message.uuid)) return;
|
|
82
|
+
seen.add(message.uuid);
|
|
83
|
+
}
|
|
84
|
+
const body = object$2(message.message);
|
|
85
|
+
const blocks = Array.isArray(body.content) ? body.content : [];
|
|
86
|
+
if (message.type === "assistant" || message.type === "user") {
|
|
87
|
+
emit({
|
|
88
|
+
...common,
|
|
89
|
+
kind: "message",
|
|
90
|
+
message: {
|
|
91
|
+
role: message.type,
|
|
92
|
+
content: body.content
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
for (const rawBlock of blocks) {
|
|
96
|
+
const block = object$2(rawBlock);
|
|
97
|
+
if (block.type === "tool_use" && typeof block.id === "string" && typeof block.name === "string") {
|
|
98
|
+
const prior = tools.get(block.id);
|
|
99
|
+
const server = [...servers].sort((a, b) => b.length - a.length).find((name) => String(block.name).startsWith(`mcp__${name}__`));
|
|
100
|
+
const tool = {
|
|
101
|
+
...prior,
|
|
102
|
+
id: block.id,
|
|
103
|
+
name: block.name,
|
|
104
|
+
nativeName: block.name,
|
|
105
|
+
input: block.input,
|
|
106
|
+
status: prior?.status === "failed" ? "failed" : "running",
|
|
107
|
+
startedAt: prior?.startedAt ?? Date.now(),
|
|
108
|
+
...server ? {
|
|
109
|
+
mcpServer: server,
|
|
110
|
+
mcpTool: block.name.slice(`mcp__${server}__`.length)
|
|
111
|
+
} : {}
|
|
112
|
+
};
|
|
113
|
+
tools.set(block.id, tool);
|
|
114
|
+
emit({
|
|
115
|
+
...common,
|
|
116
|
+
kind: "tool",
|
|
117
|
+
tool: { ...tool }
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (block.type === "tool_result" && typeof block.tool_use_id === "string") {
|
|
121
|
+
const prior = tools.get(block.tool_use_id);
|
|
122
|
+
const structured = blocks.filter((value) => object$2(value).type === "tool_result").length === 1 ? message.tool_use_result : void 0;
|
|
123
|
+
const failed = prior?.status === "failed" || block.is_error === true;
|
|
124
|
+
const detached = object$2(structured).detachedToolCall === true;
|
|
125
|
+
const tool = {
|
|
126
|
+
...prior,
|
|
127
|
+
id: block.tool_use_id,
|
|
128
|
+
output: structured === void 0 ? block.content : {
|
|
129
|
+
content: block.content,
|
|
130
|
+
structured
|
|
131
|
+
},
|
|
132
|
+
status: failed ? "failed" : detached ? "running" : "succeeded",
|
|
133
|
+
...!detached || failed ? { endedAt: Date.now() } : {},
|
|
134
|
+
...block.is_error === true ? { error: block.content } : {}
|
|
135
|
+
};
|
|
136
|
+
tools.set(block.tool_use_id, tool);
|
|
137
|
+
emit({
|
|
138
|
+
...common,
|
|
139
|
+
kind: "tool",
|
|
140
|
+
tool: { ...tool }
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (message.type === "stream_event") {
|
|
146
|
+
const delta = object$2(object$2(message.event).delta);
|
|
147
|
+
if (delta.type === "text_delta") emit({
|
|
148
|
+
...common,
|
|
149
|
+
kind: "message",
|
|
150
|
+
message: {
|
|
151
|
+
role: "assistant",
|
|
152
|
+
content: delta.text,
|
|
153
|
+
delta: true
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
if (message.type === "system" && message.subtype === "permission_denied" && typeof message.tool_use_id === "string" && typeof message.tool_name === "string") emit({
|
|
158
|
+
...common,
|
|
159
|
+
kind: "permission",
|
|
160
|
+
permission: {
|
|
161
|
+
id: message.tool_use_id,
|
|
162
|
+
capability: message.tool_name,
|
|
163
|
+
decision: "deny",
|
|
164
|
+
source: "claude_sdk_advisory"
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
if (message.type === "result") {
|
|
168
|
+
for (const rawDenial of Array.isArray(message.permission_denials) ? message.permission_denials : []) {
|
|
169
|
+
const denial = object$2(rawDenial);
|
|
170
|
+
if (typeof denial.tool_use_id === "string" && typeof denial.tool_name === "string") emit({
|
|
171
|
+
...common,
|
|
172
|
+
kind: "permission",
|
|
173
|
+
permission: {
|
|
174
|
+
id: denial.tool_use_id,
|
|
175
|
+
capability: denial.tool_name,
|
|
176
|
+
decision: "deny",
|
|
177
|
+
source: "claude_sdk_result"
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
for (const [model, rawUsage] of Object.entries(object$2(message.modelUsage))) {
|
|
182
|
+
const usage = object$2(rawUsage);
|
|
183
|
+
emit({
|
|
184
|
+
...common,
|
|
185
|
+
kind: "usage",
|
|
186
|
+
usage: {
|
|
187
|
+
scope: "session",
|
|
188
|
+
cumulative: true,
|
|
189
|
+
epoch: string$1(message.session_id) ?? "unknown",
|
|
190
|
+
model,
|
|
191
|
+
modelBasis: "reported",
|
|
192
|
+
overlapGroup: "claude_sdk_query",
|
|
193
|
+
...Object.fromEntries(counters.filter((key) => number(usage[key])).map((key) => [key, usage[key]])),
|
|
194
|
+
...number(usage.costUSD) ? {
|
|
195
|
+
amount: usage.costUSD,
|
|
196
|
+
unit: "USD"
|
|
197
|
+
} : {}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
},
|
|
203
|
+
finish() {
|
|
204
|
+
for (const tool of tools.values()) if (tool.status === "running" || tool.status === "pending") emit({
|
|
205
|
+
kind: "tool",
|
|
206
|
+
tool: {
|
|
207
|
+
...tool,
|
|
208
|
+
status: "unknown"
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
// src/lib/local-harness/process.ts
|
|
215
|
+
const exec$3 = promisify(execFile);
|
|
216
|
+
async function liveGroups() {
|
|
217
|
+
const listing = await exec$3("/bin/ps", ["-axo", "pgid=,stat="], {
|
|
218
|
+
timeout: 1e3,
|
|
219
|
+
maxBuffer: 1048576
|
|
220
|
+
});
|
|
221
|
+
return new Set(listing.stdout.split("\n").flatMap((line) => {
|
|
222
|
+
const [group, state] = line.trim().split(/\s+/);
|
|
223
|
+
return group && state && !state.startsWith("Z") ? [Number(group)] : [];
|
|
224
|
+
}));
|
|
225
|
+
}
|
|
226
|
+
async function signalGroup(pid, signal) {
|
|
227
|
+
try {
|
|
228
|
+
process.kill(-pid, signal);
|
|
229
|
+
return true;
|
|
230
|
+
} catch (error) {
|
|
231
|
+
if (error.code === "ESRCH") return false;
|
|
232
|
+
if (error.code === "EPERM" && !(await liveGroups()).has(pid)) return false;
|
|
233
|
+
throw new Error("Harness process cleanup failed.", { cause: error });
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function createSupervisor() {
|
|
237
|
+
const children = /* @__PURE__ */ new Set();
|
|
238
|
+
let stopping;
|
|
239
|
+
return {
|
|
240
|
+
spawn(options) {
|
|
241
|
+
if (stopping) throw new Error("Harness supervisor is closed.");
|
|
242
|
+
const child = spawn(options.command, options.args, {
|
|
243
|
+
cwd: options.cwd,
|
|
244
|
+
env: options.env,
|
|
245
|
+
detached: true,
|
|
246
|
+
stdio: [
|
|
247
|
+
"pipe",
|
|
248
|
+
"pipe",
|
|
249
|
+
"pipe"
|
|
250
|
+
]
|
|
251
|
+
});
|
|
252
|
+
children.add(child);
|
|
253
|
+
let outputBytes = 0;
|
|
254
|
+
child.stdout?.on("data", (chunk) => {
|
|
255
|
+
outputBytes += chunk.length;
|
|
256
|
+
if (outputBytes > 16777216) child.stdout?.destroy(/* @__PURE__ */ new Error("Harness output limit exceeded."));
|
|
257
|
+
});
|
|
258
|
+
child.stderr?.resume();
|
|
259
|
+
return child;
|
|
260
|
+
},
|
|
261
|
+
stop() {
|
|
262
|
+
stopping ??= (async () => {
|
|
263
|
+
const pids = [...children].flatMap((child) => child.pid ? [child.pid] : []);
|
|
264
|
+
const active = [];
|
|
265
|
+
for (const pid of pids) if (await signalGroup(pid, "SIGTERM")) active.push(pid);
|
|
266
|
+
if (!active.length) return;
|
|
267
|
+
await setTimeout$1(250);
|
|
268
|
+
for (const pid of active) await signalGroup(pid, "SIGKILL");
|
|
269
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
270
|
+
const groups = await liveGroups();
|
|
271
|
+
if (!active.some((pid) => groups.has(pid))) return;
|
|
272
|
+
await setTimeout$1(50);
|
|
273
|
+
}
|
|
274
|
+
throw new Error("Harness descendants did not stop before cleanup deadline.");
|
|
275
|
+
})();
|
|
276
|
+
return stopping;
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
// src/lib/local-harness/compatibility.ts
|
|
281
|
+
/** Exact implementation tuples; platform acceptance evidence is recorded separately from protocol support. */
|
|
282
|
+
const HARNESS_COMPATIBILITY = {
|
|
283
|
+
claudeNative: {
|
|
284
|
+
harness: "2.1.314",
|
|
285
|
+
sdk: "0.3.308",
|
|
286
|
+
protocol: "claude-agent-sdk",
|
|
287
|
+
profile: "controlled-mcp"
|
|
288
|
+
},
|
|
289
|
+
codexNative: {
|
|
290
|
+
harness: "0.154.0",
|
|
291
|
+
protocol: "app-server",
|
|
292
|
+
profile: "controlled-mcp"
|
|
293
|
+
},
|
|
294
|
+
claudeAcp: {
|
|
295
|
+
harness: "2.1.314",
|
|
296
|
+
adapter: "0.99.1",
|
|
297
|
+
sdk: "0.3.312",
|
|
298
|
+
protocol: "1",
|
|
299
|
+
profile: "controlled-mcp"
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
// src/lib/local-harness/backends/claude/index.ts
|
|
303
|
+
const CLAUDE_SDK_VERSION = HARNESS_COMPATIBILITY.claudeNative.sdk;
|
|
304
|
+
const CLAUDE_RUNTIME_VERSION = HARNESS_COMPATIBILITY.claudeNative.harness;
|
|
305
|
+
const exec$2 = promisify(execFile);
|
|
306
|
+
const coverage$1 = {
|
|
307
|
+
tools: { state: "available" },
|
|
308
|
+
messages: { state: "available" },
|
|
309
|
+
native: { state: "available" },
|
|
310
|
+
usage: {
|
|
311
|
+
state: "available",
|
|
312
|
+
reason: "SDK query pipeline estimates; excludes helpers outside that pipeline."
|
|
313
|
+
},
|
|
314
|
+
timing: {
|
|
315
|
+
state: "partial",
|
|
316
|
+
reason: "Tool timestamps are local observations; SDK reports aggregate durations."
|
|
317
|
+
},
|
|
318
|
+
lineage: {
|
|
319
|
+
state: "disabled",
|
|
320
|
+
reason: "Built-in subagent tools are disabled in controlled-mcp."
|
|
321
|
+
},
|
|
322
|
+
reasoning: {
|
|
323
|
+
state: "partial",
|
|
324
|
+
reason: "Only summaries emitted by the SDK are captured."
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
function validate$1(config) {
|
|
328
|
+
if (process.platform !== "darwin" && process.platform !== "linux") throw new Error("Claude controlled harness requires macOS or Linux.");
|
|
329
|
+
if (config.harness !== "claude" || config.backend !== "native" || config.profile !== "controlled-mcp") throw new Error("This backend supports Claude native controlled-mcp only.");
|
|
330
|
+
if (!Number.isInteger(config.maxTurns) || config.maxTurns < 1 || !Number.isFinite(config.timeoutMs) || config.timeoutMs < 1) throw new Error("Claude requires positive turn and timeout limits.");
|
|
331
|
+
const names = /* @__PURE__ */ new Set();
|
|
332
|
+
for (const server of config.mcpServers) {
|
|
333
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(server.name) || names.has(server.name)) throw new Error("Invalid or duplicate MCP server name.");
|
|
334
|
+
const url = new URL(server.url);
|
|
335
|
+
if (url.username || url.password || url.protocol !== "https:" && !(url.protocol === "http:" && [
|
|
336
|
+
"localhost",
|
|
337
|
+
"127.0.0.1",
|
|
338
|
+
"[::1]"
|
|
339
|
+
].includes(url.hostname))) throw new Error("MCP requires HTTPS or loopback HTTP without URL credentials.");
|
|
340
|
+
names.add(server.name);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function provenance$1(config, version) {
|
|
344
|
+
return {
|
|
345
|
+
harness: "claude",
|
|
346
|
+
backend: "native",
|
|
347
|
+
harnessVersion: version,
|
|
348
|
+
sdkVersion: CLAUDE_SDK_VERSION,
|
|
349
|
+
protocolVersion: "claude-agent-sdk",
|
|
350
|
+
profile: config.profile,
|
|
351
|
+
coverage: coverage$1,
|
|
352
|
+
configuration: {
|
|
353
|
+
builtInTools: false,
|
|
354
|
+
settingSources: [],
|
|
355
|
+
strictMcpConfig: true,
|
|
356
|
+
permissionMode: "dontAsk",
|
|
357
|
+
mcpServers: config.mcpServers.map((server) => server.name),
|
|
358
|
+
requestedModel: config.model ?? null
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
const ENVIRONMENT_ALLOWLIST = [
|
|
363
|
+
"PATH",
|
|
364
|
+
"HOME",
|
|
365
|
+
"USER",
|
|
366
|
+
"LOGNAME",
|
|
367
|
+
"SHELL",
|
|
368
|
+
"TMPDIR",
|
|
369
|
+
"TMP",
|
|
370
|
+
"TEMP",
|
|
371
|
+
"LANG",
|
|
372
|
+
"LC_ALL",
|
|
373
|
+
"LC_CTYPE",
|
|
374
|
+
"TERM",
|
|
375
|
+
"ANTHROPIC_API_KEY",
|
|
376
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
377
|
+
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
378
|
+
"ANTHROPIC_BASE_URL",
|
|
379
|
+
"CLAUDE_CONFIG_DIR",
|
|
380
|
+
"HTTP_PROXY",
|
|
381
|
+
"HTTPS_PROXY",
|
|
382
|
+
"ALL_PROXY",
|
|
383
|
+
"NO_PROXY",
|
|
384
|
+
"http_proxy",
|
|
385
|
+
"https_proxy",
|
|
386
|
+
"all_proxy",
|
|
387
|
+
"no_proxy",
|
|
388
|
+
"SSL_CERT_FILE",
|
|
389
|
+
"SSL_CERT_DIR",
|
|
390
|
+
"NODE_EXTRA_CA_CERTS"
|
|
391
|
+
];
|
|
392
|
+
function controlledEnvironment$1() {
|
|
393
|
+
return {
|
|
394
|
+
...Object.fromEntries(ENVIRONMENT_ALLOWLIST.flatMap((key) => process.env[key] === void 0 ? [] : [[key, process.env[key]]])),
|
|
395
|
+
CLAUDE_AGENT_SDK_CLIENT_APP: "runtype-cli-local-harness",
|
|
396
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: "1"
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
function options(config, cwd, supervisor, abortController) {
|
|
400
|
+
return {
|
|
401
|
+
pathToClaudeCodeExecutable: config.executable ?? "claude",
|
|
402
|
+
systemPrompt: {
|
|
403
|
+
type: "preset",
|
|
404
|
+
preset: "claude_code"
|
|
405
|
+
},
|
|
406
|
+
tools: [],
|
|
407
|
+
strictMcpConfig: true,
|
|
408
|
+
mcpServers: Object.fromEntries(config.mcpServers.map(({ name, url, headers }) => [name, {
|
|
409
|
+
type: "http",
|
|
410
|
+
url,
|
|
411
|
+
...headers ? { headers } : {}
|
|
412
|
+
}])),
|
|
413
|
+
allowedTools: config.mcpServers.map(({ name }) => `mcp__${name}`),
|
|
414
|
+
permissionMode: "dontAsk",
|
|
415
|
+
allowDangerouslySkipPermissions: false,
|
|
416
|
+
settingSources: [],
|
|
417
|
+
includePartialMessages: true,
|
|
418
|
+
promptSuggestions: false,
|
|
419
|
+
cwd,
|
|
420
|
+
maxTurns: config.maxTurns,
|
|
421
|
+
...config.model ? { model: config.model } : {},
|
|
422
|
+
env: controlledEnvironment$1(),
|
|
423
|
+
abortController,
|
|
424
|
+
spawnClaudeCodeProcess: supervisor.spawn,
|
|
425
|
+
stderr: () => {}
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
async function bounded(work, signal, deadlineAt) {
|
|
429
|
+
signal.throwIfAborted();
|
|
430
|
+
let timer;
|
|
431
|
+
let abort;
|
|
432
|
+
try {
|
|
433
|
+
return await Promise.race([work, new Promise((_, reject) => {
|
|
434
|
+
abort = () => reject(/* @__PURE__ */ new Error("Claude operation cancelled."));
|
|
435
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
436
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("Claude operation deadline exceeded.")), Math.max(0, deadlineAt - Date.now()));
|
|
437
|
+
})]);
|
|
438
|
+
} finally {
|
|
439
|
+
if (timer) clearTimeout(timer);
|
|
440
|
+
if (abort) signal.removeEventListener("abort", abort);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
function createClaudeBackend(dependencies = {}) {
|
|
444
|
+
const loadSdk = dependencies.loadSdk ?? (() => import("@anthropic-ai/claude-agent-sdk"));
|
|
445
|
+
const authStatus = dependencies.authStatus ?? (async (executable, context) => {
|
|
446
|
+
let stdout;
|
|
447
|
+
let failed = false;
|
|
448
|
+
try {
|
|
449
|
+
stdout = (await exec$2(executable, [
|
|
450
|
+
"--setting-sources",
|
|
451
|
+
"",
|
|
452
|
+
"auth",
|
|
453
|
+
"status",
|
|
454
|
+
"--json"
|
|
455
|
+
], {
|
|
456
|
+
cwd: context.cwd,
|
|
457
|
+
env: controlledEnvironment$1(),
|
|
458
|
+
signal: context.signal,
|
|
459
|
+
timeout: Math.max(1, Math.min(5e3, context.deadlineAt - Date.now())),
|
|
460
|
+
maxBuffer: 16384
|
|
461
|
+
})).stdout;
|
|
462
|
+
} catch (error) {
|
|
463
|
+
const failure = object$2(error);
|
|
464
|
+
if (failure.code !== 1 || typeof failure.stdout !== "string") throw error;
|
|
465
|
+
stdout = failure.stdout;
|
|
466
|
+
failed = true;
|
|
467
|
+
}
|
|
468
|
+
const status = object$2(JSON.parse(stdout));
|
|
469
|
+
if (failed && status.loggedIn !== false) throw new Error("Claude auth status failed.");
|
|
470
|
+
return {
|
|
471
|
+
loggedIn: status.loggedIn,
|
|
472
|
+
apiProvider: status.apiProvider
|
|
473
|
+
};
|
|
474
|
+
});
|
|
475
|
+
const version = dependencies.version ?? (async (executable) => {
|
|
476
|
+
try {
|
|
477
|
+
return (await exec$2(executable, ["--version"], {
|
|
478
|
+
timeout: 5e3,
|
|
479
|
+
maxBuffer: 8192,
|
|
480
|
+
env: controlledEnvironment$1()
|
|
481
|
+
})).stdout.match(/^(\d+\.\d+\.\d+)/)?.[1] ?? "unknown";
|
|
482
|
+
} catch {
|
|
483
|
+
throw new Error("Claude executable is unavailable or failed version discovery.");
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
async function check(config) {
|
|
487
|
+
validate$1(config);
|
|
488
|
+
const actual = await version(config.executable ?? "claude");
|
|
489
|
+
if (actual !== CLAUDE_RUNTIME_VERSION) throw new Error(`Unsupported Claude version. This backend requires ${CLAUDE_RUNTIME_VERSION}.`);
|
|
490
|
+
return actual;
|
|
491
|
+
}
|
|
492
|
+
return {
|
|
493
|
+
async doctor(config) {
|
|
494
|
+
let actual = "unknown";
|
|
495
|
+
let cwd;
|
|
496
|
+
let run;
|
|
497
|
+
const supervisor = createSupervisor();
|
|
498
|
+
const controller = new AbortController();
|
|
499
|
+
let releaseInput;
|
|
500
|
+
try {
|
|
501
|
+
actual = await check(config);
|
|
502
|
+
cwd = await mkdtemp(join(tmpdir(), "runtype-claude-doctor-"));
|
|
503
|
+
const sdk = await loadSdk();
|
|
504
|
+
const input = { [Symbol.asyncIterator]() {
|
|
505
|
+
return { async next() {
|
|
506
|
+
await new Promise((resolve) => {
|
|
507
|
+
releaseInput = resolve;
|
|
508
|
+
});
|
|
509
|
+
return {
|
|
510
|
+
done: true,
|
|
511
|
+
value: void 0
|
|
512
|
+
};
|
|
513
|
+
} };
|
|
514
|
+
} };
|
|
515
|
+
run = sdk.query({
|
|
516
|
+
prompt: input,
|
|
517
|
+
options: options(config, cwd, supervisor, controller)
|
|
518
|
+
});
|
|
519
|
+
const deadline = Date.now() + Math.min(config.timeoutMs, 15e3);
|
|
520
|
+
const [account] = await bounded(Promise.all([
|
|
521
|
+
run.accountInfo(),
|
|
522
|
+
run.mcpServerStatus(),
|
|
523
|
+
run.initializationResult()
|
|
524
|
+
]), controller.signal, deadline);
|
|
525
|
+
let authentication = account.apiProvider && account.apiProvider !== "firstParty" ? "unsupported" : account.tokenSource && account.tokenSource !== "none" || account.apiKeySource && account.apiKeySource !== "none" ? "ready" : "unknown";
|
|
526
|
+
if (authentication === "unknown") try {
|
|
527
|
+
const status = object$2(await bounded(authStatus(config.executable ?? "claude", {
|
|
528
|
+
cwd,
|
|
529
|
+
signal: controller.signal,
|
|
530
|
+
deadlineAt: deadline
|
|
531
|
+
}), controller.signal, deadline));
|
|
532
|
+
authentication = status.apiProvider !== void 0 && typeof status.apiProvider !== "string" ? "unknown" : typeof status.apiProvider === "string" && status.apiProvider !== "firstParty" ? "unsupported" : status.loggedIn === false ? "absent" : status.loggedIn === true && (status.apiProvider === "firstParty" || account.apiProvider === "firstParty") ? "ready" : "unknown";
|
|
533
|
+
} catch {
|
|
534
|
+
authentication = "unknown";
|
|
535
|
+
}
|
|
536
|
+
let connected = config.mcpServers.length === 0;
|
|
537
|
+
while (!connected && Date.now() < deadline) {
|
|
538
|
+
const servers = await bounded(run.mcpServerStatus(), controller.signal, deadline);
|
|
539
|
+
connected = config.mcpServers.every((server) => servers.some((status) => status.name === server.name && status.status === "connected"));
|
|
540
|
+
if (connected || servers.some((server) => server.status === "failed" || server.status === "needs-auth")) break;
|
|
541
|
+
await setTimeout$1(100);
|
|
542
|
+
}
|
|
543
|
+
return {
|
|
544
|
+
ready: authentication === "ready" && connected,
|
|
545
|
+
authentication,
|
|
546
|
+
provenance: provenance$1(config, actual),
|
|
547
|
+
diagnostics: [authentication === "ready" ? "Credentials are present; expiry and live authorization are not verified without a provider request." : authentication === "absent" ? "Sign in with Claude or configure an Anthropic API key." : authentication === "unsupported" ? "This backend requires first-party Anthropic authentication; third-party providers are unsupported." : "Credential status could not be verified. Run Claude auth status to inspect local authentication.", ...connected ? [] : ["One or more configured MCP servers are not connected."]]
|
|
548
|
+
};
|
|
549
|
+
} catch {
|
|
550
|
+
return {
|
|
551
|
+
ready: false,
|
|
552
|
+
authentication: "unknown",
|
|
553
|
+
provenance: provenance$1(config, actual),
|
|
554
|
+
diagnostics: [`Claude initialization failed. This backend requires Claude ${CLAUDE_RUNTIME_VERSION}; verify authentication and MCP configuration.`]
|
|
555
|
+
};
|
|
556
|
+
} finally {
|
|
557
|
+
releaseInput?.();
|
|
558
|
+
controller.abort();
|
|
559
|
+
run?.close();
|
|
560
|
+
await supervisor.stop();
|
|
561
|
+
if (cwd) await rm(cwd, {
|
|
562
|
+
recursive: true,
|
|
563
|
+
force: true
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
},
|
|
567
|
+
async start(config, context) {
|
|
568
|
+
context.signal.throwIfAborted();
|
|
569
|
+
await bounded(check(config), context.signal, context.deadlineAt);
|
|
570
|
+
context.signal.throwIfAborted();
|
|
571
|
+
const cwd = await mkdtemp(join(tmpdir(), "runtype-claude-session-"));
|
|
572
|
+
let sdk;
|
|
573
|
+
try {
|
|
574
|
+
sdk = await bounded(loadSdk(), context.signal, context.deadlineAt);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
await rm(cwd, {
|
|
577
|
+
recursive: true,
|
|
578
|
+
force: true
|
|
579
|
+
});
|
|
580
|
+
throw error;
|
|
581
|
+
}
|
|
582
|
+
let previous;
|
|
583
|
+
let nativeSessionId;
|
|
584
|
+
let blocked = false;
|
|
585
|
+
let closed = false;
|
|
586
|
+
let cleanupFailed = false;
|
|
587
|
+
let activeController;
|
|
588
|
+
let activeRun;
|
|
589
|
+
let activeCleanup;
|
|
590
|
+
const session = {
|
|
591
|
+
id: context.sessionId,
|
|
592
|
+
async prompt(input) {
|
|
593
|
+
if (blocked || closed || activeController) throw new Error("Claude session cannot accept another turn; start a new context.");
|
|
594
|
+
input.signal.throwIfAborted();
|
|
595
|
+
const controller = new AbortController();
|
|
596
|
+
activeController = controller;
|
|
597
|
+
const supervisor = createSupervisor();
|
|
598
|
+
const observer = createObserver$1(input.emit, config.mcpServers.map((server) => server.name));
|
|
599
|
+
let result;
|
|
600
|
+
let status = "failed";
|
|
601
|
+
let cleanupDone;
|
|
602
|
+
activeCleanup = new Promise((resolve) => {
|
|
603
|
+
cleanupDone = resolve;
|
|
604
|
+
});
|
|
605
|
+
const cancel = () => controller.abort();
|
|
606
|
+
input.signal.addEventListener("abort", cancel, { once: true });
|
|
607
|
+
try {
|
|
608
|
+
const deadline = Math.min(input.deadlineAt, Date.now() + config.timeoutMs);
|
|
609
|
+
if (deadline <= Date.now()) throw new Error("Claude operation deadline exceeded.");
|
|
610
|
+
input.emit({
|
|
611
|
+
kind: "message",
|
|
612
|
+
message: {
|
|
613
|
+
role: "user",
|
|
614
|
+
content: input.prompt
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
activeRun = sdk.query({
|
|
618
|
+
prompt: input.prompt,
|
|
619
|
+
options: {
|
|
620
|
+
...options(config, cwd, supervisor, controller),
|
|
621
|
+
...nativeSessionId ? { resume: nativeSessionId } : {}
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
const consume = async () => {
|
|
625
|
+
for await (const message of activeRun) {
|
|
626
|
+
observer.observe(message);
|
|
627
|
+
if (message.type === "result") result = object$2(message);
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
await bounded(consume(), controller.signal, deadline);
|
|
631
|
+
if (result?.subtype === "success" && result.is_error !== true && typeof result.result === "string") status = "completed";
|
|
632
|
+
else blocked = true;
|
|
633
|
+
} catch {
|
|
634
|
+
status = input.signal.aborted || controller.signal.aborted ? "cancelled" : "failed";
|
|
635
|
+
blocked = true;
|
|
636
|
+
} finally {
|
|
637
|
+
controller.abort();
|
|
638
|
+
input.signal.removeEventListener("abort", cancel);
|
|
639
|
+
try {
|
|
640
|
+
activeRun?.close();
|
|
641
|
+
await supervisor.stop();
|
|
642
|
+
} catch {
|
|
643
|
+
status = "failed";
|
|
644
|
+
blocked = true;
|
|
645
|
+
cleanupFailed = true;
|
|
646
|
+
input.emit({
|
|
647
|
+
kind: "lifecycle",
|
|
648
|
+
lifecycle: {
|
|
649
|
+
state: "cleanup_failed",
|
|
650
|
+
reason: "Claude process cleanup failed."
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
observer.finish();
|
|
655
|
+
activeRun = void 0;
|
|
656
|
+
activeController = void 0;
|
|
657
|
+
cleanupDone?.();
|
|
658
|
+
}
|
|
659
|
+
const usage = result ? reportedUsage$1(result, previous) : void 0;
|
|
660
|
+
if (result) previous = result;
|
|
661
|
+
if (typeof result?.session_id === "string") nativeSessionId = result.session_id;
|
|
662
|
+
return {
|
|
663
|
+
text: status === "completed" && typeof result?.result === "string" ? result.result : "",
|
|
664
|
+
status,
|
|
665
|
+
nativeSessionId,
|
|
666
|
+
nativeTurnId: typeof result?.user_message_uuid === "string" ? result.user_message_uuid : void 0,
|
|
667
|
+
...usage ? { reportedUsage: usage } : {},
|
|
668
|
+
coverage: status === "completed" ? coverage$1 : {
|
|
669
|
+
...coverage$1,
|
|
670
|
+
tools: {
|
|
671
|
+
state: "partial",
|
|
672
|
+
reason: "Turn failed or was cancelled; completed evidence is retained."
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
},
|
|
677
|
+
async cancel() {
|
|
678
|
+
blocked = true;
|
|
679
|
+
activeController?.abort();
|
|
680
|
+
await activeCleanup;
|
|
681
|
+
},
|
|
682
|
+
async close() {
|
|
683
|
+
closed = true;
|
|
684
|
+
await session.cancel();
|
|
685
|
+
if (cleanupFailed) throw new Error("Claude cleanup failed; session files were retained.");
|
|
686
|
+
await rm(cwd, {
|
|
687
|
+
recursive: true,
|
|
688
|
+
force: true
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
return session;
|
|
693
|
+
}
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
// src/lib/local-harness/backends/codex/failures.ts
|
|
697
|
+
const object$1 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
698
|
+
const causes = {
|
|
699
|
+
model_not_found: "The requested model is unavailable for this Codex account. Choose an account-supported model or omit --model.",
|
|
700
|
+
unauthorized: "Codex authentication was rejected. Sign in again and retry.",
|
|
701
|
+
invalid_api_key: "Codex authentication was rejected. Sign in again and retry.",
|
|
702
|
+
usageLimitExceeded: "The Codex account usage limit was reached.",
|
|
703
|
+
rateLimitExceeded: "Codex was rate limited. Retry after the account limit resets.",
|
|
704
|
+
rate_limit_exceeded: "Codex was rate limited. Retry after the account limit resets.",
|
|
705
|
+
contextWindowExceeded: "The Codex context window was exceeded. Start a new session.",
|
|
706
|
+
context_length_exceeded: "The Codex context window was exceeded. Start a new session.",
|
|
707
|
+
sessionBudgetExceeded: "The Codex session budget was exceeded.",
|
|
708
|
+
serverOverloaded: "The Codex provider is overloaded. Retry later.",
|
|
709
|
+
internalServerError: "The Codex provider returned an internal error. Retry later.",
|
|
710
|
+
sandboxError: "Codex could not establish its sandbox. Check the local installation.",
|
|
711
|
+
cyberPolicy: "The Codex provider blocked the turn under its policy.",
|
|
712
|
+
misalignmentPolicyViolation: "The Codex provider blocked the turn under its policy.",
|
|
713
|
+
badRequest: "The Codex provider rejected the request. Check the model and configuration."
|
|
714
|
+
};
|
|
715
|
+
var CodexCapabilityError = class extends Error {
|
|
716
|
+
constructor() {
|
|
717
|
+
super("Codex controlled MCP requires isolated Code Mode with built-in tools excluded. Install the matching codex-code-mode-host beside Codex and remove overrides that disable code_mode_host, code_mode_only, or the functions/clock exclusions.");
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
var CodexNativeError = class extends Error {
|
|
721
|
+
failure;
|
|
722
|
+
constructor(value) {
|
|
723
|
+
const failure = nativeFailure(value);
|
|
724
|
+
super(failure?.text ?? "Codex App Server request failed.");
|
|
725
|
+
this.failure = failure;
|
|
726
|
+
}
|
|
727
|
+
};
|
|
728
|
+
function codeModeUnavailable(value) {
|
|
729
|
+
return typeof value === "string" && value.length <= 16384 && /Code Mode is unavailable|code-mode host (?:is disabled|unavailable|failed)|failed to (?:start|spawn|initialize).*code.mode.host/i.test(value);
|
|
730
|
+
}
|
|
731
|
+
function nativeFailure(value) {
|
|
732
|
+
if (value instanceof CodexNativeError) return value.failure;
|
|
733
|
+
if (value instanceof CodexCapabilityError) return {
|
|
734
|
+
reason: "code_mode_unavailable",
|
|
735
|
+
text: value.message
|
|
736
|
+
};
|
|
737
|
+
const error = object$1(value);
|
|
738
|
+
const message = typeof error.message === "string" ? error.message : "";
|
|
739
|
+
if (codeModeUnavailable(message)) return {
|
|
740
|
+
reason: "code_mode_unavailable",
|
|
741
|
+
text: new CodexCapabilityError().message
|
|
742
|
+
};
|
|
743
|
+
let provider = error;
|
|
744
|
+
if (message.length <= 16384) try {
|
|
745
|
+
provider = object$1(JSON.parse(message));
|
|
746
|
+
} catch {
|
|
747
|
+
provider = error;
|
|
748
|
+
}
|
|
749
|
+
const code = object$1(provider.error).code ?? provider.code ?? error.codexErrorInfo;
|
|
750
|
+
if (typeof code === "string" && Object.hasOwn(causes, code)) return {
|
|
751
|
+
reason: code,
|
|
752
|
+
text: `Codex turn failed (${code}). ${causes[code]}`
|
|
753
|
+
};
|
|
754
|
+
const info = object$1(error.codexErrorInfo);
|
|
755
|
+
for (const name of [
|
|
756
|
+
"httpConnectionFailed",
|
|
757
|
+
"responseStreamConnectionFailed",
|
|
758
|
+
"responseStreamDisconnected",
|
|
759
|
+
"responseTooManyFailedAttempts"
|
|
760
|
+
]) {
|
|
761
|
+
const status = object$1(info[name]).httpStatusCode;
|
|
762
|
+
if (Number.isInteger(status) && Number(status) >= 100 && Number(status) <= 599) return {
|
|
763
|
+
reason: name,
|
|
764
|
+
text: `Codex turn failed (${name}, HTTP ${status}). Check account access and provider connectivity.`
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
// src/lib/local-harness/backends/codex/config.ts
|
|
769
|
+
const codeModeFeatures = {
|
|
770
|
+
code_mode: {
|
|
771
|
+
enabled: true,
|
|
772
|
+
excluded_tool_namespaces: ["functions", "clock"]
|
|
773
|
+
},
|
|
774
|
+
code_mode_only: true,
|
|
775
|
+
code_mode_host: {
|
|
776
|
+
enabled: true,
|
|
777
|
+
disable_in_process_fallback: true
|
|
778
|
+
}
|
|
779
|
+
};
|
|
780
|
+
function verifyCodeModeFeatures(config) {
|
|
781
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
782
|
+
const features = record(record(config).features);
|
|
783
|
+
const mode = record(features.code_mode);
|
|
784
|
+
const host = record(features.code_mode_host);
|
|
785
|
+
const excluded = mode.excluded_tool_namespaces;
|
|
786
|
+
if (mode.enabled !== true || features.code_mode_only !== true || host.enabled !== true || host.disable_in_process_fallback !== true || !Array.isArray(excluded) || !["functions", "clock"].every((name) => excluded.includes(name))) throw new CodexCapabilityError();
|
|
787
|
+
}
|
|
788
|
+
function controlledEnvironment(source = process.env) {
|
|
789
|
+
return Object.fromEntries([
|
|
790
|
+
"PATH",
|
|
791
|
+
"OPENAI_API_KEY",
|
|
792
|
+
"CODEX_API_KEY",
|
|
793
|
+
"SSL_CERT_FILE",
|
|
794
|
+
"SSL_CERT_DIR",
|
|
795
|
+
"NODE_EXTRA_CA_CERTS",
|
|
796
|
+
"SystemRoot",
|
|
797
|
+
"LANG",
|
|
798
|
+
"LC_ALL",
|
|
799
|
+
"HTTP_PROXY",
|
|
800
|
+
"HTTPS_PROXY",
|
|
801
|
+
"ALL_PROXY",
|
|
802
|
+
"NO_PROXY",
|
|
803
|
+
"http_proxy",
|
|
804
|
+
"https_proxy",
|
|
805
|
+
"all_proxy",
|
|
806
|
+
"no_proxy"
|
|
807
|
+
].filter((key) => typeof source[key] === "string").map((key) => [key, source[key]]));
|
|
808
|
+
}
|
|
809
|
+
function controlledConfig(config) {
|
|
810
|
+
return [
|
|
811
|
+
"approval_policy = \"never\"",
|
|
812
|
+
"sandbox_mode = \"read-only\"",
|
|
813
|
+
"web_search = \"disabled\"",
|
|
814
|
+
"project_doc_max_bytes = 0",
|
|
815
|
+
"check_for_update_on_startup = false",
|
|
816
|
+
"suppress_unstable_features_warning = true",
|
|
817
|
+
"mcp_oauth_credentials_store = \"file\"",
|
|
818
|
+
"cli_auth_credentials_store = \"file\"",
|
|
819
|
+
"service_tier = \"default\"",
|
|
820
|
+
"[analytics]",
|
|
821
|
+
"enabled = false",
|
|
822
|
+
"[features]",
|
|
823
|
+
...[
|
|
824
|
+
"shell_tool",
|
|
825
|
+
"unified_exec",
|
|
826
|
+
"shell_snapshot",
|
|
827
|
+
"apps",
|
|
828
|
+
"plugins",
|
|
829
|
+
"remote_plugin",
|
|
830
|
+
"multi_agent",
|
|
831
|
+
"multi_agent_v2",
|
|
832
|
+
"browser_use",
|
|
833
|
+
"browser_use_external",
|
|
834
|
+
"computer_use",
|
|
835
|
+
"image_generation",
|
|
836
|
+
"view_image",
|
|
837
|
+
"in_app_browser",
|
|
838
|
+
"in_app_chat",
|
|
839
|
+
"in_app_local_automation",
|
|
840
|
+
"in_app_updates",
|
|
841
|
+
"request_permissions_tool",
|
|
842
|
+
"goals",
|
|
843
|
+
"hooks",
|
|
844
|
+
"memories",
|
|
845
|
+
"skill_search",
|
|
846
|
+
"skill_mcp_dependency_install",
|
|
847
|
+
"workspace_dependencies",
|
|
848
|
+
"tool_suggest",
|
|
849
|
+
"sleep_tool"
|
|
850
|
+
].map((name) => `${name} = false`),
|
|
851
|
+
...Object.entries(codeModeFeatures).map(([name, value]) => `${name} = ${typeof value === "boolean" ? JSON.stringify(value) : `{ ${Object.entries(value).map(([key, field]) => `${key} = ${JSON.stringify(field)}`).join(", ")} }`}`),
|
|
852
|
+
"skip_host_skill_discovery = true",
|
|
853
|
+
"[tools.update_plan]",
|
|
854
|
+
"enabled = false",
|
|
855
|
+
"[tools.experimental_request_user_input]",
|
|
856
|
+
"enabled = false",
|
|
857
|
+
"[skills]",
|
|
858
|
+
"include_instructions = false",
|
|
859
|
+
"[skills.bundled]",
|
|
860
|
+
"enabled = false",
|
|
861
|
+
...config.mcpServers.flatMap(({ name, url, headers }) => [
|
|
862
|
+
`[mcp_servers.${JSON.stringify(name)}]`,
|
|
863
|
+
`url = ${JSON.stringify(url)}`,
|
|
864
|
+
"required = true",
|
|
865
|
+
"startup_timeout_sec = 20",
|
|
866
|
+
"tool_timeout_sec = 60",
|
|
867
|
+
...headers ? [`http_headers = { ${Object.entries(headers).map(([key, value]) => `${JSON.stringify(key)} = ${JSON.stringify(value)}`).join(", ")} }`] : []
|
|
868
|
+
]),
|
|
869
|
+
""
|
|
870
|
+
].join("\n");
|
|
871
|
+
}
|
|
872
|
+
async function createEnvironment(config) {
|
|
873
|
+
const root = await mkdtemp(join(tmpdir(), "runtype-app-server-"));
|
|
874
|
+
const home = join(root, "home");
|
|
875
|
+
const harnessHome = join(home, ".codex");
|
|
876
|
+
const cwd = join(root, "work");
|
|
877
|
+
const cleanup = () => rm(root, {
|
|
878
|
+
recursive: true,
|
|
879
|
+
force: true
|
|
880
|
+
});
|
|
881
|
+
try {
|
|
882
|
+
await mkdir(harnessHome, {
|
|
883
|
+
recursive: true,
|
|
884
|
+
mode: 448
|
|
885
|
+
});
|
|
886
|
+
await mkdir(cwd, { mode: 448 });
|
|
887
|
+
for (const name of ["auth.json", ".credentials.json"]) try {
|
|
888
|
+
const target = join(harnessHome, name);
|
|
889
|
+
await copyFile(join(process.env.CODEX_HOME || join(homedir(), ".codex"), name), target);
|
|
890
|
+
await chmod(target, 384);
|
|
891
|
+
} catch (error) {
|
|
892
|
+
if (error.code !== "ENOENT") throw error;
|
|
893
|
+
}
|
|
894
|
+
await writeFile(join(harnessHome, "config.toml"), controlledConfig(config), { mode: 384 });
|
|
895
|
+
return {
|
|
896
|
+
root,
|
|
897
|
+
cwd,
|
|
898
|
+
cleanup,
|
|
899
|
+
env: {
|
|
900
|
+
...controlledEnvironment(),
|
|
901
|
+
HOME: home,
|
|
902
|
+
CODEX_HOME: harnessHome,
|
|
903
|
+
XDG_CONFIG_HOME: join(home, ".config"),
|
|
904
|
+
XDG_DATA_HOME: join(home, ".local/share"),
|
|
905
|
+
XDG_CACHE_HOME: join(home, ".cache"),
|
|
906
|
+
XDG_STATE_HOME: join(home, ".local/state")
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
} catch (error) {
|
|
910
|
+
await cleanup();
|
|
911
|
+
throw error;
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
// src/lib/local-harness/backends/codex/rpc.ts
|
|
915
|
+
function object(value) {
|
|
916
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
917
|
+
}
|
|
918
|
+
function deniedResponse(method) {
|
|
919
|
+
if (method === "item/commandExecution/requestApproval" || method === "item/fileChange/requestApproval") return { decision: "cancel" };
|
|
920
|
+
if (method === "item/permissions/requestApproval") return {
|
|
921
|
+
permissions: {},
|
|
922
|
+
scope: "turn"
|
|
923
|
+
};
|
|
924
|
+
if (method === "mcpServer/elicitation/request") return { action: "decline" };
|
|
925
|
+
if (method === "item/tool/requestUserInput") return { answers: {} };
|
|
926
|
+
if (method === "item/tool/call") return {
|
|
927
|
+
contentItems: [],
|
|
928
|
+
success: false
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
function startRpc(command, cwd, env) {
|
|
932
|
+
const supervisor = createSupervisor();
|
|
933
|
+
const child = supervisor.spawn({
|
|
934
|
+
command,
|
|
935
|
+
args: [
|
|
936
|
+
"app-server",
|
|
937
|
+
"--strict-config",
|
|
938
|
+
"--stdio"
|
|
939
|
+
],
|
|
940
|
+
cwd,
|
|
941
|
+
env,
|
|
942
|
+
signal: new AbortController().signal
|
|
943
|
+
});
|
|
944
|
+
const pending = /* @__PURE__ */ new Map();
|
|
945
|
+
let sequence = 0;
|
|
946
|
+
let buffer = "";
|
|
947
|
+
let ended;
|
|
948
|
+
let listener = () => {};
|
|
949
|
+
let failure = () => {};
|
|
950
|
+
const fail = (error) => {
|
|
951
|
+
if (ended) return;
|
|
952
|
+
ended = error;
|
|
953
|
+
for (const entry of pending.values()) {
|
|
954
|
+
entry.dispose();
|
|
955
|
+
entry.reject(error);
|
|
956
|
+
}
|
|
957
|
+
pending.clear();
|
|
958
|
+
failure(error);
|
|
959
|
+
};
|
|
960
|
+
const send = (value) => {
|
|
961
|
+
if (ended) throw ended;
|
|
962
|
+
child.stdin.write(`${JSON.stringify(value)}\n`);
|
|
963
|
+
};
|
|
964
|
+
child.stdout.setEncoding("utf8");
|
|
965
|
+
child.stdout.on("data", (chunk) => {
|
|
966
|
+
buffer += chunk;
|
|
967
|
+
if (Buffer.byteLength(buffer) > 1048576) {
|
|
968
|
+
fail(/* @__PURE__ */ new Error("Codex protocol frame limit exceeded."));
|
|
969
|
+
supervisor.stop().catch(() => {});
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
let offset;
|
|
973
|
+
while ((offset = buffer.indexOf("\n")) >= 0) {
|
|
974
|
+
const line = buffer.slice(0, offset);
|
|
975
|
+
buffer = buffer.slice(offset + 1);
|
|
976
|
+
if (!line.trim()) continue;
|
|
977
|
+
try {
|
|
978
|
+
let decoded;
|
|
979
|
+
try {
|
|
980
|
+
decoded = JSON.parse(line);
|
|
981
|
+
} catch {
|
|
982
|
+
throw new Error("Codex emitted invalid JSON.");
|
|
983
|
+
}
|
|
984
|
+
const message = object(decoded);
|
|
985
|
+
listener(message);
|
|
986
|
+
if (typeof message.method === "string") {
|
|
987
|
+
if (message.id !== void 0) {
|
|
988
|
+
const result = deniedResponse(message.method);
|
|
989
|
+
send(result === void 0 ? {
|
|
990
|
+
id: message.id,
|
|
991
|
+
error: {
|
|
992
|
+
code: -32601,
|
|
993
|
+
message: "Interactive operation is unavailable."
|
|
994
|
+
}
|
|
995
|
+
} : {
|
|
996
|
+
id: message.id,
|
|
997
|
+
result
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
} else if (typeof message.id === "number") {
|
|
1001
|
+
const entry = pending.get(message.id);
|
|
1002
|
+
if (!entry) continue;
|
|
1003
|
+
pending.delete(message.id);
|
|
1004
|
+
entry.dispose();
|
|
1005
|
+
if (message.error) entry.reject(new CodexNativeError(message.error));
|
|
1006
|
+
else entry.resolve(object(message.result));
|
|
1007
|
+
}
|
|
1008
|
+
} catch (error) {
|
|
1009
|
+
fail(error instanceof Error ? error : /* @__PURE__ */ new Error("Invalid Codex protocol message."));
|
|
1010
|
+
supervisor.stop().catch(() => {});
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
});
|
|
1014
|
+
child.stdout.on("error", () => fail(/* @__PURE__ */ new Error("Codex output stream failed.")));
|
|
1015
|
+
child.stdin.on("error", () => fail(/* @__PURE__ */ new Error("Codex input stream failed.")));
|
|
1016
|
+
child.on("error", () => fail(/* @__PURE__ */ new Error("Codex process could not start.")));
|
|
1017
|
+
child.on("exit", () => fail(/* @__PURE__ */ new Error("Codex App Server exited.")));
|
|
1018
|
+
return {
|
|
1019
|
+
observe(next, onFailure) {
|
|
1020
|
+
listener = next;
|
|
1021
|
+
failure = onFailure;
|
|
1022
|
+
if (ended) failure(ended);
|
|
1023
|
+
},
|
|
1024
|
+
notify(method) {
|
|
1025
|
+
send({ method });
|
|
1026
|
+
},
|
|
1027
|
+
request(method, params, signal, deadlineAt) {
|
|
1028
|
+
if (ended) return Promise.reject(ended);
|
|
1029
|
+
if (signal.aborted || Date.now() >= deadlineAt) return Promise.reject(/* @__PURE__ */ new Error("Codex request cancelled or deadline exceeded."));
|
|
1030
|
+
const id = ++sequence;
|
|
1031
|
+
return new Promise((resolve, reject) => {
|
|
1032
|
+
const abort = () => {
|
|
1033
|
+
pending.delete(id);
|
|
1034
|
+
dispose();
|
|
1035
|
+
reject(/* @__PURE__ */ new Error("Codex request cancelled or deadline exceeded."));
|
|
1036
|
+
};
|
|
1037
|
+
const timer = setTimeout(abort, Math.max(1, deadlineAt - Date.now()));
|
|
1038
|
+
const dispose = () => {
|
|
1039
|
+
clearTimeout(timer);
|
|
1040
|
+
signal.removeEventListener("abort", abort);
|
|
1041
|
+
};
|
|
1042
|
+
pending.set(id, {
|
|
1043
|
+
resolve,
|
|
1044
|
+
reject,
|
|
1045
|
+
dispose
|
|
1046
|
+
});
|
|
1047
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1048
|
+
try {
|
|
1049
|
+
send({
|
|
1050
|
+
id,
|
|
1051
|
+
method,
|
|
1052
|
+
params
|
|
1053
|
+
});
|
|
1054
|
+
} catch (error) {
|
|
1055
|
+
pending.delete(id);
|
|
1056
|
+
dispose();
|
|
1057
|
+
reject(error);
|
|
1058
|
+
}
|
|
1059
|
+
});
|
|
1060
|
+
},
|
|
1061
|
+
async close() {
|
|
1062
|
+
failure = () => {};
|
|
1063
|
+
fail(/* @__PURE__ */ new Error("Codex session closed."));
|
|
1064
|
+
await supervisor.stop();
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
}
|
|
1068
|
+
// src/lib/local-harness/backends/codex/observations.ts
|
|
1069
|
+
const PRICING_PROVENANCE = {
|
|
1070
|
+
source: "https://developers.openai.com/api/docs/pricing",
|
|
1071
|
+
checkedAt: "2026-10-30",
|
|
1072
|
+
basis: "requested-model standard API list estimate; not subscription charges"
|
|
1073
|
+
};
|
|
1074
|
+
const rates = {
|
|
1075
|
+
"gpt-5.4": {
|
|
1076
|
+
input: 2.5,
|
|
1077
|
+
cached: .25,
|
|
1078
|
+
output: 15
|
|
1079
|
+
},
|
|
1080
|
+
"gpt-5.4-mini": {
|
|
1081
|
+
input: .75,
|
|
1082
|
+
cached: .075,
|
|
1083
|
+
output: 4.5
|
|
1084
|
+
},
|
|
1085
|
+
"gpt-5.2": {
|
|
1086
|
+
input: 1.75,
|
|
1087
|
+
cached: .175,
|
|
1088
|
+
output: 14
|
|
1089
|
+
},
|
|
1090
|
+
"gpt-6.1-sol": {
|
|
1091
|
+
input: 2,
|
|
1092
|
+
cached: .1,
|
|
1093
|
+
output: 10,
|
|
1094
|
+
writes: true
|
|
1095
|
+
}
|
|
1096
|
+
};
|
|
1097
|
+
const counter = (value) => Number.isSafeInteger(value) && Number(value) >= 0;
|
|
1098
|
+
const string = (value) => typeof value === "string" ? value : void 0;
|
|
1099
|
+
const fields = [
|
|
1100
|
+
"inputTokens",
|
|
1101
|
+
"cachedInputTokens",
|
|
1102
|
+
"cacheWriteInputTokens",
|
|
1103
|
+
"outputTokens",
|
|
1104
|
+
"reasoningOutputTokens"
|
|
1105
|
+
];
|
|
1106
|
+
function usageDelta(current, previous) {
|
|
1107
|
+
const delta = {};
|
|
1108
|
+
for (const field of fields) {
|
|
1109
|
+
const value = current[field];
|
|
1110
|
+
const before = previous ? previous[field] : 0;
|
|
1111
|
+
if (field === "cacheWriteInputTokens" && value === void 0 && (!previous || before === void 0)) continue;
|
|
1112
|
+
if (!counter(value) || !counter(before) || value < before) return void 0;
|
|
1113
|
+
delta[field] = value - before;
|
|
1114
|
+
}
|
|
1115
|
+
if (Number(delta.cachedInputTokens) > Number(delta.inputTokens) || Number(delta.reasoningOutputTokens) > Number(delta.outputTokens)) return void 0;
|
|
1116
|
+
return delta;
|
|
1117
|
+
}
|
|
1118
|
+
function reportedUsage(current, previous, model, complete, modelChanged = false) {
|
|
1119
|
+
const result = {
|
|
1120
|
+
costUsd: null,
|
|
1121
|
+
completeness: "partial",
|
|
1122
|
+
scope: "codex_app_server_turn_requested_model"
|
|
1123
|
+
};
|
|
1124
|
+
const delta = current && usageDelta(current, previous);
|
|
1125
|
+
if (!delta) return result;
|
|
1126
|
+
const entry = {
|
|
1127
|
+
model: model.slice(0, 200),
|
|
1128
|
+
provider: "openai",
|
|
1129
|
+
pricingBasis: "unknown",
|
|
1130
|
+
costUsd: null,
|
|
1131
|
+
inputTokens: Number(delta.inputTokens) - Number(delta.cachedInputTokens),
|
|
1132
|
+
outputTokens: Number(delta.outputTokens),
|
|
1133
|
+
cacheReadInputTokens: Number(delta.cachedInputTokens),
|
|
1134
|
+
...counter(delta.cacheWriteInputTokens) ? { cacheCreationInputTokens: delta.cacheWriteInputTokens } : {}
|
|
1135
|
+
};
|
|
1136
|
+
result.models = [entry];
|
|
1137
|
+
const rate = rates[model];
|
|
1138
|
+
if (!rate || modelChanged || !counter(current?.inputTokens) || current.inputTokens > 272e3 || rate.writes && delta.cacheWriteInputTokens === void 0 || delta.cacheWriteInputTokens !== void 0 && delta.cacheWriteInputTokens !== 0) return result;
|
|
1139
|
+
const amount = (entry.inputTokens * rate.input + entry.cacheReadInputTokens * rate.cached + entry.outputTokens * rate.output) / 1e6;
|
|
1140
|
+
if (!Number.isFinite(amount) || !complete && amount === 0) return result;
|
|
1141
|
+
result.costUsd = entry.costUsd = amount;
|
|
1142
|
+
entry.pricingBasis = "list";
|
|
1143
|
+
result.completeness = complete ? "complete" : "partial";
|
|
1144
|
+
return result;
|
|
1145
|
+
}
|
|
1146
|
+
function createObserver(emit, threadId, model, servers) {
|
|
1147
|
+
const tools = /* @__PURE__ */ new Map();
|
|
1148
|
+
const messages = /* @__PURE__ */ new Map();
|
|
1149
|
+
let usage;
|
|
1150
|
+
let usageUpdates = 0;
|
|
1151
|
+
let usageInvalid = false;
|
|
1152
|
+
let lastUsage = "";
|
|
1153
|
+
let modelChanged = false;
|
|
1154
|
+
let turnId;
|
|
1155
|
+
let terminal;
|
|
1156
|
+
let failure;
|
|
1157
|
+
return {
|
|
1158
|
+
observe(raw) {
|
|
1159
|
+
const params = object(raw.params);
|
|
1160
|
+
const item = object(params.item);
|
|
1161
|
+
const nativeTurnId = string(params.turnId) ?? string(object(params.turn).id);
|
|
1162
|
+
const common = {
|
|
1163
|
+
nativeId: string(item.id) ?? string(params.itemId),
|
|
1164
|
+
nativeSessionId: string(params.threadId) ?? threadId,
|
|
1165
|
+
nativeTurnId
|
|
1166
|
+
};
|
|
1167
|
+
emit({
|
|
1168
|
+
...common,
|
|
1169
|
+
kind: "native",
|
|
1170
|
+
native: raw,
|
|
1171
|
+
...typeof raw.method === "string" && /delta$/i.test(raw.method) ? { fragment: true } : {}
|
|
1172
|
+
});
|
|
1173
|
+
if (params.threadId !== void 0 && params.threadId !== threadId) throw new Error("Codex emitted a foreign thread event.");
|
|
1174
|
+
if (nativeTurnId) {
|
|
1175
|
+
if (turnId && nativeTurnId !== turnId) throw new Error("Codex emitted a foreign turn event.");
|
|
1176
|
+
turnId = nativeTurnId;
|
|
1177
|
+
}
|
|
1178
|
+
if (raw.method === "model/rerouted" || raw.method === "thread/model/changed") modelChanged = true;
|
|
1179
|
+
if (raw.method === "warning" && codeModeUnavailable(params.message)) throw new CodexCapabilityError();
|
|
1180
|
+
if (raw.method === "error" && params.willRetry !== true) failure = nativeFailure(params.error);
|
|
1181
|
+
if (raw.id !== void 0 && typeof raw.method === "string") emit({
|
|
1182
|
+
...common,
|
|
1183
|
+
kind: "permission",
|
|
1184
|
+
permission: {
|
|
1185
|
+
id: String(raw.id),
|
|
1186
|
+
capability: raw.method,
|
|
1187
|
+
decision: "deny",
|
|
1188
|
+
source: "codex_app_server"
|
|
1189
|
+
}
|
|
1190
|
+
});
|
|
1191
|
+
if (raw.method === "turn/completed") {
|
|
1192
|
+
terminal = string(object(params.turn).status) ?? "unknown";
|
|
1193
|
+
failure = terminal === "completed" ? void 0 : nativeFailure(object(params.turn).error) ?? failure;
|
|
1194
|
+
}
|
|
1195
|
+
if ((raw.method === "item/started" || raw.method === "item/completed") && typeof item.id === "string") {
|
|
1196
|
+
if ([
|
|
1197
|
+
"commandExecution",
|
|
1198
|
+
"fileChange",
|
|
1199
|
+
"webSearch",
|
|
1200
|
+
"imageGeneration",
|
|
1201
|
+
"collabAgentToolCall"
|
|
1202
|
+
].includes(String(item.type))) throw new Error("Codex attempted a disabled built-in operation.");
|
|
1203
|
+
if (item.type === "agentMessage" && raw.method === "item/completed" && typeof item.text === "string") {
|
|
1204
|
+
if (messages.get(item.id) !== item.text) emit({
|
|
1205
|
+
...common,
|
|
1206
|
+
kind: "message",
|
|
1207
|
+
message: {
|
|
1208
|
+
role: "assistant",
|
|
1209
|
+
content: item.text
|
|
1210
|
+
}
|
|
1211
|
+
});
|
|
1212
|
+
messages.set(item.id, item.text);
|
|
1213
|
+
}
|
|
1214
|
+
if (item.type === "mcpToolCall") {
|
|
1215
|
+
const previous = tools.get(item.id);
|
|
1216
|
+
const server = string(item.server) ?? previous?.mcpServer;
|
|
1217
|
+
const toolName = string(item.tool) ?? previous?.mcpTool;
|
|
1218
|
+
if (!server || !servers.includes(server) || !toolName) throw new Error("Codex emitted an unconfigured MCP tool.");
|
|
1219
|
+
const failed = previous?.status === "failed" || item.status === "failed" || !!item.error || object(item.result).isError === true;
|
|
1220
|
+
const done = raw.method === "item/completed" && item.status === "completed";
|
|
1221
|
+
const tool = {
|
|
1222
|
+
...previous,
|
|
1223
|
+
id: item.id,
|
|
1224
|
+
name: `mcp__${server}__${toolName}`,
|
|
1225
|
+
nativeName: toolName,
|
|
1226
|
+
mcpServer: server,
|
|
1227
|
+
mcpTool: toolName,
|
|
1228
|
+
...item.arguments !== void 0 ? { input: item.arguments } : {},
|
|
1229
|
+
...item.result !== void 0 && item.result !== null ? { output: item.result } : {},
|
|
1230
|
+
...item.error ? { error: item.error } : {},
|
|
1231
|
+
status: failed ? "failed" : done ? "succeeded" : previous?.status === "succeeded" ? "succeeded" : "running",
|
|
1232
|
+
startedAt: previous?.startedAt ?? Date.now(),
|
|
1233
|
+
...done || failed ? { endedAt: Date.now() } : {},
|
|
1234
|
+
...counter(item.durationMs) ? { durationMs: item.durationMs } : {}
|
|
1235
|
+
};
|
|
1236
|
+
tools.set(item.id, tool);
|
|
1237
|
+
emit({
|
|
1238
|
+
...common,
|
|
1239
|
+
kind: "tool",
|
|
1240
|
+
tool: { ...tool }
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
if (raw.method === "item/agentMessage/delta" && typeof params.delta === "string") emit({
|
|
1245
|
+
...common,
|
|
1246
|
+
kind: "message",
|
|
1247
|
+
message: {
|
|
1248
|
+
role: "assistant",
|
|
1249
|
+
content: params.delta,
|
|
1250
|
+
delta: true
|
|
1251
|
+
}
|
|
1252
|
+
});
|
|
1253
|
+
if (raw.method === "thread/tokenUsage/updated") {
|
|
1254
|
+
const total = object(object(params.tokenUsage).total);
|
|
1255
|
+
const serialized = JSON.stringify(fields.map((field) => total[field]));
|
|
1256
|
+
if (serialized !== lastUsage) {
|
|
1257
|
+
usageUpdates += 1;
|
|
1258
|
+
lastUsage = serialized;
|
|
1259
|
+
}
|
|
1260
|
+
if (usage && !usageDelta(total, usage)) usageInvalid = true;
|
|
1261
|
+
usage = total;
|
|
1262
|
+
if (counter(total.inputTokens) && counter(total.cachedInputTokens) && total.cachedInputTokens <= total.inputTokens) emit({
|
|
1263
|
+
...common,
|
|
1264
|
+
kind: "usage",
|
|
1265
|
+
usage: {
|
|
1266
|
+
scope: "session",
|
|
1267
|
+
cumulative: true,
|
|
1268
|
+
epoch: threadId,
|
|
1269
|
+
model,
|
|
1270
|
+
modelBasis: "requested",
|
|
1271
|
+
overlapGroup: "codex_thread_total",
|
|
1272
|
+
inputTokens: total.inputTokens - total.cachedInputTokens,
|
|
1273
|
+
cacheReadInputTokens: total.cachedInputTokens,
|
|
1274
|
+
...counter(total.outputTokens) ? { outputTokens: total.outputTokens } : {},
|
|
1275
|
+
...counter(total.reasoningOutputTokens) ? { reasoningTokens: total.reasoningOutputTokens } : {},
|
|
1276
|
+
...counter(total.cacheWriteInputTokens) ? { cacheCreationInputTokens: total.cacheWriteInputTokens } : {}
|
|
1277
|
+
}
|
|
1278
|
+
});
|
|
1279
|
+
}
|
|
1280
|
+
},
|
|
1281
|
+
state() {
|
|
1282
|
+
return {
|
|
1283
|
+
usage,
|
|
1284
|
+
usageInvalid,
|
|
1285
|
+
usageUpdates,
|
|
1286
|
+
modelChanged,
|
|
1287
|
+
turnId,
|
|
1288
|
+
terminal,
|
|
1289
|
+
failure,
|
|
1290
|
+
text: [...messages.values()].join("\n")
|
|
1291
|
+
};
|
|
1292
|
+
},
|
|
1293
|
+
finish() {
|
|
1294
|
+
for (const tool of tools.values()) if (tool.status === "running" || tool.status === "pending") emit({
|
|
1295
|
+
nativeSessionId: threadId,
|
|
1296
|
+
nativeTurnId: turnId,
|
|
1297
|
+
kind: "tool",
|
|
1298
|
+
tool: {
|
|
1299
|
+
...tool,
|
|
1300
|
+
status: "unknown"
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
// src/lib/local-harness/backends/codex/index.ts
|
|
1307
|
+
const exec$1 = promisify(execFile);
|
|
1308
|
+
const supported = HARNESS_COMPATIBILITY.codexNative;
|
|
1309
|
+
const coverage = {
|
|
1310
|
+
profile: {
|
|
1311
|
+
state: "partial",
|
|
1312
|
+
reason: "Isolated Code Mode exposes configured MCP tools only; read-only sandbox and denied interactive requests remain additional boundaries."
|
|
1313
|
+
},
|
|
1314
|
+
requestLimit: {
|
|
1315
|
+
state: "partial",
|
|
1316
|
+
reason: "Limits distinct cumulative usage updates; App Server does not expose authoritative per-request identifiers."
|
|
1317
|
+
},
|
|
1318
|
+
tools: { state: "available" },
|
|
1319
|
+
messages: { state: "available" },
|
|
1320
|
+
native: { state: "available" },
|
|
1321
|
+
usage: {
|
|
1322
|
+
state: "partial",
|
|
1323
|
+
reason: "Thread token deltas; configured-model API list estimates only where cache and context-tier inputs are known."
|
|
1324
|
+
},
|
|
1325
|
+
timing: {
|
|
1326
|
+
state: "partial",
|
|
1327
|
+
reason: "Native MCP durations when present; otherwise local observations."
|
|
1328
|
+
},
|
|
1329
|
+
lineage: {
|
|
1330
|
+
state: "disabled",
|
|
1331
|
+
reason: "Subagents are disabled in controlled-mcp."
|
|
1332
|
+
},
|
|
1333
|
+
reasoning: {
|
|
1334
|
+
state: "partial",
|
|
1335
|
+
reason: "Only summaries exposed by App Server are retained."
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
function provenance(config, version) {
|
|
1339
|
+
return {
|
|
1340
|
+
harness: "codex",
|
|
1341
|
+
backend: "native",
|
|
1342
|
+
harnessVersion: version,
|
|
1343
|
+
protocolVersion: supported.protocol,
|
|
1344
|
+
profile: config.profile,
|
|
1345
|
+
coverage,
|
|
1346
|
+
configuration: {
|
|
1347
|
+
approvalPolicy: "never",
|
|
1348
|
+
sandbox: "read-only",
|
|
1349
|
+
strictConfig: true,
|
|
1350
|
+
userConfiguration: false,
|
|
1351
|
+
executableTools: false,
|
|
1352
|
+
webSearch: false,
|
|
1353
|
+
subagents: false,
|
|
1354
|
+
codeMode: "required-isolated-host",
|
|
1355
|
+
excludedToolNamespaces: ["functions", "clock"],
|
|
1356
|
+
nativeToolLimitations: ["Code Mode JavaScript has no filesystem, process, module-import or network APIs.", "Configured MCP servers retain their own server-side capabilities."],
|
|
1357
|
+
mcpServers: config.mcpServers.map((server) => server.name),
|
|
1358
|
+
requestedModel: config.model ?? null,
|
|
1359
|
+
pricing: PRICING_PROVENANCE
|
|
1360
|
+
}
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
function validate(config) {
|
|
1364
|
+
if (config.harness !== "codex" || config.backend !== "native" || config.profile !== "controlled-mcp" || config.cwd) throw new Error("Codex native supports controlled-mcp with an isolated workspace only.");
|
|
1365
|
+
if (process.platform === "win32") throw new Error("Codex native process supervision is not qualified on Windows.");
|
|
1366
|
+
}
|
|
1367
|
+
const INTERRUPTED_TURN_END_MS = 300;
|
|
1368
|
+
function createCodexBackend(dependencies = {}) {
|
|
1369
|
+
const version = dependencies.version ?? (async (executable) => {
|
|
1370
|
+
return (await exec$1(executable, ["--version"], {
|
|
1371
|
+
env: controlledEnvironment(),
|
|
1372
|
+
timeout: 5e3,
|
|
1373
|
+
maxBuffer: 4096
|
|
1374
|
+
})).stdout.trim().replace(/^codex-cli\s+/, "");
|
|
1375
|
+
});
|
|
1376
|
+
async function open(config, signal, deadlineAt) {
|
|
1377
|
+
validate(config);
|
|
1378
|
+
signal.throwIfAborted();
|
|
1379
|
+
const executable = config.executable ?? "codex";
|
|
1380
|
+
const actual = await version(executable);
|
|
1381
|
+
if (actual !== supported.harness) throw new Error(`Codex ${supported.harness} is required; found ${actual}.`);
|
|
1382
|
+
signal.throwIfAborted();
|
|
1383
|
+
if (Date.now() >= deadlineAt) throw new Error("Codex startup deadline exceeded.");
|
|
1384
|
+
const environment = await createEnvironment(config);
|
|
1385
|
+
let rpc;
|
|
1386
|
+
try {
|
|
1387
|
+
rpc = (dependencies.rpc ?? startRpc)(executable, environment.cwd, environment.env);
|
|
1388
|
+
let capabilityFailure;
|
|
1389
|
+
rpc.observe?.((message) => {
|
|
1390
|
+
if (message.method === "warning" && codeModeUnavailable(object(message.params).message)) capabilityFailure = new CodexCapabilityError();
|
|
1391
|
+
}, () => {});
|
|
1392
|
+
await rpc.request("initialize", { clientInfo: {
|
|
1393
|
+
name: "runtype_local_harness",
|
|
1394
|
+
version: "1.0.0"
|
|
1395
|
+
} }, signal, deadlineAt);
|
|
1396
|
+
rpc.notify("initialized");
|
|
1397
|
+
verifyCodeModeFeatures((await rpc.request("config/read", {}, signal, deadlineAt)).config);
|
|
1398
|
+
const account = await rpc.request("account/read", { refreshToken: false }, signal, deadlineAt);
|
|
1399
|
+
const authentication = account.account || account.requiresOpenaiAuth === false ? "ready" : "absent";
|
|
1400
|
+
const statuses = /* @__PURE__ */ new Map();
|
|
1401
|
+
do {
|
|
1402
|
+
let cursor;
|
|
1403
|
+
const cursors = /* @__PURE__ */ new Set();
|
|
1404
|
+
do {
|
|
1405
|
+
const page = await rpc.request("mcpServerStatus/list", {
|
|
1406
|
+
limit: 100,
|
|
1407
|
+
detail: "toolsAndAuthOnly",
|
|
1408
|
+
...cursor ? { cursor } : {}
|
|
1409
|
+
}, signal, deadlineAt);
|
|
1410
|
+
if (!Array.isArray(page.data)) throw new Error("Codex MCP discovery returned no catalog.");
|
|
1411
|
+
for (const raw of page.data) {
|
|
1412
|
+
const entry = object(raw);
|
|
1413
|
+
if (typeof entry.name === "string") statuses.set(entry.name, entry);
|
|
1414
|
+
}
|
|
1415
|
+
const next = page.nextCursor;
|
|
1416
|
+
if (next !== null && next !== void 0 && (typeof next !== "string" || cursors.has(next) || cursors.size >= 100)) throw new Error("Invalid Codex discovery pagination.");
|
|
1417
|
+
cursor = typeof next === "string" ? next : void 0;
|
|
1418
|
+
if (cursor) cursors.add(cursor);
|
|
1419
|
+
} while (cursor);
|
|
1420
|
+
if (config.mcpServers.every(({ name }) => {
|
|
1421
|
+
const status = statuses.get(name);
|
|
1422
|
+
return status?.tools && !Array.isArray(status.tools) && status.toolsError === null && (!status.runtimeStatus || status.runtimeStatus === "connected");
|
|
1423
|
+
})) break;
|
|
1424
|
+
await setTimeout$1(100, void 0, { signal });
|
|
1425
|
+
} while (Date.now() < deadlineAt);
|
|
1426
|
+
if (!config.mcpServers.every(({ name }) => {
|
|
1427
|
+
const status = statuses.get(name);
|
|
1428
|
+
return status?.tools && !Array.isArray(status.tools) && status.toolsError === null && (!status.runtimeStatus || status.runtimeStatus === "connected");
|
|
1429
|
+
})) throw new Error("Codex MCP servers did not connect before the startup deadline.");
|
|
1430
|
+
const initialized = authentication === "ready" ? await rpc.request("thread/start", {
|
|
1431
|
+
...config.model ? { model: config.model } : {},
|
|
1432
|
+
cwd: environment.cwd,
|
|
1433
|
+
approvalPolicy: "never",
|
|
1434
|
+
sandbox: "read-only",
|
|
1435
|
+
ephemeral: true,
|
|
1436
|
+
serviceTier: "default"
|
|
1437
|
+
}, signal, deadlineAt) : void 0;
|
|
1438
|
+
if (capabilityFailure) throw capabilityFailure;
|
|
1439
|
+
if (initialized && (typeof object(initialized.thread).id !== "string" || typeof initialized.model !== "string" || initialized.approvalPolicy !== "never" || object(initialized.sandbox).type !== "readOnly")) throw new Error("Codex did not establish the controlled profile.");
|
|
1440
|
+
return {
|
|
1441
|
+
rpc,
|
|
1442
|
+
environment,
|
|
1443
|
+
authentication,
|
|
1444
|
+
initialized,
|
|
1445
|
+
provenance: provenance(config, actual)
|
|
1446
|
+
};
|
|
1447
|
+
} catch (error) {
|
|
1448
|
+
await rpc?.close();
|
|
1449
|
+
await environment.cleanup();
|
|
1450
|
+
throw error;
|
|
1451
|
+
}
|
|
1452
|
+
}
|
|
1453
|
+
return {
|
|
1454
|
+
async doctor(config) {
|
|
1455
|
+
let active;
|
|
1456
|
+
let result;
|
|
1457
|
+
try {
|
|
1458
|
+
const timeout = Math.min(config.timeoutMs, 45e3);
|
|
1459
|
+
active = await open(config, AbortSignal.timeout(timeout), Date.now() + timeout);
|
|
1460
|
+
result = {
|
|
1461
|
+
ready: active.authentication === "ready",
|
|
1462
|
+
authentication: active.authentication,
|
|
1463
|
+
provenance: active.provenance,
|
|
1464
|
+
diagnostics: [
|
|
1465
|
+
"MCP servers connected without a model turn.",
|
|
1466
|
+
"Account presence checked without refreshing tokens; expiry is not verified.",
|
|
1467
|
+
"No inference was requested; model entitlement and actual MCP tool execution are not verified."
|
|
1468
|
+
]
|
|
1469
|
+
};
|
|
1470
|
+
} catch (error) {
|
|
1471
|
+
result = {
|
|
1472
|
+
ready: false,
|
|
1473
|
+
authentication: "unknown",
|
|
1474
|
+
provenance: provenance(config, "unknown"),
|
|
1475
|
+
diagnostics: [error instanceof CodexCapabilityError || error instanceof CodexNativeError && error.failure ? error.message : `Codex ${supported.harness} is required. Initialization failed; check the executable, authentication and MCP configuration.`]
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
if (active) try {
|
|
1479
|
+
await active.rpc.close();
|
|
1480
|
+
await active.environment.cleanup();
|
|
1481
|
+
} catch {
|
|
1482
|
+
throw new Error("Codex cleanup failed. Inspect the local harness processes before retrying.");
|
|
1483
|
+
}
|
|
1484
|
+
return result;
|
|
1485
|
+
},
|
|
1486
|
+
async start(config, context) {
|
|
1487
|
+
const active = await open(config, context.signal, context.deadlineAt);
|
|
1488
|
+
const { rpc, environment } = active;
|
|
1489
|
+
let closed = false;
|
|
1490
|
+
let invalid = false;
|
|
1491
|
+
let busy = false;
|
|
1492
|
+
let cancelActive;
|
|
1493
|
+
let activeTurn;
|
|
1494
|
+
let previousUsage;
|
|
1495
|
+
let modelChanged = false;
|
|
1496
|
+
let closing;
|
|
1497
|
+
const close = () => closing ??= (async () => {
|
|
1498
|
+
closed = true;
|
|
1499
|
+
cancelActive?.();
|
|
1500
|
+
await rpc.close();
|
|
1501
|
+
await environment.cleanup();
|
|
1502
|
+
})();
|
|
1503
|
+
let threadId;
|
|
1504
|
+
let model;
|
|
1505
|
+
try {
|
|
1506
|
+
if (active.authentication !== "ready") throw new Error("Codex authentication is unavailable.");
|
|
1507
|
+
const initialized = active.initialized;
|
|
1508
|
+
const thread = object(initialized.thread);
|
|
1509
|
+
if (typeof thread.id !== "string" || typeof initialized.model !== "string" || initialized.approvalPolicy !== "never" || object(initialized.sandbox).type !== "readOnly") throw new Error("Codex did not establish the controlled profile.");
|
|
1510
|
+
threadId = thread.id;
|
|
1511
|
+
model = initialized.model;
|
|
1512
|
+
} catch (error) {
|
|
1513
|
+
await close();
|
|
1514
|
+
throw error;
|
|
1515
|
+
}
|
|
1516
|
+
return {
|
|
1517
|
+
id: context.sessionId,
|
|
1518
|
+
async prompt(input) {
|
|
1519
|
+
if (closed || invalid) throw new Error("Cannot continue a closed or failed Codex context.");
|
|
1520
|
+
if (busy) throw new Error("Concurrent Codex turns are not allowed.");
|
|
1521
|
+
busy = true;
|
|
1522
|
+
let turnReported = () => {};
|
|
1523
|
+
activeTurn = new Promise((resolve) => turnReported = resolve);
|
|
1524
|
+
const observer = createObserver(input.emit, threadId, model, config.mcpServers.map((server) => server.name));
|
|
1525
|
+
const deadlineAt = Math.min(input.deadlineAt, Date.now() + config.timeoutMs);
|
|
1526
|
+
const controller = new AbortController();
|
|
1527
|
+
const abort = () => controller.abort();
|
|
1528
|
+
input.signal.addEventListener("abort", abort, { once: true });
|
|
1529
|
+
if (input.signal.aborted) abort();
|
|
1530
|
+
const timer = setTimeout(abort, Math.max(0, deadlineAt - Date.now()));
|
|
1531
|
+
let settle;
|
|
1532
|
+
let rejectTerminal;
|
|
1533
|
+
const terminal = new Promise((resolve, reject) => {
|
|
1534
|
+
settle = resolve;
|
|
1535
|
+
rejectTerminal = reject;
|
|
1536
|
+
});
|
|
1537
|
+
terminal.catch(() => {});
|
|
1538
|
+
let markEnded = () => {};
|
|
1539
|
+
const ended = new Promise((resolve) => markEnded = resolve);
|
|
1540
|
+
cancelActive = abort;
|
|
1541
|
+
const cancelled = () => rejectTerminal?.(/* @__PURE__ */ new Error("Codex turn cancelled or deadline exceeded."));
|
|
1542
|
+
controller.signal.addEventListener("abort", cancelled, { once: true });
|
|
1543
|
+
rpc.observe((message) => {
|
|
1544
|
+
observer.observe(message);
|
|
1545
|
+
const state = observer.state();
|
|
1546
|
+
if (state.usageUpdates > config.maxTurns) throw new Error("Codex observed usage-update limit exceeded.");
|
|
1547
|
+
if (state.terminal) {
|
|
1548
|
+
settle?.();
|
|
1549
|
+
markEnded();
|
|
1550
|
+
}
|
|
1551
|
+
}, (error) => rejectTerminal?.(error));
|
|
1552
|
+
let status = "failed";
|
|
1553
|
+
let failureReason = "native_turn_incomplete";
|
|
1554
|
+
let failureText;
|
|
1555
|
+
try {
|
|
1556
|
+
await rpc.request("turn/start", {
|
|
1557
|
+
threadId,
|
|
1558
|
+
input: [{
|
|
1559
|
+
type: "text",
|
|
1560
|
+
text: input.prompt
|
|
1561
|
+
}]
|
|
1562
|
+
}, controller.signal, deadlineAt);
|
|
1563
|
+
if (controller.signal.aborted) throw new Error("Codex turn cancelled.");
|
|
1564
|
+
await terminal;
|
|
1565
|
+
const state = observer.state();
|
|
1566
|
+
status = state.terminal === "completed" ? "completed" : state.terminal === "interrupted" ? "cancelled" : "failed";
|
|
1567
|
+
if (status !== "completed") invalid = true;
|
|
1568
|
+
if (state.failure) {
|
|
1569
|
+
failureReason = state.failure.reason;
|
|
1570
|
+
failureText = state.failure.text;
|
|
1571
|
+
}
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
const cause = nativeFailure(error);
|
|
1574
|
+
failureReason = controller.signal.aborted ? "cancelled_or_deadline" : cause?.reason ?? "protocol_or_process_failure";
|
|
1575
|
+
failureText = controller.signal.aborted ? void 0 : cause?.text;
|
|
1576
|
+
status = controller.signal.aborted ? "cancelled" : "failed";
|
|
1577
|
+
invalid = true;
|
|
1578
|
+
const turnId = observer.state().turnId;
|
|
1579
|
+
if (turnId) {
|
|
1580
|
+
await rpc.request("turn/interrupt", {
|
|
1581
|
+
threadId,
|
|
1582
|
+
turnId
|
|
1583
|
+
}, new AbortController().signal, Date.now() + 500).catch(() => {});
|
|
1584
|
+
await Promise.race([ended, setTimeout$1(INTERRUPTED_TURN_END_MS, void 0, { ref: false })]);
|
|
1585
|
+
}
|
|
1586
|
+
} finally {
|
|
1587
|
+
turnReported();
|
|
1588
|
+
clearTimeout(timer);
|
|
1589
|
+
input.signal.removeEventListener("abort", abort);
|
|
1590
|
+
controller.signal.removeEventListener("abort", cancelled);
|
|
1591
|
+
cancelActive = void 0;
|
|
1592
|
+
try {
|
|
1593
|
+
if (invalid) await close();
|
|
1594
|
+
} finally {
|
|
1595
|
+
rpc.observe(() => {}, () => {});
|
|
1596
|
+
observer.finish();
|
|
1597
|
+
if (status !== "completed") input.emit({
|
|
1598
|
+
kind: "lifecycle",
|
|
1599
|
+
nativeSessionId: threadId,
|
|
1600
|
+
nativeTurnId: observer.state().turnId,
|
|
1601
|
+
lifecycle: {
|
|
1602
|
+
state: status,
|
|
1603
|
+
reason: failureReason
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
busy = false;
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
const state = observer.state();
|
|
1610
|
+
modelChanged ||= state.modelChanged;
|
|
1611
|
+
const usage = reportedUsage(state.usageInvalid ? void 0 : state.usage, previousUsage, model, status === "completed", modelChanged);
|
|
1612
|
+
if (state.usage) previousUsage = state.usage;
|
|
1613
|
+
return {
|
|
1614
|
+
nativeSessionId: threadId,
|
|
1615
|
+
nativeTurnId: state.turnId,
|
|
1616
|
+
text: [state.text, status === "completed" ? "" : failureText ?? "Codex turn did not complete."].filter(Boolean).join("\n\n"),
|
|
1617
|
+
status,
|
|
1618
|
+
reportedUsage: usage,
|
|
1619
|
+
coverage: status === "completed" ? coverage : {
|
|
1620
|
+
...coverage,
|
|
1621
|
+
native: {
|
|
1622
|
+
state: "partial",
|
|
1623
|
+
reason: "Turn ended before complete native evidence was observed."
|
|
1624
|
+
},
|
|
1625
|
+
messages: {
|
|
1626
|
+
state: "partial",
|
|
1627
|
+
reason: "Turn ended before complete messages were observed."
|
|
1628
|
+
},
|
|
1629
|
+
tools: {
|
|
1630
|
+
state: "partial",
|
|
1631
|
+
reason: "Turn ended before all tool outcomes were observed."
|
|
1632
|
+
},
|
|
1633
|
+
usage: {
|
|
1634
|
+
state: "partial",
|
|
1635
|
+
reason: "Turn ended before complete usage was observed."
|
|
1636
|
+
}
|
|
1637
|
+
}
|
|
1638
|
+
};
|
|
1639
|
+
},
|
|
1640
|
+
async cancel() {
|
|
1641
|
+
cancelActive?.();
|
|
1642
|
+
await activeTurn;
|
|
1643
|
+
},
|
|
1644
|
+
close
|
|
1645
|
+
};
|
|
1646
|
+
}
|
|
1647
|
+
};
|
|
1648
|
+
}
|
|
1649
|
+
// src/lib/local-harness/backends/acp/protocol.ts
|
|
1650
|
+
async function deadline(work, signal, until) {
|
|
1651
|
+
signal.throwIfAborted();
|
|
1652
|
+
let timer;
|
|
1653
|
+
let abort;
|
|
1654
|
+
try {
|
|
1655
|
+
return await Promise.race([work, new Promise((_, reject) => {
|
|
1656
|
+
abort = () => reject(/* @__PURE__ */ new Error("ACP operation cancelled."));
|
|
1657
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
1658
|
+
timer = setTimeout(() => reject(/* @__PURE__ */ new Error("ACP operation deadline exceeded.")), Math.max(0, until - Date.now()));
|
|
1659
|
+
})]);
|
|
1660
|
+
} finally {
|
|
1661
|
+
if (timer) clearTimeout(timer);
|
|
1662
|
+
if (abort) signal.removeEventListener("abort", abort);
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
function stdioStream(stdin, stdout) {
|
|
1666
|
+
return ndJsonStream(Writable.toWeb(stdin), Readable.toWeb(stdout), { maxMessageBytes: 2097152 });
|
|
1667
|
+
}
|
|
1668
|
+
/** Capture before typed decoding so unknown extensions survive the SDK boundary. */
|
|
1669
|
+
function createAcpClient(stream, observe, permission) {
|
|
1670
|
+
const pending = /* @__PURE__ */ new Map();
|
|
1671
|
+
const writer = stream.writable.getWriter();
|
|
1672
|
+
const tapped = {
|
|
1673
|
+
writable: new WritableStream({
|
|
1674
|
+
async write(message) {
|
|
1675
|
+
if ("method" in message && "id" in message) {
|
|
1676
|
+
const params = message.params;
|
|
1677
|
+
if (typeof params?.sessionId === "string") {
|
|
1678
|
+
if (pending.size >= 32) throw new Error("Too many concurrent ACP requests.");
|
|
1679
|
+
pending.set(String(message.id), {
|
|
1680
|
+
method: message.method,
|
|
1681
|
+
sessionId: params.sessionId
|
|
1682
|
+
});
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
await writer.write(message);
|
|
1686
|
+
},
|
|
1687
|
+
async close() {
|
|
1688
|
+
pending.clear();
|
|
1689
|
+
await writer.close();
|
|
1690
|
+
},
|
|
1691
|
+
async abort(reason) {
|
|
1692
|
+
pending.clear();
|
|
1693
|
+
await writer.abort(reason);
|
|
1694
|
+
}
|
|
1695
|
+
}),
|
|
1696
|
+
readable: stream.readable.pipeThrough(new TransformStream({ transform(message, controller) {
|
|
1697
|
+
observe(message);
|
|
1698
|
+
if (!("method" in message) && "id" in message) {
|
|
1699
|
+
const request = pending.get(String(message.id));
|
|
1700
|
+
pending.delete(String(message.id));
|
|
1701
|
+
if (request) observe({
|
|
1702
|
+
jsonrpc: "2.0",
|
|
1703
|
+
method: "_runtype/acp_response",
|
|
1704
|
+
params: {
|
|
1705
|
+
sessionId: request.sessionId,
|
|
1706
|
+
requestMethod: request.method,
|
|
1707
|
+
response: message
|
|
1708
|
+
}
|
|
1709
|
+
});
|
|
1710
|
+
}
|
|
1711
|
+
controller.enqueue(message);
|
|
1712
|
+
} }))
|
|
1713
|
+
};
|
|
1714
|
+
return client({ name: "runtype-local-harness" }).onNotification("*", () => {}).onNotification(methods.client.session.update, () => {}).onRequest(methods.client.session.requestPermission, ({ params }) => {
|
|
1715
|
+
permission({
|
|
1716
|
+
kind: "permission",
|
|
1717
|
+
nativeSessionId: params.sessionId,
|
|
1718
|
+
permission: {
|
|
1719
|
+
id: params.toolCall.toolCallId,
|
|
1720
|
+
capability: params.toolCall.name ?? params.toolCall.title ?? "unknown",
|
|
1721
|
+
decision: "deny",
|
|
1722
|
+
source: "acp_client_policy"
|
|
1723
|
+
}
|
|
1724
|
+
});
|
|
1725
|
+
const deny = params.options.find((option) => option.kind === "reject_once");
|
|
1726
|
+
return { outcome: deny ? {
|
|
1727
|
+
outcome: "selected",
|
|
1728
|
+
optionId: deny.optionId
|
|
1729
|
+
} : { outcome: "cancelled" } };
|
|
1730
|
+
}).onRequest("*", () => {
|
|
1731
|
+
throw RequestError.methodNotFound("unsupported_client_capability");
|
|
1732
|
+
}).connect(tapped);
|
|
1733
|
+
}
|
|
1734
|
+
// src/lib/local-harness/backends/acp/observations.ts
|
|
1735
|
+
const status = (value) => value === "completed" ? "succeeded" : value === "failed" ? "failed" : value === "in_progress" ? "running" : value === "pending" ? "pending" : "unknown";
|
|
1736
|
+
/** ACP context occupancy and cumulative cost remain separate from native billing observations. */
|
|
1737
|
+
function createAcpObserver(sessionId, emit, servers, rawClaude) {
|
|
1738
|
+
const tools = /* @__PURE__ */ new Map();
|
|
1739
|
+
let text = "";
|
|
1740
|
+
let textBytes = 0;
|
|
1741
|
+
let answerTruncated = false;
|
|
1742
|
+
let result;
|
|
1743
|
+
let dropped = false;
|
|
1744
|
+
let rawSeen = false;
|
|
1745
|
+
let bytes = 0;
|
|
1746
|
+
let events = 0;
|
|
1747
|
+
const canonical = (event, native) => {
|
|
1748
|
+
if (!event.tool) {
|
|
1749
|
+
emit(event);
|
|
1750
|
+
return;
|
|
1751
|
+
}
|
|
1752
|
+
const prior = tools.get(event.tool.id);
|
|
1753
|
+
const incoming = Object.fromEntries(Object.entries(event.tool).filter(([, value]) => value != null));
|
|
1754
|
+
if (prior?.native && !native) {
|
|
1755
|
+
delete incoming.input;
|
|
1756
|
+
delete incoming.output;
|
|
1757
|
+
delete incoming.name;
|
|
1758
|
+
delete incoming.nativeName;
|
|
1759
|
+
}
|
|
1760
|
+
const merged = {
|
|
1761
|
+
...prior?.value,
|
|
1762
|
+
...incoming,
|
|
1763
|
+
id: event.tool.id,
|
|
1764
|
+
status: event.tool.status
|
|
1765
|
+
};
|
|
1766
|
+
if (prior?.value.status === "failed") merged.status = "failed";
|
|
1767
|
+
else if (prior?.value.status === "succeeded" && [
|
|
1768
|
+
"unknown",
|
|
1769
|
+
"pending",
|
|
1770
|
+
"running"
|
|
1771
|
+
].includes(merged.status)) merged.status = "succeeded";
|
|
1772
|
+
tools.set(merged.id, {
|
|
1773
|
+
value: merged,
|
|
1774
|
+
native: native || prior?.native === true
|
|
1775
|
+
});
|
|
1776
|
+
emit({
|
|
1777
|
+
...event,
|
|
1778
|
+
tool: merged
|
|
1779
|
+
});
|
|
1780
|
+
};
|
|
1781
|
+
const native = createObserver$1((event) => {
|
|
1782
|
+
if (event.kind !== "native" && event.kind !== "message") canonical(event, true);
|
|
1783
|
+
}, servers);
|
|
1784
|
+
return {
|
|
1785
|
+
observe(message) {
|
|
1786
|
+
if (!("method" in message)) return;
|
|
1787
|
+
const params = object$2(message.params);
|
|
1788
|
+
if (params.sessionId !== sessionId) return;
|
|
1789
|
+
const update = object$2(params.update);
|
|
1790
|
+
if (!("id" in message)) {
|
|
1791
|
+
if (message.method === "session/update" && update.sessionUpdate === "agent_message_chunk") {
|
|
1792
|
+
const content = object$2(update.content);
|
|
1793
|
+
if (content.type === "text" && typeof content.text === "string") {
|
|
1794
|
+
const remaining = answerTruncated ? 0 : HARNESS_LIMITS.maxControlReplyBytes - textBytes;
|
|
1795
|
+
const chunk = Buffer.from(content.text);
|
|
1796
|
+
let end = Math.min(remaining, chunk.length);
|
|
1797
|
+
while (end > 0 && end < chunk.length && (chunk[end] & 192) === 128) end -= 1;
|
|
1798
|
+
text += chunk.subarray(0, end).toString("utf8");
|
|
1799
|
+
textBytes += end;
|
|
1800
|
+
answerTruncated ||= end < chunk.length;
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (message.method === "_claude/sdkMessage" && rawClaude) {
|
|
1804
|
+
rawSeen = true;
|
|
1805
|
+
const sdk = object$2(params.message);
|
|
1806
|
+
if (sdk.type === "result") {
|
|
1807
|
+
const { type, subtype, session_id, uuid, user_message_uuid, is_error, total_cost_usd, modelUsage } = sdk;
|
|
1808
|
+
result = {
|
|
1809
|
+
type,
|
|
1810
|
+
subtype,
|
|
1811
|
+
session_id,
|
|
1812
|
+
uuid,
|
|
1813
|
+
user_message_uuid,
|
|
1814
|
+
is_error,
|
|
1815
|
+
total_cost_usd,
|
|
1816
|
+
modelUsage
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
}
|
|
1821
|
+
const size = Buffer.byteLength(JSON.stringify(message));
|
|
1822
|
+
bytes += size;
|
|
1823
|
+
events += 1;
|
|
1824
|
+
if (size > HARNESS_LIMITS.maxEventBytes || bytes > HARNESS_LIMITS.maxTurnBytes || events > 1e3) {
|
|
1825
|
+
dropped = true;
|
|
1826
|
+
return;
|
|
1827
|
+
}
|
|
1828
|
+
emit({
|
|
1829
|
+
kind: "native",
|
|
1830
|
+
nativeSessionId: sessionId,
|
|
1831
|
+
native: {
|
|
1832
|
+
protocol: "acp",
|
|
1833
|
+
method: message.method,
|
|
1834
|
+
params
|
|
1835
|
+
},
|
|
1836
|
+
...message.method === "session/update" && typeof update.sessionUpdate === "string" && update.sessionUpdate.endsWith("_chunk") || message.method === "_claude/sdkMessage" && object$2(params.message).type === "stream_event" ? { fragment: true } : {}
|
|
1837
|
+
});
|
|
1838
|
+
if ("id" in message) return;
|
|
1839
|
+
if (message.method === "_claude/sdkMessage" && rawClaude) {
|
|
1840
|
+
native.observe(params.message);
|
|
1841
|
+
return;
|
|
1842
|
+
}
|
|
1843
|
+
if (message.method !== "session/update") return;
|
|
1844
|
+
const common = { nativeSessionId: sessionId };
|
|
1845
|
+
if (update.sessionUpdate === "agent_message_chunk") {
|
|
1846
|
+
const content = object$2(update.content);
|
|
1847
|
+
if (content.type === "text" && typeof content.text === "string") emit({
|
|
1848
|
+
...common,
|
|
1849
|
+
kind: "message",
|
|
1850
|
+
message: {
|
|
1851
|
+
role: "assistant",
|
|
1852
|
+
content: content.text,
|
|
1853
|
+
delta: true
|
|
1854
|
+
}
|
|
1855
|
+
});
|
|
1856
|
+
}
|
|
1857
|
+
if ((update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") && typeof update.toolCallId === "string") {
|
|
1858
|
+
const prior = tools.get(update.toolCallId)?.value;
|
|
1859
|
+
const name = typeof update.name === "string" ? update.name : void 0;
|
|
1860
|
+
const server = name && [...servers].sort((a, b) => b.length - a.length).find((entry) => name.startsWith(`mcp__${entry}__`));
|
|
1861
|
+
canonical({
|
|
1862
|
+
...common,
|
|
1863
|
+
kind: "tool",
|
|
1864
|
+
tool: {
|
|
1865
|
+
id: update.toolCallId,
|
|
1866
|
+
status: update.status == null ? prior?.status ?? "unknown" : status(update.status),
|
|
1867
|
+
...name ? {
|
|
1868
|
+
name,
|
|
1869
|
+
nativeName: name
|
|
1870
|
+
} : {},
|
|
1871
|
+
...typeof update.title === "string" ? { title: update.title } : {},
|
|
1872
|
+
...server ? {
|
|
1873
|
+
mcpServer: server,
|
|
1874
|
+
mcpTool: name.slice(`mcp__${server}__`.length)
|
|
1875
|
+
} : {},
|
|
1876
|
+
...update.rawInput == null ? {} : { input: update.rawInput },
|
|
1877
|
+
...update.rawOutput == null ? {} : { output: update.rawOutput },
|
|
1878
|
+
...!prior ? { startedAt: Date.now() } : {},
|
|
1879
|
+
...["completed", "failed"].includes(String(update.status)) ? { endedAt: Date.now() } : {}
|
|
1880
|
+
}
|
|
1881
|
+
}, false);
|
|
1882
|
+
}
|
|
1883
|
+
if (update.sessionUpdate === "usage_update") {
|
|
1884
|
+
if (typeof update.used === "number" && Number.isFinite(update.used) && update.used >= 0) emit({
|
|
1885
|
+
...common,
|
|
1886
|
+
kind: "usage",
|
|
1887
|
+
usage: {
|
|
1888
|
+
scope: "context",
|
|
1889
|
+
cumulative: false,
|
|
1890
|
+
epoch: sessionId,
|
|
1891
|
+
amount: update.used,
|
|
1892
|
+
unit: "context_tokens"
|
|
1893
|
+
}
|
|
1894
|
+
});
|
|
1895
|
+
const cost = object$2(update.cost);
|
|
1896
|
+
if (!rawClaude && typeof cost.amount === "number" && Number.isFinite(cost.amount) && cost.amount >= 0 && typeof cost.currency === "string") emit({
|
|
1897
|
+
...common,
|
|
1898
|
+
kind: "usage",
|
|
1899
|
+
usage: {
|
|
1900
|
+
scope: "session",
|
|
1901
|
+
cumulative: true,
|
|
1902
|
+
epoch: sessionId,
|
|
1903
|
+
amount: cost.amount,
|
|
1904
|
+
unit: cost.currency
|
|
1905
|
+
}
|
|
1906
|
+
});
|
|
1907
|
+
}
|
|
1908
|
+
},
|
|
1909
|
+
finish() {
|
|
1910
|
+
native.finish();
|
|
1911
|
+
for (const { value } of tools.values()) if (value.status === "running" || value.status === "pending") canonical({
|
|
1912
|
+
kind: "tool",
|
|
1913
|
+
tool: {
|
|
1914
|
+
...value,
|
|
1915
|
+
status: "unknown"
|
|
1916
|
+
}
|
|
1917
|
+
}, false);
|
|
1918
|
+
return {
|
|
1919
|
+
text,
|
|
1920
|
+
result,
|
|
1921
|
+
coverage: {
|
|
1922
|
+
...rawClaude && !rawSeen ? { native: {
|
|
1923
|
+
state: "unavailable",
|
|
1924
|
+
reason: "Negotiated raw SDK extension emitted no evidence."
|
|
1925
|
+
} } : {},
|
|
1926
|
+
...dropped ? {
|
|
1927
|
+
native: {
|
|
1928
|
+
state: "truncated",
|
|
1929
|
+
reason: "ACP capture exceeded the bounded event or turn limit."
|
|
1930
|
+
},
|
|
1931
|
+
tools: {
|
|
1932
|
+
state: "partial",
|
|
1933
|
+
reason: "ACP telemetry exceeded its capture limit."
|
|
1934
|
+
},
|
|
1935
|
+
messages: {
|
|
1936
|
+
state: "partial",
|
|
1937
|
+
reason: "ACP telemetry exceeded its capture limit."
|
|
1938
|
+
}
|
|
1939
|
+
} : {},
|
|
1940
|
+
...answerTruncated ? { controlProjection: {
|
|
1941
|
+
state: "truncated",
|
|
1942
|
+
reason: "Assistant answer exceeded the separate control response limit."
|
|
1943
|
+
} } : {}
|
|
1944
|
+
}
|
|
1945
|
+
};
|
|
1946
|
+
}
|
|
1947
|
+
};
|
|
1948
|
+
}
|
|
1949
|
+
// src/lib/local-harness/backends/acp/qualification.ts
|
|
1950
|
+
const tuple = HARNESS_COMPATIBILITY.claudeAcp;
|
|
1951
|
+
const exec = promisify(execFile);
|
|
1952
|
+
const ACP_COVERAGE = {
|
|
1953
|
+
tools: { state: "available" },
|
|
1954
|
+
messages: { state: "available" },
|
|
1955
|
+
native: { state: "available" },
|
|
1956
|
+
usage: {
|
|
1957
|
+
state: "partial",
|
|
1958
|
+
reason: "SDK aggregate estimates; adapter helper requests are not individually exposed. ACP context occupancy is separate."
|
|
1959
|
+
},
|
|
1960
|
+
timing: {
|
|
1961
|
+
state: "partial",
|
|
1962
|
+
reason: "Local observation times, not native tool execution timestamps."
|
|
1963
|
+
},
|
|
1964
|
+
lineage: {
|
|
1965
|
+
state: "disabled",
|
|
1966
|
+
reason: "Built-in tools and subagents are disabled in controlled-mcp."
|
|
1967
|
+
},
|
|
1968
|
+
reasoning: {
|
|
1969
|
+
state: "partial",
|
|
1970
|
+
reason: "Only reasoning exposed by the adapter is captured."
|
|
1971
|
+
}
|
|
1972
|
+
};
|
|
1973
|
+
function acpProvenance(config) {
|
|
1974
|
+
return {
|
|
1975
|
+
harness: "claude",
|
|
1976
|
+
backend: "acp",
|
|
1977
|
+
harnessVersion: tuple.harness,
|
|
1978
|
+
adapterVersion: tuple.adapter,
|
|
1979
|
+
sdkVersion: tuple.sdk,
|
|
1980
|
+
protocolVersion: tuple.protocol,
|
|
1981
|
+
profile: config.profile,
|
|
1982
|
+
coverage: ACP_COVERAGE,
|
|
1983
|
+
configuration: {
|
|
1984
|
+
builtInTools: false,
|
|
1985
|
+
settingSources: [],
|
|
1986
|
+
strictMcpConfig: true,
|
|
1987
|
+
permissionRequests: "deny",
|
|
1988
|
+
authentication: "environment_credentials_only",
|
|
1989
|
+
rawSdkMessages: true,
|
|
1990
|
+
mcpServers: config.mcpServers.map((entry) => entry.name),
|
|
1991
|
+
requestedModel: config.model ?? null
|
|
1992
|
+
}
|
|
1993
|
+
};
|
|
1994
|
+
}
|
|
1995
|
+
function isolatedEnvironment(root, executable) {
|
|
1996
|
+
return {
|
|
1997
|
+
...controlledEnvironment$1(),
|
|
1998
|
+
HOME: root,
|
|
1999
|
+
CLAUDE_CONFIG_DIR: join(root, ".claude"),
|
|
2000
|
+
CLAUDE_CODE_EXECUTABLE: executable,
|
|
2001
|
+
CLAUDE_AGENT_ACP_PROTOCOL: "v1"
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
function environmentAuthentication() {
|
|
2005
|
+
return [
|
|
2006
|
+
"ANTHROPIC_API_KEY",
|
|
2007
|
+
"ANTHROPIC_AUTH_TOKEN",
|
|
2008
|
+
"CLAUDE_CODE_OAUTH_TOKEN"
|
|
2009
|
+
].some((key) => Boolean(process.env[key]));
|
|
2010
|
+
}
|
|
2011
|
+
async function executablePath(value) {
|
|
2012
|
+
const paths = isAbsolute(value) || value.includes("/") ? [resolve(value)] : (process.env.PATH ?? "").split(":").map((path) => join(path, value));
|
|
2013
|
+
for (const path of paths) try {
|
|
2014
|
+
await access(path, constants.X_OK);
|
|
2015
|
+
return await realpath(path);
|
|
2016
|
+
} catch {
|
|
2017
|
+
continue;
|
|
2018
|
+
}
|
|
2019
|
+
throw new Error("Qualified ACP executable is unavailable.");
|
|
2020
|
+
}
|
|
2021
|
+
async function qualify(config, root) {
|
|
2022
|
+
if (!["darwin", "linux"].includes(process.platform) || config.harness !== "claude" || config.backend !== "acp" || config.profile !== "controlled-mcp") throw new Error("ACP supports the qualified Claude controlled-mcp tuple on macOS or Linux only.");
|
|
2023
|
+
if (!config.adapter) throw new Error("Select an explicit pinned ACP adapter with --adapter.");
|
|
2024
|
+
if (!Number.isInteger(config.maxTurns) || config.maxTurns < 1 || !Number.isFinite(config.timeoutMs) || config.timeoutMs <= 0) throw new Error("ACP requires positive turn and timeout limits.");
|
|
2025
|
+
const names = /* @__PURE__ */ new Set();
|
|
2026
|
+
for (const entry of config.mcpServers) {
|
|
2027
|
+
const url = new URL(entry.url);
|
|
2028
|
+
if (!/^[A-Za-z0-9_-]{1,64}$/.test(entry.name) || names.has(entry.name) || url.username || url.password || url.protocol !== "https:" && !(url.protocol === "http:" && [
|
|
2029
|
+
"localhost",
|
|
2030
|
+
"127.0.0.1",
|
|
2031
|
+
"[::1]"
|
|
2032
|
+
].includes(url.hostname))) throw new Error("ACP MCP servers require unique names and HTTPS or loopback HTTP URLs without credentials.");
|
|
2033
|
+
names.add(entry.name);
|
|
2034
|
+
}
|
|
2035
|
+
const [adapter, executable] = await Promise.all([executablePath(config.adapter), executablePath(config.executable ?? "claude")]);
|
|
2036
|
+
const packagePath = createRequire(join(dirname(adapter), "runtype-qualification.cjs")).resolve("@agentclientprotocol/claude-agent-acp/package.json");
|
|
2037
|
+
const manifest = JSON.parse(await readFile(packagePath, "utf8"));
|
|
2038
|
+
const adapterRequire = createRequire(packagePath);
|
|
2039
|
+
const sdk = JSON.parse(await readFile(join(dirname(adapterRequire.resolve("@anthropic-ai/claude-agent-sdk")), "package.json"), "utf8"));
|
|
2040
|
+
if (manifest.name !== "@agentclientprotocol/claude-agent-acp" || manifest.version !== tuple.adapter || sdk.version !== tuple.sdk) throw new Error(`ACP requires claude-agent-acp ${tuple.adapter} with Claude SDK ${tuple.sdk}.`);
|
|
2041
|
+
const env = isolatedEnvironment(root, executable);
|
|
2042
|
+
const [adapterVersion, harnessVersion] = await Promise.all([exec(adapter, ["--version"], {
|
|
2043
|
+
env,
|
|
2044
|
+
timeout: 5e3,
|
|
2045
|
+
maxBuffer: 8192
|
|
2046
|
+
}), exec(executable, ["--version"], {
|
|
2047
|
+
env,
|
|
2048
|
+
timeout: 5e3,
|
|
2049
|
+
maxBuffer: 8192
|
|
2050
|
+
})]);
|
|
2051
|
+
if (adapterVersion.stdout.trim() !== tuple.adapter || harnessVersion.stdout.match(/^(\d+\.\d+\.\d+)/)?.[1] !== tuple.harness) throw new Error(`ACP requires adapter ${tuple.adapter} and Claude ${tuple.harness}.`);
|
|
2052
|
+
return {
|
|
2053
|
+
adapter,
|
|
2054
|
+
executable,
|
|
2055
|
+
env
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
// src/lib/local-harness/backends/acp/index.ts
|
|
2059
|
+
function sessionParameters(config, cwd) {
|
|
2060
|
+
return {
|
|
2061
|
+
cwd,
|
|
2062
|
+
mcpServers: config.mcpServers.map(({ name, url, headers }) => ({
|
|
2063
|
+
name,
|
|
2064
|
+
type: "http",
|
|
2065
|
+
url,
|
|
2066
|
+
headers: Object.entries(headers ?? {}).map(([name, value]) => ({
|
|
2067
|
+
name,
|
|
2068
|
+
value
|
|
2069
|
+
}))
|
|
2070
|
+
})),
|
|
2071
|
+
_meta: { claudeCode: {
|
|
2072
|
+
emitRawSDKMessages: true,
|
|
2073
|
+
options: {
|
|
2074
|
+
tools: [],
|
|
2075
|
+
settingSources: [],
|
|
2076
|
+
strictMcpConfig: true,
|
|
2077
|
+
allowedTools: config.mcpServers.map(({ name }) => `mcp__${name}`),
|
|
2078
|
+
allowDangerouslySkipPermissions: false,
|
|
2079
|
+
promptSuggestions: false,
|
|
2080
|
+
settings: { apiKeyHelper: "" },
|
|
2081
|
+
maxTurns: config.maxTurns,
|
|
2082
|
+
...config.model ? { model: config.model } : {}
|
|
2083
|
+
}
|
|
2084
|
+
} }
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
async function discoverMcp(config, signal, until) {
|
|
2088
|
+
await Promise.all(config.mcpServers.map(async (server) => {
|
|
2089
|
+
const client = new Client({
|
|
2090
|
+
name: "runtype-harness-doctor",
|
|
2091
|
+
version: "1"
|
|
2092
|
+
});
|
|
2093
|
+
try {
|
|
2094
|
+
const transport = new StreamableHTTPClientTransport(new URL(server.url), { requestInit: { headers: server.headers } });
|
|
2095
|
+
await deadline(client.connect(transport), signal, until);
|
|
2096
|
+
await deadline(client.listTools(), signal, until);
|
|
2097
|
+
} finally {
|
|
2098
|
+
await deadline(client.close(), new AbortController().signal, Date.now() + HARNESS_LIMITS.cancelGraceMs);
|
|
2099
|
+
}
|
|
2100
|
+
}));
|
|
2101
|
+
}
|
|
2102
|
+
async function launch(config, signal, until) {
|
|
2103
|
+
const root = await mkdtemp(join(tmpdir(), "runtype-acp-"));
|
|
2104
|
+
const supervisor = createSupervisor();
|
|
2105
|
+
let connection;
|
|
2106
|
+
let active;
|
|
2107
|
+
let permission;
|
|
2108
|
+
let authentication = "unknown";
|
|
2109
|
+
const close = async () => {
|
|
2110
|
+
connection?.close();
|
|
2111
|
+
await supervisor.stop();
|
|
2112
|
+
await rm(root, {
|
|
2113
|
+
recursive: true,
|
|
2114
|
+
force: true
|
|
2115
|
+
});
|
|
2116
|
+
};
|
|
2117
|
+
try {
|
|
2118
|
+
if (!environmentAuthentication()) throw new Error("ACP controlled profile requires an environment API key or OAuth token; local login files are unsupported.");
|
|
2119
|
+
const qualified = await deadline(qualify(config, root), signal, until);
|
|
2120
|
+
const child = supervisor.spawn({
|
|
2121
|
+
command: qualified.adapter,
|
|
2122
|
+
args: [],
|
|
2123
|
+
cwd: root,
|
|
2124
|
+
env: qualified.env
|
|
2125
|
+
});
|
|
2126
|
+
connection = createAcpClient(stdioStream(child.stdin, child.stdout), (message) => {
|
|
2127
|
+
if ("method" in message && message.method === "_auth/status_update") {
|
|
2128
|
+
const kind = object$2(object$2(message.params).authStatus).kind;
|
|
2129
|
+
authentication = kind === "none" ? "absent" : [
|
|
2130
|
+
"api_key",
|
|
2131
|
+
"account",
|
|
2132
|
+
"gateway"
|
|
2133
|
+
].includes(String(kind)) ? "ready" : "unsupported";
|
|
2134
|
+
}
|
|
2135
|
+
active?.(message);
|
|
2136
|
+
}, (event) => permission?.(event));
|
|
2137
|
+
child.on("error", () => connection?.close(/* @__PURE__ */ new Error("ACP adapter process failed.")));
|
|
2138
|
+
child.on("exit", () => connection?.close(/* @__PURE__ */ new Error("ACP adapter process exited.")));
|
|
2139
|
+
const agent = connection.agent;
|
|
2140
|
+
const init = await deadline(agent.request(methods.agent.initialize, {
|
|
2141
|
+
protocolVersion: 1,
|
|
2142
|
+
clientInfo: {
|
|
2143
|
+
name: "runtype",
|
|
2144
|
+
version: "1"
|
|
2145
|
+
},
|
|
2146
|
+
clientCapabilities: {
|
|
2147
|
+
fs: {
|
|
2148
|
+
readTextFile: false,
|
|
2149
|
+
writeTextFile: false
|
|
2150
|
+
},
|
|
2151
|
+
terminal: false
|
|
2152
|
+
}
|
|
2153
|
+
}), signal, until);
|
|
2154
|
+
if (init.protocolVersion !== 1 || init.agentInfo?.name !== "@agentclientprotocol/claude-agent-acp" || init.agentInfo.version !== HARNESS_COMPATIBILITY.claudeAcp.adapter || !init.agentCapabilities?.mcpCapabilities?.http || !init.agentCapabilities.sessionCapabilities?.resume || !init.agentCapabilities.sessionCapabilities.close) throw new Error("ACP adapter did not negotiate the qualified protocol and capabilities.");
|
|
2155
|
+
const params = sessionParameters(config, root);
|
|
2156
|
+
return {
|
|
2157
|
+
agent,
|
|
2158
|
+
sessionId: (await deadline(agent.request(methods.agent.session.new, params), signal, until)).sessionId,
|
|
2159
|
+
params,
|
|
2160
|
+
close,
|
|
2161
|
+
authentication: () => authentication,
|
|
2162
|
+
observe(next, onPermission) {
|
|
2163
|
+
active = next;
|
|
2164
|
+
permission = onPermission;
|
|
2165
|
+
}
|
|
2166
|
+
};
|
|
2167
|
+
} catch (error) {
|
|
2168
|
+
await close();
|
|
2169
|
+
throw error;
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
function createAcpBackend() {
|
|
2173
|
+
return {
|
|
2174
|
+
async doctor(config) {
|
|
2175
|
+
let current;
|
|
2176
|
+
const controller = new AbortController();
|
|
2177
|
+
try {
|
|
2178
|
+
if (!environmentAuthentication()) return {
|
|
2179
|
+
ready: false,
|
|
2180
|
+
authentication: "unsupported",
|
|
2181
|
+
provenance: acpProvenance(config),
|
|
2182
|
+
diagnostics: ["The controlled ACP profile supports environment API keys and OAuth tokens; local login files are not loaded."]
|
|
2183
|
+
};
|
|
2184
|
+
const until = Date.now() + Math.min(config.timeoutMs, 15e3);
|
|
2185
|
+
current = await launch(config, controller.signal, until);
|
|
2186
|
+
await discoverMcp(config, controller.signal, until);
|
|
2187
|
+
const authentication = current.authentication();
|
|
2188
|
+
return {
|
|
2189
|
+
ready: authentication === "ready",
|
|
2190
|
+
authentication,
|
|
2191
|
+
provenance: acpProvenance(config),
|
|
2192
|
+
diagnostics: ["ACP initialized and configured MCP discovery connected without a model request. Credential expiry is not verified."]
|
|
2193
|
+
};
|
|
2194
|
+
} catch {
|
|
2195
|
+
return {
|
|
2196
|
+
ready: false,
|
|
2197
|
+
authentication: "unknown",
|
|
2198
|
+
provenance: acpProvenance(config),
|
|
2199
|
+
diagnostics: [`ACP initialization failed. Requires adapter ${HARNESS_COMPATIBILITY.claudeAcp.adapter}, SDK ${HARNESS_COMPATIBILITY.claudeAcp.sdk}, Claude ${HARNESS_COMPATIBILITY.claudeAcp.harness}, and environment credentials.`]
|
|
2200
|
+
};
|
|
2201
|
+
} finally {
|
|
2202
|
+
controller.abort();
|
|
2203
|
+
await current?.close();
|
|
2204
|
+
}
|
|
2205
|
+
},
|
|
2206
|
+
async start(config, context) {
|
|
2207
|
+
context.signal.throwIfAborted();
|
|
2208
|
+
const current = await launch(config, context.signal, context.deadlineAt);
|
|
2209
|
+
let previous;
|
|
2210
|
+
let resumeBeforePrompt = false;
|
|
2211
|
+
let blocked = false;
|
|
2212
|
+
let closed = false;
|
|
2213
|
+
let cleanupFailed = false;
|
|
2214
|
+
let active;
|
|
2215
|
+
let abort;
|
|
2216
|
+
const session = {
|
|
2217
|
+
id: context.sessionId,
|
|
2218
|
+
async prompt(input) {
|
|
2219
|
+
if (closed || blocked || active) throw new Error("ACP session cannot accept another turn; start a new context.");
|
|
2220
|
+
input.signal.throwIfAborted();
|
|
2221
|
+
const observer = createAcpObserver(current.sessionId, input.emit, config.mcpServers.map((entry) => entry.name), true);
|
|
2222
|
+
let done;
|
|
2223
|
+
active = new Promise((resolve) => {
|
|
2224
|
+
done = resolve;
|
|
2225
|
+
});
|
|
2226
|
+
abort = new AbortController();
|
|
2227
|
+
const cancel = () => abort?.abort();
|
|
2228
|
+
input.signal.addEventListener("abort", cancel, { once: true });
|
|
2229
|
+
const until = Math.min(input.deadlineAt, Date.now() + config.timeoutMs);
|
|
2230
|
+
let status = "failed";
|
|
2231
|
+
try {
|
|
2232
|
+
if (resumeBeforePrompt) {
|
|
2233
|
+
await deadline(current.agent.request(methods.agent.session.resume, {
|
|
2234
|
+
...current.params,
|
|
2235
|
+
sessionId: current.sessionId
|
|
2236
|
+
}), abort.signal, until);
|
|
2237
|
+
resumeBeforePrompt = false;
|
|
2238
|
+
}
|
|
2239
|
+
current.observe(observer.observe, input.emit);
|
|
2240
|
+
const response = await deadline(current.agent.request(methods.agent.session.prompt, {
|
|
2241
|
+
sessionId: current.sessionId,
|
|
2242
|
+
prompt: [{
|
|
2243
|
+
type: "text",
|
|
2244
|
+
text: input.prompt
|
|
2245
|
+
}]
|
|
2246
|
+
}), abort.signal, until);
|
|
2247
|
+
status = response.stopReason === "end_turn" ? "completed" : response.stopReason === "cancelled" ? "cancelled" : "failed";
|
|
2248
|
+
if (status === "completed") resumeBeforePrompt = true;
|
|
2249
|
+
else blocked = true;
|
|
2250
|
+
} catch {
|
|
2251
|
+
status = input.signal.aborted || abort.signal.aborted ? "cancelled" : "failed";
|
|
2252
|
+
blocked = true;
|
|
2253
|
+
try {
|
|
2254
|
+
await deadline(current.agent.notify(methods.agent.session.cancel, { sessionId: current.sessionId }), new AbortController().signal, Date.now() + HARNESS_LIMITS.cancelGraceMs);
|
|
2255
|
+
} catch {
|
|
2256
|
+
blocked = true;
|
|
2257
|
+
}
|
|
2258
|
+
try {
|
|
2259
|
+
await current.close();
|
|
2260
|
+
} catch {
|
|
2261
|
+
cleanupFailed = true;
|
|
2262
|
+
status = "failed";
|
|
2263
|
+
input.emit({
|
|
2264
|
+
kind: "lifecycle",
|
|
2265
|
+
lifecycle: {
|
|
2266
|
+
state: "cleanup_failed",
|
|
2267
|
+
reason: "ACP descendants did not stop cleanly."
|
|
2268
|
+
}
|
|
2269
|
+
});
|
|
2270
|
+
}
|
|
2271
|
+
closed = true;
|
|
2272
|
+
} finally {
|
|
2273
|
+
current.observe();
|
|
2274
|
+
input.signal.removeEventListener("abort", cancel);
|
|
2275
|
+
abort = void 0;
|
|
2276
|
+
active = void 0;
|
|
2277
|
+
done?.();
|
|
2278
|
+
}
|
|
2279
|
+
const captured = observer.finish();
|
|
2280
|
+
if (captured.result?.is_error === true) {
|
|
2281
|
+
status = "failed";
|
|
2282
|
+
blocked = true;
|
|
2283
|
+
}
|
|
2284
|
+
const usage = captured.result ? reportedUsage$1(captured.result, previous) : void 0;
|
|
2285
|
+
if (captured.result) previous = captured.result;
|
|
2286
|
+
return {
|
|
2287
|
+
nativeSessionId: current.sessionId,
|
|
2288
|
+
nativeTurnId: typeof captured.result?.user_message_uuid === "string" ? captured.result.user_message_uuid : void 0,
|
|
2289
|
+
text: captured.text,
|
|
2290
|
+
status,
|
|
2291
|
+
...usage ? { reportedUsage: usage } : {},
|
|
2292
|
+
coverage: {
|
|
2293
|
+
...ACP_COVERAGE,
|
|
2294
|
+
...captured.coverage,
|
|
2295
|
+
...status === "completed" ? {} : { tools: {
|
|
2296
|
+
state: "partial",
|
|
2297
|
+
reason: "Turn interrupted; partial tool evidence is retained."
|
|
2298
|
+
} }
|
|
2299
|
+
}
|
|
2300
|
+
};
|
|
2301
|
+
},
|
|
2302
|
+
async cancel() {
|
|
2303
|
+
blocked = true;
|
|
2304
|
+
abort?.abort();
|
|
2305
|
+
await active;
|
|
2306
|
+
},
|
|
2307
|
+
async close() {
|
|
2308
|
+
if (cleanupFailed) throw new Error("ACP cleanup failed; isolated session files were retained.");
|
|
2309
|
+
if (closed) return;
|
|
2310
|
+
closed = true;
|
|
2311
|
+
await session.cancel();
|
|
2312
|
+
await current.close();
|
|
2313
|
+
}
|
|
2314
|
+
};
|
|
2315
|
+
return session;
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
// src/lib/local-harness/backend.ts
|
|
2320
|
+
function createHarnessBackend(config) {
|
|
2321
|
+
const backend = config.backend === "native" ? config.harness === "claude" ? createClaudeBackend() : createCodexBackend() : config.harness === "claude" ? createAcpBackend() : void 0;
|
|
2322
|
+
if (!backend) throw new Error("This harness backend is not available in this build.");
|
|
2323
|
+
return {
|
|
2324
|
+
...backend,
|
|
2325
|
+
async doctor(configuration) {
|
|
2326
|
+
const result = await backend.doctor(configuration);
|
|
2327
|
+
return {
|
|
2328
|
+
...result,
|
|
2329
|
+
provenance: {
|
|
2330
|
+
...result.provenance,
|
|
2331
|
+
cliVersion: getCliVersion()
|
|
2332
|
+
}
|
|
2333
|
+
};
|
|
2334
|
+
}
|
|
2335
|
+
};
|
|
2336
|
+
}
|
|
2337
|
+
export { createHarnessBackend };
|
|
2338
|
+
|