svamp-cli 0.1.28 → 0.1.30
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/cli.mjs +114 -42
- package/dist/commands-B2xQb9u7.mjs +862 -0
- package/dist/commands-C5pW2VmI.mjs +862 -0
- package/dist/commands-CtO1WRt4.mjs +862 -0
- package/dist/commands-TZkNivgV.mjs +862 -0
- package/dist/index.mjs +8 -2
- package/dist/{package-CgBD49cA.mjs → package-CKOQ5lA7.mjs} +2 -2
- package/dist/{run-DjfPjgOb.mjs → run-BCSNMhiz.mjs} +1200 -132
- package/dist/{run-Cmostc0S.mjs → run-CtCTd6if.mjs} +1225 -169
- package/dist/{run-DYhBROuo.mjs → run-Cxdc5Zmw.mjs} +1253 -211
- package/dist/{run-CL-FS4Yc.mjs → run-dBWhjQRf.mjs} +1255 -211
- package/package.json +3 -6
- package/bin/svamp-agent.mjs +0 -34
- package/dist/agent-cli.mjs +0 -453
- package/dist/commands-1CYZC6Xh.mjs +0 -481
- package/dist/commands-B1DcpgLW.mjs +0 -481
- package/dist/commands-BGmdgMAC.mjs +0 -485
- package/dist/commands-BOeSil-P.mjs +0 -459
- package/dist/commands-BU4GZQuH.mjs +0 -481
- package/dist/commands-Ba66PxtQ.mjs +0 -481
- package/dist/commands-C0-xqIIc.mjs +0 -481
- package/dist/commands-C7Qy5n6d.mjs +0 -481
- package/dist/commands-CKpC8R9T.mjs +0 -481
- package/dist/commands-CNqOjR1y.mjs +0 -481
- package/dist/commands-CVKh1tWr.mjs +0 -485
- package/dist/commands-CYBblX73.mjs +0 -485
- package/dist/commands-CZBYmj16.mjs +0 -485
- package/dist/commands-CcWIvCA4.mjs +0 -481
- package/dist/commands-Cfwf-cQG.mjs +0 -481
- package/dist/commands-DCNO2m66.mjs +0 -471
- package/dist/commands-DRIFvhmC.mjs +0 -481
- package/dist/commands-DXmw2dzy.mjs +0 -481
- package/dist/commands-DkSvlKFF.mjs +0 -485
- package/dist/commands-DnDd4Sew.mjs +0 -481
- package/dist/commands-DnpnAFQW.mjs +0 -485
- package/dist/commands-Do-TVYFm.mjs +0 -481
- package/dist/commands-Kzm0_XNH.mjs +0 -481
- package/dist/commands-MQvNbIid.mjs +0 -481
- package/dist/commands-_uCC3U1U.mjs +0 -481
- package/dist/commands-y2WG29W9.mjs +0 -485
- package/dist/hyphaClient-DLkclazm.mjs +0 -39
- package/dist/package-B2FOzHaM.mjs +0 -57
- package/dist/package-Bk_PFVA0.mjs +0 -57
- package/dist/package-Bnij-ZtR.mjs +0 -57
- package/dist/package-BtRbHfjz.mjs +0 -57
- package/dist/package-C5B0twb8.mjs +0 -57
- package/dist/package-CC5d8_0L.mjs +0 -57
- package/dist/package-CCJ045H0.mjs +0 -60
- package/dist/package-CS219SXn.mjs +0 -57
- package/dist/package-Cd-9ktpd.mjs +0 -60
- package/dist/package-CvnNnsm7.mjs +0 -60
- package/dist/package-DpqWz9Cr.mjs +0 -60
- package/dist/package-JqEt5Ib4.mjs +0 -57
- package/dist/package-nzkXV1aM.mjs +0 -57
- package/dist/package-pNo6GC3a.mjs +0 -60
- package/dist/package-pZp14zKI.mjs +0 -57
- package/dist/run-4fyJcaRE.mjs +0 -3856
- package/dist/run-BI32lPRK.mjs +0 -3870
- package/dist/run-BQHneHfW.mjs +0 -3834
- package/dist/run-Bb4fyIWZ.mjs +0 -3812
- package/dist/run-BglwnB-A.mjs +0 -3889
- package/dist/run-BjVWuitO.mjs +0 -3919
- package/dist/run-BzUE-JUT.mjs +0 -3708
- package/dist/run-BzqS97Sx.mjs +0 -3666
- package/dist/run-C6snRxyh.mjs +0 -3826
- package/dist/run-C8CI8Ujj.mjs +0 -3693
- package/dist/run-CS1Z4GcM.mjs +0 -3786
- package/dist/run-CW26vPqj.mjs +0 -3919
- package/dist/run-CkTufc0D.mjs +0 -3875
- package/dist/run-Cp3kKdzm.mjs +0 -3865
- package/dist/run-D0bCTY72.mjs +0 -3816
- package/dist/run-DMW8ibIw.mjs +0 -3958
- package/dist/run-DO52unxE.mjs +0 -3950
- package/dist/run-Dptna3Je.mjs +0 -3867
- package/dist/run-DwK3dfHd.mjs +0 -3875
- package/dist/run-M_SMt96j.mjs +0 -3913
- package/dist/run-MlpxQUPN.mjs +0 -3869
- package/dist/run-PuTIelbv.mjs +0 -3706
- package/dist/run-h37iSCUB.mjs +0 -3934
- package/dist/run-lpV0oguG.mjs +0 -3897
|
@@ -8,8 +8,15 @@ import { randomUUID as randomUUID$1 } from 'crypto';
|
|
|
8
8
|
import { randomUUID } from 'node:crypto';
|
|
9
9
|
import { existsSync, readFileSync, mkdirSync, appendFileSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import { join } from 'node:path';
|
|
11
|
-
import { spawn } from 'node:child_process';
|
|
11
|
+
import { spawn, execSync, execFile } from 'node:child_process';
|
|
12
12
|
import { ndJsonStream, ClientSideConnection } from '@agentclientprotocol/sdk';
|
|
13
|
+
import { homedir, tmpdir } from 'node:os';
|
|
14
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
15
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
16
|
+
import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
17
|
+
import { z } from 'zod';
|
|
18
|
+
import { mkdir, access, mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
19
|
+
import { promisify } from 'node:util';
|
|
13
20
|
|
|
14
21
|
let connectToServerFn = null;
|
|
15
22
|
async function getConnectToServer() {
|
|
@@ -49,6 +56,37 @@ function getHyphaServerUrl() {
|
|
|
49
56
|
return process.env.HYPHA_SERVER_URL || "http://localhost:9527";
|
|
50
57
|
}
|
|
51
58
|
|
|
59
|
+
const ROLE_HIERARCHY = {
|
|
60
|
+
view: 0,
|
|
61
|
+
interact: 1,
|
|
62
|
+
admin: 2
|
|
63
|
+
};
|
|
64
|
+
const ISOLATION_PREFERENCE = ["srt", "bwrap", "docker", "podman"];
|
|
65
|
+
|
|
66
|
+
function authorizeRequest(context, sharing, requiredRole = "view") {
|
|
67
|
+
if (!context?.user) return;
|
|
68
|
+
const userEmail = context.user.email;
|
|
69
|
+
if (!sharing) return;
|
|
70
|
+
if (userEmail && userEmail === sharing.owner) return;
|
|
71
|
+
if (!sharing.enabled) {
|
|
72
|
+
throw new Error("Access denied: this resource is not shared");
|
|
73
|
+
}
|
|
74
|
+
if (!userEmail) {
|
|
75
|
+
throw new Error("Access denied: no email in user context");
|
|
76
|
+
}
|
|
77
|
+
const sharedUser = sharing.allowedUsers.find(
|
|
78
|
+
(u) => u.email.toLowerCase() === userEmail.toLowerCase()
|
|
79
|
+
);
|
|
80
|
+
if (!sharedUser) {
|
|
81
|
+
throw new Error("Access denied: you are not in the allowed users list");
|
|
82
|
+
}
|
|
83
|
+
if (ROLE_HIERARCHY[sharedUser.role] < ROLE_HIERARCHY[requiredRole]) {
|
|
84
|
+
throw new Error(
|
|
85
|
+
`Access denied: requires '${requiredRole}' role, you have '${sharedUser.role}'`
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
52
90
|
async function registerMachineService(server, machineId, metadata, daemonState, handlers) {
|
|
53
91
|
let currentMetadata = { ...metadata };
|
|
54
92
|
let currentDaemonState = { ...daemonState };
|
|
@@ -89,27 +127,38 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
89
127
|
id: "default",
|
|
90
128
|
name: `Svamp Machine (${metadata.displayName || machineId})`,
|
|
91
129
|
type: "svamp-machine",
|
|
92
|
-
config: { visibility: "public" },
|
|
130
|
+
config: { visibility: "public", require_context: true },
|
|
93
131
|
// Machine info
|
|
94
|
-
getMachineInfo: async () =>
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
132
|
+
getMachineInfo: async (context) => {
|
|
133
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
134
|
+
return {
|
|
135
|
+
machineId,
|
|
136
|
+
metadata: currentMetadata,
|
|
137
|
+
metadataVersion,
|
|
138
|
+
daemonState: currentDaemonState,
|
|
139
|
+
daemonStateVersion
|
|
140
|
+
};
|
|
141
|
+
},
|
|
101
142
|
// Heartbeat
|
|
102
|
-
heartbeat: async () => ({
|
|
143
|
+
heartbeat: async (context) => ({
|
|
103
144
|
time: Date.now(),
|
|
104
145
|
status: currentDaemonState.status,
|
|
105
146
|
machineId
|
|
106
147
|
}),
|
|
107
148
|
// List active sessions on this machine
|
|
108
|
-
listSessions: async () => {
|
|
149
|
+
listSessions: async (context) => {
|
|
150
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
109
151
|
return handlers.getTrackedSessions();
|
|
110
152
|
},
|
|
111
153
|
// Spawn a new session
|
|
112
|
-
spawnSession: async (options) => {
|
|
154
|
+
spawnSession: async (options, context) => {
|
|
155
|
+
authorizeRequest(context, currentMetadata.sharing, "interact");
|
|
156
|
+
if (options.sharing?.enabled && !options.sharing.owner && context?.user?.email) {
|
|
157
|
+
options = {
|
|
158
|
+
...options,
|
|
159
|
+
sharing: { ...options.sharing, owner: context.user.email }
|
|
160
|
+
};
|
|
161
|
+
}
|
|
113
162
|
const result = await handlers.spawnSession({
|
|
114
163
|
...options,
|
|
115
164
|
machineId
|
|
@@ -124,7 +173,8 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
124
173
|
return result;
|
|
125
174
|
},
|
|
126
175
|
// Stop a session
|
|
127
|
-
stopSession: async (sessionId) => {
|
|
176
|
+
stopSession: async (sessionId, context) => {
|
|
177
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
128
178
|
const result = handlers.stopSession(sessionId);
|
|
129
179
|
notifyListeners({
|
|
130
180
|
type: "session-stopped",
|
|
@@ -133,17 +183,27 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
133
183
|
});
|
|
134
184
|
return result;
|
|
135
185
|
},
|
|
186
|
+
// Restart agent process in a session (machine-level fallback)
|
|
187
|
+
restartSession: async (sessionId, context) => {
|
|
188
|
+
authorizeRequest(context, currentMetadata.sharing, "interact");
|
|
189
|
+
return await handlers.restartSession(sessionId);
|
|
190
|
+
},
|
|
136
191
|
// Stop the daemon
|
|
137
|
-
stopDaemon: async () => {
|
|
192
|
+
stopDaemon: async (context) => {
|
|
193
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
138
194
|
handlers.requestShutdown();
|
|
139
195
|
return { success: true };
|
|
140
196
|
},
|
|
141
197
|
// Metadata management (with optimistic concurrency)
|
|
142
|
-
getMetadata: async () =>
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
198
|
+
getMetadata: async (context) => {
|
|
199
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
200
|
+
return {
|
|
201
|
+
metadata: currentMetadata,
|
|
202
|
+
version: metadataVersion
|
|
203
|
+
};
|
|
204
|
+
},
|
|
205
|
+
updateMetadata: async (newMetadata, expectedVersion, context) => {
|
|
206
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
147
207
|
if (expectedVersion !== metadataVersion) {
|
|
148
208
|
return {
|
|
149
209
|
result: "version-mismatch",
|
|
@@ -165,11 +225,15 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
165
225
|
};
|
|
166
226
|
},
|
|
167
227
|
// Daemon state management
|
|
168
|
-
getDaemonState: async () =>
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
228
|
+
getDaemonState: async (context) => {
|
|
229
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
230
|
+
return {
|
|
231
|
+
daemonState: currentDaemonState,
|
|
232
|
+
version: daemonStateVersion
|
|
233
|
+
};
|
|
234
|
+
},
|
|
235
|
+
updateDaemonState: async (newState, expectedVersion, context) => {
|
|
236
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
173
237
|
if (expectedVersion !== daemonStateVersion) {
|
|
174
238
|
return {
|
|
175
239
|
result: "version-mismatch",
|
|
@@ -190,14 +254,35 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
190
254
|
daemonState: currentDaemonState
|
|
191
255
|
};
|
|
192
256
|
},
|
|
257
|
+
// ── Sharing Management ──
|
|
258
|
+
getSharing: async (context) => {
|
|
259
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
260
|
+
return { sharing: currentMetadata.sharing || null };
|
|
261
|
+
},
|
|
262
|
+
updateSharing: async (newSharing, context) => {
|
|
263
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
264
|
+
if (currentMetadata.sharing && context?.user?.email && context.user.email !== currentMetadata.sharing.owner) {
|
|
265
|
+
throw new Error("Only the machine owner can update sharing settings");
|
|
266
|
+
}
|
|
267
|
+
currentMetadata = { ...currentMetadata, sharing: newSharing };
|
|
268
|
+
metadataVersion++;
|
|
269
|
+
notifyListeners({
|
|
270
|
+
type: "update-machine",
|
|
271
|
+
machineId,
|
|
272
|
+
metadata: { value: currentMetadata, version: metadataVersion }
|
|
273
|
+
});
|
|
274
|
+
return { success: true, sharing: newSharing };
|
|
275
|
+
},
|
|
193
276
|
// Register a listener for real-time updates (app calls this with _rintf callback)
|
|
194
|
-
registerListener: async (callback) => {
|
|
277
|
+
registerListener: async (callback, context) => {
|
|
278
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
195
279
|
listeners.push(callback);
|
|
196
280
|
console.log(`[HYPHA MACHINE] Listener registered (total: ${listeners.length})`);
|
|
197
281
|
return { success: true, listenerId: listeners.length - 1 };
|
|
198
282
|
},
|
|
199
283
|
// Shell access
|
|
200
|
-
bash: async (command, cwd) => {
|
|
284
|
+
bash: async (command, cwd, context) => {
|
|
285
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
201
286
|
const { exec } = await import('child_process');
|
|
202
287
|
const { homedir } = await import('os');
|
|
203
288
|
return new Promise((resolve) => {
|
|
@@ -211,7 +296,8 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
211
296
|
});
|
|
212
297
|
},
|
|
213
298
|
// WISE voice — create ephemeral token for OpenAI Realtime API
|
|
214
|
-
wiseCreateEphemeralToken: async (params) => {
|
|
299
|
+
wiseCreateEphemeralToken: async (params, context) => {
|
|
300
|
+
authorizeRequest(context, currentMetadata.sharing, "interact");
|
|
215
301
|
const apiKey = params.apiKey || process.env.OPENAI_API_KEY;
|
|
216
302
|
if (!apiKey) {
|
|
217
303
|
return { success: false, error: "No OpenAI API key found. Set OPENAI_API_KEY or pass apiKey." };
|
|
@@ -344,6 +430,8 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
344
430
|
data.uuid = randomUUID();
|
|
345
431
|
}
|
|
346
432
|
wrappedContent = { role: "agent", content: { type: "output", data } };
|
|
433
|
+
} else if (role === "event") {
|
|
434
|
+
wrappedContent = { role: "agent", content: { type: "event", data: content } };
|
|
347
435
|
} else if (role === "session") {
|
|
348
436
|
wrappedContent = { role: "session", content: { type: "session", data: content } };
|
|
349
437
|
} else {
|
|
@@ -359,6 +447,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
359
447
|
updatedAt: Date.now()
|
|
360
448
|
};
|
|
361
449
|
messages.push(msg);
|
|
450
|
+
if (messages.length > 1e3) messages.splice(0, messages.length - 1e3);
|
|
362
451
|
if (options?.messagesDir) {
|
|
363
452
|
appendMessage(options.messagesDir, sessionId, msg);
|
|
364
453
|
}
|
|
@@ -374,9 +463,10 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
374
463
|
id: `svamp-session-${sessionId}`,
|
|
375
464
|
name: `Svamp Session ${sessionId.slice(0, 8)}`,
|
|
376
465
|
type: "svamp-session",
|
|
377
|
-
config: { visibility: "public" },
|
|
466
|
+
config: { visibility: "public", require_context: true },
|
|
378
467
|
// ── Messages ──
|
|
379
|
-
getMessages: async (afterSeq, limit) => {
|
|
468
|
+
getMessages: async (afterSeq, limit, context) => {
|
|
469
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
380
470
|
const after = afterSeq ?? 0;
|
|
381
471
|
const lim = Math.min(limit ?? 100, 500);
|
|
382
472
|
const filtered = messages.filter((m) => m.seq > after);
|
|
@@ -386,7 +476,8 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
386
476
|
hasMore: filtered.length > lim
|
|
387
477
|
};
|
|
388
478
|
},
|
|
389
|
-
sendMessage: async (content, localId, meta) => {
|
|
479
|
+
sendMessage: async (content, localId, meta, context) => {
|
|
480
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
390
481
|
if (localId) {
|
|
391
482
|
const existing = messages.find((m) => m.localId === localId);
|
|
392
483
|
if (existing) {
|
|
@@ -417,6 +508,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
417
508
|
updatedAt: Date.now()
|
|
418
509
|
};
|
|
419
510
|
messages.push(msg);
|
|
511
|
+
if (messages.length > 1e3) messages.splice(0, messages.length - 1e3);
|
|
420
512
|
if (options?.messagesDir) {
|
|
421
513
|
appendMessage(options.messagesDir, sessionId, msg);
|
|
422
514
|
}
|
|
@@ -429,11 +521,15 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
429
521
|
return { id: msg.id, seq: msg.seq, localId: msg.localId };
|
|
430
522
|
},
|
|
431
523
|
// ── Metadata ──
|
|
432
|
-
getMetadata: async () =>
|
|
433
|
-
metadata,
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
524
|
+
getMetadata: async (context) => {
|
|
525
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
526
|
+
return {
|
|
527
|
+
metadata,
|
|
528
|
+
version: metadataVersion
|
|
529
|
+
};
|
|
530
|
+
},
|
|
531
|
+
updateMetadata: async (newMetadata, expectedVersion, context) => {
|
|
532
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
437
533
|
if (expectedVersion !== void 0 && expectedVersion !== metadataVersion) {
|
|
438
534
|
return {
|
|
439
535
|
result: "version-mismatch",
|
|
@@ -455,11 +551,15 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
455
551
|
};
|
|
456
552
|
},
|
|
457
553
|
// ── Agent State ──
|
|
458
|
-
getAgentState: async () =>
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
554
|
+
getAgentState: async (context) => {
|
|
555
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
556
|
+
return {
|
|
557
|
+
agentState,
|
|
558
|
+
version: agentStateVersion
|
|
559
|
+
};
|
|
560
|
+
},
|
|
561
|
+
updateAgentState: async (newState, expectedVersion, context) => {
|
|
562
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
463
563
|
if (expectedVersion !== void 0 && expectedVersion !== agentStateVersion) {
|
|
464
564
|
return {
|
|
465
565
|
result: "version-mismatch",
|
|
@@ -481,27 +581,32 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
481
581
|
};
|
|
482
582
|
},
|
|
483
583
|
// ── Session Control RPCs ──
|
|
484
|
-
abort: async () => {
|
|
584
|
+
abort: async (context) => {
|
|
585
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
485
586
|
callbacks.onAbort();
|
|
486
587
|
return { success: true };
|
|
487
588
|
},
|
|
488
|
-
permissionResponse: async (params) => {
|
|
589
|
+
permissionResponse: async (params, context) => {
|
|
590
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
489
591
|
callbacks.onPermissionResponse(params);
|
|
490
592
|
return { success: true };
|
|
491
593
|
},
|
|
492
|
-
switchMode: async (mode) => {
|
|
594
|
+
switchMode: async (mode, context) => {
|
|
595
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
493
596
|
callbacks.onSwitchMode(mode);
|
|
494
597
|
return { success: true };
|
|
495
598
|
},
|
|
496
|
-
restartClaude: async () => {
|
|
599
|
+
restartClaude: async (context) => {
|
|
600
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
497
601
|
return await callbacks.onRestartClaude();
|
|
498
602
|
},
|
|
499
|
-
killSession: async () => {
|
|
603
|
+
killSession: async (context) => {
|
|
604
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
500
605
|
callbacks.onKillSession();
|
|
501
606
|
return { success: true };
|
|
502
607
|
},
|
|
503
608
|
// ── Activity ──
|
|
504
|
-
keepAlive: async (thinking, mode) => {
|
|
609
|
+
keepAlive: async (thinking, mode, context) => {
|
|
505
610
|
lastActivity = { active: true, thinking: thinking || false, mode: mode || "remote", time: Date.now() };
|
|
506
611
|
notifyListeners({
|
|
507
612
|
type: "activity",
|
|
@@ -509,7 +614,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
509
614
|
...lastActivity
|
|
510
615
|
});
|
|
511
616
|
},
|
|
512
|
-
sessionEnd: async () => {
|
|
617
|
+
sessionEnd: async (context) => {
|
|
513
618
|
lastActivity = { active: false, thinking: false, mode: "remote", time: Date.now() };
|
|
514
619
|
notifyListeners({
|
|
515
620
|
type: "activity",
|
|
@@ -518,28 +623,34 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
518
623
|
});
|
|
519
624
|
},
|
|
520
625
|
// ── Activity State Query ──
|
|
521
|
-
getActivityState: async () => {
|
|
626
|
+
getActivityState: async (context) => {
|
|
627
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
522
628
|
return { ...lastActivity, sessionId };
|
|
523
629
|
},
|
|
524
|
-
// ── File Operations (optional) ──
|
|
525
|
-
readFile: async (path) => {
|
|
630
|
+
// ── File Operations (optional, admin-only) ──
|
|
631
|
+
readFile: async (path, context) => {
|
|
632
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
526
633
|
if (!callbacks.onReadFile) throw new Error("readFile not supported");
|
|
527
634
|
return await callbacks.onReadFile(path);
|
|
528
635
|
},
|
|
529
|
-
writeFile: async (path, content) => {
|
|
636
|
+
writeFile: async (path, content, context) => {
|
|
637
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
530
638
|
if (!callbacks.onWriteFile) throw new Error("writeFile not supported");
|
|
531
639
|
await callbacks.onWriteFile(path, content);
|
|
532
640
|
return { success: true };
|
|
533
641
|
},
|
|
534
|
-
listDirectory: async (path) => {
|
|
642
|
+
listDirectory: async (path, context) => {
|
|
643
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
535
644
|
if (!callbacks.onListDirectory) throw new Error("listDirectory not supported");
|
|
536
645
|
return await callbacks.onListDirectory(path);
|
|
537
646
|
},
|
|
538
|
-
bash: async (command, cwd) => {
|
|
647
|
+
bash: async (command, cwd, context) => {
|
|
648
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
539
649
|
if (!callbacks.onBash) throw new Error("bash not supported");
|
|
540
650
|
return await callbacks.onBash(command, cwd);
|
|
541
651
|
},
|
|
542
|
-
ripgrep: async (args, cwd) => {
|
|
652
|
+
ripgrep: async (args, cwd, context) => {
|
|
653
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
543
654
|
if (!callbacks.onRipgrep) throw new Error("ripgrep not supported");
|
|
544
655
|
try {
|
|
545
656
|
const stdout = await callbacks.onRipgrep(args, cwd);
|
|
@@ -548,15 +659,41 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
548
659
|
return { success: false, stdout: "", stderr: err.message || "", exitCode: 1, error: err.message };
|
|
549
660
|
}
|
|
550
661
|
},
|
|
551
|
-
getDirectoryTree: async (path, maxDepth) => {
|
|
662
|
+
getDirectoryTree: async (path, maxDepth, context) => {
|
|
663
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
552
664
|
if (!callbacks.onGetDirectoryTree) throw new Error("getDirectoryTree not supported");
|
|
553
665
|
return await callbacks.onGetDirectoryTree(path, maxDepth ?? 3);
|
|
554
666
|
},
|
|
667
|
+
// ── Sharing Management ──
|
|
668
|
+
getSharing: async (context) => {
|
|
669
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
670
|
+
return { sharing: metadata.sharing || null };
|
|
671
|
+
},
|
|
672
|
+
updateSharing: async (newSharing, context) => {
|
|
673
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
674
|
+
if (metadata.sharing && context?.user?.email && context.user.email !== metadata.sharing.owner) {
|
|
675
|
+
throw new Error("Only the session owner can update sharing settings");
|
|
676
|
+
}
|
|
677
|
+
if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
|
|
678
|
+
newSharing = { ...newSharing, owner: context.user.email };
|
|
679
|
+
}
|
|
680
|
+
metadata = { ...metadata, sharing: newSharing };
|
|
681
|
+
metadataVersion++;
|
|
682
|
+
notifyListeners({
|
|
683
|
+
type: "update-session",
|
|
684
|
+
sessionId,
|
|
685
|
+
metadata: { value: metadata, version: metadataVersion }
|
|
686
|
+
});
|
|
687
|
+
return { success: true, sharing: newSharing };
|
|
688
|
+
},
|
|
555
689
|
// ── Listener Registration ──
|
|
556
|
-
registerListener: async (callback) => {
|
|
690
|
+
registerListener: async (callback, context) => {
|
|
691
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
557
692
|
listeners.push(callback);
|
|
558
|
-
|
|
559
|
-
|
|
693
|
+
const replayMessages = messages.slice(-50);
|
|
694
|
+
console.log(`[HYPHA SESSION ${sessionId}] Listener registered (total: ${listeners.length}), replaying ${replayMessages.length} of ${messages.length} messages`);
|
|
695
|
+
for (const msg of replayMessages) {
|
|
696
|
+
if (listeners.indexOf(callback) < 0) break;
|
|
560
697
|
try {
|
|
561
698
|
const result = callback.onUpdate({
|
|
562
699
|
type: "new-message",
|
|
@@ -564,14 +701,23 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
564
701
|
message: msg
|
|
565
702
|
});
|
|
566
703
|
if (result && typeof result.catch === "function") {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
})
|
|
704
|
+
try {
|
|
705
|
+
await result;
|
|
706
|
+
} catch (err) {
|
|
707
|
+
console.error(`[HYPHA SESSION ${sessionId}] Replay listener error, removing:`, err?.message ?? err);
|
|
708
|
+
removeListener(callback, "replay error");
|
|
709
|
+
return { success: false, error: "Listener removed during replay" };
|
|
710
|
+
}
|
|
570
711
|
}
|
|
571
712
|
} catch (err) {
|
|
572
|
-
console.error(`[HYPHA SESSION ${sessionId}] Replay listener error:`, err);
|
|
713
|
+
console.error(`[HYPHA SESSION ${sessionId}] Replay listener error, removing:`, err?.message ?? err);
|
|
714
|
+
removeListener(callback, "replay error");
|
|
715
|
+
return { success: false, error: "Listener removed during replay" };
|
|
573
716
|
}
|
|
574
717
|
}
|
|
718
|
+
if (listeners.indexOf(callback) < 0) {
|
|
719
|
+
return { success: false, error: "Listener was removed during replay" };
|
|
720
|
+
}
|
|
575
721
|
try {
|
|
576
722
|
const result = callback.onUpdate({
|
|
577
723
|
type: "update-session",
|
|
@@ -851,6 +997,7 @@ class SessionArtifactSync {
|
|
|
851
997
|
lastSyncAt: Date.now(),
|
|
852
998
|
...sessionData || {}
|
|
853
999
|
},
|
|
1000
|
+
stage: true,
|
|
854
1001
|
_rkwargs: true
|
|
855
1002
|
});
|
|
856
1003
|
} catch {
|
|
@@ -867,6 +1014,7 @@ class SessionArtifactSync {
|
|
|
867
1014
|
lastSyncAt: Date.now(),
|
|
868
1015
|
...sessionData || {}
|
|
869
1016
|
},
|
|
1017
|
+
stage: true,
|
|
870
1018
|
_rkwargs: true
|
|
871
1019
|
});
|
|
872
1020
|
artifactId = artifact.id;
|
|
@@ -1056,6 +1204,106 @@ var DefaultTransport$1 = /*#__PURE__*/Object.freeze({
|
|
|
1056
1204
|
DefaultTransport: DefaultTransport
|
|
1057
1205
|
});
|
|
1058
1206
|
|
|
1207
|
+
const SVAMP_TOOLS_DIR$1 = join(homedir(), ".svamp", "tools");
|
|
1208
|
+
const SVAMP_TOOLS_BIN$1 = join(SVAMP_TOOLS_DIR$1, "node_modules", ".bin");
|
|
1209
|
+
const SVAMP_TOOLS_RG_BIN$1 = join(SVAMP_TOOLS_DIR$1, "node_modules", "@vscode", "ripgrep", "bin");
|
|
1210
|
+
function wrapWithIsolation(originalCommand, originalArgs, config) {
|
|
1211
|
+
switch (config.method) {
|
|
1212
|
+
case "srt":
|
|
1213
|
+
return wrapWithSrt(originalCommand, originalArgs, config);
|
|
1214
|
+
case "bwrap":
|
|
1215
|
+
return wrapWithBwrap(originalCommand, originalArgs, config);
|
|
1216
|
+
case "docker":
|
|
1217
|
+
return wrapWithContainer("docker", originalCommand, originalArgs, config);
|
|
1218
|
+
case "podman":
|
|
1219
|
+
return wrapWithContainer("podman", originalCommand, originalArgs, config);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
function wrapWithSrt(command, args, config) {
|
|
1223
|
+
const settings = {
|
|
1224
|
+
filesystem: {
|
|
1225
|
+
denyRead: config.srtConfig?.filesystem?.denyRead ?? [],
|
|
1226
|
+
allowWrite: config.srtConfig?.filesystem?.allowWrite ?? [config.workspacePath],
|
|
1227
|
+
denyWrite: config.srtConfig?.filesystem?.denyWrite ?? []
|
|
1228
|
+
},
|
|
1229
|
+
network: {
|
|
1230
|
+
allowedDomains: config.srtConfig?.network?.allowedDomains ?? [],
|
|
1231
|
+
deniedDomains: config.srtConfig?.network?.deniedDomains ?? []
|
|
1232
|
+
}
|
|
1233
|
+
};
|
|
1234
|
+
const settingsPath = join(tmpdir(), `srt-settings-${process.pid}-${Date.now()}.json`);
|
|
1235
|
+
writeFileSync(settingsPath, JSON.stringify(settings));
|
|
1236
|
+
const pathParts = [SVAMP_TOOLS_BIN$1, SVAMP_TOOLS_RG_BIN$1];
|
|
1237
|
+
if (process.env.PATH) pathParts.push(process.env.PATH);
|
|
1238
|
+
return {
|
|
1239
|
+
command: config.binaryPath,
|
|
1240
|
+
args: ["--settings", settingsPath, command, ...args],
|
|
1241
|
+
env: { PATH: pathParts.join(":") },
|
|
1242
|
+
cleanupFiles: [settingsPath]
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
1245
|
+
function wrapWithBwrap(command, args, config) {
|
|
1246
|
+
const bwrapArgs = [
|
|
1247
|
+
// Mount root filesystem read-only
|
|
1248
|
+
"--ro-bind",
|
|
1249
|
+
"/",
|
|
1250
|
+
"/",
|
|
1251
|
+
// Mount workspace read-write
|
|
1252
|
+
"--bind",
|
|
1253
|
+
config.workspacePath,
|
|
1254
|
+
config.workspacePath,
|
|
1255
|
+
// Mount /tmp read-write (many tools need it)
|
|
1256
|
+
"--bind",
|
|
1257
|
+
"/tmp",
|
|
1258
|
+
"/tmp",
|
|
1259
|
+
// Mount /dev read-write
|
|
1260
|
+
"--dev",
|
|
1261
|
+
"/dev",
|
|
1262
|
+
// Mount /proc
|
|
1263
|
+
"--proc",
|
|
1264
|
+
"/proc",
|
|
1265
|
+
// Unshare network — no network access inside sandbox
|
|
1266
|
+
"--unshare-net",
|
|
1267
|
+
// Unshare PID namespace
|
|
1268
|
+
"--unshare-pid",
|
|
1269
|
+
// Die when parent dies
|
|
1270
|
+
"--die-with-parent",
|
|
1271
|
+
// The actual command
|
|
1272
|
+
"--",
|
|
1273
|
+
command,
|
|
1274
|
+
...args
|
|
1275
|
+
];
|
|
1276
|
+
return {
|
|
1277
|
+
command: config.binaryPath,
|
|
1278
|
+
args: bwrapArgs
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1281
|
+
function wrapWithContainer(runtime, command, args, config) {
|
|
1282
|
+
const image = config.containerConfig?.image || "node:lts-slim";
|
|
1283
|
+
const networkMode = config.containerConfig?.networkMode || "none";
|
|
1284
|
+
const containerArgs = [
|
|
1285
|
+
"run",
|
|
1286
|
+
"--rm",
|
|
1287
|
+
"-i",
|
|
1288
|
+
// interactive (for stdin/stdout piping)
|
|
1289
|
+
`--network=${networkMode}`,
|
|
1290
|
+
"-v",
|
|
1291
|
+
`${config.workspacePath}:${config.workspacePath}`,
|
|
1292
|
+
"-w",
|
|
1293
|
+
config.workspacePath
|
|
1294
|
+
];
|
|
1295
|
+
if (config.containerConfig?.extraMounts) {
|
|
1296
|
+
for (const mount of config.containerConfig.extraMounts) {
|
|
1297
|
+
containerArgs.push("-v", mount);
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
containerArgs.push(image, command, ...args);
|
|
1301
|
+
return {
|
|
1302
|
+
command: config.binaryPath,
|
|
1303
|
+
args: containerArgs
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1059
1307
|
const DEFAULT_IDLE_TIMEOUT_MS = 500;
|
|
1060
1308
|
const DEFAULT_TOOL_CALL_TIMEOUT_MS = 12e4;
|
|
1061
1309
|
function parseArgsFromContent(content) {
|
|
@@ -1374,21 +1622,41 @@ class AcpBackend {
|
|
|
1374
1622
|
let startupStatusErrorEmitted = false;
|
|
1375
1623
|
try {
|
|
1376
1624
|
const args = this.options.args || [];
|
|
1625
|
+
let spawnCommand = this.options.command;
|
|
1626
|
+
let spawnArgs = args;
|
|
1627
|
+
let isoEnv = {};
|
|
1628
|
+
let isoCleanupFiles = [];
|
|
1629
|
+
if (this.options.isolationConfig) {
|
|
1630
|
+
const wrapped = wrapWithIsolation(spawnCommand, spawnArgs, this.options.isolationConfig);
|
|
1631
|
+
spawnCommand = wrapped.command;
|
|
1632
|
+
spawnArgs = wrapped.args;
|
|
1633
|
+
if (wrapped.env) isoEnv = wrapped.env;
|
|
1634
|
+
if (wrapped.cleanupFiles) isoCleanupFiles = wrapped.cleanupFiles;
|
|
1635
|
+
this.log(`[ACP] Isolation: ${this.options.isolationConfig.method}`);
|
|
1636
|
+
}
|
|
1637
|
+
const spawnEnv = { ...process.env, ...this.options.env, ...isoEnv };
|
|
1377
1638
|
if (process.platform === "win32") {
|
|
1378
|
-
const fullCommand = [
|
|
1639
|
+
const fullCommand = [spawnCommand, ...spawnArgs].join(" ");
|
|
1379
1640
|
this.process = spawn("cmd.exe", ["/c", fullCommand], {
|
|
1380
1641
|
cwd: this.options.cwd,
|
|
1381
|
-
env:
|
|
1642
|
+
env: spawnEnv,
|
|
1382
1643
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1383
1644
|
windowsHide: true
|
|
1384
1645
|
});
|
|
1385
1646
|
} else {
|
|
1386
|
-
this.process = spawn(
|
|
1647
|
+
this.process = spawn(spawnCommand, spawnArgs, {
|
|
1387
1648
|
cwd: this.options.cwd,
|
|
1388
|
-
env:
|
|
1649
|
+
env: spawnEnv,
|
|
1389
1650
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1390
1651
|
});
|
|
1391
1652
|
}
|
|
1653
|
+
if (isoCleanupFiles.length > 0) {
|
|
1654
|
+
this.process.on("exit", async () => {
|
|
1655
|
+
const { rm } = await import('node:fs/promises');
|
|
1656
|
+
for (const f of isoCleanupFiles) rm(f, { force: true }).catch(() => {
|
|
1657
|
+
});
|
|
1658
|
+
});
|
|
1659
|
+
}
|
|
1392
1660
|
if (!this.process.stdin || !this.process.stdout || !this.process.stderr) {
|
|
1393
1661
|
throw new Error("Failed to create stdio pipes");
|
|
1394
1662
|
}
|
|
@@ -1907,6 +2175,9 @@ const KNOWN_ACP_AGENTS = {
|
|
|
1907
2175
|
gemini: { command: "gemini", args: ["--experimental-acp"] },
|
|
1908
2176
|
opencode: { command: "opencode", args: ["acp"] }
|
|
1909
2177
|
};
|
|
2178
|
+
const KNOWN_MCP_AGENTS = {
|
|
2179
|
+
codex: { command: "codex", args: ["mcp-server"] }
|
|
2180
|
+
};
|
|
1910
2181
|
function resolveAcpAgentConfig(cliArgs) {
|
|
1911
2182
|
if (cliArgs.length === 0) {
|
|
1912
2183
|
throw new Error("Usage: svamp agent <agent-name> or svamp agent -- <command> [args]");
|
|
@@ -1923,13 +2194,21 @@ function resolveAcpAgentConfig(cliArgs) {
|
|
|
1923
2194
|
};
|
|
1924
2195
|
}
|
|
1925
2196
|
const agentName = cliArgs[0];
|
|
1926
|
-
const
|
|
1927
|
-
if (
|
|
2197
|
+
const knownAcp = KNOWN_ACP_AGENTS[agentName];
|
|
2198
|
+
if (knownAcp) {
|
|
1928
2199
|
const passthroughArgs = cliArgs.slice(1).filter((arg) => !(agentName === "opencode" && arg === "--acp"));
|
|
1929
2200
|
return {
|
|
1930
2201
|
agentName,
|
|
1931
|
-
command:
|
|
1932
|
-
args: [...
|
|
2202
|
+
command: knownAcp.command,
|
|
2203
|
+
args: [...knownAcp.args, ...passthroughArgs]
|
|
2204
|
+
};
|
|
2205
|
+
}
|
|
2206
|
+
const knownMcp = KNOWN_MCP_AGENTS[agentName];
|
|
2207
|
+
if (knownMcp) {
|
|
2208
|
+
return {
|
|
2209
|
+
agentName,
|
|
2210
|
+
command: knownMcp.command,
|
|
2211
|
+
args: [...knownMcp.args, ...cliArgs.slice(1)]
|
|
1933
2212
|
};
|
|
1934
2213
|
}
|
|
1935
2214
|
return {
|
|
@@ -1942,6 +2221,7 @@ function resolveAcpAgentConfig(cliArgs) {
|
|
|
1942
2221
|
var acpAgentConfig = /*#__PURE__*/Object.freeze({
|
|
1943
2222
|
__proto__: null,
|
|
1944
2223
|
KNOWN_ACP_AGENTS: KNOWN_ACP_AGENTS,
|
|
2224
|
+
KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS,
|
|
1945
2225
|
resolveAcpAgentConfig: resolveAcpAgentConfig
|
|
1946
2226
|
});
|
|
1947
2227
|
|
|
@@ -1989,15 +2269,15 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
1989
2269
|
setMetadata((m) => ({ ...m, lifecycleState: "running" }));
|
|
1990
2270
|
} else if (msg.status === "error") {
|
|
1991
2271
|
flushText();
|
|
1992
|
-
sessionService.pushMessage(
|
|
1993
|
-
type: "
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
sessionService.
|
|
2272
|
+
sessionService.pushMessage(
|
|
2273
|
+
{ type: "message", message: `Agent process exited unexpectedly: ${msg.detail || "Unknown error"}` },
|
|
2274
|
+
"event"
|
|
2275
|
+
);
|
|
2276
|
+
sessionService.sendSessionEnd();
|
|
1997
2277
|
setMetadata((m) => ({ ...m, lifecycleState: "error" }));
|
|
1998
2278
|
} else if (msg.status === "stopped") {
|
|
1999
2279
|
flushText();
|
|
2000
|
-
sessionService.
|
|
2280
|
+
sessionService.sendSessionEnd();
|
|
2001
2281
|
setMetadata((m) => ({ ...m, lifecycleState: "stopped" }));
|
|
2002
2282
|
}
|
|
2003
2283
|
break;
|
|
@@ -2118,6 +2398,426 @@ class HyphaPermissionHandler {
|
|
|
2118
2398
|
}
|
|
2119
2399
|
}
|
|
2120
2400
|
|
|
2401
|
+
const DEFAULT_TIMEOUT = 14 * 24 * 60 * 60 * 1e3;
|
|
2402
|
+
function getCodexMcpCommand() {
|
|
2403
|
+
try {
|
|
2404
|
+
const version = execSync("codex --version", { encoding: "utf8" }).trim();
|
|
2405
|
+
const match = version.match(/codex-cli\s+(\d+\.\d+\.\d+(?:-alpha\.\d+)?)/);
|
|
2406
|
+
if (!match) return "mcp-server";
|
|
2407
|
+
const versionStr = match[1];
|
|
2408
|
+
const [major, minor, patch] = versionStr.split(/[-.]/).map(Number);
|
|
2409
|
+
if (major > 0 || minor > 43) return "mcp-server";
|
|
2410
|
+
if (minor === 43 && patch === 0) {
|
|
2411
|
+
if (versionStr.includes("-alpha.")) {
|
|
2412
|
+
const alphaNum = parseInt(versionStr.split("-alpha.")[1]);
|
|
2413
|
+
return alphaNum >= 5 ? "mcp-server" : "mcp";
|
|
2414
|
+
}
|
|
2415
|
+
return "mcp-server";
|
|
2416
|
+
}
|
|
2417
|
+
return "mcp";
|
|
2418
|
+
} catch {
|
|
2419
|
+
return null;
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2422
|
+
class CodexMcpBackend {
|
|
2423
|
+
listeners = [];
|
|
2424
|
+
client;
|
|
2425
|
+
transport = null;
|
|
2426
|
+
disposed = false;
|
|
2427
|
+
codexSessionId = null;
|
|
2428
|
+
conversationId = null;
|
|
2429
|
+
svampSessionId = null;
|
|
2430
|
+
log;
|
|
2431
|
+
options;
|
|
2432
|
+
connected = false;
|
|
2433
|
+
// Pending elicitation approvals
|
|
2434
|
+
pendingApprovals = /* @__PURE__ */ new Map();
|
|
2435
|
+
// Temp files from isolation wrapping (cleaned up on disconnect)
|
|
2436
|
+
_isolationCleanupFiles = [];
|
|
2437
|
+
constructor(options) {
|
|
2438
|
+
this.options = options;
|
|
2439
|
+
this.log = options.log || (() => {
|
|
2440
|
+
});
|
|
2441
|
+
this.client = new Client(
|
|
2442
|
+
{ name: "svamp-codex-client", version: "1.0.0" },
|
|
2443
|
+
{ capabilities: { elicitation: {} } }
|
|
2444
|
+
);
|
|
2445
|
+
this.client.setNotificationHandler(z.object({
|
|
2446
|
+
method: z.literal("codex/event"),
|
|
2447
|
+
params: z.object({ msg: z.any() })
|
|
2448
|
+
}).passthrough(), (data) => {
|
|
2449
|
+
const msg = data.params.msg;
|
|
2450
|
+
this.updateIdentifiersFromEvent(msg);
|
|
2451
|
+
this.handleCodexEvent(msg);
|
|
2452
|
+
});
|
|
2453
|
+
}
|
|
2454
|
+
// ── AgentBackend interface ──────────────────────────────────────────
|
|
2455
|
+
onMessage(handler) {
|
|
2456
|
+
this.listeners.push(handler);
|
|
2457
|
+
}
|
|
2458
|
+
offMessage(handler) {
|
|
2459
|
+
const idx = this.listeners.indexOf(handler);
|
|
2460
|
+
if (idx !== -1) this.listeners.splice(idx, 1);
|
|
2461
|
+
}
|
|
2462
|
+
async startSession(initialPrompt) {
|
|
2463
|
+
const sessionId = randomUUID();
|
|
2464
|
+
this.svampSessionId = sessionId;
|
|
2465
|
+
this.emit({ type: "status", status: "starting" });
|
|
2466
|
+
await this.connect();
|
|
2467
|
+
this.emit({ type: "status", status: "idle" });
|
|
2468
|
+
if (initialPrompt) {
|
|
2469
|
+
this.sendPrompt(sessionId, initialPrompt).catch((err) => {
|
|
2470
|
+
this.log(`[Codex] Error sending initial prompt: ${err.message}`);
|
|
2471
|
+
this.emit({ type: "status", status: "error", detail: err.message });
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
return { sessionId };
|
|
2475
|
+
}
|
|
2476
|
+
async sendPrompt(sessionId, prompt) {
|
|
2477
|
+
if (!this.connected) throw new Error("Codex not connected");
|
|
2478
|
+
this.emit({ type: "status", status: "running" });
|
|
2479
|
+
try {
|
|
2480
|
+
let response;
|
|
2481
|
+
if (this.codexSessionId) {
|
|
2482
|
+
response = await this.continueSession(prompt);
|
|
2483
|
+
} else {
|
|
2484
|
+
const config = {
|
|
2485
|
+
prompt,
|
|
2486
|
+
cwd: this.options.cwd,
|
|
2487
|
+
...this.options.model ? { model: this.options.model } : {}
|
|
2488
|
+
};
|
|
2489
|
+
response = await this.startCodexSession(config);
|
|
2490
|
+
}
|
|
2491
|
+
if (response?.content && Array.isArray(response.content)) {
|
|
2492
|
+
for (const block of response.content) {
|
|
2493
|
+
if (block.type === "text" && block.text) {
|
|
2494
|
+
this.emit({ type: "model-output", fullText: block.text });
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
2498
|
+
} catch (err) {
|
|
2499
|
+
this.log(`[Codex] Error in sendPrompt: ${err.message}`);
|
|
2500
|
+
this.emit({ type: "status", status: "error", detail: err.message });
|
|
2501
|
+
throw err;
|
|
2502
|
+
} finally {
|
|
2503
|
+
this.emit({ type: "status", status: "idle" });
|
|
2504
|
+
}
|
|
2505
|
+
}
|
|
2506
|
+
async cancel(_sessionId) {
|
|
2507
|
+
this.log("[Codex] Cancel requested");
|
|
2508
|
+
this.emit({ type: "status", status: "idle" });
|
|
2509
|
+
}
|
|
2510
|
+
async respondToPermission(requestId, approved) {
|
|
2511
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
2512
|
+
if (pending) {
|
|
2513
|
+
this.pendingApprovals.delete(requestId);
|
|
2514
|
+
pending.resolve({
|
|
2515
|
+
action: approved ? "accept" : "decline",
|
|
2516
|
+
content: { approval: approved ? "approve" : "deny" }
|
|
2517
|
+
});
|
|
2518
|
+
this.emit({ type: "permission-response", id: requestId, approved });
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
async dispose() {
|
|
2522
|
+
if (this.disposed) return;
|
|
2523
|
+
this.disposed = true;
|
|
2524
|
+
this.log("[Codex] Disposing backend");
|
|
2525
|
+
for (const [, pending] of this.pendingApprovals) {
|
|
2526
|
+
pending.resolve({ action: "decline" });
|
|
2527
|
+
}
|
|
2528
|
+
this.pendingApprovals.clear();
|
|
2529
|
+
await this.disconnect();
|
|
2530
|
+
this.listeners = [];
|
|
2531
|
+
}
|
|
2532
|
+
/** Get the transport's child process PID for tracking */
|
|
2533
|
+
get pid() {
|
|
2534
|
+
return this.transport?.pid ?? null;
|
|
2535
|
+
}
|
|
2536
|
+
/**
|
|
2537
|
+
* Return a process-like object for TrackedSession.childProcess.
|
|
2538
|
+
* We expose a minimal { kill } object that closes the transport.
|
|
2539
|
+
*/
|
|
2540
|
+
getProcess() {
|
|
2541
|
+
if (!this.transport) return null;
|
|
2542
|
+
const pid = this.pid;
|
|
2543
|
+
return {
|
|
2544
|
+
kill: (signal) => {
|
|
2545
|
+
if (pid) {
|
|
2546
|
+
try {
|
|
2547
|
+
process.kill(pid, signal || "SIGTERM");
|
|
2548
|
+
} catch {
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
// ── MCP connection ─────────────────────────────────────────────────
|
|
2555
|
+
async connect() {
|
|
2556
|
+
if (this.connected) return;
|
|
2557
|
+
const mcpCommand = getCodexMcpCommand();
|
|
2558
|
+
if (mcpCommand === null) {
|
|
2559
|
+
throw new Error(
|
|
2560
|
+
"Codex CLI not found or not executable.\n\nTo install codex:\n npm install -g @openai/codex\n"
|
|
2561
|
+
);
|
|
2562
|
+
}
|
|
2563
|
+
this.log(`[Codex] Connecting via: codex ${mcpCommand}`);
|
|
2564
|
+
const env = {};
|
|
2565
|
+
for (const key of Object.keys(process.env)) {
|
|
2566
|
+
const value = process.env[key];
|
|
2567
|
+
if (typeof value === "string") env[key] = value;
|
|
2568
|
+
}
|
|
2569
|
+
if (this.options.env) {
|
|
2570
|
+
Object.assign(env, this.options.env);
|
|
2571
|
+
}
|
|
2572
|
+
const rolloutFilter = "codex_core::rollout::list=off";
|
|
2573
|
+
const existingRustLog = env.RUST_LOG?.trim();
|
|
2574
|
+
if (!existingRustLog) {
|
|
2575
|
+
env.RUST_LOG = rolloutFilter;
|
|
2576
|
+
} else if (!existingRustLog.includes("codex_core::rollout::list=")) {
|
|
2577
|
+
env.RUST_LOG = `${existingRustLog},${rolloutFilter}`;
|
|
2578
|
+
}
|
|
2579
|
+
let transportCommand = "codex";
|
|
2580
|
+
let transportArgs = [mcpCommand];
|
|
2581
|
+
if (this.options.isolationConfig) {
|
|
2582
|
+
const wrapped = wrapWithIsolation(transportCommand, transportArgs, this.options.isolationConfig);
|
|
2583
|
+
transportCommand = wrapped.command;
|
|
2584
|
+
transportArgs = wrapped.args;
|
|
2585
|
+
if (wrapped.env) Object.assign(env, wrapped.env);
|
|
2586
|
+
if (wrapped.cleanupFiles) {
|
|
2587
|
+
this._isolationCleanupFiles = wrapped.cleanupFiles;
|
|
2588
|
+
}
|
|
2589
|
+
this.log(`[Codex] Isolation: ${this.options.isolationConfig.method}`);
|
|
2590
|
+
}
|
|
2591
|
+
this.transport = new StdioClientTransport({
|
|
2592
|
+
command: transportCommand,
|
|
2593
|
+
args: transportArgs,
|
|
2594
|
+
env
|
|
2595
|
+
});
|
|
2596
|
+
this.registerPermissionHandlers();
|
|
2597
|
+
try {
|
|
2598
|
+
await this.client.connect(this.transport);
|
|
2599
|
+
this.connected = true;
|
|
2600
|
+
this.log("[Codex] MCP connection established");
|
|
2601
|
+
} catch (err) {
|
|
2602
|
+
this.transport = null;
|
|
2603
|
+
throw err;
|
|
2604
|
+
}
|
|
2605
|
+
}
|
|
2606
|
+
async disconnect() {
|
|
2607
|
+
if (!this.connected) return;
|
|
2608
|
+
const pid = this.pid;
|
|
2609
|
+
this.log(`[Codex] Disconnecting (pid=${pid ?? "none"})`);
|
|
2610
|
+
try {
|
|
2611
|
+
await this.client.close();
|
|
2612
|
+
} catch {
|
|
2613
|
+
try {
|
|
2614
|
+
await this.transport?.close?.();
|
|
2615
|
+
} catch {
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2618
|
+
if (pid) {
|
|
2619
|
+
try {
|
|
2620
|
+
process.kill(pid, 0);
|
|
2621
|
+
try {
|
|
2622
|
+
process.kill(pid, "SIGKILL");
|
|
2623
|
+
} catch {
|
|
2624
|
+
}
|
|
2625
|
+
} catch {
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
this.transport = null;
|
|
2629
|
+
this.connected = false;
|
|
2630
|
+
if (this._isolationCleanupFiles.length > 0) {
|
|
2631
|
+
const { rm } = await import('node:fs/promises');
|
|
2632
|
+
for (const f of this._isolationCleanupFiles) rm(f, { force: true }).catch(() => {
|
|
2633
|
+
});
|
|
2634
|
+
this._isolationCleanupFiles = [];
|
|
2635
|
+
}
|
|
2636
|
+
}
|
|
2637
|
+
// ── Permission handling ────────────────────────────────────────────
|
|
2638
|
+
registerPermissionHandlers() {
|
|
2639
|
+
this.client.setRequestHandler(
|
|
2640
|
+
ElicitRequestSchema,
|
|
2641
|
+
async (request) => {
|
|
2642
|
+
const params = request.params;
|
|
2643
|
+
const callId = params.codex_call_id || randomUUID();
|
|
2644
|
+
this.log(`[Codex] Elicitation request: ${params.message}`);
|
|
2645
|
+
this.emit({
|
|
2646
|
+
type: "permission-request",
|
|
2647
|
+
id: callId,
|
|
2648
|
+
reason: params.codex_command ? `Execute: ${Array.isArray(params.codex_command) ? params.codex_command.join(" ") : params.codex_command}` : params.message || "Codex requires approval",
|
|
2649
|
+
payload: {
|
|
2650
|
+
toolName: "CodexBash",
|
|
2651
|
+
input: {
|
|
2652
|
+
command: params.codex_command,
|
|
2653
|
+
cwd: params.codex_cwd
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
});
|
|
2657
|
+
return new Promise((resolve) => {
|
|
2658
|
+
this.pendingApprovals.set(callId, { resolve });
|
|
2659
|
+
setTimeout(() => {
|
|
2660
|
+
if (this.pendingApprovals.has(callId)) {
|
|
2661
|
+
this.pendingApprovals.delete(callId);
|
|
2662
|
+
resolve({ action: "decline" });
|
|
2663
|
+
}
|
|
2664
|
+
}, 5 * 60 * 1e3);
|
|
2665
|
+
});
|
|
2666
|
+
}
|
|
2667
|
+
);
|
|
2668
|
+
}
|
|
2669
|
+
// ── Codex MCP tools ────────────────────────────────────────────────
|
|
2670
|
+
async startCodexSession(config) {
|
|
2671
|
+
const response = await this.client.callTool({
|
|
2672
|
+
name: "codex",
|
|
2673
|
+
arguments: config
|
|
2674
|
+
}, void 0, { timeout: DEFAULT_TIMEOUT });
|
|
2675
|
+
this.extractIdentifiers(response);
|
|
2676
|
+
return response;
|
|
2677
|
+
}
|
|
2678
|
+
async continueSession(prompt) {
|
|
2679
|
+
if (!this.conversationId) {
|
|
2680
|
+
this.conversationId = this.codexSessionId;
|
|
2681
|
+
}
|
|
2682
|
+
const response = await this.client.callTool({
|
|
2683
|
+
name: "codex-reply",
|
|
2684
|
+
arguments: {
|
|
2685
|
+
sessionId: this.codexSessionId,
|
|
2686
|
+
conversationId: this.conversationId,
|
|
2687
|
+
prompt
|
|
2688
|
+
}
|
|
2689
|
+
}, void 0, { timeout: DEFAULT_TIMEOUT });
|
|
2690
|
+
this.extractIdentifiers(response);
|
|
2691
|
+
return response;
|
|
2692
|
+
}
|
|
2693
|
+
// ── Event handling ─────────────────────────────────────────────────
|
|
2694
|
+
handleCodexEvent(event) {
|
|
2695
|
+
if (!event || typeof event !== "object") return;
|
|
2696
|
+
const eventType = event.type;
|
|
2697
|
+
this.log(`[Codex] Event: ${eventType}`);
|
|
2698
|
+
switch (eventType) {
|
|
2699
|
+
case "task_started":
|
|
2700
|
+
this.emit({ type: "status", status: "running" });
|
|
2701
|
+
break;
|
|
2702
|
+
case "task_complete":
|
|
2703
|
+
case "turn_aborted":
|
|
2704
|
+
this.emit({ type: "status", status: "idle" });
|
|
2705
|
+
break;
|
|
2706
|
+
case "agent_message": {
|
|
2707
|
+
const content = event.content;
|
|
2708
|
+
if (Array.isArray(content)) {
|
|
2709
|
+
for (const block of content) {
|
|
2710
|
+
if (block.type === "output_text" && block.text) {
|
|
2711
|
+
this.emit({ type: "model-output", fullText: block.text });
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
}
|
|
2715
|
+
break;
|
|
2716
|
+
}
|
|
2717
|
+
case "agent_reasoning_delta": {
|
|
2718
|
+
const text = event.delta?.text || event.text;
|
|
2719
|
+
if (text) {
|
|
2720
|
+
this.emit({ type: "event", name: "thinking", payload: { text } });
|
|
2721
|
+
}
|
|
2722
|
+
break;
|
|
2723
|
+
}
|
|
2724
|
+
case "agent_reasoning": {
|
|
2725
|
+
const text = event.text || (Array.isArray(event.content) ? event.content.map((c) => c.text).join("") : "");
|
|
2726
|
+
if (text) {
|
|
2727
|
+
this.emit({ type: "event", name: "thinking", payload: { text } });
|
|
2728
|
+
}
|
|
2729
|
+
break;
|
|
2730
|
+
}
|
|
2731
|
+
case "exec_command_begin": {
|
|
2732
|
+
const callId = event.call_id || event.callId || randomUUID();
|
|
2733
|
+
const command = Array.isArray(event.command) ? event.command.join(" ") : String(event.command || "");
|
|
2734
|
+
this.emit({
|
|
2735
|
+
type: "tool-call",
|
|
2736
|
+
toolName: "CodexBash",
|
|
2737
|
+
callId,
|
|
2738
|
+
args: { command, cwd: event.cwd }
|
|
2739
|
+
});
|
|
2740
|
+
break;
|
|
2741
|
+
}
|
|
2742
|
+
case "exec_command_end": {
|
|
2743
|
+
const callId = event.call_id || event.callId || "";
|
|
2744
|
+
this.emit({
|
|
2745
|
+
type: "tool-result",
|
|
2746
|
+
toolName: "CodexBash",
|
|
2747
|
+
callId,
|
|
2748
|
+
result: {
|
|
2749
|
+
exitCode: event.exit_code ?? event.exitCode,
|
|
2750
|
+
stdout: event.stdout || "",
|
|
2751
|
+
stderr: event.stderr || ""
|
|
2752
|
+
}
|
|
2753
|
+
});
|
|
2754
|
+
break;
|
|
2755
|
+
}
|
|
2756
|
+
case "patch_apply_begin": {
|
|
2757
|
+
const callId = event.call_id || event.callId || randomUUID();
|
|
2758
|
+
this.emit({
|
|
2759
|
+
type: "tool-call",
|
|
2760
|
+
toolName: "CodexPatch",
|
|
2761
|
+
callId,
|
|
2762
|
+
args: { filePath: event.file_path || event.filePath, patch: event.patch }
|
|
2763
|
+
});
|
|
2764
|
+
break;
|
|
2765
|
+
}
|
|
2766
|
+
case "patch_apply_end": {
|
|
2767
|
+
const callId = event.call_id || event.callId || "";
|
|
2768
|
+
this.emit({
|
|
2769
|
+
type: "tool-result",
|
|
2770
|
+
toolName: "CodexPatch",
|
|
2771
|
+
callId,
|
|
2772
|
+
result: {
|
|
2773
|
+
filePath: event.file_path || event.filePath,
|
|
2774
|
+
applied: event.applied,
|
|
2775
|
+
error: event.error
|
|
2776
|
+
}
|
|
2777
|
+
});
|
|
2778
|
+
break;
|
|
2779
|
+
}
|
|
2780
|
+
default:
|
|
2781
|
+
this.log(`[Codex] Unhandled event: ${eventType}`);
|
|
2782
|
+
break;
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
// ── Identifier extraction ──────────────────────────────────────────
|
|
2786
|
+
updateIdentifiersFromEvent(event) {
|
|
2787
|
+
if (!event || typeof event !== "object") return;
|
|
2788
|
+
const candidates = [event];
|
|
2789
|
+
if (event.data && typeof event.data === "object") candidates.push(event.data);
|
|
2790
|
+
for (const c of candidates) {
|
|
2791
|
+
const sid = c.session_id ?? c.sessionId;
|
|
2792
|
+
if (sid) this.codexSessionId = sid;
|
|
2793
|
+
const cid = c.conversation_id ?? c.conversationId;
|
|
2794
|
+
if (cid) this.conversationId = cid;
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
extractIdentifiers(response) {
|
|
2798
|
+
const meta = response?.meta || {};
|
|
2799
|
+
this.codexSessionId = meta.sessionId || response?.sessionId || this.codexSessionId;
|
|
2800
|
+
this.conversationId = meta.conversationId || response?.conversationId || this.conversationId;
|
|
2801
|
+
const content = response?.content;
|
|
2802
|
+
if (Array.isArray(content)) {
|
|
2803
|
+
for (const item of content) {
|
|
2804
|
+
if (!this.codexSessionId && item?.sessionId) this.codexSessionId = item.sessionId;
|
|
2805
|
+
if (!this.conversationId && item?.conversationId) this.conversationId = item.conversationId;
|
|
2806
|
+
}
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
2810
|
+
emit(msg) {
|
|
2811
|
+
if (this.disposed) return;
|
|
2812
|
+
for (const h of this.listeners) {
|
|
2813
|
+
try {
|
|
2814
|
+
h(msg);
|
|
2815
|
+
} catch {
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
|
|
2121
2821
|
const GEMINI_TIMEOUTS = {
|
|
2122
2822
|
init: 12e4,
|
|
2123
2823
|
toolCall: 12e4,
|
|
@@ -2253,6 +2953,264 @@ var GeminiTransport$1 = /*#__PURE__*/Object.freeze({
|
|
|
2253
2953
|
GeminiTransport: GeminiTransport
|
|
2254
2954
|
});
|
|
2255
2955
|
|
|
2956
|
+
const execFileAsync = promisify(execFile);
|
|
2957
|
+
const SVAMP_TOOLS_DIR = join(homedir(), ".svamp", "tools");
|
|
2958
|
+
const SVAMP_TOOLS_BIN = join(SVAMP_TOOLS_DIR, "node_modules", ".bin");
|
|
2959
|
+
const SVAMP_TOOLS_RG_BIN = join(SVAMP_TOOLS_DIR, "node_modules", "@vscode", "ripgrep", "bin");
|
|
2960
|
+
function getToolsPath() {
|
|
2961
|
+
const parts = [SVAMP_TOOLS_BIN, SVAMP_TOOLS_RG_BIN];
|
|
2962
|
+
if (process.env.PATH) parts.push(process.env.PATH);
|
|
2963
|
+
return parts.join(":");
|
|
2964
|
+
}
|
|
2965
|
+
async function checkCommand(command, versionArgs, extraEnv) {
|
|
2966
|
+
const envWithPath = extraEnv ? { ...process.env, ...extraEnv } : void 0;
|
|
2967
|
+
try {
|
|
2968
|
+
const { stdout } = await execFileAsync(command, versionArgs, {
|
|
2969
|
+
timeout: 5e3,
|
|
2970
|
+
env: envWithPath
|
|
2971
|
+
});
|
|
2972
|
+
const version = stdout.trim().split("\n")[0];
|
|
2973
|
+
return { found: true, version, path: command };
|
|
2974
|
+
} catch {
|
|
2975
|
+
}
|
|
2976
|
+
const localPath = join(SVAMP_TOOLS_BIN, command);
|
|
2977
|
+
try {
|
|
2978
|
+
const { stdout } = await execFileAsync(localPath, versionArgs, {
|
|
2979
|
+
timeout: 5e3,
|
|
2980
|
+
env: envWithPath
|
|
2981
|
+
});
|
|
2982
|
+
const version = stdout.trim().split("\n")[0];
|
|
2983
|
+
return { found: true, version, path: localPath };
|
|
2984
|
+
} catch {
|
|
2985
|
+
}
|
|
2986
|
+
return { found: false };
|
|
2987
|
+
}
|
|
2988
|
+
async function installSrt() {
|
|
2989
|
+
const platform = process.platform;
|
|
2990
|
+
if (platform !== "darwin" && platform !== "linux") {
|
|
2991
|
+
return false;
|
|
2992
|
+
}
|
|
2993
|
+
console.log("[isolation] srt not found. Installing @anthropic-ai/sandbox-runtime...");
|
|
2994
|
+
try {
|
|
2995
|
+
await mkdir(SVAMP_TOOLS_DIR, { recursive: true });
|
|
2996
|
+
await execFileAsync("npm", [
|
|
2997
|
+
"install",
|
|
2998
|
+
"--prefix",
|
|
2999
|
+
SVAMP_TOOLS_DIR,
|
|
3000
|
+
"@anthropic-ai/sandbox-runtime@latest",
|
|
3001
|
+
"@vscode/ripgrep"
|
|
3002
|
+
], { timeout: 12e4 });
|
|
3003
|
+
const srtPath = join(SVAMP_TOOLS_BIN, "srt");
|
|
3004
|
+
try {
|
|
3005
|
+
await access(srtPath);
|
|
3006
|
+
console.log(`[isolation] srt installed successfully at ${srtPath}`);
|
|
3007
|
+
return true;
|
|
3008
|
+
} catch {
|
|
3009
|
+
console.warn("[isolation] npm install completed but srt binary not found");
|
|
3010
|
+
return false;
|
|
3011
|
+
}
|
|
3012
|
+
} catch (e) {
|
|
3013
|
+
console.warn(`[isolation] Failed to install srt: ${e.message}`);
|
|
3014
|
+
return false;
|
|
3015
|
+
}
|
|
3016
|
+
}
|
|
3017
|
+
async function parseIsolationTestOutput(stdout, probeFile) {
|
|
3018
|
+
const output = stdout.trim();
|
|
3019
|
+
const wMatch = output.match(/W=(\d+)/);
|
|
3020
|
+
const pMatch = output.match(/P=(\d+)/);
|
|
3021
|
+
if (!wMatch || !pMatch) {
|
|
3022
|
+
return { passed: false, error: `unexpected test output: ${output}` };
|
|
3023
|
+
}
|
|
3024
|
+
const workspaceWriteOk = wMatch[1] === "0";
|
|
3025
|
+
const probeExitCode = parseInt(pMatch[1], 10);
|
|
3026
|
+
if (!workspaceWriteOk) {
|
|
3027
|
+
return { passed: false, error: "sandbox blocked writes to allowed workspace path" };
|
|
3028
|
+
}
|
|
3029
|
+
if (probeExitCode === 0) {
|
|
3030
|
+
try {
|
|
3031
|
+
await access(probeFile);
|
|
3032
|
+
return {
|
|
3033
|
+
passed: false,
|
|
3034
|
+
error: "writes outside workspace were NOT blocked \u2014 file leaked to host filesystem"
|
|
3035
|
+
};
|
|
3036
|
+
} catch {
|
|
3037
|
+
return { passed: true };
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
return { passed: true };
|
|
3041
|
+
}
|
|
3042
|
+
async function verifySrtIsolation(binaryPath) {
|
|
3043
|
+
const testBase = "/tmp";
|
|
3044
|
+
const workDir = await mkdtemp(join(testBase, "svamp-iso-work-"));
|
|
3045
|
+
const probeDir = await mkdtemp(join(testBase, "svamp-iso-probe-"));
|
|
3046
|
+
const probeFile = join(probeDir, "leak-test");
|
|
3047
|
+
const settingsFile = join(workDir, "srt-settings.json");
|
|
3048
|
+
try {
|
|
3049
|
+
await writeFile(settingsFile, JSON.stringify({
|
|
3050
|
+
filesystem: {
|
|
3051
|
+
denyRead: [],
|
|
3052
|
+
allowWrite: [workDir],
|
|
3053
|
+
denyWrite: []
|
|
3054
|
+
},
|
|
3055
|
+
network: {
|
|
3056
|
+
allowedDomains: [],
|
|
3057
|
+
deniedDomains: []
|
|
3058
|
+
}
|
|
3059
|
+
}));
|
|
3060
|
+
const testScript = [
|
|
3061
|
+
`echo ok > "${workDir}/test" 2>/dev/null; W=$?`,
|
|
3062
|
+
`echo leak > "${probeFile}" 2>/dev/null; P=$?`,
|
|
3063
|
+
`echo "W=$W P=$P"`
|
|
3064
|
+
].join("; ");
|
|
3065
|
+
const { stdout } = await execFileAsync(binaryPath, [
|
|
3066
|
+
"--settings",
|
|
3067
|
+
settingsFile,
|
|
3068
|
+
"sh",
|
|
3069
|
+
"-c",
|
|
3070
|
+
testScript
|
|
3071
|
+
], {
|
|
3072
|
+
timeout: 15e3,
|
|
3073
|
+
env: { ...process.env, PATH: getToolsPath() }
|
|
3074
|
+
});
|
|
3075
|
+
return parseIsolationTestOutput(stdout, probeFile);
|
|
3076
|
+
} catch (e) {
|
|
3077
|
+
return { passed: false, error: e.message };
|
|
3078
|
+
} finally {
|
|
3079
|
+
await rm(workDir, { recursive: true, force: true }).catch(() => {
|
|
3080
|
+
});
|
|
3081
|
+
await rm(probeDir, { recursive: true, force: true }).catch(() => {
|
|
3082
|
+
});
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
async function verifyBwrapIsolation(binaryPath) {
|
|
3086
|
+
const testBase = "/tmp";
|
|
3087
|
+
const workDir = await mkdtemp(join(testBase, "svamp-iso-work-"));
|
|
3088
|
+
const probeDir = await mkdtemp(join(testBase, "svamp-iso-probe-"));
|
|
3089
|
+
const probeFile = join(probeDir, "leak-test");
|
|
3090
|
+
try {
|
|
3091
|
+
const testScript = [
|
|
3092
|
+
`echo ok > "${workDir}/test" 2>/dev/null; W=$?`,
|
|
3093
|
+
`echo leak > "${probeFile}" 2>/dev/null; P=$?`,
|
|
3094
|
+
`echo "W=$W P=$P"`
|
|
3095
|
+
].join("; ");
|
|
3096
|
+
const { stdout } = await execFileAsync(binaryPath, [
|
|
3097
|
+
"--ro-bind",
|
|
3098
|
+
"/",
|
|
3099
|
+
"/",
|
|
3100
|
+
"--bind",
|
|
3101
|
+
workDir,
|
|
3102
|
+
workDir,
|
|
3103
|
+
"--dev",
|
|
3104
|
+
"/dev",
|
|
3105
|
+
"--proc",
|
|
3106
|
+
"/proc",
|
|
3107
|
+
"--unshare-pid",
|
|
3108
|
+
"--die-with-parent",
|
|
3109
|
+
"--",
|
|
3110
|
+
"sh",
|
|
3111
|
+
"-c",
|
|
3112
|
+
testScript
|
|
3113
|
+
], { timeout: 15e3 });
|
|
3114
|
+
return parseIsolationTestOutput(stdout, probeFile);
|
|
3115
|
+
} catch (e) {
|
|
3116
|
+
return { passed: false, error: e.message };
|
|
3117
|
+
} finally {
|
|
3118
|
+
await rm(workDir, { recursive: true, force: true }).catch(() => {
|
|
3119
|
+
});
|
|
3120
|
+
await rm(probeDir, { recursive: true, force: true }).catch(() => {
|
|
3121
|
+
});
|
|
3122
|
+
}
|
|
3123
|
+
}
|
|
3124
|
+
async function verifyContainerIsolation(method, binaryPath) {
|
|
3125
|
+
try {
|
|
3126
|
+
await execFileAsync(binaryPath, ["info"], { timeout: 15e3 });
|
|
3127
|
+
} catch (e) {
|
|
3128
|
+
return { passed: false, error: `${method} daemon not accessible: ${e.message}` };
|
|
3129
|
+
}
|
|
3130
|
+
try {
|
|
3131
|
+
const { stdout } = await execFileAsync(
|
|
3132
|
+
binaryPath,
|
|
3133
|
+
["run", "--rm", "--network=none", "alpine", "echo", "isolation-ok"],
|
|
3134
|
+
{ timeout: 3e4 }
|
|
3135
|
+
);
|
|
3136
|
+
if (!stdout.includes("isolation-ok")) {
|
|
3137
|
+
return { passed: false, error: `container test returned unexpected output: ${stdout.trim()}` };
|
|
3138
|
+
}
|
|
3139
|
+
return { passed: true };
|
|
3140
|
+
} catch (e) {
|
|
3141
|
+
if (e.message?.includes("pull access denied") || e.message?.includes("not found")) {
|
|
3142
|
+
return { passed: true };
|
|
3143
|
+
}
|
|
3144
|
+
return { passed: false, error: `container test failed: ${e.message}` };
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
async function verifyIsolation(method, binaryPath) {
|
|
3148
|
+
switch (method) {
|
|
3149
|
+
case "srt":
|
|
3150
|
+
return verifySrtIsolation(binaryPath);
|
|
3151
|
+
case "bwrap":
|
|
3152
|
+
return verifyBwrapIsolation(binaryPath);
|
|
3153
|
+
case "docker":
|
|
3154
|
+
case "podman":
|
|
3155
|
+
return verifyContainerIsolation(method, binaryPath);
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
async function detectIsolationCapabilities() {
|
|
3159
|
+
const srtEnv = { PATH: getToolsPath() };
|
|
3160
|
+
const checks = {
|
|
3161
|
+
srt: checkCommand("srt", ["--version"], srtEnv),
|
|
3162
|
+
bwrap: checkCommand("bwrap", ["--version"]),
|
|
3163
|
+
docker: checkCommand("docker", ["--version"]),
|
|
3164
|
+
podman: checkCommand("podman", ["--version"])
|
|
3165
|
+
};
|
|
3166
|
+
const checkResults = await Promise.all(
|
|
3167
|
+
Object.entries(checks).map(async ([method, promise]) => {
|
|
3168
|
+
const detail = await promise;
|
|
3169
|
+
return [method, detail];
|
|
3170
|
+
})
|
|
3171
|
+
);
|
|
3172
|
+
const details = Object.fromEntries(checkResults);
|
|
3173
|
+
if (!details.srt.found) {
|
|
3174
|
+
const installed = await installSrt();
|
|
3175
|
+
if (installed) {
|
|
3176
|
+
details.srt = await checkCommand("srt", ["--version"], srtEnv);
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
const foundMethods = ISOLATION_PREFERENCE.filter((m) => details[m].found);
|
|
3180
|
+
if (foundMethods.length > 0) {
|
|
3181
|
+
console.log(`[isolation] Found runtimes: ${foundMethods.join(", ")}. Verifying isolation...`);
|
|
3182
|
+
await Promise.all(
|
|
3183
|
+
foundMethods.map(async (method) => {
|
|
3184
|
+
const binaryPath = details[method].path || method;
|
|
3185
|
+
const result = await verifyIsolation(method, binaryPath);
|
|
3186
|
+
details[method].verified = result.passed;
|
|
3187
|
+
if (!result.passed) {
|
|
3188
|
+
details[method].verificationError = result.error;
|
|
3189
|
+
console.warn(
|
|
3190
|
+
`[isolation] WARNING: ${method} found but verification FAILED: ${result.error}. This runtime will NOT be used for isolation.`
|
|
3191
|
+
);
|
|
3192
|
+
} else {
|
|
3193
|
+
console.log(`[isolation] ${method}: verified OK`);
|
|
3194
|
+
}
|
|
3195
|
+
})
|
|
3196
|
+
);
|
|
3197
|
+
}
|
|
3198
|
+
const available = ISOLATION_PREFERENCE.filter(
|
|
3199
|
+
(m) => details[m].found && details[m].verified === true
|
|
3200
|
+
);
|
|
3201
|
+
const preferred = available.length > 0 ? available[0] : null;
|
|
3202
|
+
if (available.length === 0 && foundMethods.length > 0) {
|
|
3203
|
+
console.warn(
|
|
3204
|
+
`[isolation] No isolation runtime passed verification (found: ${foundMethods.join(", ")}). Session sharing will be DISABLED.`
|
|
3205
|
+
);
|
|
3206
|
+
} else if (available.length === 0) {
|
|
3207
|
+
console.log("[isolation] No isolation runtimes found. Session sharing will be unavailable.");
|
|
3208
|
+
} else {
|
|
3209
|
+
console.log(`[isolation] Preferred isolation method: ${preferred}`);
|
|
3210
|
+
}
|
|
3211
|
+
return { available, preferred, details };
|
|
3212
|
+
}
|
|
3213
|
+
|
|
2256
3214
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
2257
3215
|
const __dirname$1 = dirname(__filename$1);
|
|
2258
3216
|
function loadDotEnv() {
|
|
@@ -2423,22 +3381,24 @@ function deletePersistedSession(sessionId) {
|
|
|
2423
3381
|
function loadPersistedSessions() {
|
|
2424
3382
|
const sessions = [];
|
|
2425
3383
|
const index = loadSessionIndex();
|
|
3384
|
+
let indexChanged = false;
|
|
2426
3385
|
for (const [sessionId, entry] of Object.entries(index)) {
|
|
2427
3386
|
const filePath = getSessionFilePath(entry.directory, sessionId);
|
|
2428
3387
|
try {
|
|
2429
3388
|
const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
|
|
2430
3389
|
if (data.sessionId && data.directory) {
|
|
2431
|
-
if (data.stopped) {
|
|
2432
|
-
delete index[sessionId];
|
|
2433
|
-
} else {
|
|
3390
|
+
if (!data.stopped) {
|
|
2434
3391
|
sessions.push(data);
|
|
2435
3392
|
}
|
|
2436
3393
|
}
|
|
2437
3394
|
} catch {
|
|
2438
3395
|
delete index[sessionId];
|
|
3396
|
+
indexChanged = true;
|
|
2439
3397
|
}
|
|
2440
3398
|
}
|
|
2441
|
-
|
|
3399
|
+
if (indexChanged) {
|
|
3400
|
+
saveSessionIndex(index);
|
|
3401
|
+
}
|
|
2442
3402
|
return sessions;
|
|
2443
3403
|
}
|
|
2444
3404
|
function ensureHomeDir() {
|
|
@@ -2530,6 +3490,7 @@ async function startDaemon() {
|
|
|
2530
3490
|
let isReconnecting = false;
|
|
2531
3491
|
process.on("SIGINT", () => requestShutdown("os-signal"));
|
|
2532
3492
|
process.on("SIGTERM", () => requestShutdown("os-signal"));
|
|
3493
|
+
process.on("SIGUSR1", () => requestShutdown("os-signal-cleanup"));
|
|
2533
3494
|
process.on("uncaughtException", (error) => {
|
|
2534
3495
|
if (shutdownRequested) return;
|
|
2535
3496
|
logger.error("Uncaught exception:", error);
|
|
@@ -2681,8 +3642,8 @@ async function startDaemon() {
|
|
|
2681
3642
|
}
|
|
2682
3643
|
const sessionId = options.sessionId || randomUUID$1();
|
|
2683
3644
|
const agentName = options.agent || agentConfig.agent_type || "claude";
|
|
2684
|
-
if (agentName !== "claude" && KNOWN_ACP_AGENTS[agentName]) {
|
|
2685
|
-
return await
|
|
3645
|
+
if (agentName !== "claude" && (KNOWN_ACP_AGENTS[agentName] || KNOWN_MCP_AGENTS[agentName])) {
|
|
3646
|
+
return await spawnAgentSession(sessionId, directory, agentName, options, resumeSessionId);
|
|
2686
3647
|
}
|
|
2687
3648
|
try {
|
|
2688
3649
|
let parseBashPermission2 = function(permission) {
|
|
@@ -2731,8 +3692,19 @@ async function startDaemon() {
|
|
|
2731
3692
|
proc.kill(signal);
|
|
2732
3693
|
}
|
|
2733
3694
|
});
|
|
3695
|
+
}, buildIsolationConfig2 = function(dir) {
|
|
3696
|
+
if (!sessionMetadata.sharing?.enabled) return null;
|
|
3697
|
+
const method = isolationCapabilities.preferred;
|
|
3698
|
+
if (!method) return null;
|
|
3699
|
+
const detail = isolationCapabilities.details[method];
|
|
3700
|
+
if (!detail.found || detail.verified === false) return null;
|
|
3701
|
+
return {
|
|
3702
|
+
method,
|
|
3703
|
+
binaryPath: detail.path || method,
|
|
3704
|
+
workspacePath: dir
|
|
3705
|
+
};
|
|
2734
3706
|
};
|
|
2735
|
-
var parseBashPermission = parseBashPermission2, shouldAutoAllow = shouldAutoAllow2, killAndWaitForExit = killAndWaitForExit2;
|
|
3707
|
+
var parseBashPermission = parseBashPermission2, shouldAutoAllow = shouldAutoAllow2, killAndWaitForExit = killAndWaitForExit2, buildIsolationConfig = buildIsolationConfig2;
|
|
2736
3708
|
let sessionMetadata = {
|
|
2737
3709
|
path: directory,
|
|
2738
3710
|
host: os.hostname(),
|
|
@@ -2744,7 +3716,8 @@ async function startDaemon() {
|
|
|
2744
3716
|
svampToolsDir: join$1(__dirname$1, "..", "tools"),
|
|
2745
3717
|
startedFromDaemon: true,
|
|
2746
3718
|
startedBy: "daemon",
|
|
2747
|
-
lifecycleState: resumeSessionId ? "idle" : "starting"
|
|
3719
|
+
lifecycleState: resumeSessionId ? "idle" : "starting",
|
|
3720
|
+
sharing: options.sharing
|
|
2748
3721
|
};
|
|
2749
3722
|
let claudeProcess = null;
|
|
2750
3723
|
const allPersisted = loadPersistedSessions();
|
|
@@ -2780,8 +3753,14 @@ async function startDaemon() {
|
|
|
2780
3753
|
let turnInitiatedByUser = true;
|
|
2781
3754
|
let isKillingClaude = false;
|
|
2782
3755
|
let checkSvampConfig;
|
|
3756
|
+
const CLAUDE_PERMISSION_MODE_MAP = {
|
|
3757
|
+
"auto-approve-all": "bypassPermissions"
|
|
3758
|
+
};
|
|
3759
|
+
const toClaudePermissionMode = (mode) => CLAUDE_PERMISSION_MODE_MAP[mode] || mode;
|
|
3760
|
+
let isolationCleanupFiles = [];
|
|
2783
3761
|
const spawnClaude = (initialMessage, meta) => {
|
|
2784
|
-
const
|
|
3762
|
+
const rawPermissionMode = meta?.permissionMode || agentConfig.default_permission_mode || currentPermissionMode;
|
|
3763
|
+
const permissionMode = toClaudePermissionMode(rawPermissionMode);
|
|
2785
3764
|
currentPermissionMode = permissionMode;
|
|
2786
3765
|
const model = meta?.model || agentConfig.default_model || void 0;
|
|
2787
3766
|
const appendSystemPrompt = meta?.appendSystemPrompt || agentConfig.append_system_prompt || void 0;
|
|
@@ -2799,10 +3778,26 @@ async function startDaemon() {
|
|
|
2799
3778
|
if (model) args.push("--model", model);
|
|
2800
3779
|
if (appendSystemPrompt) args.push("--append-system-prompt", appendSystemPrompt);
|
|
2801
3780
|
if (claudeResumeId) args.push("--resume", claudeResumeId);
|
|
2802
|
-
|
|
2803
|
-
|
|
3781
|
+
let spawnCommand = "claude";
|
|
3782
|
+
let spawnArgs = args;
|
|
3783
|
+
let extraEnv = {};
|
|
3784
|
+
isolationCleanupFiles = [];
|
|
3785
|
+
const isoConfig = buildIsolationConfig2(directory);
|
|
3786
|
+
if (isoConfig) {
|
|
3787
|
+
const wrapped = wrapWithIsolation(spawnCommand, spawnArgs, isoConfig);
|
|
3788
|
+
spawnCommand = wrapped.command;
|
|
3789
|
+
spawnArgs = wrapped.args;
|
|
3790
|
+
if (wrapped.env) extraEnv = wrapped.env;
|
|
3791
|
+
if (wrapped.cleanupFiles) isolationCleanupFiles = wrapped.cleanupFiles;
|
|
3792
|
+
sessionMetadata = { ...sessionMetadata, isolationMethod: isoConfig.method };
|
|
3793
|
+
logger.log(`[Session ${sessionId}] Isolation: ${isoConfig.method} (binary: ${isoConfig.binaryPath})`);
|
|
3794
|
+
} else {
|
|
3795
|
+
sessionMetadata = { ...sessionMetadata, isolationMethod: void 0 };
|
|
3796
|
+
}
|
|
3797
|
+
logger.log(`[Session ${sessionId}] Spawning Claude: ${spawnCommand} ${spawnArgs.join(" ")} (cwd: ${directory})`);
|
|
3798
|
+
const spawnEnv = { ...process.env, ...extraEnv };
|
|
2804
3799
|
delete spawnEnv.CLAUDECODE;
|
|
2805
|
-
const child = spawn$1(
|
|
3800
|
+
const child = spawn$1(spawnCommand, spawnArgs, {
|
|
2806
3801
|
cwd: directory,
|
|
2807
3802
|
stdio: ["pipe", "pipe", "pipe"],
|
|
2808
3803
|
env: spawnEnv,
|
|
@@ -2812,17 +3807,11 @@ async function startDaemon() {
|
|
|
2812
3807
|
logger.log(`[Session ${sessionId}] Claude PID: ${child.pid}, stdin: ${!!child.stdin}, stdout: ${!!child.stdout}, stderr: ${!!child.stderr}`);
|
|
2813
3808
|
child.on("error", (err) => {
|
|
2814
3809
|
logger.log(`[Session ${sessionId}] Claude process error: ${err.message}`);
|
|
2815
|
-
sessionService.pushMessage(
|
|
2816
|
-
type: "
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
Please ensure Claude Code CLI is installed on this machine. You can install it with:
|
|
2822
|
-
\`npm install -g @anthropic-ai/claude-code\``
|
|
2823
|
-
}]
|
|
2824
|
-
}, "agent");
|
|
2825
|
-
sessionService.sendKeepAlive(false);
|
|
3810
|
+
sessionService.pushMessage(
|
|
3811
|
+
{ type: "message", message: `Agent process exited unexpectedly: ${err.message}. Please ensure Claude Code CLI is installed.` },
|
|
3812
|
+
"event"
|
|
3813
|
+
);
|
|
3814
|
+
sessionService.sendSessionEnd();
|
|
2826
3815
|
});
|
|
2827
3816
|
let stdoutBuffer = "";
|
|
2828
3817
|
let lastErrorMessagePushed = false;
|
|
@@ -2924,15 +3913,24 @@ Please ensure Claude Code CLI is installed on this machine. You can install it w
|
|
|
2924
3913
|
}
|
|
2925
3914
|
}
|
|
2926
3915
|
if (msg.type === "result") {
|
|
2927
|
-
if (msg.is_error
|
|
3916
|
+
if (msg.is_error) {
|
|
2928
3917
|
const resultText = msg.result || "";
|
|
2929
|
-
logger.error(`[Session ${sessionId}] Claude
|
|
2930
|
-
const
|
|
2931
|
-
const
|
|
2932
|
-
const
|
|
3918
|
+
logger.error(`[Session ${sessionId}] Claude error (is_error=true, api_ms=${msg.duration_api_ms}): "${resultText}"`);
|
|
3919
|
+
const lower = resultText.toLowerCase();
|
|
3920
|
+
const isLoginIssue = lower.includes("login") || lower.includes("logged in") || lower.includes("auth") || lower.includes("api key") || lower.includes("unauthorized");
|
|
3921
|
+
const isResumeIssue = lower.includes("tool_use.name") || lower.includes("invalid_request") || lower.includes("messages.");
|
|
3922
|
+
let hint = "";
|
|
3923
|
+
if (isLoginIssue) {
|
|
3924
|
+
hint = "\n\nRun `claude login` in your terminal on the machine running the daemon to re-authenticate.";
|
|
3925
|
+
} else if (isResumeIssue) {
|
|
3926
|
+
hint = "\n\nThe conversation history may be corrupted. Try starting a fresh session.";
|
|
3927
|
+
} else {
|
|
3928
|
+
hint = "\n\nCheck that the Claude Code CLI is properly installed and configured.";
|
|
3929
|
+
}
|
|
3930
|
+
const displayMsg = resultText || "Claude Code exited with an error.";
|
|
2933
3931
|
sessionService.pushMessage({
|
|
2934
3932
|
type: "assistant",
|
|
2935
|
-
content: [{ type: "text", text:
|
|
3933
|
+
content: [{ type: "text", text: `**Error:** ${displayMsg}${hint}` }]
|
|
2936
3934
|
}, "agent");
|
|
2937
3935
|
lastErrorMessagePushed = true;
|
|
2938
3936
|
}
|
|
@@ -2969,10 +3967,8 @@ Please ensure Claude Code CLI is installed on this machine. You can install it w
|
|
|
2969
3967
|
userMessagePending = false;
|
|
2970
3968
|
if (msg.session_id) {
|
|
2971
3969
|
claudeResumeId = msg.session_id;
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
claudeSessionId: msg.session_id
|
|
2975
|
-
});
|
|
3970
|
+
sessionMetadata = { ...sessionMetadata, claudeSessionId: msg.session_id };
|
|
3971
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
2976
3972
|
saveSession({
|
|
2977
3973
|
sessionId,
|
|
2978
3974
|
directory,
|
|
@@ -3009,27 +4005,25 @@ Please ensure Claude Code CLI is installed on this machine. You can install it w
|
|
|
3009
4005
|
child.on("exit", (code, signal) => {
|
|
3010
4006
|
logger.log(`[Session ${sessionId}] Claude exited: code=${code}, signal=${signal}`);
|
|
3011
4007
|
claudeProcess = null;
|
|
4008
|
+
for (const f of isolationCleanupFiles) {
|
|
4009
|
+
fs.rm(f, { force: true }).catch(() => {
|
|
4010
|
+
});
|
|
4011
|
+
}
|
|
4012
|
+
isolationCleanupFiles = [];
|
|
3012
4013
|
for (const [id, pending] of pendingPermissions) {
|
|
3013
4014
|
pending.resolve({ behavior: "deny", message: "Claude process exited" });
|
|
3014
4015
|
}
|
|
3015
4016
|
pendingPermissions.clear();
|
|
3016
4017
|
if (code !== 0 && code !== null && !lastErrorMessagePushed) {
|
|
3017
|
-
sessionService.pushMessage(
|
|
3018
|
-
type: "
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
text: `Error: Claude process exited with code ${code}${signal ? ` (signal: ${signal})` : ""}.
|
|
3022
|
-
|
|
3023
|
-
This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
3024
|
-
}]
|
|
3025
|
-
}, "agent");
|
|
4018
|
+
sessionService.pushMessage(
|
|
4019
|
+
{ type: "message", message: `Agent process exited unexpectedly (code ${code}${signal ? `, signal: ${signal}` : ""})` },
|
|
4020
|
+
"event"
|
|
4021
|
+
);
|
|
3026
4022
|
}
|
|
3027
4023
|
lastErrorMessagePushed = false;
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
});
|
|
3032
|
-
sessionService.sendKeepAlive(false);
|
|
4024
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: claudeResumeId ? "idle" : "stopped" };
|
|
4025
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
4026
|
+
sessionService.sendSessionEnd();
|
|
3033
4027
|
if (claudeResumeId && !trackedSession.stopped) {
|
|
3034
4028
|
saveSession({
|
|
3035
4029
|
sessionId,
|
|
@@ -3053,6 +4047,30 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3053
4047
|
}
|
|
3054
4048
|
return child;
|
|
3055
4049
|
};
|
|
4050
|
+
const restartClaudeHandler = async () => {
|
|
4051
|
+
logger.log(`[Session ${sessionId}] Restart Claude requested`);
|
|
4052
|
+
try {
|
|
4053
|
+
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
4054
|
+
isKillingClaude = true;
|
|
4055
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "restarting" };
|
|
4056
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
4057
|
+
await killAndWaitForExit2(claudeProcess);
|
|
4058
|
+
isKillingClaude = false;
|
|
4059
|
+
}
|
|
4060
|
+
if (claudeResumeId) {
|
|
4061
|
+
spawnClaude(void 0, { permissionMode: currentPermissionMode });
|
|
4062
|
+
logger.log(`[Session ${sessionId}] Claude respawned with --resume ${claudeResumeId}`);
|
|
4063
|
+
return { success: true, message: "Claude process restarted successfully." };
|
|
4064
|
+
} else {
|
|
4065
|
+
logger.log(`[Session ${sessionId}] No resume ID \u2014 cannot restart`);
|
|
4066
|
+
return { success: false, message: "No session to resume. Send a message to start a new session." };
|
|
4067
|
+
}
|
|
4068
|
+
} catch (err) {
|
|
4069
|
+
isKillingClaude = false;
|
|
4070
|
+
logger.log(`[Session ${sessionId}] Restart failed: ${err.message}`);
|
|
4071
|
+
return { success: false, message: `Restart failed: ${err.message}` };
|
|
4072
|
+
}
|
|
4073
|
+
};
|
|
3056
4074
|
const sessionService = await registerSessionService(
|
|
3057
4075
|
server,
|
|
3058
4076
|
sessionId,
|
|
@@ -3080,7 +4098,7 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3080
4098
|
text = typeof content === "string" ? content : JSON.stringify(content);
|
|
3081
4099
|
}
|
|
3082
4100
|
if (msgMeta?.permissionMode) {
|
|
3083
|
-
currentPermissionMode = msgMeta.permissionMode;
|
|
4101
|
+
currentPermissionMode = toClaudePermissionMode(msgMeta.permissionMode);
|
|
3084
4102
|
logger.log(`[Session ${sessionId}] Permission mode updated to: ${currentPermissionMode}`);
|
|
3085
4103
|
}
|
|
3086
4104
|
if (isKillingClaude) {
|
|
@@ -3110,8 +4128,8 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3110
4128
|
const requestId = params.id;
|
|
3111
4129
|
const pending = pendingPermissions.get(requestId);
|
|
3112
4130
|
if (params.mode) {
|
|
3113
|
-
|
|
3114
|
-
|
|
4131
|
+
currentPermissionMode = toClaudePermissionMode(params.mode);
|
|
4132
|
+
logger.log(`[Session ${sessionId}] Permission mode changed to: ${currentPermissionMode}`);
|
|
3115
4133
|
}
|
|
3116
4134
|
if (params.allowTools && Array.isArray(params.allowTools)) {
|
|
3117
4135
|
for (const tool of params.allowTools) {
|
|
@@ -3150,32 +4168,7 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3150
4168
|
spawnClaude(void 0, { permissionMode: mode });
|
|
3151
4169
|
}
|
|
3152
4170
|
},
|
|
3153
|
-
onRestartClaude:
|
|
3154
|
-
logger.log(`[Session ${sessionId}] Restart Claude requested`);
|
|
3155
|
-
try {
|
|
3156
|
-
if (claudeProcess && claudeProcess.exitCode === null) {
|
|
3157
|
-
isKillingClaude = true;
|
|
3158
|
-
sessionService.updateMetadata({
|
|
3159
|
-
...sessionMetadata,
|
|
3160
|
-
lifecycleState: "restarting"
|
|
3161
|
-
});
|
|
3162
|
-
await killAndWaitForExit2(claudeProcess);
|
|
3163
|
-
isKillingClaude = false;
|
|
3164
|
-
}
|
|
3165
|
-
if (claudeResumeId) {
|
|
3166
|
-
spawnClaude(void 0, { permissionMode: currentPermissionMode });
|
|
3167
|
-
logger.log(`[Session ${sessionId}] Claude respawned with --resume ${claudeResumeId}`);
|
|
3168
|
-
return { success: true, message: "Claude process restarted successfully." };
|
|
3169
|
-
} else {
|
|
3170
|
-
logger.log(`[Session ${sessionId}] No resume ID \u2014 cannot restart`);
|
|
3171
|
-
return { success: false, message: "No session to resume. Send a message to start a new session." };
|
|
3172
|
-
}
|
|
3173
|
-
} catch (err) {
|
|
3174
|
-
isKillingClaude = false;
|
|
3175
|
-
logger.log(`[Session ${sessionId}] Restart failed: ${err.message}`);
|
|
3176
|
-
return { success: false, message: `Restart failed: ${err.message}` };
|
|
3177
|
-
}
|
|
3178
|
-
},
|
|
4171
|
+
onRestartClaude: restartClaudeHandler,
|
|
3179
4172
|
onKillSession: () => {
|
|
3180
4173
|
logger.log(`[Session ${sessionId}] Kill session requested`);
|
|
3181
4174
|
stopSession(sessionId);
|
|
@@ -3293,10 +4286,8 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3293
4286
|
}
|
|
3294
4287
|
};
|
|
3295
4288
|
pidToTrackedSession.set(process.pid + Math.floor(Math.random() * 1e5), trackedSession);
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
lifecycleState: "idle"
|
|
3299
|
-
});
|
|
4289
|
+
sessionMetadata = { ...sessionMetadata, lifecycleState: "idle" };
|
|
4290
|
+
sessionService.updateMetadata(sessionMetadata);
|
|
3300
4291
|
logger.log(`Session ${sessionId} registered on Hypha, waiting for first message to spawn Claude`);
|
|
3301
4292
|
return {
|
|
3302
4293
|
type: "success",
|
|
@@ -3311,8 +4302,8 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3311
4302
|
};
|
|
3312
4303
|
}
|
|
3313
4304
|
};
|
|
3314
|
-
const
|
|
3315
|
-
logger.log(`[
|
|
4305
|
+
const spawnAgentSession = async (sessionId, directory, agentName, options, resumeSessionId) => {
|
|
4306
|
+
logger.log(`[Agent] Spawning ${agentName} session: ${sessionId}`);
|
|
3316
4307
|
try {
|
|
3317
4308
|
let parseBashPermission2 = function(permission) {
|
|
3318
4309
|
if (permission === "Bash") return;
|
|
@@ -3353,7 +4344,8 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3353
4344
|
startedFromDaemon: true,
|
|
3354
4345
|
startedBy: "daemon",
|
|
3355
4346
|
lifecycleState: "starting",
|
|
3356
|
-
flavor: agentName
|
|
4347
|
+
flavor: agentName,
|
|
4348
|
+
sharing: options.sharing
|
|
3357
4349
|
};
|
|
3358
4350
|
let currentPermissionMode = "default";
|
|
3359
4351
|
const allowedTools = /* @__PURE__ */ new Set();
|
|
@@ -3367,7 +4359,7 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3367
4359
|
{ controlledByUser: false },
|
|
3368
4360
|
{
|
|
3369
4361
|
onUserMessage: (content, meta) => {
|
|
3370
|
-
logger.log(`[
|
|
4362
|
+
logger.log(`[${agentName} Session ${sessionId}] User message received`);
|
|
3371
4363
|
let text;
|
|
3372
4364
|
let msgMeta = meta;
|
|
3373
4365
|
try {
|
|
@@ -3387,17 +4379,17 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3387
4379
|
if (msgMeta?.permissionMode) {
|
|
3388
4380
|
currentPermissionMode = msgMeta.permissionMode;
|
|
3389
4381
|
}
|
|
3390
|
-
|
|
3391
|
-
logger.error(`[
|
|
4382
|
+
agentBackend.sendPrompt(sessionId, text).catch((err) => {
|
|
4383
|
+
logger.error(`[${agentName} Session ${sessionId}] Error sending prompt:`, err);
|
|
3392
4384
|
});
|
|
3393
4385
|
},
|
|
3394
4386
|
onAbort: () => {
|
|
3395
|
-
logger.log(`[
|
|
3396
|
-
|
|
4387
|
+
logger.log(`[${agentName} Session ${sessionId}] Abort requested`);
|
|
4388
|
+
agentBackend.cancel(sessionId).catch(() => {
|
|
3397
4389
|
});
|
|
3398
4390
|
},
|
|
3399
4391
|
onPermissionResponse: (params) => {
|
|
3400
|
-
logger.log(`[
|
|
4392
|
+
logger.log(`[${agentName} Session ${sessionId}] Permission response:`, JSON.stringify(params));
|
|
3401
4393
|
const requestId = params.id;
|
|
3402
4394
|
if (params.mode) currentPermissionMode = params.mode;
|
|
3403
4395
|
if (params.allowTools && Array.isArray(params.allowTools)) {
|
|
@@ -3410,17 +4402,18 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3410
4402
|
}
|
|
3411
4403
|
}
|
|
3412
4404
|
permissionHandler.resolvePermission(requestId, params.approved);
|
|
4405
|
+
agentBackend.respondToPermission?.(requestId, params.approved);
|
|
3413
4406
|
},
|
|
3414
4407
|
onSwitchMode: (mode) => {
|
|
3415
|
-
logger.log(`[
|
|
4408
|
+
logger.log(`[${agentName} Session ${sessionId}] Switch mode: ${mode}`);
|
|
3416
4409
|
currentPermissionMode = mode;
|
|
3417
4410
|
},
|
|
3418
4411
|
onRestartClaude: async () => {
|
|
3419
|
-
logger.log(`[
|
|
4412
|
+
logger.log(`[${agentName} Session ${sessionId}] Restart agent requested`);
|
|
3420
4413
|
return { success: false, message: "Restart is not supported for this agent type." };
|
|
3421
4414
|
},
|
|
3422
4415
|
onKillSession: () => {
|
|
3423
|
-
logger.log(`[
|
|
4416
|
+
logger.log(`[${agentName} Session ${sessionId}] Kill session requested`);
|
|
3424
4417
|
stopSession(sessionId);
|
|
3425
4418
|
},
|
|
3426
4419
|
onBash: async (command, cwd) => {
|
|
@@ -3522,21 +4515,46 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3522
4515
|
sessionService,
|
|
3523
4516
|
logger
|
|
3524
4517
|
);
|
|
3525
|
-
const transportHandler = agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(agentName);
|
|
3526
4518
|
const permissionHandler = new HyphaPermissionHandler(shouldAutoAllow2, logger.log);
|
|
3527
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
4519
|
+
let agentIsoConfig;
|
|
4520
|
+
if (sessionMetadata.sharing?.enabled && isolationCapabilities.preferred) {
|
|
4521
|
+
const method = isolationCapabilities.preferred;
|
|
4522
|
+
const detail = isolationCapabilities.details[method];
|
|
4523
|
+
if (detail.found && detail.verified !== false) {
|
|
4524
|
+
agentIsoConfig = {
|
|
4525
|
+
method,
|
|
4526
|
+
binaryPath: detail.path || method,
|
|
4527
|
+
workspacePath: directory
|
|
4528
|
+
};
|
|
4529
|
+
sessionMetadata = { ...sessionMetadata, isolationMethod: method };
|
|
4530
|
+
logger.log(`[Agent Session ${sessionId}] Isolation: ${method}`);
|
|
4531
|
+
}
|
|
4532
|
+
}
|
|
4533
|
+
let agentBackend;
|
|
4534
|
+
if (KNOWN_MCP_AGENTS[agentName]) {
|
|
4535
|
+
agentBackend = new CodexMcpBackend({
|
|
4536
|
+
cwd: directory,
|
|
4537
|
+
env: options.environmentVariables,
|
|
4538
|
+
log: logger.log,
|
|
4539
|
+
isolationConfig: agentIsoConfig
|
|
4540
|
+
});
|
|
4541
|
+
} else {
|
|
4542
|
+
const transportHandler = agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(agentName);
|
|
4543
|
+
const acpConfig = KNOWN_ACP_AGENTS[agentName];
|
|
4544
|
+
agentBackend = new AcpBackend({
|
|
4545
|
+
agentName,
|
|
4546
|
+
cwd: directory,
|
|
4547
|
+
command: acpConfig.command,
|
|
4548
|
+
args: acpConfig.args,
|
|
4549
|
+
env: options.environmentVariables,
|
|
4550
|
+
permissionHandler,
|
|
4551
|
+
transportHandler,
|
|
4552
|
+
log: logger.log,
|
|
4553
|
+
isolationConfig: agentIsoConfig
|
|
4554
|
+
});
|
|
4555
|
+
}
|
|
3538
4556
|
bridgeAcpToSession(
|
|
3539
|
-
|
|
4557
|
+
agentBackend,
|
|
3540
4558
|
sessionService,
|
|
3541
4559
|
() => sessionMetadata,
|
|
3542
4560
|
(updater) => {
|
|
@@ -3555,33 +4573,28 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3555
4573
|
directory,
|
|
3556
4574
|
resumeSessionId,
|
|
3557
4575
|
get childProcess() {
|
|
3558
|
-
return
|
|
4576
|
+
return agentBackend.getProcess?.() || void 0;
|
|
3559
4577
|
}
|
|
3560
4578
|
};
|
|
3561
4579
|
pidToTrackedSession.set(process.pid + Math.floor(Math.random() * 1e5), trackedSession);
|
|
3562
|
-
logger.log(`[
|
|
3563
|
-
|
|
3564
|
-
logger.log(`[
|
|
4580
|
+
logger.log(`[Agent Session ${sessionId}] Starting ${agentName} backend...`);
|
|
4581
|
+
agentBackend.startSession().then(() => {
|
|
4582
|
+
logger.log(`[Agent Session ${sessionId}] ${agentName} backend started, waiting for first message`);
|
|
3565
4583
|
}).catch((err) => {
|
|
3566
|
-
logger.error(`[
|
|
3567
|
-
sessionService.pushMessage(
|
|
3568
|
-
type: "
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
Please ensure the ${agentName} CLI is installed.`
|
|
3574
|
-
}]
|
|
3575
|
-
}, "agent");
|
|
3576
|
-
sessionService.sendKeepAlive(false);
|
|
4584
|
+
logger.error(`[Agent Session ${sessionId}] Failed to start ${agentName}:`, err);
|
|
4585
|
+
sessionService.pushMessage(
|
|
4586
|
+
{ type: "message", message: `Agent process exited unexpectedly: ${err.message}. Please ensure the ${agentName} CLI is installed.` },
|
|
4587
|
+
"event"
|
|
4588
|
+
);
|
|
4589
|
+
sessionService.sendSessionEnd();
|
|
3577
4590
|
});
|
|
3578
4591
|
return {
|
|
3579
4592
|
type: "success",
|
|
3580
4593
|
sessionId,
|
|
3581
|
-
message: `
|
|
4594
|
+
message: `Agent session (${agentName}) registered as svamp-session-${sessionId}`
|
|
3582
4595
|
};
|
|
3583
4596
|
} catch (err) {
|
|
3584
|
-
logger.error(`[
|
|
4597
|
+
logger.error(`[Agent] Failed to spawn ${agentName} session:`, err);
|
|
3585
4598
|
return {
|
|
3586
4599
|
type: "error",
|
|
3587
4600
|
errorMessage: `Failed to spawn ${agentName} session: ${err.message}`
|
|
@@ -3610,6 +4623,25 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3610
4623
|
logger.log(`Session ${sessionId} not found`);
|
|
3611
4624
|
return false;
|
|
3612
4625
|
};
|
|
4626
|
+
const restartSession = async (sessionId) => {
|
|
4627
|
+
for (const session of pidToTrackedSession.values()) {
|
|
4628
|
+
if (session.svampSessionId === sessionId && !session.stopped) {
|
|
4629
|
+
if (session.restartAgent) {
|
|
4630
|
+
return await session.restartAgent();
|
|
4631
|
+
}
|
|
4632
|
+
return { success: false, message: "This session does not support restart." };
|
|
4633
|
+
}
|
|
4634
|
+
}
|
|
4635
|
+
return { success: false, message: `Session ${sessionId} not found or already stopped.` };
|
|
4636
|
+
};
|
|
4637
|
+
let isolationCapabilities;
|
|
4638
|
+
try {
|
|
4639
|
+
isolationCapabilities = await detectIsolationCapabilities();
|
|
4640
|
+
logger.log(`Isolation capabilities: ${isolationCapabilities.available.join(", ") || "none"} (preferred: ${isolationCapabilities.preferred || "none"})`);
|
|
4641
|
+
} catch (err) {
|
|
4642
|
+
logger.log(`Failed to detect isolation capabilities: ${err}`);
|
|
4643
|
+
isolationCapabilities = { available: [], preferred: null, details: { srt: { found: false }, bwrap: { found: false }, docker: { found: false }, podman: { found: false } } };
|
|
4644
|
+
}
|
|
3613
4645
|
const defaultHomeDir = existsSync$1("/data") ? "/data" : os.homedir();
|
|
3614
4646
|
const machineMetadata = {
|
|
3615
4647
|
host: os.hostname(),
|
|
@@ -3618,7 +4650,8 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3618
4650
|
homeDir: defaultHomeDir,
|
|
3619
4651
|
svampHomeDir: SVAMP_HOME,
|
|
3620
4652
|
svampLibDir: join$1(__dirname$1, ".."),
|
|
3621
|
-
displayName: process.env.SVAMP_DISPLAY_NAME || void 0
|
|
4653
|
+
displayName: process.env.SVAMP_DISPLAY_NAME || void 0,
|
|
4654
|
+
isolationCapabilities
|
|
3622
4655
|
};
|
|
3623
4656
|
const initialDaemonState = {
|
|
3624
4657
|
status: "running",
|
|
@@ -3633,6 +4666,7 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3633
4666
|
{
|
|
3634
4667
|
spawnSession,
|
|
3635
4668
|
stopSession,
|
|
4669
|
+
restartSession,
|
|
3636
4670
|
requestShutdown: () => requestShutdown("hypha-app"),
|
|
3637
4671
|
getTrackedSessions: getCurrentChildren
|
|
3638
4672
|
}
|
|
@@ -3727,7 +4761,7 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3727
4761
|
console.log(` Service: svamp-machine-${machineId}`);
|
|
3728
4762
|
console.log(` Log file: ${logger.logFilePath}`);
|
|
3729
4763
|
let consecutiveHeartbeatFailures = 0;
|
|
3730
|
-
const MAX_HEARTBEAT_FAILURES =
|
|
4764
|
+
const MAX_HEARTBEAT_FAILURES = 60;
|
|
3731
4765
|
const heartbeatInterval = setInterval(async () => {
|
|
3732
4766
|
try {
|
|
3733
4767
|
const state = readDaemonStateFile();
|
|
@@ -3828,16 +4862,22 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3828
4862
|
}
|
|
3829
4863
|
}
|
|
3830
4864
|
}
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
const
|
|
3835
|
-
|
|
3836
|
-
const
|
|
3837
|
-
|
|
4865
|
+
const shouldMarkStopped = source === "os-signal-cleanup";
|
|
4866
|
+
if (shouldMarkStopped) {
|
|
4867
|
+
try {
|
|
4868
|
+
const index = loadSessionIndex();
|
|
4869
|
+
for (const [sessionId, entry] of Object.entries(index)) {
|
|
4870
|
+
const filePath = getSessionFilePath(entry.directory, sessionId);
|
|
4871
|
+
if (existsSync$1(filePath)) {
|
|
4872
|
+
const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
|
|
4873
|
+
writeFileSync$1(filePath, JSON.stringify({ ...data, stopped: true }, null, 2), "utf-8");
|
|
4874
|
+
}
|
|
3838
4875
|
}
|
|
4876
|
+
logger.log("Marked all sessions as stopped (--cleanup mode)");
|
|
4877
|
+
} catch {
|
|
3839
4878
|
}
|
|
3840
|
-
}
|
|
4879
|
+
} else {
|
|
4880
|
+
logger.log("Sessions preserved for auto-restore on next start");
|
|
3841
4881
|
}
|
|
3842
4882
|
try {
|
|
3843
4883
|
await machineService.disconnect();
|
|
@@ -3873,16 +4913,18 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3873
4913
|
process.exit(1);
|
|
3874
4914
|
}
|
|
3875
4915
|
}
|
|
3876
|
-
async function stopDaemon() {
|
|
4916
|
+
async function stopDaemon(options) {
|
|
3877
4917
|
const state = readDaemonStateFile();
|
|
3878
4918
|
if (!state) {
|
|
3879
4919
|
console.log("No daemon running");
|
|
3880
4920
|
return;
|
|
3881
4921
|
}
|
|
4922
|
+
const signal = options?.cleanup ? "SIGUSR1" : "SIGTERM";
|
|
4923
|
+
const mode = options?.cleanup ? "cleanup (sessions will be stopped)" : "quick (sessions preserved for auto-restore)";
|
|
3882
4924
|
try {
|
|
3883
4925
|
process.kill(state.pid, 0);
|
|
3884
|
-
process.kill(state.pid,
|
|
3885
|
-
console.log(`Sent
|
|
4926
|
+
process.kill(state.pid, signal);
|
|
4927
|
+
console.log(`Sent ${signal} to daemon PID ${state.pid} \u2014 ${mode}`);
|
|
3886
4928
|
for (let i = 0; i < 30; i++) {
|
|
3887
4929
|
await new Promise((r) => setTimeout(r, 100));
|
|
3888
4930
|
try {
|