@mclawnet/agent 0.6.33 → 0.6.34
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/__tests__/backend-adapter-types.test.d.ts +2 -0
- package/dist/__tests__/backend-adapter-types.test.d.ts.map +1 -0
- package/dist/__tests__/backend-factory.test.d.ts +2 -0
- package/dist/__tests__/backend-factory.test.d.ts.map +1 -0
- package/dist/__tests__/normalize-backend-output.test.d.ts +2 -0
- package/dist/__tests__/normalize-backend-output.test.d.ts.map +1 -0
- package/dist/__tests__/session-manager-backend.test.d.ts +2 -0
- package/dist/__tests__/session-manager-backend.test.d.ts.map +1 -0
- package/dist/__tests__/session-manager-permission.test.d.ts +2 -0
- package/dist/__tests__/session-manager-permission.test.d.ts.map +1 -0
- package/dist/__tests__/templates-roles-bridge.test.d.ts +2 -0
- package/dist/__tests__/templates-roles-bridge.test.d.ts.map +1 -0
- package/dist/backend-adapter.d.ts +102 -10
- package/dist/backend-adapter.d.ts.map +1 -1
- package/dist/backend-factory-RUYUBJVF.js +9 -0
- package/dist/backend-factory-RUYUBJVF.js.map +1 -0
- package/dist/backend-factory.d.ts +19 -0
- package/dist/backend-factory.d.ts.map +1 -0
- package/dist/checkpoint.d.ts +1 -1
- package/dist/checkpoint.d.ts.map +1 -1
- package/dist/{chunk-QPLG5WHL.js → chunk-2JDX6XFD.js} +445 -84
- package/dist/chunk-2JDX6XFD.js.map +1 -0
- package/dist/chunk-MFXF77LG.js +49 -0
- package/dist/chunk-MFXF77LG.js.map +1 -0
- package/dist/dist-VLBO5CT3.js +775 -0
- package/dist/dist-VLBO5CT3.js.map +1 -0
- package/dist/fs-handler.d.ts +1 -1
- package/dist/fs-handler.d.ts.map +1 -1
- package/dist/hub-connection.d.ts.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -1
- package/dist/index.js.map +1 -1
- package/dist/normalize-backend-output.d.ts +5 -0
- package/dist/normalize-backend-output.d.ts.map +1 -0
- package/dist/session-manager.d.ts +35 -4
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/start.d.ts +2 -0
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +1 -1
- package/dist/swarm-control-dispatch.d.ts +33 -1
- package/dist/swarm-control-dispatch.d.ts.map +1 -1
- package/dist/swarm-session-bridge.d.ts +3 -3
- package/dist/swarm-session-bridge.d.ts.map +1 -1
- package/dist/templates-roles-bridge.d.ts +14 -0
- package/dist/templates-roles-bridge.d.ts.map +1 -0
- package/package.json +13 -10
- package/dist/chunk-QPLG5WHL.js.map +0 -1
|
@@ -0,0 +1,775 @@
|
|
|
1
|
+
// ../codex-adapter/dist/codex-adapter.js
|
|
2
|
+
import { spawn } from "child_process";
|
|
3
|
+
import { EventEmitter } from "events";
|
|
4
|
+
import { existsSync } from "fs";
|
|
5
|
+
import { createRequire } from "module";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
import { join as join2 } from "path";
|
|
8
|
+
|
|
9
|
+
// ../codex-adapter/dist/codex-spawn-args.js
|
|
10
|
+
import { writeFileSync, unlinkSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
import { tmpdir } from "os";
|
|
13
|
+
import { randomUUID } from "crypto";
|
|
14
|
+
import { DEFAULT_SANDBOX } from "@mclawnet/shared";
|
|
15
|
+
function escapeToml(value) {
|
|
16
|
+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
17
|
+
}
|
|
18
|
+
function buildCodexSpawnArgs(options) {
|
|
19
|
+
const args = ["app-server"];
|
|
20
|
+
let briefingFile;
|
|
21
|
+
void options.resumeId;
|
|
22
|
+
args.push("-c", 'approval_policy="never"');
|
|
23
|
+
const sandboxLevel = options.sandbox ?? DEFAULT_SANDBOX;
|
|
24
|
+
const codexSandboxMode = sandboxLevel === "read-only" ? "read-only" : sandboxLevel === "full-access" ? "danger-full-access" : "workspace-write";
|
|
25
|
+
args.push("-c", `sandbox_mode="${codexSandboxMode}"`);
|
|
26
|
+
if (options.mcpServer) {
|
|
27
|
+
const { command, args: serverArgs, env } = options.mcpServer;
|
|
28
|
+
args.push("-c", `mcp_servers.clawnet-mcp.command="${escapeToml(command)}"`);
|
|
29
|
+
if (serverArgs && serverArgs.length > 0) {
|
|
30
|
+
const argsToml = "[" + serverArgs.map((a) => `"${escapeToml(a)}"`).join(", ") + "]";
|
|
31
|
+
args.push("-c", `mcp_servers.clawnet-mcp.args=${argsToml}`);
|
|
32
|
+
}
|
|
33
|
+
if (env) {
|
|
34
|
+
for (const [key, value] of Object.entries(env)) {
|
|
35
|
+
args.push("-c", `mcp_servers.clawnet-mcp.env.${key}="${escapeToml(value)}"`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
args.push("-c", 'mcp_servers.clawnet-mcp.default_tools_approval_mode="approve"');
|
|
39
|
+
}
|
|
40
|
+
if (options.systemPrompt) {
|
|
41
|
+
briefingFile = join(tmpdir(), `clawnet-briefing-${options.sessionId}-${randomUUID().slice(0, 8)}.md`);
|
|
42
|
+
writeFileSync(briefingFile, options.systemPrompt, { encoding: "utf-8", mode: 384 });
|
|
43
|
+
args.push("-c", `model_instructions_file="${escapeToml(briefingFile)}"`);
|
|
44
|
+
}
|
|
45
|
+
return { args, briefingFile };
|
|
46
|
+
}
|
|
47
|
+
function cleanupBriefingFile(filePath) {
|
|
48
|
+
try {
|
|
49
|
+
unlinkSync(filePath);
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ../codex-adapter/dist/codex-adapter.js
|
|
55
|
+
import { createLogger, preview } from "@mclawnet/logger";
|
|
56
|
+
|
|
57
|
+
// ../codex-adapter/dist/json-rpc-client.js
|
|
58
|
+
var JsonRpcClient = class {
|
|
59
|
+
stdin;
|
|
60
|
+
buffer = "";
|
|
61
|
+
nextId = 1;
|
|
62
|
+
pending = /* @__PURE__ */ new Map();
|
|
63
|
+
onRequest;
|
|
64
|
+
onNotification;
|
|
65
|
+
onMalformedLine;
|
|
66
|
+
constructor(opts) {
|
|
67
|
+
this.stdin = opts.stdin;
|
|
68
|
+
this.onRequest = opts.onRequest;
|
|
69
|
+
this.onNotification = opts.onNotification;
|
|
70
|
+
this.onMalformedLine = opts.onMalformedLine;
|
|
71
|
+
opts.stdout.on("data", (chunk) => this.feed(chunk.toString("utf-8")));
|
|
72
|
+
}
|
|
73
|
+
request(method, params) {
|
|
74
|
+
const id = this.nextId++;
|
|
75
|
+
const frame = { jsonrpc: "2.0", id, method, params };
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
this.pending.set(id, { resolve, reject });
|
|
78
|
+
this.stdin.write(JSON.stringify(frame) + "\n");
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
notify(method, params) {
|
|
82
|
+
this.stdin.write(JSON.stringify({ jsonrpc: "2.0", method, params }) + "\n");
|
|
83
|
+
}
|
|
84
|
+
feed(s) {
|
|
85
|
+
this.buffer += s;
|
|
86
|
+
let nl;
|
|
87
|
+
while ((nl = this.buffer.indexOf("\n")) >= 0) {
|
|
88
|
+
const line = this.buffer.slice(0, nl).trim();
|
|
89
|
+
this.buffer = this.buffer.slice(nl + 1);
|
|
90
|
+
if (!line)
|
|
91
|
+
continue;
|
|
92
|
+
this.dispatch(line);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
dispatch(line) {
|
|
96
|
+
let msg;
|
|
97
|
+
try {
|
|
98
|
+
msg = JSON.parse(line);
|
|
99
|
+
} catch (err) {
|
|
100
|
+
this.onMalformedLine?.(line, err);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (typeof msg.id === "number" && (msg.result !== void 0 || msg.error !== void 0)) {
|
|
104
|
+
const pending = this.pending.get(msg.id);
|
|
105
|
+
if (!pending)
|
|
106
|
+
return;
|
|
107
|
+
this.pending.delete(msg.id);
|
|
108
|
+
if (msg.error)
|
|
109
|
+
pending.reject(new Error(msg.error.message));
|
|
110
|
+
else
|
|
111
|
+
pending.resolve(msg.result);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (msg.method && typeof msg.id === "number") {
|
|
115
|
+
const id = msg.id;
|
|
116
|
+
const method = msg.method;
|
|
117
|
+
const params = msg.params;
|
|
118
|
+
void Promise.resolve(this.onRequest?.(method, params)).then((result) => {
|
|
119
|
+
this.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, result }) + "\n");
|
|
120
|
+
}).catch((err) => {
|
|
121
|
+
this.stdin.write(JSON.stringify({
|
|
122
|
+
jsonrpc: "2.0",
|
|
123
|
+
id,
|
|
124
|
+
error: { code: -32e3, message: err.message }
|
|
125
|
+
}) + "\n");
|
|
126
|
+
});
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (msg.method) {
|
|
130
|
+
this.onNotification?.(msg.method, msg.params);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
// ../codex-adapter/dist/permission-mapper.js
|
|
136
|
+
function decisionToWire(d) {
|
|
137
|
+
switch (d) {
|
|
138
|
+
case "allow":
|
|
139
|
+
return "approved";
|
|
140
|
+
case "allow_session":
|
|
141
|
+
return "approved_for_session";
|
|
142
|
+
case "deny":
|
|
143
|
+
return "denied";
|
|
144
|
+
case "abort":
|
|
145
|
+
return "abort";
|
|
146
|
+
default: {
|
|
147
|
+
const _exhaustive = d;
|
|
148
|
+
return _exhaustive;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function decisionToV2Wire(d) {
|
|
153
|
+
switch (d) {
|
|
154
|
+
case "allow":
|
|
155
|
+
return "accept";
|
|
156
|
+
case "allow_session":
|
|
157
|
+
return "acceptForSession";
|
|
158
|
+
case "deny":
|
|
159
|
+
return "decline";
|
|
160
|
+
case "abort":
|
|
161
|
+
return "cancel";
|
|
162
|
+
default: {
|
|
163
|
+
const _exhaustive = d;
|
|
164
|
+
return _exhaustive;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function decisionToMcpAction(d) {
|
|
169
|
+
switch (d) {
|
|
170
|
+
case "allow":
|
|
171
|
+
case "allow_session":
|
|
172
|
+
return "accept";
|
|
173
|
+
case "deny":
|
|
174
|
+
return "decline";
|
|
175
|
+
case "abort":
|
|
176
|
+
return "cancel";
|
|
177
|
+
default: {
|
|
178
|
+
const _exhaustive = d;
|
|
179
|
+
return _exhaustive;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function buildApprovalReply(family, decision) {
|
|
184
|
+
switch (family) {
|
|
185
|
+
case "legacy":
|
|
186
|
+
return { decision: decisionToWire(decision) };
|
|
187
|
+
case "v2":
|
|
188
|
+
return { decision: decisionToV2Wire(decision) };
|
|
189
|
+
case "mcp":
|
|
190
|
+
return { action: decisionToMcpAction(decision) };
|
|
191
|
+
default: {
|
|
192
|
+
const _exhaustive = family;
|
|
193
|
+
return _exhaustive;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function parseApprovalRequest(method, params) {
|
|
198
|
+
const p = params ?? {};
|
|
199
|
+
const callId = p.callId ?? p.itemId;
|
|
200
|
+
if (!callId)
|
|
201
|
+
return null;
|
|
202
|
+
const reason = p.reason;
|
|
203
|
+
const cwd = p.cwd;
|
|
204
|
+
if (method === "execCommandApproval") {
|
|
205
|
+
return {
|
|
206
|
+
req: {
|
|
207
|
+
callId,
|
|
208
|
+
toolName: "shell",
|
|
209
|
+
input: { command: p.command, cwd },
|
|
210
|
+
meta: { backend: "codex", reason, cwd }
|
|
211
|
+
},
|
|
212
|
+
wireFamily: "legacy"
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
if (method === "item/commandExecution/requestApproval") {
|
|
216
|
+
return {
|
|
217
|
+
req: {
|
|
218
|
+
callId,
|
|
219
|
+
toolName: "shell",
|
|
220
|
+
input: { command: p.command, cwd },
|
|
221
|
+
meta: { backend: "codex", reason, cwd }
|
|
222
|
+
},
|
|
223
|
+
wireFamily: "v2"
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
if (method === "applyPatchApproval") {
|
|
227
|
+
return {
|
|
228
|
+
req: {
|
|
229
|
+
callId,
|
|
230
|
+
toolName: "apply_patch",
|
|
231
|
+
input: { fileChanges: p.fileChanges },
|
|
232
|
+
meta: { backend: "codex", reason, cwd }
|
|
233
|
+
},
|
|
234
|
+
wireFamily: "legacy"
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
if (method === "item/fileChange/requestApproval") {
|
|
238
|
+
return {
|
|
239
|
+
req: {
|
|
240
|
+
callId,
|
|
241
|
+
toolName: "apply_patch",
|
|
242
|
+
input: { fileChanges: p.fileChanges },
|
|
243
|
+
meta: { backend: "codex", reason, cwd }
|
|
244
|
+
},
|
|
245
|
+
wireFamily: "v2"
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
if (method === "mcpServer/elicitation/request") {
|
|
249
|
+
return {
|
|
250
|
+
req: {
|
|
251
|
+
callId,
|
|
252
|
+
toolName: p.serverName ?? "mcp",
|
|
253
|
+
input: { message: p.message, meta: p._meta },
|
|
254
|
+
meta: { backend: "codex", reason }
|
|
255
|
+
},
|
|
256
|
+
wireFamily: "mcp"
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ../codex-adapter/dist/output-mapper.js
|
|
263
|
+
function mapCodexFrame(frame) {
|
|
264
|
+
const params = frame.params ?? {};
|
|
265
|
+
switch (frame.method) {
|
|
266
|
+
// ── v2 frames (real codex app-server v2 protocol) ─────────────────
|
|
267
|
+
case "item/agentMessage/delta": {
|
|
268
|
+
const delta = params.delta;
|
|
269
|
+
if (typeof delta === "string") {
|
|
270
|
+
return { kind: "assistant_text", text: delta };
|
|
271
|
+
}
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
case "item/completed": {
|
|
275
|
+
const item = params.item;
|
|
276
|
+
if (item?.type === "agentMessage" && typeof item.text === "string") {
|
|
277
|
+
return { kind: "assistant_text", text: item.text };
|
|
278
|
+
}
|
|
279
|
+
if (item?.type === "commandExecution") {
|
|
280
|
+
const failed = item.status === "failed" || item.status === "declined";
|
|
281
|
+
const badExit = typeof item.exitCode === "number" && item.exitCode !== 0;
|
|
282
|
+
return {
|
|
283
|
+
kind: "tool_result",
|
|
284
|
+
callId: String(item.id ?? ""),
|
|
285
|
+
output: item.aggregatedOutput ?? null,
|
|
286
|
+
isError: failed || badExit
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (item?.type === "fileChange") {
|
|
290
|
+
const failed = item.status === "failed" || item.status === "declined";
|
|
291
|
+
return {
|
|
292
|
+
kind: "tool_result",
|
|
293
|
+
callId: String(item.id ?? ""),
|
|
294
|
+
output: { changes: item.changes },
|
|
295
|
+
isError: failed
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
if (item?.type === "mcpToolCall") {
|
|
299
|
+
const failed = item.status === "failed" || item.error != null;
|
|
300
|
+
return {
|
|
301
|
+
kind: "tool_result",
|
|
302
|
+
callId: String(item.id ?? ""),
|
|
303
|
+
output: failed ? item.error : item.result,
|
|
304
|
+
isError: failed
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
break;
|
|
308
|
+
}
|
|
309
|
+
case "item/started": {
|
|
310
|
+
const item = params.item;
|
|
311
|
+
if (item?.type === "commandExecution") {
|
|
312
|
+
return {
|
|
313
|
+
kind: "tool_use",
|
|
314
|
+
callId: String(item.id ?? ""),
|
|
315
|
+
toolName: "shell",
|
|
316
|
+
input: { command: item.command }
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
if (item?.type === "mcpToolCall") {
|
|
320
|
+
return {
|
|
321
|
+
kind: "tool_use",
|
|
322
|
+
callId: String(item.id ?? ""),
|
|
323
|
+
toolName: String(item.name ?? "mcp"),
|
|
324
|
+
input: item.input
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
// ── legacy frame names (kept for back-compat with older codex / tests) ──
|
|
330
|
+
case "thread/message": {
|
|
331
|
+
const role = params.role;
|
|
332
|
+
const content = params.content;
|
|
333
|
+
if (role === "assistant" && content) {
|
|
334
|
+
const text = content.filter((c) => c.type === "text" && typeof c.text === "string").map((c) => c.text).join("");
|
|
335
|
+
return { kind: "assistant_text", text };
|
|
336
|
+
}
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
case "thread/toolUse":
|
|
340
|
+
return {
|
|
341
|
+
kind: "tool_use",
|
|
342
|
+
callId: String(params.callId ?? ""),
|
|
343
|
+
toolName: String(params.name ?? ""),
|
|
344
|
+
input: params.input
|
|
345
|
+
};
|
|
346
|
+
case "thread/toolResult":
|
|
347
|
+
return {
|
|
348
|
+
kind: "tool_result",
|
|
349
|
+
callId: String(params.callId ?? ""),
|
|
350
|
+
output: params.output,
|
|
351
|
+
isError: Boolean(params.isError)
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
return { kind: "raw", backend: "codex", payload: frame };
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// ../codex-adapter/dist/codex-adapter.js
|
|
358
|
+
var log = createLogger({ module: "codex-adapter" });
|
|
359
|
+
var CLAWNET_CLIENT_INFO = {
|
|
360
|
+
name: "clawnet",
|
|
361
|
+
version: "0.1.0",
|
|
362
|
+
title: "ClawNet Agent"
|
|
363
|
+
};
|
|
364
|
+
var CodexProcess = class extends EventEmitter {
|
|
365
|
+
id;
|
|
366
|
+
workDir;
|
|
367
|
+
pid;
|
|
368
|
+
proc;
|
|
369
|
+
killed = false;
|
|
370
|
+
/** callId → JSON-RPC server-request id we still owe a result to. */
|
|
371
|
+
pendingApprovals = /* @__PURE__ */ new Map();
|
|
372
|
+
/** callId → originating wire family (drives v2/legacy/mcp reply shape). */
|
|
373
|
+
approvalMethods = /* @__PURE__ */ new Map();
|
|
374
|
+
/** Monotonic counter for approval resolver keys. */
|
|
375
|
+
nextResolverKey = 1;
|
|
376
|
+
/** Resolver registered by the rpc client for each pending approval. */
|
|
377
|
+
approvalResolvers = /* @__PURE__ */ new Map();
|
|
378
|
+
rpc;
|
|
379
|
+
backendSessionId;
|
|
380
|
+
/**
|
|
381
|
+
* Thread id to resume on agent restart. When set, handshake() sends
|
|
382
|
+
* `thread/resume { threadId }` instead of `thread/start { cwd }`.
|
|
383
|
+
* Set from SpawnOptions.resumeId in CodexAdapter.spawn(). Must NOT be
|
|
384
|
+
* passed as `--resume` CLI flag — app-server rejects it.
|
|
385
|
+
*/
|
|
386
|
+
resumeId;
|
|
387
|
+
/** True once `initialize` round-trip + `thread/start` (or `thread/resume`) have completed. */
|
|
388
|
+
handshakeComplete = false;
|
|
389
|
+
/** Inputs queued by send() while the handshake is still in flight. */
|
|
390
|
+
pendingInputs = [];
|
|
391
|
+
/** Temp file for briefing injection; cleaned up on kill(). */
|
|
392
|
+
briefingFile;
|
|
393
|
+
/**
|
|
394
|
+
* Set by CodexAdapter.spawn() so handshake() can surface real process-exit
|
|
395
|
+
* info if the codex CLI died (e.g. arg parse error). Without this, a dead
|
|
396
|
+
* process leads to a generic "handshake timeout" instead of the actual
|
|
397
|
+
* "error: unexpected argument …" stderr message.
|
|
398
|
+
*
|
|
399
|
+
* Default returns (null, "") for tests that construct CodexProcess
|
|
400
|
+
* directly without going through spawn() (e.g. attachRpc mock pipes).
|
|
401
|
+
* spawn() overrides these with real getters wired to the child process.
|
|
402
|
+
*/
|
|
403
|
+
getExitInfo = () => null;
|
|
404
|
+
getStderr = () => "";
|
|
405
|
+
/** Allocate a resolver key. Monotonic; never reused. */
|
|
406
|
+
allocResolverKey() {
|
|
407
|
+
return this.nextResolverKey++;
|
|
408
|
+
}
|
|
409
|
+
constructor(sessionId, workDir, proc) {
|
|
410
|
+
super();
|
|
411
|
+
this.id = sessionId;
|
|
412
|
+
this.workDir = workDir;
|
|
413
|
+
this.proc = proc;
|
|
414
|
+
this.pid = proc?.pid;
|
|
415
|
+
}
|
|
416
|
+
async kill() {
|
|
417
|
+
this.killed = true;
|
|
418
|
+
for (const [, resolver] of this.approvalResolvers) {
|
|
419
|
+
try {
|
|
420
|
+
resolver({ decision: "abort" });
|
|
421
|
+
} catch {
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
this.pendingApprovals.clear();
|
|
425
|
+
this.approvalMethods.clear();
|
|
426
|
+
this.approvalResolvers.clear();
|
|
427
|
+
this.pendingInputs.length = 0;
|
|
428
|
+
if (this.briefingFile) {
|
|
429
|
+
cleanupBriefingFile(this.briefingFile);
|
|
430
|
+
this.briefingFile = void 0;
|
|
431
|
+
}
|
|
432
|
+
if (this.proc && !this.proc.killed) {
|
|
433
|
+
try {
|
|
434
|
+
this.proc.kill("SIGTERM");
|
|
435
|
+
} catch {
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
isAlive() {
|
|
440
|
+
if (this.killed)
|
|
441
|
+
return false;
|
|
442
|
+
if (!this.proc)
|
|
443
|
+
return true;
|
|
444
|
+
return !this.proc.killed && this.proc.exitCode === null;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
var CodexAdapter = class {
|
|
448
|
+
type = "codex";
|
|
449
|
+
codexBin;
|
|
450
|
+
handshakeTimeoutMs;
|
|
451
|
+
constructor(options) {
|
|
452
|
+
this.codexBin = options?.codexBin ?? "codex";
|
|
453
|
+
const envTimeout = Number(process.env.CLAWNET_CODEX_HANDSHAKE_TIMEOUT_MS);
|
|
454
|
+
this.handshakeTimeoutMs = options?.handshakeTimeoutMs ?? (Number.isFinite(envTimeout) && envTimeout > 0 ? envTimeout : 15e3);
|
|
455
|
+
}
|
|
456
|
+
async spawn(options) {
|
|
457
|
+
let cwd = options.workDir || process.cwd();
|
|
458
|
+
if (!existsSync(cwd))
|
|
459
|
+
cwd = homedir();
|
|
460
|
+
const mcpServer = this.buildMcpServerConfig(options);
|
|
461
|
+
const { args, briefingFile } = buildCodexSpawnArgs({
|
|
462
|
+
sessionId: options.sessionId,
|
|
463
|
+
resumeId: options.resumeId,
|
|
464
|
+
systemPrompt: options.systemPrompt,
|
|
465
|
+
mcpServer,
|
|
466
|
+
sandbox: options.sandbox
|
|
467
|
+
});
|
|
468
|
+
log.info({ sessionId: options.sessionId, bin: this.codexBin, args, cwd, resumeId: options.resumeId }, "codex spawn: forking app-server");
|
|
469
|
+
const proc = spawn(this.codexBin, args, {
|
|
470
|
+
cwd,
|
|
471
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
472
|
+
env: process.env,
|
|
473
|
+
windowsHide: true
|
|
474
|
+
});
|
|
475
|
+
const stderrChunks = [];
|
|
476
|
+
proc.stderr?.on("data", (chunk) => {
|
|
477
|
+
const text = chunk.toString("utf8").trim();
|
|
478
|
+
if (text) {
|
|
479
|
+
stderrChunks.push(text);
|
|
480
|
+
log.warn({ sessionId: options.sessionId, stderr: text }, "codex stderr");
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
let exitInfo = null;
|
|
484
|
+
proc.on("exit", (code, signal) => {
|
|
485
|
+
const isClean = code === 0 || signal === "SIGTERM";
|
|
486
|
+
const level = isClean ? "debug" : "warn";
|
|
487
|
+
log[level]({ sessionId: options.sessionId, code, signal }, "codex process exited");
|
|
488
|
+
if (!exitInfo)
|
|
489
|
+
exitInfo = { code, signal };
|
|
490
|
+
});
|
|
491
|
+
await new Promise((resolve, reject) => {
|
|
492
|
+
const t = setTimeout(() => resolve(), 2e3);
|
|
493
|
+
proc.on("error", (err) => {
|
|
494
|
+
clearTimeout(t);
|
|
495
|
+
reject(new Error(`Failed to spawn codex CLI: ${err.message}`));
|
|
496
|
+
});
|
|
497
|
+
proc.on("spawn", () => {
|
|
498
|
+
clearTimeout(t);
|
|
499
|
+
log.debug({ sessionId: options.sessionId, pid: proc.pid }, "codex spawn: process started");
|
|
500
|
+
resolve();
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
const cp = new CodexProcess(options.sessionId, cwd, proc);
|
|
504
|
+
cp.briefingFile = briefingFile;
|
|
505
|
+
cp.resumeId = options.resumeId;
|
|
506
|
+
cp.getExitInfo = () => exitInfo;
|
|
507
|
+
cp.getStderr = () => stderrChunks.join("\n").slice(0, 800);
|
|
508
|
+
this.wireRpc(cp);
|
|
509
|
+
return cp;
|
|
510
|
+
}
|
|
511
|
+
buildMcpServerConfig(options) {
|
|
512
|
+
let serverPath;
|
|
513
|
+
try {
|
|
514
|
+
const require2 = createRequire(import.meta.url);
|
|
515
|
+
serverPath = require2.resolve("@mclawnet/mcp-server/dist/server.js");
|
|
516
|
+
} catch {
|
|
517
|
+
const devPath = join2(import.meta.dirname ?? __dirname, "../../mcp-server/dist/server.js");
|
|
518
|
+
if (existsSync(devPath))
|
|
519
|
+
serverPath = devPath;
|
|
520
|
+
}
|
|
521
|
+
if (!serverPath) {
|
|
522
|
+
log.warn({ pkg: "@mclawnet/mcp-server" }, "codex: clawnet-mcp-server not resolvable, codex role will lack MCP tools");
|
|
523
|
+
return void 0;
|
|
524
|
+
}
|
|
525
|
+
const env = {};
|
|
526
|
+
if (options.workDir)
|
|
527
|
+
env.CLAWNET_WORK_DIR = options.workDir;
|
|
528
|
+
const home = process.env.CLAWNET_HOME ?? homedir();
|
|
529
|
+
env.CLAWNET_HOME = home;
|
|
530
|
+
return { command: "node", args: [serverPath], env };
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Bind a CodexProcess to a JSON-RPC duplex. Public so tests can inject a
|
|
534
|
+
* mocked stdin/stdout pair without spawning a real subprocess.
|
|
535
|
+
*
|
|
536
|
+
* Drives the v2 handshake automatically: `initialize` → `thread/start`.
|
|
537
|
+
* Once `thread/start` resolves, `cp.backendSessionId` is set and a
|
|
538
|
+
* `session_started` event fires so the agent's hub bridge can persist it.
|
|
539
|
+
*/
|
|
540
|
+
attachRpc(cp, stdin, stdout) {
|
|
541
|
+
const rpc = new JsonRpcClient({
|
|
542
|
+
stdin,
|
|
543
|
+
stdout,
|
|
544
|
+
onRequest: (method, params) => this.handleServerRequest(cp, method, params),
|
|
545
|
+
onNotification: (method, params) => this.handleNotification(cp, method, params),
|
|
546
|
+
onMalformedLine: (line, err) => log.warn({ sessionId: cp.id, err: err.message, linePreview: line.slice(0, 200) }, "codex stdout: non-JSON line ignored")
|
|
547
|
+
});
|
|
548
|
+
cp.rpc = rpc;
|
|
549
|
+
void this.handshake(cp);
|
|
550
|
+
}
|
|
551
|
+
async handshake(cp) {
|
|
552
|
+
if (!cp.rpc)
|
|
553
|
+
return;
|
|
554
|
+
const rpc = cp.rpc;
|
|
555
|
+
const resumeId = cp.resumeId;
|
|
556
|
+
log.info({ sessionId: cp.id, workDir: cp.workDir, resumeId }, "codex handshake: start");
|
|
557
|
+
let raceSettled = false;
|
|
558
|
+
const handshakeAttempt = (async () => {
|
|
559
|
+
await rpc.request("initialize", { clientInfo: CLAWNET_CLIENT_INFO });
|
|
560
|
+
log.info({ sessionId: cp.id }, "codex handshake: initialize ok");
|
|
561
|
+
let threadId;
|
|
562
|
+
if (resumeId) {
|
|
563
|
+
const resumed = await rpc.request("thread/resume", { threadId: resumeId });
|
|
564
|
+
threadId = resumed?.thread?.id ?? resumed?.threadId;
|
|
565
|
+
} else {
|
|
566
|
+
const started = await rpc.request("thread/start", { cwd: cp.workDir });
|
|
567
|
+
threadId = started?.thread?.id ?? started?.threadId;
|
|
568
|
+
}
|
|
569
|
+
if (!threadId) {
|
|
570
|
+
throw new Error(`codex handshake: ${resumeId ? "thread/resume" : "thread/start"} returned no threadId`);
|
|
571
|
+
}
|
|
572
|
+
if (raceSettled) {
|
|
573
|
+
log.warn({ sessionId: cp.id, threadId }, "codex handshake resolved AFTER race already lost \u2014 discarding result");
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
cp.backendSessionId = threadId;
|
|
577
|
+
cp.handshakeComplete = true;
|
|
578
|
+
log.info({ sessionId: cp.id, threadId, queuedInputs: cp.pendingInputs.length }, "codex handshake: complete, flushing queued inputs");
|
|
579
|
+
cp.emit("session_started", { backendSessionId: threadId });
|
|
580
|
+
const queued = cp.pendingInputs.splice(0);
|
|
581
|
+
for (const input of queued) {
|
|
582
|
+
this.dispatchTurn(cp, input);
|
|
583
|
+
}
|
|
584
|
+
})();
|
|
585
|
+
handshakeAttempt.catch(() => {
|
|
586
|
+
});
|
|
587
|
+
let timeoutHandle;
|
|
588
|
+
let exitPollHandle;
|
|
589
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
590
|
+
timeoutHandle = setTimeout(() => {
|
|
591
|
+
reject(new Error(`codex handshake timeout after ${this.handshakeTimeoutMs}ms (process may have died)`));
|
|
592
|
+
}, this.handshakeTimeoutMs);
|
|
593
|
+
});
|
|
594
|
+
const deathPromise = new Promise((_, reject) => {
|
|
595
|
+
const poll = () => {
|
|
596
|
+
if (raceSettled)
|
|
597
|
+
return;
|
|
598
|
+
const exit = cp.getExitInfo();
|
|
599
|
+
if (exit) {
|
|
600
|
+
const stderr = cp.getStderr();
|
|
601
|
+
reject(new Error(`codex process died during handshake (code=${exit.code}, signal=${exit.signal}). stderr: ${stderr || "(empty)"}`));
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
exitPollHandle = setTimeout(poll, 100);
|
|
605
|
+
};
|
|
606
|
+
poll();
|
|
607
|
+
});
|
|
608
|
+
try {
|
|
609
|
+
await Promise.race([handshakeAttempt, timeoutPromise, deathPromise]);
|
|
610
|
+
} catch (err) {
|
|
611
|
+
log.error({ err, sessionId: cp.id }, "codex v2 handshake failed");
|
|
612
|
+
cp.emit("error", err);
|
|
613
|
+
} finally {
|
|
614
|
+
raceSettled = true;
|
|
615
|
+
if (timeoutHandle)
|
|
616
|
+
clearTimeout(timeoutHandle);
|
|
617
|
+
if (exitPollHandle)
|
|
618
|
+
clearTimeout(exitPollHandle);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
wireRpc(cp) {
|
|
622
|
+
const proc = cp.proc;
|
|
623
|
+
if (proc?.stdin && proc?.stdout) {
|
|
624
|
+
this.attachRpc(cp, proc.stdin, proc.stdout);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
handleServerRequest(cp, method, params) {
|
|
628
|
+
const parsed = parseApprovalRequest(method, params);
|
|
629
|
+
if (!parsed) {
|
|
630
|
+
log.warn({ method }, "codex server-request: unrecognized method");
|
|
631
|
+
return Promise.resolve({ decision: "denied" });
|
|
632
|
+
}
|
|
633
|
+
const { req, wireFamily } = parsed;
|
|
634
|
+
return new Promise((resolve) => {
|
|
635
|
+
const resolverKey = cp.allocResolverKey();
|
|
636
|
+
cp.pendingApprovals.set(req.callId, resolverKey);
|
|
637
|
+
cp.approvalMethods.set(req.callId, wireFamily);
|
|
638
|
+
cp.approvalResolvers.set(resolverKey, (reply) => {
|
|
639
|
+
resolve(reply);
|
|
640
|
+
});
|
|
641
|
+
cp.emit("permission_request", req);
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
handleNotification(cp, method, params) {
|
|
645
|
+
if (method === "thread/started") {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
if (method === "error") {
|
|
649
|
+
log.warn({ sessionId: cp.id, params }, "codex error notification");
|
|
650
|
+
}
|
|
651
|
+
if (method === "turn/completed" || method === "thread/turnComplete" || method === "turn/complete") {
|
|
652
|
+
log.info({ sessionId: cp.id, method }, "codex turn complete");
|
|
653
|
+
cp.emit("turn_complete", {
|
|
654
|
+
backendSessionId: cp.backendSessionId
|
|
655
|
+
});
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (method === "turn/started") {
|
|
659
|
+
log.info({ sessionId: cp.id }, "codex turn started");
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
const out = mapCodexFrame({ method, params });
|
|
663
|
+
this.logBackendOutput(cp.id, method, out);
|
|
664
|
+
cp.emit("output", out);
|
|
665
|
+
}
|
|
666
|
+
/**
|
|
667
|
+
* Selective notification logging. The previous catch-all `log.debug("codex
|
|
668
|
+
* notification")` fired for every streaming `item/agentMessage/delta` chunk
|
|
669
|
+
* (many per turn), drowning the actually useful events. Instead, log only
|
|
670
|
+
* structural events at INFO and surface unrecognised methods as WARN so the
|
|
671
|
+
* `{kind:"raw"}` degradation isn't silent.
|
|
672
|
+
*/
|
|
673
|
+
logBackendOutput(sessionId, method, out) {
|
|
674
|
+
if (method === "item/agentMessage/delta")
|
|
675
|
+
return;
|
|
676
|
+
if (method === "thread/tokenUsageUpdated")
|
|
677
|
+
return;
|
|
678
|
+
switch (out.kind) {
|
|
679
|
+
case "tool_use":
|
|
680
|
+
log.info({ sessionId, callId: out.callId, tool: out.toolName, input: preview(out.input, 120) }, "codex tool_use");
|
|
681
|
+
return;
|
|
682
|
+
case "tool_result":
|
|
683
|
+
log.info({ sessionId, callId: out.callId, isError: out.isError, output: preview(out.output, 120) }, "codex tool_result");
|
|
684
|
+
return;
|
|
685
|
+
case "assistant_text":
|
|
686
|
+
log.debug({ sessionId, len: out.text.length, text: preview(out.text, 120) }, "codex assistant_text");
|
|
687
|
+
return;
|
|
688
|
+
case "raw":
|
|
689
|
+
log.warn({ sessionId, method, payload: preview(out.payload, 200) }, "codex unhandled frame");
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
async stop(process2) {
|
|
694
|
+
await process2.kill();
|
|
695
|
+
}
|
|
696
|
+
send(process2, input) {
|
|
697
|
+
if (!(process2 instanceof CodexProcess)) {
|
|
698
|
+
log.warn({ sessionId: process2?.id, len: input.length }, "codex send: not a CodexProcess \u2014 input dropped");
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
if (!process2.rpc) {
|
|
702
|
+
log.warn({ sessionId: process2.id, len: input.length }, "codex send: no rpc \u2014 input dropped");
|
|
703
|
+
return;
|
|
704
|
+
}
|
|
705
|
+
if (!process2.backendSessionId) {
|
|
706
|
+
process2.pendingInputs.push(input);
|
|
707
|
+
log.info({ sessionId: process2.id, queueDepth: process2.pendingInputs.length, len: input.length }, "codex send: handshake pending, queued");
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
log.info({ sessionId: process2.id, threadId: process2.backendSessionId, len: input.length }, "codex send: dispatching turn/start");
|
|
711
|
+
this.dispatchTurn(process2, input);
|
|
712
|
+
}
|
|
713
|
+
dispatchTurn(process2, input) {
|
|
714
|
+
if (!process2.rpc || !process2.backendSessionId)
|
|
715
|
+
return;
|
|
716
|
+
void process2.rpc.request("turn/start", {
|
|
717
|
+
threadId: process2.backendSessionId,
|
|
718
|
+
input: [{ type: "text", text: input }]
|
|
719
|
+
}).then(() => {
|
|
720
|
+
log.info({ sessionId: process2.id, threadId: process2.backendSessionId }, "codex turn/start: accepted by server");
|
|
721
|
+
}).catch((err) => {
|
|
722
|
+
log.warn({ err, sessionId: process2.id }, "turn/start failed");
|
|
723
|
+
process2.emit("error", err);
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
onOutput(process2, handler) {
|
|
727
|
+
if (process2 instanceof CodexProcess)
|
|
728
|
+
process2.on("output", handler);
|
|
729
|
+
}
|
|
730
|
+
onPermissionRequest(process2, handler) {
|
|
731
|
+
if (process2 instanceof CodexProcess)
|
|
732
|
+
process2.on("permission_request", handler);
|
|
733
|
+
}
|
|
734
|
+
async respondToPermission(process2, decision) {
|
|
735
|
+
if (!(process2 instanceof CodexProcess))
|
|
736
|
+
return;
|
|
737
|
+
const key = process2.pendingApprovals.get(decision.callId);
|
|
738
|
+
if (key === void 0) {
|
|
739
|
+
log.warn({ callId: decision.callId }, "respondToPermission: no pending approval");
|
|
740
|
+
return;
|
|
741
|
+
}
|
|
742
|
+
const family = process2.approvalMethods.get(decision.callId) ?? "legacy";
|
|
743
|
+
const resolver = process2.approvalResolvers.get(key);
|
|
744
|
+
process2.pendingApprovals.delete(decision.callId);
|
|
745
|
+
process2.approvalMethods.delete(decision.callId);
|
|
746
|
+
process2.approvalResolvers.delete(key);
|
|
747
|
+
const reply = buildApprovalReply(family, decision.decision);
|
|
748
|
+
resolver?.(reply);
|
|
749
|
+
}
|
|
750
|
+
onTurnComplete(process2, handler) {
|
|
751
|
+
if (process2 instanceof CodexProcess)
|
|
752
|
+
process2.on("turn_complete", handler);
|
|
753
|
+
}
|
|
754
|
+
onSessionStarted(process2, handler) {
|
|
755
|
+
if (process2 instanceof CodexProcess)
|
|
756
|
+
process2.on("session_started", handler);
|
|
757
|
+
}
|
|
758
|
+
onError(process2, handler) {
|
|
759
|
+
if (process2 instanceof CodexProcess)
|
|
760
|
+
process2.on("error", handler);
|
|
761
|
+
}
|
|
762
|
+
onExit(process2, handler) {
|
|
763
|
+
if (process2 instanceof CodexProcess)
|
|
764
|
+
process2.on("exit", handler);
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
export {
|
|
768
|
+
CodexAdapter,
|
|
769
|
+
CodexProcess,
|
|
770
|
+
JsonRpcClient,
|
|
771
|
+
decisionToWire,
|
|
772
|
+
mapCodexFrame,
|
|
773
|
+
parseApprovalRequest
|
|
774
|
+
};
|
|
775
|
+
//# sourceMappingURL=dist-VLBO5CT3.js.map
|