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
|
@@ -5,12 +5,87 @@ import { dirname, join as join$1, resolve, basename } from 'path';
|
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
6
|
import { spawn as spawn$1 } from 'child_process';
|
|
7
7
|
import { randomUUID as randomUUID$1 } from 'crypto';
|
|
8
|
-
import { c as connectToHypha } from './hyphaClient-DLkclazm.mjs';
|
|
9
8
|
import { randomUUID } from 'node:crypto';
|
|
10
9
|
import { existsSync, readFileSync, mkdirSync, appendFileSync, writeFileSync } from 'node:fs';
|
|
11
10
|
import { join } from 'node:path';
|
|
12
|
-
import { spawn } from 'node:child_process';
|
|
11
|
+
import { spawn, execSync, execFile } from 'node:child_process';
|
|
13
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';
|
|
20
|
+
|
|
21
|
+
let connectToServerFn = null;
|
|
22
|
+
async function getConnectToServer() {
|
|
23
|
+
if (!connectToServerFn) {
|
|
24
|
+
const mod = await import('hypha-rpc');
|
|
25
|
+
connectToServerFn = mod.connectToServer || mod.default && mod.default.connectToServer || mod.hyphaWebsocketClient && mod.hyphaWebsocketClient.connectToServer;
|
|
26
|
+
if (!connectToServerFn) {
|
|
27
|
+
throw new Error("Could not find connectToServer in hypha-rpc module");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return connectToServerFn;
|
|
31
|
+
}
|
|
32
|
+
async function connectToHypha(config) {
|
|
33
|
+
const connectToServer = await getConnectToServer();
|
|
34
|
+
const workspace = config.token ? parseWorkspaceFromToken(config.token) : void 0;
|
|
35
|
+
const server = await connectToServer({
|
|
36
|
+
server_url: config.serverUrl,
|
|
37
|
+
token: config.token,
|
|
38
|
+
client_id: config.clientId,
|
|
39
|
+
name: config.name || "svamp-cli",
|
|
40
|
+
workspace,
|
|
41
|
+
...config.transport ? { transport: config.transport } : {}
|
|
42
|
+
});
|
|
43
|
+
return server;
|
|
44
|
+
}
|
|
45
|
+
function parseWorkspaceFromToken(token) {
|
|
46
|
+
try {
|
|
47
|
+
const payload = JSON.parse(Buffer.from(token.split(".")[1], "base64").toString());
|
|
48
|
+
const scope = payload.scope || "";
|
|
49
|
+
const match = scope.match(/wid:([^\s]+)/);
|
|
50
|
+
return match ? match[1] : void 0;
|
|
51
|
+
} catch {
|
|
52
|
+
return void 0;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
function getHyphaServerUrl() {
|
|
56
|
+
return process.env.HYPHA_SERVER_URL || "http://localhost:9527";
|
|
57
|
+
}
|
|
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
|
+
}
|
|
14
89
|
|
|
15
90
|
async function registerMachineService(server, machineId, metadata, daemonState, handlers) {
|
|
16
91
|
let currentMetadata = { ...metadata };
|
|
@@ -52,27 +127,38 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
52
127
|
id: "default",
|
|
53
128
|
name: `Svamp Machine (${metadata.displayName || machineId})`,
|
|
54
129
|
type: "svamp-machine",
|
|
55
|
-
config: { visibility: "public" },
|
|
130
|
+
config: { visibility: "public", require_context: true },
|
|
56
131
|
// Machine info
|
|
57
|
-
getMachineInfo: async () =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
+
},
|
|
64
142
|
// Heartbeat
|
|
65
|
-
heartbeat: async () => ({
|
|
143
|
+
heartbeat: async (context) => ({
|
|
66
144
|
time: Date.now(),
|
|
67
145
|
status: currentDaemonState.status,
|
|
68
146
|
machineId
|
|
69
147
|
}),
|
|
70
148
|
// List active sessions on this machine
|
|
71
|
-
listSessions: async () => {
|
|
149
|
+
listSessions: async (context) => {
|
|
150
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
72
151
|
return handlers.getTrackedSessions();
|
|
73
152
|
},
|
|
74
153
|
// Spawn a new session
|
|
75
|
-
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
|
+
}
|
|
76
162
|
const result = await handlers.spawnSession({
|
|
77
163
|
...options,
|
|
78
164
|
machineId
|
|
@@ -87,7 +173,8 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
87
173
|
return result;
|
|
88
174
|
},
|
|
89
175
|
// Stop a session
|
|
90
|
-
stopSession: async (sessionId) => {
|
|
176
|
+
stopSession: async (sessionId, context) => {
|
|
177
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
91
178
|
const result = handlers.stopSession(sessionId);
|
|
92
179
|
notifyListeners({
|
|
93
180
|
type: "session-stopped",
|
|
@@ -97,16 +184,21 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
97
184
|
return result;
|
|
98
185
|
},
|
|
99
186
|
// Stop the daemon
|
|
100
|
-
stopDaemon: async () => {
|
|
187
|
+
stopDaemon: async (context) => {
|
|
188
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
101
189
|
handlers.requestShutdown();
|
|
102
190
|
return { success: true };
|
|
103
191
|
},
|
|
104
192
|
// Metadata management (with optimistic concurrency)
|
|
105
|
-
getMetadata: async () =>
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
193
|
+
getMetadata: async (context) => {
|
|
194
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
195
|
+
return {
|
|
196
|
+
metadata: currentMetadata,
|
|
197
|
+
version: metadataVersion
|
|
198
|
+
};
|
|
199
|
+
},
|
|
200
|
+
updateMetadata: async (newMetadata, expectedVersion, context) => {
|
|
201
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
110
202
|
if (expectedVersion !== metadataVersion) {
|
|
111
203
|
return {
|
|
112
204
|
result: "version-mismatch",
|
|
@@ -128,11 +220,15 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
128
220
|
};
|
|
129
221
|
},
|
|
130
222
|
// Daemon state management
|
|
131
|
-
getDaemonState: async () =>
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
223
|
+
getDaemonState: async (context) => {
|
|
224
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
225
|
+
return {
|
|
226
|
+
daemonState: currentDaemonState,
|
|
227
|
+
version: daemonStateVersion
|
|
228
|
+
};
|
|
229
|
+
},
|
|
230
|
+
updateDaemonState: async (newState, expectedVersion, context) => {
|
|
231
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
136
232
|
if (expectedVersion !== daemonStateVersion) {
|
|
137
233
|
return {
|
|
138
234
|
result: "version-mismatch",
|
|
@@ -153,14 +249,35 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
153
249
|
daemonState: currentDaemonState
|
|
154
250
|
};
|
|
155
251
|
},
|
|
252
|
+
// ── Sharing Management ──
|
|
253
|
+
getSharing: async (context) => {
|
|
254
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
255
|
+
return { sharing: currentMetadata.sharing || null };
|
|
256
|
+
},
|
|
257
|
+
updateSharing: async (newSharing, context) => {
|
|
258
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
259
|
+
if (currentMetadata.sharing && context?.user?.email && context.user.email !== currentMetadata.sharing.owner) {
|
|
260
|
+
throw new Error("Only the machine owner can update sharing settings");
|
|
261
|
+
}
|
|
262
|
+
currentMetadata = { ...currentMetadata, sharing: newSharing };
|
|
263
|
+
metadataVersion++;
|
|
264
|
+
notifyListeners({
|
|
265
|
+
type: "update-machine",
|
|
266
|
+
machineId,
|
|
267
|
+
metadata: { value: currentMetadata, version: metadataVersion }
|
|
268
|
+
});
|
|
269
|
+
return { success: true, sharing: newSharing };
|
|
270
|
+
},
|
|
156
271
|
// Register a listener for real-time updates (app calls this with _rintf callback)
|
|
157
|
-
registerListener: async (callback) => {
|
|
272
|
+
registerListener: async (callback, context) => {
|
|
273
|
+
authorizeRequest(context, currentMetadata.sharing, "view");
|
|
158
274
|
listeners.push(callback);
|
|
159
275
|
console.log(`[HYPHA MACHINE] Listener registered (total: ${listeners.length})`);
|
|
160
276
|
return { success: true, listenerId: listeners.length - 1 };
|
|
161
277
|
},
|
|
162
278
|
// Shell access
|
|
163
|
-
bash: async (command, cwd) => {
|
|
279
|
+
bash: async (command, cwd, context) => {
|
|
280
|
+
authorizeRequest(context, currentMetadata.sharing, "admin");
|
|
164
281
|
const { exec } = await import('child_process');
|
|
165
282
|
const { homedir } = await import('os');
|
|
166
283
|
return new Promise((resolve) => {
|
|
@@ -174,7 +291,8 @@ async function registerMachineService(server, machineId, metadata, daemonState,
|
|
|
174
291
|
});
|
|
175
292
|
},
|
|
176
293
|
// WISE voice — create ephemeral token for OpenAI Realtime API
|
|
177
|
-
wiseCreateEphemeralToken: async (params) => {
|
|
294
|
+
wiseCreateEphemeralToken: async (params, context) => {
|
|
295
|
+
authorizeRequest(context, currentMetadata.sharing, "interact");
|
|
178
296
|
const apiKey = params.apiKey || process.env.OPENAI_API_KEY;
|
|
179
297
|
if (!apiKey) {
|
|
180
298
|
return { success: false, error: "No OpenAI API key found. Set OPENAI_API_KEY or pass apiKey." };
|
|
@@ -338,9 +456,10 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
338
456
|
id: `svamp-session-${sessionId}`,
|
|
339
457
|
name: `Svamp Session ${sessionId.slice(0, 8)}`,
|
|
340
458
|
type: "svamp-session",
|
|
341
|
-
config: { visibility: "public" },
|
|
459
|
+
config: { visibility: "public", require_context: true },
|
|
342
460
|
// ── Messages ──
|
|
343
|
-
getMessages: async (afterSeq, limit) => {
|
|
461
|
+
getMessages: async (afterSeq, limit, context) => {
|
|
462
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
344
463
|
const after = afterSeq ?? 0;
|
|
345
464
|
const lim = Math.min(limit ?? 100, 500);
|
|
346
465
|
const filtered = messages.filter((m) => m.seq > after);
|
|
@@ -350,7 +469,8 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
350
469
|
hasMore: filtered.length > lim
|
|
351
470
|
};
|
|
352
471
|
},
|
|
353
|
-
sendMessage: async (content, localId, meta) => {
|
|
472
|
+
sendMessage: async (content, localId, meta, context) => {
|
|
473
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
354
474
|
if (localId) {
|
|
355
475
|
const existing = messages.find((m) => m.localId === localId);
|
|
356
476
|
if (existing) {
|
|
@@ -394,11 +514,15 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
394
514
|
return { id: msg.id, seq: msg.seq, localId: msg.localId };
|
|
395
515
|
},
|
|
396
516
|
// ── Metadata ──
|
|
397
|
-
getMetadata: async () =>
|
|
398
|
-
metadata,
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
517
|
+
getMetadata: async (context) => {
|
|
518
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
519
|
+
return {
|
|
520
|
+
metadata,
|
|
521
|
+
version: metadataVersion
|
|
522
|
+
};
|
|
523
|
+
},
|
|
524
|
+
updateMetadata: async (newMetadata, expectedVersion, context) => {
|
|
525
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
402
526
|
if (expectedVersion !== void 0 && expectedVersion !== metadataVersion) {
|
|
403
527
|
return {
|
|
404
528
|
result: "version-mismatch",
|
|
@@ -420,11 +544,15 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
420
544
|
};
|
|
421
545
|
},
|
|
422
546
|
// ── Agent State ──
|
|
423
|
-
getAgentState: async () =>
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
547
|
+
getAgentState: async (context) => {
|
|
548
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
549
|
+
return {
|
|
550
|
+
agentState,
|
|
551
|
+
version: agentStateVersion
|
|
552
|
+
};
|
|
553
|
+
},
|
|
554
|
+
updateAgentState: async (newState, expectedVersion, context) => {
|
|
555
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
428
556
|
if (expectedVersion !== void 0 && expectedVersion !== agentStateVersion) {
|
|
429
557
|
return {
|
|
430
558
|
result: "version-mismatch",
|
|
@@ -446,27 +574,32 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
446
574
|
};
|
|
447
575
|
},
|
|
448
576
|
// ── Session Control RPCs ──
|
|
449
|
-
abort: async () => {
|
|
577
|
+
abort: async (context) => {
|
|
578
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
450
579
|
callbacks.onAbort();
|
|
451
580
|
return { success: true };
|
|
452
581
|
},
|
|
453
|
-
permissionResponse: async (params) => {
|
|
582
|
+
permissionResponse: async (params, context) => {
|
|
583
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
454
584
|
callbacks.onPermissionResponse(params);
|
|
455
585
|
return { success: true };
|
|
456
586
|
},
|
|
457
|
-
switchMode: async (mode) => {
|
|
587
|
+
switchMode: async (mode, context) => {
|
|
588
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
458
589
|
callbacks.onSwitchMode(mode);
|
|
459
590
|
return { success: true };
|
|
460
591
|
},
|
|
461
|
-
restartClaude: async () => {
|
|
592
|
+
restartClaude: async (context) => {
|
|
593
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
462
594
|
return await callbacks.onRestartClaude();
|
|
463
595
|
},
|
|
464
|
-
killSession: async () => {
|
|
596
|
+
killSession: async (context) => {
|
|
597
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
465
598
|
callbacks.onKillSession();
|
|
466
599
|
return { success: true };
|
|
467
600
|
},
|
|
468
601
|
// ── Activity ──
|
|
469
|
-
keepAlive: async (thinking, mode) => {
|
|
602
|
+
keepAlive: async (thinking, mode, context) => {
|
|
470
603
|
lastActivity = { active: true, thinking: thinking || false, mode: mode || "remote", time: Date.now() };
|
|
471
604
|
notifyListeners({
|
|
472
605
|
type: "activity",
|
|
@@ -474,7 +607,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
474
607
|
...lastActivity
|
|
475
608
|
});
|
|
476
609
|
},
|
|
477
|
-
sessionEnd: async () => {
|
|
610
|
+
sessionEnd: async (context) => {
|
|
478
611
|
lastActivity = { active: false, thinking: false, mode: "remote", time: Date.now() };
|
|
479
612
|
notifyListeners({
|
|
480
613
|
type: "activity",
|
|
@@ -483,28 +616,34 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
483
616
|
});
|
|
484
617
|
},
|
|
485
618
|
// ── Activity State Query ──
|
|
486
|
-
getActivityState: async () => {
|
|
619
|
+
getActivityState: async (context) => {
|
|
620
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
487
621
|
return { ...lastActivity, sessionId };
|
|
488
622
|
},
|
|
489
|
-
// ── File Operations (optional) ──
|
|
490
|
-
readFile: async (path) => {
|
|
623
|
+
// ── File Operations (optional, admin-only) ──
|
|
624
|
+
readFile: async (path, context) => {
|
|
625
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
491
626
|
if (!callbacks.onReadFile) throw new Error("readFile not supported");
|
|
492
627
|
return await callbacks.onReadFile(path);
|
|
493
628
|
},
|
|
494
|
-
writeFile: async (path, content) => {
|
|
629
|
+
writeFile: async (path, content, context) => {
|
|
630
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
495
631
|
if (!callbacks.onWriteFile) throw new Error("writeFile not supported");
|
|
496
632
|
await callbacks.onWriteFile(path, content);
|
|
497
633
|
return { success: true };
|
|
498
634
|
},
|
|
499
|
-
listDirectory: async (path) => {
|
|
635
|
+
listDirectory: async (path, context) => {
|
|
636
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
500
637
|
if (!callbacks.onListDirectory) throw new Error("listDirectory not supported");
|
|
501
638
|
return await callbacks.onListDirectory(path);
|
|
502
639
|
},
|
|
503
|
-
bash: async (command, cwd) => {
|
|
640
|
+
bash: async (command, cwd, context) => {
|
|
641
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
504
642
|
if (!callbacks.onBash) throw new Error("bash not supported");
|
|
505
643
|
return await callbacks.onBash(command, cwd);
|
|
506
644
|
},
|
|
507
|
-
ripgrep: async (args, cwd) => {
|
|
645
|
+
ripgrep: async (args, cwd, context) => {
|
|
646
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
508
647
|
if (!callbacks.onRipgrep) throw new Error("ripgrep not supported");
|
|
509
648
|
try {
|
|
510
649
|
const stdout = await callbacks.onRipgrep(args, cwd);
|
|
@@ -513,12 +652,36 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
513
652
|
return { success: false, stdout: "", stderr: err.message || "", exitCode: 1, error: err.message };
|
|
514
653
|
}
|
|
515
654
|
},
|
|
516
|
-
getDirectoryTree: async (path, maxDepth) => {
|
|
655
|
+
getDirectoryTree: async (path, maxDepth, context) => {
|
|
656
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
517
657
|
if (!callbacks.onGetDirectoryTree) throw new Error("getDirectoryTree not supported");
|
|
518
658
|
return await callbacks.onGetDirectoryTree(path, maxDepth ?? 3);
|
|
519
659
|
},
|
|
660
|
+
// ── Sharing Management ──
|
|
661
|
+
getSharing: async (context) => {
|
|
662
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
663
|
+
return { sharing: metadata.sharing || null };
|
|
664
|
+
},
|
|
665
|
+
updateSharing: async (newSharing, context) => {
|
|
666
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
667
|
+
if (metadata.sharing && context?.user?.email && context.user.email !== metadata.sharing.owner) {
|
|
668
|
+
throw new Error("Only the session owner can update sharing settings");
|
|
669
|
+
}
|
|
670
|
+
if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
|
|
671
|
+
newSharing = { ...newSharing, owner: context.user.email };
|
|
672
|
+
}
|
|
673
|
+
metadata = { ...metadata, sharing: newSharing };
|
|
674
|
+
metadataVersion++;
|
|
675
|
+
notifyListeners({
|
|
676
|
+
type: "update-session",
|
|
677
|
+
sessionId,
|
|
678
|
+
metadata: { value: metadata, version: metadataVersion }
|
|
679
|
+
});
|
|
680
|
+
return { success: true, sharing: newSharing };
|
|
681
|
+
},
|
|
520
682
|
// ── Listener Registration ──
|
|
521
|
-
registerListener: async (callback) => {
|
|
683
|
+
registerListener: async (callback, context) => {
|
|
684
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
522
685
|
listeners.push(callback);
|
|
523
686
|
const replayMessages = messages.slice(-50);
|
|
524
687
|
console.log(`[HYPHA SESSION ${sessionId}] Listener registered (total: ${listeners.length}), replaying ${replayMessages.length} of ${messages.length} messages`);
|
|
@@ -1034,6 +1197,106 @@ var DefaultTransport$1 = /*#__PURE__*/Object.freeze({
|
|
|
1034
1197
|
DefaultTransport: DefaultTransport
|
|
1035
1198
|
});
|
|
1036
1199
|
|
|
1200
|
+
const SVAMP_TOOLS_DIR$1 = join(homedir(), ".svamp", "tools");
|
|
1201
|
+
const SVAMP_TOOLS_BIN$1 = join(SVAMP_TOOLS_DIR$1, "node_modules", ".bin");
|
|
1202
|
+
const SVAMP_TOOLS_RG_BIN$1 = join(SVAMP_TOOLS_DIR$1, "node_modules", "@vscode", "ripgrep", "bin");
|
|
1203
|
+
function wrapWithIsolation(originalCommand, originalArgs, config) {
|
|
1204
|
+
switch (config.method) {
|
|
1205
|
+
case "srt":
|
|
1206
|
+
return wrapWithSrt(originalCommand, originalArgs, config);
|
|
1207
|
+
case "bwrap":
|
|
1208
|
+
return wrapWithBwrap(originalCommand, originalArgs, config);
|
|
1209
|
+
case "docker":
|
|
1210
|
+
return wrapWithContainer("docker", originalCommand, originalArgs, config);
|
|
1211
|
+
case "podman":
|
|
1212
|
+
return wrapWithContainer("podman", originalCommand, originalArgs, config);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
function wrapWithSrt(command, args, config) {
|
|
1216
|
+
const settings = {
|
|
1217
|
+
filesystem: {
|
|
1218
|
+
denyRead: config.srtConfig?.filesystem?.denyRead ?? [],
|
|
1219
|
+
allowWrite: config.srtConfig?.filesystem?.allowWrite ?? [config.workspacePath],
|
|
1220
|
+
denyWrite: config.srtConfig?.filesystem?.denyWrite ?? []
|
|
1221
|
+
},
|
|
1222
|
+
network: {
|
|
1223
|
+
allowedDomains: config.srtConfig?.network?.allowedDomains ?? [],
|
|
1224
|
+
deniedDomains: config.srtConfig?.network?.deniedDomains ?? []
|
|
1225
|
+
}
|
|
1226
|
+
};
|
|
1227
|
+
const settingsPath = join(tmpdir(), `srt-settings-${process.pid}-${Date.now()}.json`);
|
|
1228
|
+
writeFileSync(settingsPath, JSON.stringify(settings));
|
|
1229
|
+
const pathParts = [SVAMP_TOOLS_BIN$1, SVAMP_TOOLS_RG_BIN$1];
|
|
1230
|
+
if (process.env.PATH) pathParts.push(process.env.PATH);
|
|
1231
|
+
return {
|
|
1232
|
+
command: config.binaryPath,
|
|
1233
|
+
args: ["--settings", settingsPath, command, ...args],
|
|
1234
|
+
env: { PATH: pathParts.join(":") },
|
|
1235
|
+
cleanupFiles: [settingsPath]
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
1238
|
+
function wrapWithBwrap(command, args, config) {
|
|
1239
|
+
const bwrapArgs = [
|
|
1240
|
+
// Mount root filesystem read-only
|
|
1241
|
+
"--ro-bind",
|
|
1242
|
+
"/",
|
|
1243
|
+
"/",
|
|
1244
|
+
// Mount workspace read-write
|
|
1245
|
+
"--bind",
|
|
1246
|
+
config.workspacePath,
|
|
1247
|
+
config.workspacePath,
|
|
1248
|
+
// Mount /tmp read-write (many tools need it)
|
|
1249
|
+
"--bind",
|
|
1250
|
+
"/tmp",
|
|
1251
|
+
"/tmp",
|
|
1252
|
+
// Mount /dev read-write
|
|
1253
|
+
"--dev",
|
|
1254
|
+
"/dev",
|
|
1255
|
+
// Mount /proc
|
|
1256
|
+
"--proc",
|
|
1257
|
+
"/proc",
|
|
1258
|
+
// Unshare network — no network access inside sandbox
|
|
1259
|
+
"--unshare-net",
|
|
1260
|
+
// Unshare PID namespace
|
|
1261
|
+
"--unshare-pid",
|
|
1262
|
+
// Die when parent dies
|
|
1263
|
+
"--die-with-parent",
|
|
1264
|
+
// The actual command
|
|
1265
|
+
"--",
|
|
1266
|
+
command,
|
|
1267
|
+
...args
|
|
1268
|
+
];
|
|
1269
|
+
return {
|
|
1270
|
+
command: config.binaryPath,
|
|
1271
|
+
args: bwrapArgs
|
|
1272
|
+
};
|
|
1273
|
+
}
|
|
1274
|
+
function wrapWithContainer(runtime, command, args, config) {
|
|
1275
|
+
const image = config.containerConfig?.image || "node:lts-slim";
|
|
1276
|
+
const networkMode = config.containerConfig?.networkMode || "none";
|
|
1277
|
+
const containerArgs = [
|
|
1278
|
+
"run",
|
|
1279
|
+
"--rm",
|
|
1280
|
+
"-i",
|
|
1281
|
+
// interactive (for stdin/stdout piping)
|
|
1282
|
+
`--network=${networkMode}`,
|
|
1283
|
+
"-v",
|
|
1284
|
+
`${config.workspacePath}:${config.workspacePath}`,
|
|
1285
|
+
"-w",
|
|
1286
|
+
config.workspacePath
|
|
1287
|
+
];
|
|
1288
|
+
if (config.containerConfig?.extraMounts) {
|
|
1289
|
+
for (const mount of config.containerConfig.extraMounts) {
|
|
1290
|
+
containerArgs.push("-v", mount);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
containerArgs.push(image, command, ...args);
|
|
1294
|
+
return {
|
|
1295
|
+
command: config.binaryPath,
|
|
1296
|
+
args: containerArgs
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1037
1300
|
const DEFAULT_IDLE_TIMEOUT_MS = 500;
|
|
1038
1301
|
const DEFAULT_TOOL_CALL_TIMEOUT_MS = 12e4;
|
|
1039
1302
|
function parseArgsFromContent(content) {
|
|
@@ -1352,21 +1615,41 @@ class AcpBackend {
|
|
|
1352
1615
|
let startupStatusErrorEmitted = false;
|
|
1353
1616
|
try {
|
|
1354
1617
|
const args = this.options.args || [];
|
|
1618
|
+
let spawnCommand = this.options.command;
|
|
1619
|
+
let spawnArgs = args;
|
|
1620
|
+
let isoEnv = {};
|
|
1621
|
+
let isoCleanupFiles = [];
|
|
1622
|
+
if (this.options.isolationConfig) {
|
|
1623
|
+
const wrapped = wrapWithIsolation(spawnCommand, spawnArgs, this.options.isolationConfig);
|
|
1624
|
+
spawnCommand = wrapped.command;
|
|
1625
|
+
spawnArgs = wrapped.args;
|
|
1626
|
+
if (wrapped.env) isoEnv = wrapped.env;
|
|
1627
|
+
if (wrapped.cleanupFiles) isoCleanupFiles = wrapped.cleanupFiles;
|
|
1628
|
+
this.log(`[ACP] Isolation: ${this.options.isolationConfig.method}`);
|
|
1629
|
+
}
|
|
1630
|
+
const spawnEnv = { ...process.env, ...this.options.env, ...isoEnv };
|
|
1355
1631
|
if (process.platform === "win32") {
|
|
1356
|
-
const fullCommand = [
|
|
1632
|
+
const fullCommand = [spawnCommand, ...spawnArgs].join(" ");
|
|
1357
1633
|
this.process = spawn("cmd.exe", ["/c", fullCommand], {
|
|
1358
1634
|
cwd: this.options.cwd,
|
|
1359
|
-
env:
|
|
1635
|
+
env: spawnEnv,
|
|
1360
1636
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1361
1637
|
windowsHide: true
|
|
1362
1638
|
});
|
|
1363
1639
|
} else {
|
|
1364
|
-
this.process = spawn(
|
|
1640
|
+
this.process = spawn(spawnCommand, spawnArgs, {
|
|
1365
1641
|
cwd: this.options.cwd,
|
|
1366
|
-
env:
|
|
1642
|
+
env: spawnEnv,
|
|
1367
1643
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1368
1644
|
});
|
|
1369
1645
|
}
|
|
1646
|
+
if (isoCleanupFiles.length > 0) {
|
|
1647
|
+
this.process.on("exit", async () => {
|
|
1648
|
+
const { rm } = await import('node:fs/promises');
|
|
1649
|
+
for (const f of isoCleanupFiles) rm(f, { force: true }).catch(() => {
|
|
1650
|
+
});
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1370
1653
|
if (!this.process.stdin || !this.process.stdout || !this.process.stderr) {
|
|
1371
1654
|
throw new Error("Failed to create stdio pipes");
|
|
1372
1655
|
}
|
|
@@ -1885,6 +2168,9 @@ const KNOWN_ACP_AGENTS = {
|
|
|
1885
2168
|
gemini: { command: "gemini", args: ["--experimental-acp"] },
|
|
1886
2169
|
opencode: { command: "opencode", args: ["acp"] }
|
|
1887
2170
|
};
|
|
2171
|
+
const KNOWN_MCP_AGENTS = {
|
|
2172
|
+
codex: { command: "codex", args: ["mcp-server"] }
|
|
2173
|
+
};
|
|
1888
2174
|
function resolveAcpAgentConfig(cliArgs) {
|
|
1889
2175
|
if (cliArgs.length === 0) {
|
|
1890
2176
|
throw new Error("Usage: svamp agent <agent-name> or svamp agent -- <command> [args]");
|
|
@@ -1901,13 +2187,21 @@ function resolveAcpAgentConfig(cliArgs) {
|
|
|
1901
2187
|
};
|
|
1902
2188
|
}
|
|
1903
2189
|
const agentName = cliArgs[0];
|
|
1904
|
-
const
|
|
1905
|
-
if (
|
|
2190
|
+
const knownAcp = KNOWN_ACP_AGENTS[agentName];
|
|
2191
|
+
if (knownAcp) {
|
|
1906
2192
|
const passthroughArgs = cliArgs.slice(1).filter((arg) => !(agentName === "opencode" && arg === "--acp"));
|
|
1907
2193
|
return {
|
|
1908
2194
|
agentName,
|
|
1909
|
-
command:
|
|
1910
|
-
args: [...
|
|
2195
|
+
command: knownAcp.command,
|
|
2196
|
+
args: [...knownAcp.args, ...passthroughArgs]
|
|
2197
|
+
};
|
|
2198
|
+
}
|
|
2199
|
+
const knownMcp = KNOWN_MCP_AGENTS[agentName];
|
|
2200
|
+
if (knownMcp) {
|
|
2201
|
+
return {
|
|
2202
|
+
agentName,
|
|
2203
|
+
command: knownMcp.command,
|
|
2204
|
+
args: [...knownMcp.args, ...cliArgs.slice(1)]
|
|
1911
2205
|
};
|
|
1912
2206
|
}
|
|
1913
2207
|
return {
|
|
@@ -1920,6 +2214,7 @@ function resolveAcpAgentConfig(cliArgs) {
|
|
|
1920
2214
|
var acpAgentConfig = /*#__PURE__*/Object.freeze({
|
|
1921
2215
|
__proto__: null,
|
|
1922
2216
|
KNOWN_ACP_AGENTS: KNOWN_ACP_AGENTS,
|
|
2217
|
+
KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS,
|
|
1923
2218
|
resolveAcpAgentConfig: resolveAcpAgentConfig
|
|
1924
2219
|
});
|
|
1925
2220
|
|
|
@@ -2096,6 +2391,426 @@ class HyphaPermissionHandler {
|
|
|
2096
2391
|
}
|
|
2097
2392
|
}
|
|
2098
2393
|
|
|
2394
|
+
const DEFAULT_TIMEOUT = 14 * 24 * 60 * 60 * 1e3;
|
|
2395
|
+
function getCodexMcpCommand() {
|
|
2396
|
+
try {
|
|
2397
|
+
const version = execSync("codex --version", { encoding: "utf8" }).trim();
|
|
2398
|
+
const match = version.match(/codex-cli\s+(\d+\.\d+\.\d+(?:-alpha\.\d+)?)/);
|
|
2399
|
+
if (!match) return "mcp-server";
|
|
2400
|
+
const versionStr = match[1];
|
|
2401
|
+
const [major, minor, patch] = versionStr.split(/[-.]/).map(Number);
|
|
2402
|
+
if (major > 0 || minor > 43) return "mcp-server";
|
|
2403
|
+
if (minor === 43 && patch === 0) {
|
|
2404
|
+
if (versionStr.includes("-alpha.")) {
|
|
2405
|
+
const alphaNum = parseInt(versionStr.split("-alpha.")[1]);
|
|
2406
|
+
return alphaNum >= 5 ? "mcp-server" : "mcp";
|
|
2407
|
+
}
|
|
2408
|
+
return "mcp-server";
|
|
2409
|
+
}
|
|
2410
|
+
return "mcp";
|
|
2411
|
+
} catch {
|
|
2412
|
+
return null;
|
|
2413
|
+
}
|
|
2414
|
+
}
|
|
2415
|
+
class CodexMcpBackend {
|
|
2416
|
+
listeners = [];
|
|
2417
|
+
client;
|
|
2418
|
+
transport = null;
|
|
2419
|
+
disposed = false;
|
|
2420
|
+
codexSessionId = null;
|
|
2421
|
+
conversationId = null;
|
|
2422
|
+
svampSessionId = null;
|
|
2423
|
+
log;
|
|
2424
|
+
options;
|
|
2425
|
+
connected = false;
|
|
2426
|
+
// Pending elicitation approvals
|
|
2427
|
+
pendingApprovals = /* @__PURE__ */ new Map();
|
|
2428
|
+
// Temp files from isolation wrapping (cleaned up on disconnect)
|
|
2429
|
+
_isolationCleanupFiles = [];
|
|
2430
|
+
constructor(options) {
|
|
2431
|
+
this.options = options;
|
|
2432
|
+
this.log = options.log || (() => {
|
|
2433
|
+
});
|
|
2434
|
+
this.client = new Client(
|
|
2435
|
+
{ name: "svamp-codex-client", version: "1.0.0" },
|
|
2436
|
+
{ capabilities: { elicitation: {} } }
|
|
2437
|
+
);
|
|
2438
|
+
this.client.setNotificationHandler(z.object({
|
|
2439
|
+
method: z.literal("codex/event"),
|
|
2440
|
+
params: z.object({ msg: z.any() })
|
|
2441
|
+
}).passthrough(), (data) => {
|
|
2442
|
+
const msg = data.params.msg;
|
|
2443
|
+
this.updateIdentifiersFromEvent(msg);
|
|
2444
|
+
this.handleCodexEvent(msg);
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
// ── AgentBackend interface ──────────────────────────────────────────
|
|
2448
|
+
onMessage(handler) {
|
|
2449
|
+
this.listeners.push(handler);
|
|
2450
|
+
}
|
|
2451
|
+
offMessage(handler) {
|
|
2452
|
+
const idx = this.listeners.indexOf(handler);
|
|
2453
|
+
if (idx !== -1) this.listeners.splice(idx, 1);
|
|
2454
|
+
}
|
|
2455
|
+
async startSession(initialPrompt) {
|
|
2456
|
+
const sessionId = randomUUID();
|
|
2457
|
+
this.svampSessionId = sessionId;
|
|
2458
|
+
this.emit({ type: "status", status: "starting" });
|
|
2459
|
+
await this.connect();
|
|
2460
|
+
this.emit({ type: "status", status: "idle" });
|
|
2461
|
+
if (initialPrompt) {
|
|
2462
|
+
this.sendPrompt(sessionId, initialPrompt).catch((err) => {
|
|
2463
|
+
this.log(`[Codex] Error sending initial prompt: ${err.message}`);
|
|
2464
|
+
this.emit({ type: "status", status: "error", detail: err.message });
|
|
2465
|
+
});
|
|
2466
|
+
}
|
|
2467
|
+
return { sessionId };
|
|
2468
|
+
}
|
|
2469
|
+
async sendPrompt(sessionId, prompt) {
|
|
2470
|
+
if (!this.connected) throw new Error("Codex not connected");
|
|
2471
|
+
this.emit({ type: "status", status: "running" });
|
|
2472
|
+
try {
|
|
2473
|
+
let response;
|
|
2474
|
+
if (this.codexSessionId) {
|
|
2475
|
+
response = await this.continueSession(prompt);
|
|
2476
|
+
} else {
|
|
2477
|
+
const config = {
|
|
2478
|
+
prompt,
|
|
2479
|
+
cwd: this.options.cwd,
|
|
2480
|
+
...this.options.model ? { model: this.options.model } : {}
|
|
2481
|
+
};
|
|
2482
|
+
response = await this.startCodexSession(config);
|
|
2483
|
+
}
|
|
2484
|
+
if (response?.content && Array.isArray(response.content)) {
|
|
2485
|
+
for (const block of response.content) {
|
|
2486
|
+
if (block.type === "text" && block.text) {
|
|
2487
|
+
this.emit({ type: "model-output", fullText: block.text });
|
|
2488
|
+
}
|
|
2489
|
+
}
|
|
2490
|
+
}
|
|
2491
|
+
} catch (err) {
|
|
2492
|
+
this.log(`[Codex] Error in sendPrompt: ${err.message}`);
|
|
2493
|
+
this.emit({ type: "status", status: "error", detail: err.message });
|
|
2494
|
+
throw err;
|
|
2495
|
+
} finally {
|
|
2496
|
+
this.emit({ type: "status", status: "idle" });
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
async cancel(_sessionId) {
|
|
2500
|
+
this.log("[Codex] Cancel requested");
|
|
2501
|
+
this.emit({ type: "status", status: "idle" });
|
|
2502
|
+
}
|
|
2503
|
+
async respondToPermission(requestId, approved) {
|
|
2504
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
2505
|
+
if (pending) {
|
|
2506
|
+
this.pendingApprovals.delete(requestId);
|
|
2507
|
+
pending.resolve({
|
|
2508
|
+
action: approved ? "accept" : "decline",
|
|
2509
|
+
content: { approval: approved ? "approve" : "deny" }
|
|
2510
|
+
});
|
|
2511
|
+
this.emit({ type: "permission-response", id: requestId, approved });
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
async dispose() {
|
|
2515
|
+
if (this.disposed) return;
|
|
2516
|
+
this.disposed = true;
|
|
2517
|
+
this.log("[Codex] Disposing backend");
|
|
2518
|
+
for (const [, pending] of this.pendingApprovals) {
|
|
2519
|
+
pending.resolve({ action: "decline" });
|
|
2520
|
+
}
|
|
2521
|
+
this.pendingApprovals.clear();
|
|
2522
|
+
await this.disconnect();
|
|
2523
|
+
this.listeners = [];
|
|
2524
|
+
}
|
|
2525
|
+
/** Get the transport's child process PID for tracking */
|
|
2526
|
+
get pid() {
|
|
2527
|
+
return this.transport?.pid ?? null;
|
|
2528
|
+
}
|
|
2529
|
+
/**
|
|
2530
|
+
* Return a process-like object for TrackedSession.childProcess.
|
|
2531
|
+
* We expose a minimal { kill } object that closes the transport.
|
|
2532
|
+
*/
|
|
2533
|
+
getProcess() {
|
|
2534
|
+
if (!this.transport) return null;
|
|
2535
|
+
const pid = this.pid;
|
|
2536
|
+
return {
|
|
2537
|
+
kill: (signal) => {
|
|
2538
|
+
if (pid) {
|
|
2539
|
+
try {
|
|
2540
|
+
process.kill(pid, signal || "SIGTERM");
|
|
2541
|
+
} catch {
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
// ── MCP connection ─────────────────────────────────────────────────
|
|
2548
|
+
async connect() {
|
|
2549
|
+
if (this.connected) return;
|
|
2550
|
+
const mcpCommand = getCodexMcpCommand();
|
|
2551
|
+
if (mcpCommand === null) {
|
|
2552
|
+
throw new Error(
|
|
2553
|
+
"Codex CLI not found or not executable.\n\nTo install codex:\n npm install -g @openai/codex\n"
|
|
2554
|
+
);
|
|
2555
|
+
}
|
|
2556
|
+
this.log(`[Codex] Connecting via: codex ${mcpCommand}`);
|
|
2557
|
+
const env = {};
|
|
2558
|
+
for (const key of Object.keys(process.env)) {
|
|
2559
|
+
const value = process.env[key];
|
|
2560
|
+
if (typeof value === "string") env[key] = value;
|
|
2561
|
+
}
|
|
2562
|
+
if (this.options.env) {
|
|
2563
|
+
Object.assign(env, this.options.env);
|
|
2564
|
+
}
|
|
2565
|
+
const rolloutFilter = "codex_core::rollout::list=off";
|
|
2566
|
+
const existingRustLog = env.RUST_LOG?.trim();
|
|
2567
|
+
if (!existingRustLog) {
|
|
2568
|
+
env.RUST_LOG = rolloutFilter;
|
|
2569
|
+
} else if (!existingRustLog.includes("codex_core::rollout::list=")) {
|
|
2570
|
+
env.RUST_LOG = `${existingRustLog},${rolloutFilter}`;
|
|
2571
|
+
}
|
|
2572
|
+
let transportCommand = "codex";
|
|
2573
|
+
let transportArgs = [mcpCommand];
|
|
2574
|
+
if (this.options.isolationConfig) {
|
|
2575
|
+
const wrapped = wrapWithIsolation(transportCommand, transportArgs, this.options.isolationConfig);
|
|
2576
|
+
transportCommand = wrapped.command;
|
|
2577
|
+
transportArgs = wrapped.args;
|
|
2578
|
+
if (wrapped.env) Object.assign(env, wrapped.env);
|
|
2579
|
+
if (wrapped.cleanupFiles) {
|
|
2580
|
+
this._isolationCleanupFiles = wrapped.cleanupFiles;
|
|
2581
|
+
}
|
|
2582
|
+
this.log(`[Codex] Isolation: ${this.options.isolationConfig.method}`);
|
|
2583
|
+
}
|
|
2584
|
+
this.transport = new StdioClientTransport({
|
|
2585
|
+
command: transportCommand,
|
|
2586
|
+
args: transportArgs,
|
|
2587
|
+
env
|
|
2588
|
+
});
|
|
2589
|
+
this.registerPermissionHandlers();
|
|
2590
|
+
try {
|
|
2591
|
+
await this.client.connect(this.transport);
|
|
2592
|
+
this.connected = true;
|
|
2593
|
+
this.log("[Codex] MCP connection established");
|
|
2594
|
+
} catch (err) {
|
|
2595
|
+
this.transport = null;
|
|
2596
|
+
throw err;
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
async disconnect() {
|
|
2600
|
+
if (!this.connected) return;
|
|
2601
|
+
const pid = this.pid;
|
|
2602
|
+
this.log(`[Codex] Disconnecting (pid=${pid ?? "none"})`);
|
|
2603
|
+
try {
|
|
2604
|
+
await this.client.close();
|
|
2605
|
+
} catch {
|
|
2606
|
+
try {
|
|
2607
|
+
await this.transport?.close?.();
|
|
2608
|
+
} catch {
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
if (pid) {
|
|
2612
|
+
try {
|
|
2613
|
+
process.kill(pid, 0);
|
|
2614
|
+
try {
|
|
2615
|
+
process.kill(pid, "SIGKILL");
|
|
2616
|
+
} catch {
|
|
2617
|
+
}
|
|
2618
|
+
} catch {
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
this.transport = null;
|
|
2622
|
+
this.connected = false;
|
|
2623
|
+
if (this._isolationCleanupFiles.length > 0) {
|
|
2624
|
+
const { rm } = await import('node:fs/promises');
|
|
2625
|
+
for (const f of this._isolationCleanupFiles) rm(f, { force: true }).catch(() => {
|
|
2626
|
+
});
|
|
2627
|
+
this._isolationCleanupFiles = [];
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
// ── Permission handling ────────────────────────────────────────────
|
|
2631
|
+
registerPermissionHandlers() {
|
|
2632
|
+
this.client.setRequestHandler(
|
|
2633
|
+
ElicitRequestSchema,
|
|
2634
|
+
async (request) => {
|
|
2635
|
+
const params = request.params;
|
|
2636
|
+
const callId = params.codex_call_id || randomUUID();
|
|
2637
|
+
this.log(`[Codex] Elicitation request: ${params.message}`);
|
|
2638
|
+
this.emit({
|
|
2639
|
+
type: "permission-request",
|
|
2640
|
+
id: callId,
|
|
2641
|
+
reason: params.codex_command ? `Execute: ${Array.isArray(params.codex_command) ? params.codex_command.join(" ") : params.codex_command}` : params.message || "Codex requires approval",
|
|
2642
|
+
payload: {
|
|
2643
|
+
toolName: "CodexBash",
|
|
2644
|
+
input: {
|
|
2645
|
+
command: params.codex_command,
|
|
2646
|
+
cwd: params.codex_cwd
|
|
2647
|
+
}
|
|
2648
|
+
}
|
|
2649
|
+
});
|
|
2650
|
+
return new Promise((resolve) => {
|
|
2651
|
+
this.pendingApprovals.set(callId, { resolve });
|
|
2652
|
+
setTimeout(() => {
|
|
2653
|
+
if (this.pendingApprovals.has(callId)) {
|
|
2654
|
+
this.pendingApprovals.delete(callId);
|
|
2655
|
+
resolve({ action: "decline" });
|
|
2656
|
+
}
|
|
2657
|
+
}, 5 * 60 * 1e3);
|
|
2658
|
+
});
|
|
2659
|
+
}
|
|
2660
|
+
);
|
|
2661
|
+
}
|
|
2662
|
+
// ── Codex MCP tools ────────────────────────────────────────────────
|
|
2663
|
+
async startCodexSession(config) {
|
|
2664
|
+
const response = await this.client.callTool({
|
|
2665
|
+
name: "codex",
|
|
2666
|
+
arguments: config
|
|
2667
|
+
}, void 0, { timeout: DEFAULT_TIMEOUT });
|
|
2668
|
+
this.extractIdentifiers(response);
|
|
2669
|
+
return response;
|
|
2670
|
+
}
|
|
2671
|
+
async continueSession(prompt) {
|
|
2672
|
+
if (!this.conversationId) {
|
|
2673
|
+
this.conversationId = this.codexSessionId;
|
|
2674
|
+
}
|
|
2675
|
+
const response = await this.client.callTool({
|
|
2676
|
+
name: "codex-reply",
|
|
2677
|
+
arguments: {
|
|
2678
|
+
sessionId: this.codexSessionId,
|
|
2679
|
+
conversationId: this.conversationId,
|
|
2680
|
+
prompt
|
|
2681
|
+
}
|
|
2682
|
+
}, void 0, { timeout: DEFAULT_TIMEOUT });
|
|
2683
|
+
this.extractIdentifiers(response);
|
|
2684
|
+
return response;
|
|
2685
|
+
}
|
|
2686
|
+
// ── Event handling ─────────────────────────────────────────────────
|
|
2687
|
+
handleCodexEvent(event) {
|
|
2688
|
+
if (!event || typeof event !== "object") return;
|
|
2689
|
+
const eventType = event.type;
|
|
2690
|
+
this.log(`[Codex] Event: ${eventType}`);
|
|
2691
|
+
switch (eventType) {
|
|
2692
|
+
case "task_started":
|
|
2693
|
+
this.emit({ type: "status", status: "running" });
|
|
2694
|
+
break;
|
|
2695
|
+
case "task_complete":
|
|
2696
|
+
case "turn_aborted":
|
|
2697
|
+
this.emit({ type: "status", status: "idle" });
|
|
2698
|
+
break;
|
|
2699
|
+
case "agent_message": {
|
|
2700
|
+
const content = event.content;
|
|
2701
|
+
if (Array.isArray(content)) {
|
|
2702
|
+
for (const block of content) {
|
|
2703
|
+
if (block.type === "output_text" && block.text) {
|
|
2704
|
+
this.emit({ type: "model-output", fullText: block.text });
|
|
2705
|
+
}
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
break;
|
|
2709
|
+
}
|
|
2710
|
+
case "agent_reasoning_delta": {
|
|
2711
|
+
const text = event.delta?.text || event.text;
|
|
2712
|
+
if (text) {
|
|
2713
|
+
this.emit({ type: "event", name: "thinking", payload: { text } });
|
|
2714
|
+
}
|
|
2715
|
+
break;
|
|
2716
|
+
}
|
|
2717
|
+
case "agent_reasoning": {
|
|
2718
|
+
const text = event.text || (Array.isArray(event.content) ? event.content.map((c) => c.text).join("") : "");
|
|
2719
|
+
if (text) {
|
|
2720
|
+
this.emit({ type: "event", name: "thinking", payload: { text } });
|
|
2721
|
+
}
|
|
2722
|
+
break;
|
|
2723
|
+
}
|
|
2724
|
+
case "exec_command_begin": {
|
|
2725
|
+
const callId = event.call_id || event.callId || randomUUID();
|
|
2726
|
+
const command = Array.isArray(event.command) ? event.command.join(" ") : String(event.command || "");
|
|
2727
|
+
this.emit({
|
|
2728
|
+
type: "tool-call",
|
|
2729
|
+
toolName: "CodexBash",
|
|
2730
|
+
callId,
|
|
2731
|
+
args: { command, cwd: event.cwd }
|
|
2732
|
+
});
|
|
2733
|
+
break;
|
|
2734
|
+
}
|
|
2735
|
+
case "exec_command_end": {
|
|
2736
|
+
const callId = event.call_id || event.callId || "";
|
|
2737
|
+
this.emit({
|
|
2738
|
+
type: "tool-result",
|
|
2739
|
+
toolName: "CodexBash",
|
|
2740
|
+
callId,
|
|
2741
|
+
result: {
|
|
2742
|
+
exitCode: event.exit_code ?? event.exitCode,
|
|
2743
|
+
stdout: event.stdout || "",
|
|
2744
|
+
stderr: event.stderr || ""
|
|
2745
|
+
}
|
|
2746
|
+
});
|
|
2747
|
+
break;
|
|
2748
|
+
}
|
|
2749
|
+
case "patch_apply_begin": {
|
|
2750
|
+
const callId = event.call_id || event.callId || randomUUID();
|
|
2751
|
+
this.emit({
|
|
2752
|
+
type: "tool-call",
|
|
2753
|
+
toolName: "CodexPatch",
|
|
2754
|
+
callId,
|
|
2755
|
+
args: { filePath: event.file_path || event.filePath, patch: event.patch }
|
|
2756
|
+
});
|
|
2757
|
+
break;
|
|
2758
|
+
}
|
|
2759
|
+
case "patch_apply_end": {
|
|
2760
|
+
const callId = event.call_id || event.callId || "";
|
|
2761
|
+
this.emit({
|
|
2762
|
+
type: "tool-result",
|
|
2763
|
+
toolName: "CodexPatch",
|
|
2764
|
+
callId,
|
|
2765
|
+
result: {
|
|
2766
|
+
filePath: event.file_path || event.filePath,
|
|
2767
|
+
applied: event.applied,
|
|
2768
|
+
error: event.error
|
|
2769
|
+
}
|
|
2770
|
+
});
|
|
2771
|
+
break;
|
|
2772
|
+
}
|
|
2773
|
+
default:
|
|
2774
|
+
this.log(`[Codex] Unhandled event: ${eventType}`);
|
|
2775
|
+
break;
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
// ── Identifier extraction ──────────────────────────────────────────
|
|
2779
|
+
updateIdentifiersFromEvent(event) {
|
|
2780
|
+
if (!event || typeof event !== "object") return;
|
|
2781
|
+
const candidates = [event];
|
|
2782
|
+
if (event.data && typeof event.data === "object") candidates.push(event.data);
|
|
2783
|
+
for (const c of candidates) {
|
|
2784
|
+
const sid = c.session_id ?? c.sessionId;
|
|
2785
|
+
if (sid) this.codexSessionId = sid;
|
|
2786
|
+
const cid = c.conversation_id ?? c.conversationId;
|
|
2787
|
+
if (cid) this.conversationId = cid;
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
extractIdentifiers(response) {
|
|
2791
|
+
const meta = response?.meta || {};
|
|
2792
|
+
this.codexSessionId = meta.sessionId || response?.sessionId || this.codexSessionId;
|
|
2793
|
+
this.conversationId = meta.conversationId || response?.conversationId || this.conversationId;
|
|
2794
|
+
const content = response?.content;
|
|
2795
|
+
if (Array.isArray(content)) {
|
|
2796
|
+
for (const item of content) {
|
|
2797
|
+
if (!this.codexSessionId && item?.sessionId) this.codexSessionId = item.sessionId;
|
|
2798
|
+
if (!this.conversationId && item?.conversationId) this.conversationId = item.conversationId;
|
|
2799
|
+
}
|
|
2800
|
+
}
|
|
2801
|
+
}
|
|
2802
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
2803
|
+
emit(msg) {
|
|
2804
|
+
if (this.disposed) return;
|
|
2805
|
+
for (const h of this.listeners) {
|
|
2806
|
+
try {
|
|
2807
|
+
h(msg);
|
|
2808
|
+
} catch {
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2099
2814
|
const GEMINI_TIMEOUTS = {
|
|
2100
2815
|
init: 12e4,
|
|
2101
2816
|
toolCall: 12e4,
|
|
@@ -2231,6 +2946,264 @@ var GeminiTransport$1 = /*#__PURE__*/Object.freeze({
|
|
|
2231
2946
|
GeminiTransport: GeminiTransport
|
|
2232
2947
|
});
|
|
2233
2948
|
|
|
2949
|
+
const execFileAsync = promisify(execFile);
|
|
2950
|
+
const SVAMP_TOOLS_DIR = join(homedir(), ".svamp", "tools");
|
|
2951
|
+
const SVAMP_TOOLS_BIN = join(SVAMP_TOOLS_DIR, "node_modules", ".bin");
|
|
2952
|
+
const SVAMP_TOOLS_RG_BIN = join(SVAMP_TOOLS_DIR, "node_modules", "@vscode", "ripgrep", "bin");
|
|
2953
|
+
function getToolsPath() {
|
|
2954
|
+
const parts = [SVAMP_TOOLS_BIN, SVAMP_TOOLS_RG_BIN];
|
|
2955
|
+
if (process.env.PATH) parts.push(process.env.PATH);
|
|
2956
|
+
return parts.join(":");
|
|
2957
|
+
}
|
|
2958
|
+
async function checkCommand(command, versionArgs, extraEnv) {
|
|
2959
|
+
const envWithPath = extraEnv ? { ...process.env, ...extraEnv } : void 0;
|
|
2960
|
+
try {
|
|
2961
|
+
const { stdout } = await execFileAsync(command, versionArgs, {
|
|
2962
|
+
timeout: 5e3,
|
|
2963
|
+
env: envWithPath
|
|
2964
|
+
});
|
|
2965
|
+
const version = stdout.trim().split("\n")[0];
|
|
2966
|
+
return { found: true, version, path: command };
|
|
2967
|
+
} catch {
|
|
2968
|
+
}
|
|
2969
|
+
const localPath = join(SVAMP_TOOLS_BIN, command);
|
|
2970
|
+
try {
|
|
2971
|
+
const { stdout } = await execFileAsync(localPath, versionArgs, {
|
|
2972
|
+
timeout: 5e3,
|
|
2973
|
+
env: envWithPath
|
|
2974
|
+
});
|
|
2975
|
+
const version = stdout.trim().split("\n")[0];
|
|
2976
|
+
return { found: true, version, path: localPath };
|
|
2977
|
+
} catch {
|
|
2978
|
+
}
|
|
2979
|
+
return { found: false };
|
|
2980
|
+
}
|
|
2981
|
+
async function installSrt() {
|
|
2982
|
+
const platform = process.platform;
|
|
2983
|
+
if (platform !== "darwin" && platform !== "linux") {
|
|
2984
|
+
return false;
|
|
2985
|
+
}
|
|
2986
|
+
console.log("[isolation] srt not found. Installing @anthropic-ai/sandbox-runtime...");
|
|
2987
|
+
try {
|
|
2988
|
+
await mkdir(SVAMP_TOOLS_DIR, { recursive: true });
|
|
2989
|
+
await execFileAsync("npm", [
|
|
2990
|
+
"install",
|
|
2991
|
+
"--prefix",
|
|
2992
|
+
SVAMP_TOOLS_DIR,
|
|
2993
|
+
"@anthropic-ai/sandbox-runtime@latest",
|
|
2994
|
+
"@vscode/ripgrep"
|
|
2995
|
+
], { timeout: 12e4 });
|
|
2996
|
+
const srtPath = join(SVAMP_TOOLS_BIN, "srt");
|
|
2997
|
+
try {
|
|
2998
|
+
await access(srtPath);
|
|
2999
|
+
console.log(`[isolation] srt installed successfully at ${srtPath}`);
|
|
3000
|
+
return true;
|
|
3001
|
+
} catch {
|
|
3002
|
+
console.warn("[isolation] npm install completed but srt binary not found");
|
|
3003
|
+
return false;
|
|
3004
|
+
}
|
|
3005
|
+
} catch (e) {
|
|
3006
|
+
console.warn(`[isolation] Failed to install srt: ${e.message}`);
|
|
3007
|
+
return false;
|
|
3008
|
+
}
|
|
3009
|
+
}
|
|
3010
|
+
async function parseIsolationTestOutput(stdout, probeFile) {
|
|
3011
|
+
const output = stdout.trim();
|
|
3012
|
+
const wMatch = output.match(/W=(\d+)/);
|
|
3013
|
+
const pMatch = output.match(/P=(\d+)/);
|
|
3014
|
+
if (!wMatch || !pMatch) {
|
|
3015
|
+
return { passed: false, error: `unexpected test output: ${output}` };
|
|
3016
|
+
}
|
|
3017
|
+
const workspaceWriteOk = wMatch[1] === "0";
|
|
3018
|
+
const probeExitCode = parseInt(pMatch[1], 10);
|
|
3019
|
+
if (!workspaceWriteOk) {
|
|
3020
|
+
return { passed: false, error: "sandbox blocked writes to allowed workspace path" };
|
|
3021
|
+
}
|
|
3022
|
+
if (probeExitCode === 0) {
|
|
3023
|
+
try {
|
|
3024
|
+
await access(probeFile);
|
|
3025
|
+
return {
|
|
3026
|
+
passed: false,
|
|
3027
|
+
error: "writes outside workspace were NOT blocked \u2014 file leaked to host filesystem"
|
|
3028
|
+
};
|
|
3029
|
+
} catch {
|
|
3030
|
+
return { passed: true };
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
return { passed: true };
|
|
3034
|
+
}
|
|
3035
|
+
async function verifySrtIsolation(binaryPath) {
|
|
3036
|
+
const testBase = "/tmp";
|
|
3037
|
+
const workDir = await mkdtemp(join(testBase, "svamp-iso-work-"));
|
|
3038
|
+
const probeDir = await mkdtemp(join(testBase, "svamp-iso-probe-"));
|
|
3039
|
+
const probeFile = join(probeDir, "leak-test");
|
|
3040
|
+
const settingsFile = join(workDir, "srt-settings.json");
|
|
3041
|
+
try {
|
|
3042
|
+
await writeFile(settingsFile, JSON.stringify({
|
|
3043
|
+
filesystem: {
|
|
3044
|
+
denyRead: [],
|
|
3045
|
+
allowWrite: [workDir],
|
|
3046
|
+
denyWrite: []
|
|
3047
|
+
},
|
|
3048
|
+
network: {
|
|
3049
|
+
allowedDomains: [],
|
|
3050
|
+
deniedDomains: []
|
|
3051
|
+
}
|
|
3052
|
+
}));
|
|
3053
|
+
const testScript = [
|
|
3054
|
+
`echo ok > "${workDir}/test" 2>/dev/null; W=$?`,
|
|
3055
|
+
`echo leak > "${probeFile}" 2>/dev/null; P=$?`,
|
|
3056
|
+
`echo "W=$W P=$P"`
|
|
3057
|
+
].join("; ");
|
|
3058
|
+
const { stdout } = await execFileAsync(binaryPath, [
|
|
3059
|
+
"--settings",
|
|
3060
|
+
settingsFile,
|
|
3061
|
+
"sh",
|
|
3062
|
+
"-c",
|
|
3063
|
+
testScript
|
|
3064
|
+
], {
|
|
3065
|
+
timeout: 15e3,
|
|
3066
|
+
env: { ...process.env, PATH: getToolsPath() }
|
|
3067
|
+
});
|
|
3068
|
+
return parseIsolationTestOutput(stdout, probeFile);
|
|
3069
|
+
} catch (e) {
|
|
3070
|
+
return { passed: false, error: e.message };
|
|
3071
|
+
} finally {
|
|
3072
|
+
await rm(workDir, { recursive: true, force: true }).catch(() => {
|
|
3073
|
+
});
|
|
3074
|
+
await rm(probeDir, { recursive: true, force: true }).catch(() => {
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
async function verifyBwrapIsolation(binaryPath) {
|
|
3079
|
+
const testBase = "/tmp";
|
|
3080
|
+
const workDir = await mkdtemp(join(testBase, "svamp-iso-work-"));
|
|
3081
|
+
const probeDir = await mkdtemp(join(testBase, "svamp-iso-probe-"));
|
|
3082
|
+
const probeFile = join(probeDir, "leak-test");
|
|
3083
|
+
try {
|
|
3084
|
+
const testScript = [
|
|
3085
|
+
`echo ok > "${workDir}/test" 2>/dev/null; W=$?`,
|
|
3086
|
+
`echo leak > "${probeFile}" 2>/dev/null; P=$?`,
|
|
3087
|
+
`echo "W=$W P=$P"`
|
|
3088
|
+
].join("; ");
|
|
3089
|
+
const { stdout } = await execFileAsync(binaryPath, [
|
|
3090
|
+
"--ro-bind",
|
|
3091
|
+
"/",
|
|
3092
|
+
"/",
|
|
3093
|
+
"--bind",
|
|
3094
|
+
workDir,
|
|
3095
|
+
workDir,
|
|
3096
|
+
"--dev",
|
|
3097
|
+
"/dev",
|
|
3098
|
+
"--proc",
|
|
3099
|
+
"/proc",
|
|
3100
|
+
"--unshare-pid",
|
|
3101
|
+
"--die-with-parent",
|
|
3102
|
+
"--",
|
|
3103
|
+
"sh",
|
|
3104
|
+
"-c",
|
|
3105
|
+
testScript
|
|
3106
|
+
], { timeout: 15e3 });
|
|
3107
|
+
return parseIsolationTestOutput(stdout, probeFile);
|
|
3108
|
+
} catch (e) {
|
|
3109
|
+
return { passed: false, error: e.message };
|
|
3110
|
+
} finally {
|
|
3111
|
+
await rm(workDir, { recursive: true, force: true }).catch(() => {
|
|
3112
|
+
});
|
|
3113
|
+
await rm(probeDir, { recursive: true, force: true }).catch(() => {
|
|
3114
|
+
});
|
|
3115
|
+
}
|
|
3116
|
+
}
|
|
3117
|
+
async function verifyContainerIsolation(method, binaryPath) {
|
|
3118
|
+
try {
|
|
3119
|
+
await execFileAsync(binaryPath, ["info"], { timeout: 15e3 });
|
|
3120
|
+
} catch (e) {
|
|
3121
|
+
return { passed: false, error: `${method} daemon not accessible: ${e.message}` };
|
|
3122
|
+
}
|
|
3123
|
+
try {
|
|
3124
|
+
const { stdout } = await execFileAsync(
|
|
3125
|
+
binaryPath,
|
|
3126
|
+
["run", "--rm", "--network=none", "alpine", "echo", "isolation-ok"],
|
|
3127
|
+
{ timeout: 3e4 }
|
|
3128
|
+
);
|
|
3129
|
+
if (!stdout.includes("isolation-ok")) {
|
|
3130
|
+
return { passed: false, error: `container test returned unexpected output: ${stdout.trim()}` };
|
|
3131
|
+
}
|
|
3132
|
+
return { passed: true };
|
|
3133
|
+
} catch (e) {
|
|
3134
|
+
if (e.message?.includes("pull access denied") || e.message?.includes("not found")) {
|
|
3135
|
+
return { passed: true };
|
|
3136
|
+
}
|
|
3137
|
+
return { passed: false, error: `container test failed: ${e.message}` };
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
async function verifyIsolation(method, binaryPath) {
|
|
3141
|
+
switch (method) {
|
|
3142
|
+
case "srt":
|
|
3143
|
+
return verifySrtIsolation(binaryPath);
|
|
3144
|
+
case "bwrap":
|
|
3145
|
+
return verifyBwrapIsolation(binaryPath);
|
|
3146
|
+
case "docker":
|
|
3147
|
+
case "podman":
|
|
3148
|
+
return verifyContainerIsolation(method, binaryPath);
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
async function detectIsolationCapabilities() {
|
|
3152
|
+
const srtEnv = { PATH: getToolsPath() };
|
|
3153
|
+
const checks = {
|
|
3154
|
+
srt: checkCommand("srt", ["--version"], srtEnv),
|
|
3155
|
+
bwrap: checkCommand("bwrap", ["--version"]),
|
|
3156
|
+
docker: checkCommand("docker", ["--version"]),
|
|
3157
|
+
podman: checkCommand("podman", ["--version"])
|
|
3158
|
+
};
|
|
3159
|
+
const checkResults = await Promise.all(
|
|
3160
|
+
Object.entries(checks).map(async ([method, promise]) => {
|
|
3161
|
+
const detail = await promise;
|
|
3162
|
+
return [method, detail];
|
|
3163
|
+
})
|
|
3164
|
+
);
|
|
3165
|
+
const details = Object.fromEntries(checkResults);
|
|
3166
|
+
if (!details.srt.found) {
|
|
3167
|
+
const installed = await installSrt();
|
|
3168
|
+
if (installed) {
|
|
3169
|
+
details.srt = await checkCommand("srt", ["--version"], srtEnv);
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
const foundMethods = ISOLATION_PREFERENCE.filter((m) => details[m].found);
|
|
3173
|
+
if (foundMethods.length > 0) {
|
|
3174
|
+
console.log(`[isolation] Found runtimes: ${foundMethods.join(", ")}. Verifying isolation...`);
|
|
3175
|
+
await Promise.all(
|
|
3176
|
+
foundMethods.map(async (method) => {
|
|
3177
|
+
const binaryPath = details[method].path || method;
|
|
3178
|
+
const result = await verifyIsolation(method, binaryPath);
|
|
3179
|
+
details[method].verified = result.passed;
|
|
3180
|
+
if (!result.passed) {
|
|
3181
|
+
details[method].verificationError = result.error;
|
|
3182
|
+
console.warn(
|
|
3183
|
+
`[isolation] WARNING: ${method} found but verification FAILED: ${result.error}. This runtime will NOT be used for isolation.`
|
|
3184
|
+
);
|
|
3185
|
+
} else {
|
|
3186
|
+
console.log(`[isolation] ${method}: verified OK`);
|
|
3187
|
+
}
|
|
3188
|
+
})
|
|
3189
|
+
);
|
|
3190
|
+
}
|
|
3191
|
+
const available = ISOLATION_PREFERENCE.filter(
|
|
3192
|
+
(m) => details[m].found && details[m].verified === true
|
|
3193
|
+
);
|
|
3194
|
+
const preferred = available.length > 0 ? available[0] : null;
|
|
3195
|
+
if (available.length === 0 && foundMethods.length > 0) {
|
|
3196
|
+
console.warn(
|
|
3197
|
+
`[isolation] No isolation runtime passed verification (found: ${foundMethods.join(", ")}). Session sharing will be DISABLED.`
|
|
3198
|
+
);
|
|
3199
|
+
} else if (available.length === 0) {
|
|
3200
|
+
console.log("[isolation] No isolation runtimes found. Session sharing will be unavailable.");
|
|
3201
|
+
} else {
|
|
3202
|
+
console.log(`[isolation] Preferred isolation method: ${preferred}`);
|
|
3203
|
+
}
|
|
3204
|
+
return { available, preferred, details };
|
|
3205
|
+
}
|
|
3206
|
+
|
|
2234
3207
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
2235
3208
|
const __dirname$1 = dirname(__filename$1);
|
|
2236
3209
|
function loadDotEnv() {
|
|
@@ -2401,22 +3374,24 @@ function deletePersistedSession(sessionId) {
|
|
|
2401
3374
|
function loadPersistedSessions() {
|
|
2402
3375
|
const sessions = [];
|
|
2403
3376
|
const index = loadSessionIndex();
|
|
3377
|
+
let indexChanged = false;
|
|
2404
3378
|
for (const [sessionId, entry] of Object.entries(index)) {
|
|
2405
3379
|
const filePath = getSessionFilePath(entry.directory, sessionId);
|
|
2406
3380
|
try {
|
|
2407
3381
|
const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
|
|
2408
3382
|
if (data.sessionId && data.directory) {
|
|
2409
|
-
if (data.stopped) {
|
|
2410
|
-
delete index[sessionId];
|
|
2411
|
-
} else {
|
|
3383
|
+
if (!data.stopped) {
|
|
2412
3384
|
sessions.push(data);
|
|
2413
3385
|
}
|
|
2414
3386
|
}
|
|
2415
3387
|
} catch {
|
|
2416
3388
|
delete index[sessionId];
|
|
3389
|
+
indexChanged = true;
|
|
2417
3390
|
}
|
|
2418
3391
|
}
|
|
2419
|
-
|
|
3392
|
+
if (indexChanged) {
|
|
3393
|
+
saveSessionIndex(index);
|
|
3394
|
+
}
|
|
2420
3395
|
return sessions;
|
|
2421
3396
|
}
|
|
2422
3397
|
function ensureHomeDir() {
|
|
@@ -2508,6 +3483,7 @@ async function startDaemon() {
|
|
|
2508
3483
|
let isReconnecting = false;
|
|
2509
3484
|
process.on("SIGINT", () => requestShutdown("os-signal"));
|
|
2510
3485
|
process.on("SIGTERM", () => requestShutdown("os-signal"));
|
|
3486
|
+
process.on("SIGUSR1", () => requestShutdown("os-signal-cleanup"));
|
|
2511
3487
|
process.on("uncaughtException", (error) => {
|
|
2512
3488
|
if (shutdownRequested) return;
|
|
2513
3489
|
logger.error("Uncaught exception:", error);
|
|
@@ -2659,8 +3635,8 @@ async function startDaemon() {
|
|
|
2659
3635
|
}
|
|
2660
3636
|
const sessionId = options.sessionId || randomUUID$1();
|
|
2661
3637
|
const agentName = options.agent || agentConfig.agent_type || "claude";
|
|
2662
|
-
if (agentName !== "claude" && KNOWN_ACP_AGENTS[agentName]) {
|
|
2663
|
-
return await
|
|
3638
|
+
if (agentName !== "claude" && (KNOWN_ACP_AGENTS[agentName] || KNOWN_MCP_AGENTS[agentName])) {
|
|
3639
|
+
return await spawnAgentSession(sessionId, directory, agentName, options, resumeSessionId);
|
|
2664
3640
|
}
|
|
2665
3641
|
try {
|
|
2666
3642
|
let parseBashPermission2 = function(permission) {
|
|
@@ -2709,8 +3685,19 @@ async function startDaemon() {
|
|
|
2709
3685
|
proc.kill(signal);
|
|
2710
3686
|
}
|
|
2711
3687
|
});
|
|
3688
|
+
}, buildIsolationConfig2 = function(dir) {
|
|
3689
|
+
if (!sessionMetadata.sharing?.enabled) return null;
|
|
3690
|
+
const method = isolationCapabilities.preferred;
|
|
3691
|
+
if (!method) return null;
|
|
3692
|
+
const detail = isolationCapabilities.details[method];
|
|
3693
|
+
if (!detail.found || detail.verified === false) return null;
|
|
3694
|
+
return {
|
|
3695
|
+
method,
|
|
3696
|
+
binaryPath: detail.path || method,
|
|
3697
|
+
workspacePath: dir
|
|
3698
|
+
};
|
|
2712
3699
|
};
|
|
2713
|
-
var parseBashPermission = parseBashPermission2, shouldAutoAllow = shouldAutoAllow2, killAndWaitForExit = killAndWaitForExit2;
|
|
3700
|
+
var parseBashPermission = parseBashPermission2, shouldAutoAllow = shouldAutoAllow2, killAndWaitForExit = killAndWaitForExit2, buildIsolationConfig = buildIsolationConfig2;
|
|
2714
3701
|
let sessionMetadata = {
|
|
2715
3702
|
path: directory,
|
|
2716
3703
|
host: os.hostname(),
|
|
@@ -2722,7 +3709,8 @@ async function startDaemon() {
|
|
|
2722
3709
|
svampToolsDir: join$1(__dirname$1, "..", "tools"),
|
|
2723
3710
|
startedFromDaemon: true,
|
|
2724
3711
|
startedBy: "daemon",
|
|
2725
|
-
lifecycleState: resumeSessionId ? "idle" : "starting"
|
|
3712
|
+
lifecycleState: resumeSessionId ? "idle" : "starting",
|
|
3713
|
+
sharing: options.sharing
|
|
2726
3714
|
};
|
|
2727
3715
|
let claudeProcess = null;
|
|
2728
3716
|
const allPersisted = loadPersistedSessions();
|
|
@@ -2758,8 +3746,14 @@ async function startDaemon() {
|
|
|
2758
3746
|
let turnInitiatedByUser = true;
|
|
2759
3747
|
let isKillingClaude = false;
|
|
2760
3748
|
let checkSvampConfig;
|
|
3749
|
+
const CLAUDE_PERMISSION_MODE_MAP = {
|
|
3750
|
+
"auto-approve-all": "bypassPermissions"
|
|
3751
|
+
};
|
|
3752
|
+
const toClaudePermissionMode = (mode) => CLAUDE_PERMISSION_MODE_MAP[mode] || mode;
|
|
3753
|
+
let isolationCleanupFiles = [];
|
|
2761
3754
|
const spawnClaude = (initialMessage, meta) => {
|
|
2762
|
-
const
|
|
3755
|
+
const rawPermissionMode = meta?.permissionMode || agentConfig.default_permission_mode || currentPermissionMode;
|
|
3756
|
+
const permissionMode = toClaudePermissionMode(rawPermissionMode);
|
|
2763
3757
|
currentPermissionMode = permissionMode;
|
|
2764
3758
|
const model = meta?.model || agentConfig.default_model || void 0;
|
|
2765
3759
|
const appendSystemPrompt = meta?.appendSystemPrompt || agentConfig.append_system_prompt || void 0;
|
|
@@ -2777,10 +3771,26 @@ async function startDaemon() {
|
|
|
2777
3771
|
if (model) args.push("--model", model);
|
|
2778
3772
|
if (appendSystemPrompt) args.push("--append-system-prompt", appendSystemPrompt);
|
|
2779
3773
|
if (claudeResumeId) args.push("--resume", claudeResumeId);
|
|
2780
|
-
|
|
2781
|
-
|
|
3774
|
+
let spawnCommand = "claude";
|
|
3775
|
+
let spawnArgs = args;
|
|
3776
|
+
let extraEnv = {};
|
|
3777
|
+
isolationCleanupFiles = [];
|
|
3778
|
+
const isoConfig = buildIsolationConfig2(directory);
|
|
3779
|
+
if (isoConfig) {
|
|
3780
|
+
const wrapped = wrapWithIsolation(spawnCommand, spawnArgs, isoConfig);
|
|
3781
|
+
spawnCommand = wrapped.command;
|
|
3782
|
+
spawnArgs = wrapped.args;
|
|
3783
|
+
if (wrapped.env) extraEnv = wrapped.env;
|
|
3784
|
+
if (wrapped.cleanupFiles) isolationCleanupFiles = wrapped.cleanupFiles;
|
|
3785
|
+
sessionMetadata = { ...sessionMetadata, isolationMethod: isoConfig.method };
|
|
3786
|
+
logger.log(`[Session ${sessionId}] Isolation: ${isoConfig.method} (binary: ${isoConfig.binaryPath})`);
|
|
3787
|
+
} else {
|
|
3788
|
+
sessionMetadata = { ...sessionMetadata, isolationMethod: void 0 };
|
|
3789
|
+
}
|
|
3790
|
+
logger.log(`[Session ${sessionId}] Spawning Claude: ${spawnCommand} ${spawnArgs.join(" ")} (cwd: ${directory})`);
|
|
3791
|
+
const spawnEnv = { ...process.env, ...extraEnv };
|
|
2782
3792
|
delete spawnEnv.CLAUDECODE;
|
|
2783
|
-
const child = spawn$1(
|
|
3793
|
+
const child = spawn$1(spawnCommand, spawnArgs, {
|
|
2784
3794
|
cwd: directory,
|
|
2785
3795
|
stdio: ["pipe", "pipe", "pipe"],
|
|
2786
3796
|
env: spawnEnv,
|
|
@@ -2902,15 +3912,24 @@ Please ensure Claude Code CLI is installed on this machine. You can install it w
|
|
|
2902
3912
|
}
|
|
2903
3913
|
}
|
|
2904
3914
|
if (msg.type === "result") {
|
|
2905
|
-
if (msg.is_error
|
|
3915
|
+
if (msg.is_error) {
|
|
2906
3916
|
const resultText = msg.result || "";
|
|
2907
|
-
logger.error(`[Session ${sessionId}] Claude
|
|
2908
|
-
const
|
|
2909
|
-
const
|
|
2910
|
-
const
|
|
3917
|
+
logger.error(`[Session ${sessionId}] Claude error (is_error=true, api_ms=${msg.duration_api_ms}): "${resultText}"`);
|
|
3918
|
+
const lower = resultText.toLowerCase();
|
|
3919
|
+
const isLoginIssue = lower.includes("login") || lower.includes("logged in") || lower.includes("auth") || lower.includes("api key") || lower.includes("unauthorized");
|
|
3920
|
+
const isResumeIssue = lower.includes("tool_use.name") || lower.includes("invalid_request") || lower.includes("messages.");
|
|
3921
|
+
let hint = "";
|
|
3922
|
+
if (isLoginIssue) {
|
|
3923
|
+
hint = "\n\nRun `claude login` in your terminal on the machine running the daemon to re-authenticate.";
|
|
3924
|
+
} else if (isResumeIssue) {
|
|
3925
|
+
hint = "\n\nThe conversation history may be corrupted. Try starting a fresh session.";
|
|
3926
|
+
} else {
|
|
3927
|
+
hint = "\n\nCheck that the Claude Code CLI is properly installed and configured.";
|
|
3928
|
+
}
|
|
3929
|
+
const displayMsg = resultText || "Claude Code exited with an error.";
|
|
2911
3930
|
sessionService.pushMessage({
|
|
2912
3931
|
type: "assistant",
|
|
2913
|
-
content: [{ type: "text", text:
|
|
3932
|
+
content: [{ type: "text", text: `**Error:** ${displayMsg}${hint}` }]
|
|
2914
3933
|
}, "agent");
|
|
2915
3934
|
lastErrorMessagePushed = true;
|
|
2916
3935
|
}
|
|
@@ -2985,6 +4004,11 @@ Please ensure Claude Code CLI is installed on this machine. You can install it w
|
|
|
2985
4004
|
child.on("exit", (code, signal) => {
|
|
2986
4005
|
logger.log(`[Session ${sessionId}] Claude exited: code=${code}, signal=${signal}`);
|
|
2987
4006
|
claudeProcess = null;
|
|
4007
|
+
for (const f of isolationCleanupFiles) {
|
|
4008
|
+
fs.rm(f, { force: true }).catch(() => {
|
|
4009
|
+
});
|
|
4010
|
+
}
|
|
4011
|
+
isolationCleanupFiles = [];
|
|
2988
4012
|
for (const [id, pending] of pendingPermissions) {
|
|
2989
4013
|
pending.resolve({ behavior: "deny", message: "Claude process exited" });
|
|
2990
4014
|
}
|
|
@@ -3054,7 +4078,7 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3054
4078
|
text = typeof content === "string" ? content : JSON.stringify(content);
|
|
3055
4079
|
}
|
|
3056
4080
|
if (msgMeta?.permissionMode) {
|
|
3057
|
-
currentPermissionMode = msgMeta.permissionMode;
|
|
4081
|
+
currentPermissionMode = toClaudePermissionMode(msgMeta.permissionMode);
|
|
3058
4082
|
logger.log(`[Session ${sessionId}] Permission mode updated to: ${currentPermissionMode}`);
|
|
3059
4083
|
}
|
|
3060
4084
|
if (isKillingClaude) {
|
|
@@ -3084,8 +4108,8 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3084
4108
|
const requestId = params.id;
|
|
3085
4109
|
const pending = pendingPermissions.get(requestId);
|
|
3086
4110
|
if (params.mode) {
|
|
3087
|
-
|
|
3088
|
-
|
|
4111
|
+
currentPermissionMode = toClaudePermissionMode(params.mode);
|
|
4112
|
+
logger.log(`[Session ${sessionId}] Permission mode changed to: ${currentPermissionMode}`);
|
|
3089
4113
|
}
|
|
3090
4114
|
if (params.allowTools && Array.isArray(params.allowTools)) {
|
|
3091
4115
|
for (const tool of params.allowTools) {
|
|
@@ -3281,8 +4305,8 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3281
4305
|
};
|
|
3282
4306
|
}
|
|
3283
4307
|
};
|
|
3284
|
-
const
|
|
3285
|
-
logger.log(`[
|
|
4308
|
+
const spawnAgentSession = async (sessionId, directory, agentName, options, resumeSessionId) => {
|
|
4309
|
+
logger.log(`[Agent] Spawning ${agentName} session: ${sessionId}`);
|
|
3286
4310
|
try {
|
|
3287
4311
|
let parseBashPermission2 = function(permission) {
|
|
3288
4312
|
if (permission === "Bash") return;
|
|
@@ -3323,7 +4347,8 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3323
4347
|
startedFromDaemon: true,
|
|
3324
4348
|
startedBy: "daemon",
|
|
3325
4349
|
lifecycleState: "starting",
|
|
3326
|
-
flavor: agentName
|
|
4350
|
+
flavor: agentName,
|
|
4351
|
+
sharing: options.sharing
|
|
3327
4352
|
};
|
|
3328
4353
|
let currentPermissionMode = "default";
|
|
3329
4354
|
const allowedTools = /* @__PURE__ */ new Set();
|
|
@@ -3337,7 +4362,7 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3337
4362
|
{ controlledByUser: false },
|
|
3338
4363
|
{
|
|
3339
4364
|
onUserMessage: (content, meta) => {
|
|
3340
|
-
logger.log(`[
|
|
4365
|
+
logger.log(`[${agentName} Session ${sessionId}] User message received`);
|
|
3341
4366
|
let text;
|
|
3342
4367
|
let msgMeta = meta;
|
|
3343
4368
|
try {
|
|
@@ -3357,17 +4382,17 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3357
4382
|
if (msgMeta?.permissionMode) {
|
|
3358
4383
|
currentPermissionMode = msgMeta.permissionMode;
|
|
3359
4384
|
}
|
|
3360
|
-
|
|
3361
|
-
logger.error(`[
|
|
4385
|
+
agentBackend.sendPrompt(sessionId, text).catch((err) => {
|
|
4386
|
+
logger.error(`[${agentName} Session ${sessionId}] Error sending prompt:`, err);
|
|
3362
4387
|
});
|
|
3363
4388
|
},
|
|
3364
4389
|
onAbort: () => {
|
|
3365
|
-
logger.log(`[
|
|
3366
|
-
|
|
4390
|
+
logger.log(`[${agentName} Session ${sessionId}] Abort requested`);
|
|
4391
|
+
agentBackend.cancel(sessionId).catch(() => {
|
|
3367
4392
|
});
|
|
3368
4393
|
},
|
|
3369
4394
|
onPermissionResponse: (params) => {
|
|
3370
|
-
logger.log(`[
|
|
4395
|
+
logger.log(`[${agentName} Session ${sessionId}] Permission response:`, JSON.stringify(params));
|
|
3371
4396
|
const requestId = params.id;
|
|
3372
4397
|
if (params.mode) currentPermissionMode = params.mode;
|
|
3373
4398
|
if (params.allowTools && Array.isArray(params.allowTools)) {
|
|
@@ -3380,17 +4405,18 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3380
4405
|
}
|
|
3381
4406
|
}
|
|
3382
4407
|
permissionHandler.resolvePermission(requestId, params.approved);
|
|
4408
|
+
agentBackend.respondToPermission?.(requestId, params.approved);
|
|
3383
4409
|
},
|
|
3384
4410
|
onSwitchMode: (mode) => {
|
|
3385
|
-
logger.log(`[
|
|
4411
|
+
logger.log(`[${agentName} Session ${sessionId}] Switch mode: ${mode}`);
|
|
3386
4412
|
currentPermissionMode = mode;
|
|
3387
4413
|
},
|
|
3388
4414
|
onRestartClaude: async () => {
|
|
3389
|
-
logger.log(`[
|
|
4415
|
+
logger.log(`[${agentName} Session ${sessionId}] Restart agent requested`);
|
|
3390
4416
|
return { success: false, message: "Restart is not supported for this agent type." };
|
|
3391
4417
|
},
|
|
3392
4418
|
onKillSession: () => {
|
|
3393
|
-
logger.log(`[
|
|
4419
|
+
logger.log(`[${agentName} Session ${sessionId}] Kill session requested`);
|
|
3394
4420
|
stopSession(sessionId);
|
|
3395
4421
|
},
|
|
3396
4422
|
onBash: async (command, cwd) => {
|
|
@@ -3492,21 +4518,46 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3492
4518
|
sessionService,
|
|
3493
4519
|
logger
|
|
3494
4520
|
);
|
|
3495
|
-
const transportHandler = agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(agentName);
|
|
3496
4521
|
const permissionHandler = new HyphaPermissionHandler(shouldAutoAllow2, logger.log);
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
4522
|
+
let agentIsoConfig;
|
|
4523
|
+
if (sessionMetadata.sharing?.enabled && isolationCapabilities.preferred) {
|
|
4524
|
+
const method = isolationCapabilities.preferred;
|
|
4525
|
+
const detail = isolationCapabilities.details[method];
|
|
4526
|
+
if (detail.found && detail.verified !== false) {
|
|
4527
|
+
agentIsoConfig = {
|
|
4528
|
+
method,
|
|
4529
|
+
binaryPath: detail.path || method,
|
|
4530
|
+
workspacePath: directory
|
|
4531
|
+
};
|
|
4532
|
+
sessionMetadata = { ...sessionMetadata, isolationMethod: method };
|
|
4533
|
+
logger.log(`[Agent Session ${sessionId}] Isolation: ${method}`);
|
|
4534
|
+
}
|
|
4535
|
+
}
|
|
4536
|
+
let agentBackend;
|
|
4537
|
+
if (KNOWN_MCP_AGENTS[agentName]) {
|
|
4538
|
+
agentBackend = new CodexMcpBackend({
|
|
4539
|
+
cwd: directory,
|
|
4540
|
+
env: options.environmentVariables,
|
|
4541
|
+
log: logger.log,
|
|
4542
|
+
isolationConfig: agentIsoConfig
|
|
4543
|
+
});
|
|
4544
|
+
} else {
|
|
4545
|
+
const transportHandler = agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(agentName);
|
|
4546
|
+
const acpConfig = KNOWN_ACP_AGENTS[agentName];
|
|
4547
|
+
agentBackend = new AcpBackend({
|
|
4548
|
+
agentName,
|
|
4549
|
+
cwd: directory,
|
|
4550
|
+
command: acpConfig.command,
|
|
4551
|
+
args: acpConfig.args,
|
|
4552
|
+
env: options.environmentVariables,
|
|
4553
|
+
permissionHandler,
|
|
4554
|
+
transportHandler,
|
|
4555
|
+
log: logger.log,
|
|
4556
|
+
isolationConfig: agentIsoConfig
|
|
4557
|
+
});
|
|
4558
|
+
}
|
|
3508
4559
|
bridgeAcpToSession(
|
|
3509
|
-
|
|
4560
|
+
agentBackend,
|
|
3510
4561
|
sessionService,
|
|
3511
4562
|
() => sessionMetadata,
|
|
3512
4563
|
(updater) => {
|
|
@@ -3525,15 +4576,15 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3525
4576
|
directory,
|
|
3526
4577
|
resumeSessionId,
|
|
3527
4578
|
get childProcess() {
|
|
3528
|
-
return
|
|
4579
|
+
return agentBackend.getProcess?.() || void 0;
|
|
3529
4580
|
}
|
|
3530
4581
|
};
|
|
3531
4582
|
pidToTrackedSession.set(process.pid + Math.floor(Math.random() * 1e5), trackedSession);
|
|
3532
|
-
logger.log(`[
|
|
3533
|
-
|
|
3534
|
-
logger.log(`[
|
|
4583
|
+
logger.log(`[Agent Session ${sessionId}] Starting ${agentName} backend...`);
|
|
4584
|
+
agentBackend.startSession().then(() => {
|
|
4585
|
+
logger.log(`[Agent Session ${sessionId}] ${agentName} backend started, waiting for first message`);
|
|
3535
4586
|
}).catch((err) => {
|
|
3536
|
-
logger.error(`[
|
|
4587
|
+
logger.error(`[Agent Session ${sessionId}] Failed to start ${agentName}:`, err);
|
|
3537
4588
|
sessionService.pushMessage({
|
|
3538
4589
|
type: "assistant",
|
|
3539
4590
|
content: [{
|
|
@@ -3548,10 +4599,10 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3548
4599
|
return {
|
|
3549
4600
|
type: "success",
|
|
3550
4601
|
sessionId,
|
|
3551
|
-
message: `
|
|
4602
|
+
message: `Agent session (${agentName}) registered as svamp-session-${sessionId}`
|
|
3552
4603
|
};
|
|
3553
4604
|
} catch (err) {
|
|
3554
|
-
logger.error(`[
|
|
4605
|
+
logger.error(`[Agent] Failed to spawn ${agentName} session:`, err);
|
|
3555
4606
|
return {
|
|
3556
4607
|
type: "error",
|
|
3557
4608
|
errorMessage: `Failed to spawn ${agentName} session: ${err.message}`
|
|
@@ -3580,6 +4631,14 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3580
4631
|
logger.log(`Session ${sessionId} not found`);
|
|
3581
4632
|
return false;
|
|
3582
4633
|
};
|
|
4634
|
+
let isolationCapabilities;
|
|
4635
|
+
try {
|
|
4636
|
+
isolationCapabilities = await detectIsolationCapabilities();
|
|
4637
|
+
logger.log(`Isolation capabilities: ${isolationCapabilities.available.join(", ") || "none"} (preferred: ${isolationCapabilities.preferred || "none"})`);
|
|
4638
|
+
} catch (err) {
|
|
4639
|
+
logger.log(`Failed to detect isolation capabilities: ${err}`);
|
|
4640
|
+
isolationCapabilities = { available: [], preferred: null, details: { srt: { found: false }, bwrap: { found: false }, docker: { found: false }, podman: { found: false } } };
|
|
4641
|
+
}
|
|
3583
4642
|
const defaultHomeDir = existsSync$1("/data") ? "/data" : os.homedir();
|
|
3584
4643
|
const machineMetadata = {
|
|
3585
4644
|
host: os.hostname(),
|
|
@@ -3588,7 +4647,8 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3588
4647
|
homeDir: defaultHomeDir,
|
|
3589
4648
|
svampHomeDir: SVAMP_HOME,
|
|
3590
4649
|
svampLibDir: join$1(__dirname$1, ".."),
|
|
3591
|
-
displayName: process.env.SVAMP_DISPLAY_NAME || void 0
|
|
4650
|
+
displayName: process.env.SVAMP_DISPLAY_NAME || void 0,
|
|
4651
|
+
isolationCapabilities
|
|
3592
4652
|
};
|
|
3593
4653
|
const initialDaemonState = {
|
|
3594
4654
|
status: "running",
|
|
@@ -3697,7 +4757,7 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3697
4757
|
console.log(` Service: svamp-machine-${machineId}`);
|
|
3698
4758
|
console.log(` Log file: ${logger.logFilePath}`);
|
|
3699
4759
|
let consecutiveHeartbeatFailures = 0;
|
|
3700
|
-
const MAX_HEARTBEAT_FAILURES =
|
|
4760
|
+
const MAX_HEARTBEAT_FAILURES = 60;
|
|
3701
4761
|
const heartbeatInterval = setInterval(async () => {
|
|
3702
4762
|
try {
|
|
3703
4763
|
const state = readDaemonStateFile();
|
|
@@ -3798,16 +4858,22 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3798
4858
|
}
|
|
3799
4859
|
}
|
|
3800
4860
|
}
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
const
|
|
3805
|
-
|
|
3806
|
-
const
|
|
3807
|
-
|
|
4861
|
+
const shouldMarkStopped = source === "os-signal-cleanup";
|
|
4862
|
+
if (shouldMarkStopped) {
|
|
4863
|
+
try {
|
|
4864
|
+
const index = loadSessionIndex();
|
|
4865
|
+
for (const [sessionId, entry] of Object.entries(index)) {
|
|
4866
|
+
const filePath = getSessionFilePath(entry.directory, sessionId);
|
|
4867
|
+
if (existsSync$1(filePath)) {
|
|
4868
|
+
const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
|
|
4869
|
+
writeFileSync$1(filePath, JSON.stringify({ ...data, stopped: true }, null, 2), "utf-8");
|
|
4870
|
+
}
|
|
3808
4871
|
}
|
|
4872
|
+
logger.log("Marked all sessions as stopped (--cleanup mode)");
|
|
4873
|
+
} catch {
|
|
3809
4874
|
}
|
|
3810
|
-
}
|
|
4875
|
+
} else {
|
|
4876
|
+
logger.log("Sessions preserved for auto-restore on next start");
|
|
3811
4877
|
}
|
|
3812
4878
|
try {
|
|
3813
4879
|
await machineService.disconnect();
|
|
@@ -3843,16 +4909,18 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3843
4909
|
process.exit(1);
|
|
3844
4910
|
}
|
|
3845
4911
|
}
|
|
3846
|
-
async function stopDaemon() {
|
|
4912
|
+
async function stopDaemon(options) {
|
|
3847
4913
|
const state = readDaemonStateFile();
|
|
3848
4914
|
if (!state) {
|
|
3849
4915
|
console.log("No daemon running");
|
|
3850
4916
|
return;
|
|
3851
4917
|
}
|
|
4918
|
+
const signal = options?.cleanup ? "SIGUSR1" : "SIGTERM";
|
|
4919
|
+
const mode = options?.cleanup ? "cleanup (sessions will be stopped)" : "quick (sessions preserved for auto-restore)";
|
|
3852
4920
|
try {
|
|
3853
4921
|
process.kill(state.pid, 0);
|
|
3854
|
-
process.kill(state.pid,
|
|
3855
|
-
console.log(`Sent
|
|
4922
|
+
process.kill(state.pid, signal);
|
|
4923
|
+
console.log(`Sent ${signal} to daemon PID ${state.pid} \u2014 ${mode}`);
|
|
3856
4924
|
for (let i = 0; i < 30; i++) {
|
|
3857
4925
|
await new Promise((r) => setTimeout(r, 100));
|
|
3858
4926
|
try {
|
|
@@ -3901,4 +4969,4 @@ function daemonStatus() {
|
|
|
3901
4969
|
}
|
|
3902
4970
|
}
|
|
3903
4971
|
|
|
3904
|
-
export { DefaultTransport$1 as D, GeminiTransport$1 as G, registerSessionService as a, stopDaemon as b,
|
|
4972
|
+
export { DefaultTransport$1 as D, GeminiTransport$1 as G, registerSessionService as a, stopDaemon as b, connectToHypha as c, daemonStatus as d, acpBackend as e, acpAgentConfig as f, getHyphaServerUrl as g, registerMachineService as r, startDaemon as s };
|