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." };
|
|
@@ -307,6 +425,8 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
307
425
|
data.uuid = randomUUID();
|
|
308
426
|
}
|
|
309
427
|
wrappedContent = { role: "agent", content: { type: "output", data } };
|
|
428
|
+
} else if (role === "event") {
|
|
429
|
+
wrappedContent = { role: "agent", content: { type: "event", data: content } };
|
|
310
430
|
} else if (role === "session") {
|
|
311
431
|
wrappedContent = { role: "session", content: { type: "session", data: content } };
|
|
312
432
|
} else {
|
|
@@ -338,9 +458,10 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
338
458
|
id: `svamp-session-${sessionId}`,
|
|
339
459
|
name: `Svamp Session ${sessionId.slice(0, 8)}`,
|
|
340
460
|
type: "svamp-session",
|
|
341
|
-
config: { visibility: "public" },
|
|
461
|
+
config: { visibility: "public", require_context: true },
|
|
342
462
|
// ── Messages ──
|
|
343
|
-
getMessages: async (afterSeq, limit) => {
|
|
463
|
+
getMessages: async (afterSeq, limit, context) => {
|
|
464
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
344
465
|
const after = afterSeq ?? 0;
|
|
345
466
|
const lim = Math.min(limit ?? 100, 500);
|
|
346
467
|
const filtered = messages.filter((m) => m.seq > after);
|
|
@@ -350,7 +471,8 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
350
471
|
hasMore: filtered.length > lim
|
|
351
472
|
};
|
|
352
473
|
},
|
|
353
|
-
sendMessage: async (content, localId, meta) => {
|
|
474
|
+
sendMessage: async (content, localId, meta, context) => {
|
|
475
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
354
476
|
if (localId) {
|
|
355
477
|
const existing = messages.find((m) => m.localId === localId);
|
|
356
478
|
if (existing) {
|
|
@@ -394,11 +516,15 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
394
516
|
return { id: msg.id, seq: msg.seq, localId: msg.localId };
|
|
395
517
|
},
|
|
396
518
|
// ── Metadata ──
|
|
397
|
-
getMetadata: async () =>
|
|
398
|
-
metadata,
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
519
|
+
getMetadata: async (context) => {
|
|
520
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
521
|
+
return {
|
|
522
|
+
metadata,
|
|
523
|
+
version: metadataVersion
|
|
524
|
+
};
|
|
525
|
+
},
|
|
526
|
+
updateMetadata: async (newMetadata, expectedVersion, context) => {
|
|
527
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
402
528
|
if (expectedVersion !== void 0 && expectedVersion !== metadataVersion) {
|
|
403
529
|
return {
|
|
404
530
|
result: "version-mismatch",
|
|
@@ -420,11 +546,15 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
420
546
|
};
|
|
421
547
|
},
|
|
422
548
|
// ── Agent State ──
|
|
423
|
-
getAgentState: async () =>
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
549
|
+
getAgentState: async (context) => {
|
|
550
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
551
|
+
return {
|
|
552
|
+
agentState,
|
|
553
|
+
version: agentStateVersion
|
|
554
|
+
};
|
|
555
|
+
},
|
|
556
|
+
updateAgentState: async (newState, expectedVersion, context) => {
|
|
557
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
428
558
|
if (expectedVersion !== void 0 && expectedVersion !== agentStateVersion) {
|
|
429
559
|
return {
|
|
430
560
|
result: "version-mismatch",
|
|
@@ -446,27 +576,32 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
446
576
|
};
|
|
447
577
|
},
|
|
448
578
|
// ── Session Control RPCs ──
|
|
449
|
-
abort: async () => {
|
|
579
|
+
abort: async (context) => {
|
|
580
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
450
581
|
callbacks.onAbort();
|
|
451
582
|
return { success: true };
|
|
452
583
|
},
|
|
453
|
-
permissionResponse: async (params) => {
|
|
584
|
+
permissionResponse: async (params, context) => {
|
|
585
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
454
586
|
callbacks.onPermissionResponse(params);
|
|
455
587
|
return { success: true };
|
|
456
588
|
},
|
|
457
|
-
switchMode: async (mode) => {
|
|
589
|
+
switchMode: async (mode, context) => {
|
|
590
|
+
authorizeRequest(context, metadata.sharing, "interact");
|
|
458
591
|
callbacks.onSwitchMode(mode);
|
|
459
592
|
return { success: true };
|
|
460
593
|
},
|
|
461
|
-
restartClaude: async () => {
|
|
594
|
+
restartClaude: async (context) => {
|
|
595
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
462
596
|
return await callbacks.onRestartClaude();
|
|
463
597
|
},
|
|
464
|
-
killSession: async () => {
|
|
598
|
+
killSession: async (context) => {
|
|
599
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
465
600
|
callbacks.onKillSession();
|
|
466
601
|
return { success: true };
|
|
467
602
|
},
|
|
468
603
|
// ── Activity ──
|
|
469
|
-
keepAlive: async (thinking, mode) => {
|
|
604
|
+
keepAlive: async (thinking, mode, context) => {
|
|
470
605
|
lastActivity = { active: true, thinking: thinking || false, mode: mode || "remote", time: Date.now() };
|
|
471
606
|
notifyListeners({
|
|
472
607
|
type: "activity",
|
|
@@ -474,7 +609,7 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
474
609
|
...lastActivity
|
|
475
610
|
});
|
|
476
611
|
},
|
|
477
|
-
sessionEnd: async () => {
|
|
612
|
+
sessionEnd: async (context) => {
|
|
478
613
|
lastActivity = { active: false, thinking: false, mode: "remote", time: Date.now() };
|
|
479
614
|
notifyListeners({
|
|
480
615
|
type: "activity",
|
|
@@ -483,28 +618,34 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
483
618
|
});
|
|
484
619
|
},
|
|
485
620
|
// ── Activity State Query ──
|
|
486
|
-
getActivityState: async () => {
|
|
621
|
+
getActivityState: async (context) => {
|
|
622
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
487
623
|
return { ...lastActivity, sessionId };
|
|
488
624
|
},
|
|
489
|
-
// ── File Operations (optional) ──
|
|
490
|
-
readFile: async (path) => {
|
|
625
|
+
// ── File Operations (optional, admin-only) ──
|
|
626
|
+
readFile: async (path, context) => {
|
|
627
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
491
628
|
if (!callbacks.onReadFile) throw new Error("readFile not supported");
|
|
492
629
|
return await callbacks.onReadFile(path);
|
|
493
630
|
},
|
|
494
|
-
writeFile: async (path, content) => {
|
|
631
|
+
writeFile: async (path, content, context) => {
|
|
632
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
495
633
|
if (!callbacks.onWriteFile) throw new Error("writeFile not supported");
|
|
496
634
|
await callbacks.onWriteFile(path, content);
|
|
497
635
|
return { success: true };
|
|
498
636
|
},
|
|
499
|
-
listDirectory: async (path) => {
|
|
637
|
+
listDirectory: async (path, context) => {
|
|
638
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
500
639
|
if (!callbacks.onListDirectory) throw new Error("listDirectory not supported");
|
|
501
640
|
return await callbacks.onListDirectory(path);
|
|
502
641
|
},
|
|
503
|
-
bash: async (command, cwd) => {
|
|
642
|
+
bash: async (command, cwd, context) => {
|
|
643
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
504
644
|
if (!callbacks.onBash) throw new Error("bash not supported");
|
|
505
645
|
return await callbacks.onBash(command, cwd);
|
|
506
646
|
},
|
|
507
|
-
ripgrep: async (args, cwd) => {
|
|
647
|
+
ripgrep: async (args, cwd, context) => {
|
|
648
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
508
649
|
if (!callbacks.onRipgrep) throw new Error("ripgrep not supported");
|
|
509
650
|
try {
|
|
510
651
|
const stdout = await callbacks.onRipgrep(args, cwd);
|
|
@@ -513,12 +654,36 @@ async function registerSessionService(server, sessionId, initialMetadata, initia
|
|
|
513
654
|
return { success: false, stdout: "", stderr: err.message || "", exitCode: 1, error: err.message };
|
|
514
655
|
}
|
|
515
656
|
},
|
|
516
|
-
getDirectoryTree: async (path, maxDepth) => {
|
|
657
|
+
getDirectoryTree: async (path, maxDepth, context) => {
|
|
658
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
517
659
|
if (!callbacks.onGetDirectoryTree) throw new Error("getDirectoryTree not supported");
|
|
518
660
|
return await callbacks.onGetDirectoryTree(path, maxDepth ?? 3);
|
|
519
661
|
},
|
|
662
|
+
// ── Sharing Management ──
|
|
663
|
+
getSharing: async (context) => {
|
|
664
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
665
|
+
return { sharing: metadata.sharing || null };
|
|
666
|
+
},
|
|
667
|
+
updateSharing: async (newSharing, context) => {
|
|
668
|
+
authorizeRequest(context, metadata.sharing, "admin");
|
|
669
|
+
if (metadata.sharing && context?.user?.email && context.user.email !== metadata.sharing.owner) {
|
|
670
|
+
throw new Error("Only the session owner can update sharing settings");
|
|
671
|
+
}
|
|
672
|
+
if (newSharing.enabled && !newSharing.owner && context?.user?.email) {
|
|
673
|
+
newSharing = { ...newSharing, owner: context.user.email };
|
|
674
|
+
}
|
|
675
|
+
metadata = { ...metadata, sharing: newSharing };
|
|
676
|
+
metadataVersion++;
|
|
677
|
+
notifyListeners({
|
|
678
|
+
type: "update-session",
|
|
679
|
+
sessionId,
|
|
680
|
+
metadata: { value: metadata, version: metadataVersion }
|
|
681
|
+
});
|
|
682
|
+
return { success: true, sharing: newSharing };
|
|
683
|
+
},
|
|
520
684
|
// ── Listener Registration ──
|
|
521
|
-
registerListener: async (callback) => {
|
|
685
|
+
registerListener: async (callback, context) => {
|
|
686
|
+
authorizeRequest(context, metadata.sharing, "view");
|
|
522
687
|
listeners.push(callback);
|
|
523
688
|
const replayMessages = messages.slice(-50);
|
|
524
689
|
console.log(`[HYPHA SESSION ${sessionId}] Listener registered (total: ${listeners.length}), replaying ${replayMessages.length} of ${messages.length} messages`);
|
|
@@ -827,6 +992,7 @@ class SessionArtifactSync {
|
|
|
827
992
|
lastSyncAt: Date.now(),
|
|
828
993
|
...sessionData || {}
|
|
829
994
|
},
|
|
995
|
+
stage: true,
|
|
830
996
|
_rkwargs: true
|
|
831
997
|
});
|
|
832
998
|
} catch {
|
|
@@ -843,6 +1009,7 @@ class SessionArtifactSync {
|
|
|
843
1009
|
lastSyncAt: Date.now(),
|
|
844
1010
|
...sessionData || {}
|
|
845
1011
|
},
|
|
1012
|
+
stage: true,
|
|
846
1013
|
_rkwargs: true
|
|
847
1014
|
});
|
|
848
1015
|
artifactId = artifact.id;
|
|
@@ -1032,6 +1199,106 @@ var DefaultTransport$1 = /*#__PURE__*/Object.freeze({
|
|
|
1032
1199
|
DefaultTransport: DefaultTransport
|
|
1033
1200
|
});
|
|
1034
1201
|
|
|
1202
|
+
const SVAMP_TOOLS_DIR$1 = join(homedir(), ".svamp", "tools");
|
|
1203
|
+
const SVAMP_TOOLS_BIN$1 = join(SVAMP_TOOLS_DIR$1, "node_modules", ".bin");
|
|
1204
|
+
const SVAMP_TOOLS_RG_BIN$1 = join(SVAMP_TOOLS_DIR$1, "node_modules", "@vscode", "ripgrep", "bin");
|
|
1205
|
+
function wrapWithIsolation(originalCommand, originalArgs, config) {
|
|
1206
|
+
switch (config.method) {
|
|
1207
|
+
case "srt":
|
|
1208
|
+
return wrapWithSrt(originalCommand, originalArgs, config);
|
|
1209
|
+
case "bwrap":
|
|
1210
|
+
return wrapWithBwrap(originalCommand, originalArgs, config);
|
|
1211
|
+
case "docker":
|
|
1212
|
+
return wrapWithContainer("docker", originalCommand, originalArgs, config);
|
|
1213
|
+
case "podman":
|
|
1214
|
+
return wrapWithContainer("podman", originalCommand, originalArgs, config);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
function wrapWithSrt(command, args, config) {
|
|
1218
|
+
const settings = {
|
|
1219
|
+
filesystem: {
|
|
1220
|
+
denyRead: config.srtConfig?.filesystem?.denyRead ?? [],
|
|
1221
|
+
allowWrite: config.srtConfig?.filesystem?.allowWrite ?? [config.workspacePath],
|
|
1222
|
+
denyWrite: config.srtConfig?.filesystem?.denyWrite ?? []
|
|
1223
|
+
},
|
|
1224
|
+
network: {
|
|
1225
|
+
allowedDomains: config.srtConfig?.network?.allowedDomains ?? [],
|
|
1226
|
+
deniedDomains: config.srtConfig?.network?.deniedDomains ?? []
|
|
1227
|
+
}
|
|
1228
|
+
};
|
|
1229
|
+
const settingsPath = join(tmpdir(), `srt-settings-${process.pid}-${Date.now()}.json`);
|
|
1230
|
+
writeFileSync(settingsPath, JSON.stringify(settings));
|
|
1231
|
+
const pathParts = [SVAMP_TOOLS_BIN$1, SVAMP_TOOLS_RG_BIN$1];
|
|
1232
|
+
if (process.env.PATH) pathParts.push(process.env.PATH);
|
|
1233
|
+
return {
|
|
1234
|
+
command: config.binaryPath,
|
|
1235
|
+
args: ["--settings", settingsPath, command, ...args],
|
|
1236
|
+
env: { PATH: pathParts.join(":") },
|
|
1237
|
+
cleanupFiles: [settingsPath]
|
|
1238
|
+
};
|
|
1239
|
+
}
|
|
1240
|
+
function wrapWithBwrap(command, args, config) {
|
|
1241
|
+
const bwrapArgs = [
|
|
1242
|
+
// Mount root filesystem read-only
|
|
1243
|
+
"--ro-bind",
|
|
1244
|
+
"/",
|
|
1245
|
+
"/",
|
|
1246
|
+
// Mount workspace read-write
|
|
1247
|
+
"--bind",
|
|
1248
|
+
config.workspacePath,
|
|
1249
|
+
config.workspacePath,
|
|
1250
|
+
// Mount /tmp read-write (many tools need it)
|
|
1251
|
+
"--bind",
|
|
1252
|
+
"/tmp",
|
|
1253
|
+
"/tmp",
|
|
1254
|
+
// Mount /dev read-write
|
|
1255
|
+
"--dev",
|
|
1256
|
+
"/dev",
|
|
1257
|
+
// Mount /proc
|
|
1258
|
+
"--proc",
|
|
1259
|
+
"/proc",
|
|
1260
|
+
// Unshare network — no network access inside sandbox
|
|
1261
|
+
"--unshare-net",
|
|
1262
|
+
// Unshare PID namespace
|
|
1263
|
+
"--unshare-pid",
|
|
1264
|
+
// Die when parent dies
|
|
1265
|
+
"--die-with-parent",
|
|
1266
|
+
// The actual command
|
|
1267
|
+
"--",
|
|
1268
|
+
command,
|
|
1269
|
+
...args
|
|
1270
|
+
];
|
|
1271
|
+
return {
|
|
1272
|
+
command: config.binaryPath,
|
|
1273
|
+
args: bwrapArgs
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
function wrapWithContainer(runtime, command, args, config) {
|
|
1277
|
+
const image = config.containerConfig?.image || "node:lts-slim";
|
|
1278
|
+
const networkMode = config.containerConfig?.networkMode || "none";
|
|
1279
|
+
const containerArgs = [
|
|
1280
|
+
"run",
|
|
1281
|
+
"--rm",
|
|
1282
|
+
"-i",
|
|
1283
|
+
// interactive (for stdin/stdout piping)
|
|
1284
|
+
`--network=${networkMode}`,
|
|
1285
|
+
"-v",
|
|
1286
|
+
`${config.workspacePath}:${config.workspacePath}`,
|
|
1287
|
+
"-w",
|
|
1288
|
+
config.workspacePath
|
|
1289
|
+
];
|
|
1290
|
+
if (config.containerConfig?.extraMounts) {
|
|
1291
|
+
for (const mount of config.containerConfig.extraMounts) {
|
|
1292
|
+
containerArgs.push("-v", mount);
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
containerArgs.push(image, command, ...args);
|
|
1296
|
+
return {
|
|
1297
|
+
command: config.binaryPath,
|
|
1298
|
+
args: containerArgs
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1035
1302
|
const DEFAULT_IDLE_TIMEOUT_MS = 500;
|
|
1036
1303
|
const DEFAULT_TOOL_CALL_TIMEOUT_MS = 12e4;
|
|
1037
1304
|
function parseArgsFromContent(content) {
|
|
@@ -1350,21 +1617,41 @@ class AcpBackend {
|
|
|
1350
1617
|
let startupStatusErrorEmitted = false;
|
|
1351
1618
|
try {
|
|
1352
1619
|
const args = this.options.args || [];
|
|
1620
|
+
let spawnCommand = this.options.command;
|
|
1621
|
+
let spawnArgs = args;
|
|
1622
|
+
let isoEnv = {};
|
|
1623
|
+
let isoCleanupFiles = [];
|
|
1624
|
+
if (this.options.isolationConfig) {
|
|
1625
|
+
const wrapped = wrapWithIsolation(spawnCommand, spawnArgs, this.options.isolationConfig);
|
|
1626
|
+
spawnCommand = wrapped.command;
|
|
1627
|
+
spawnArgs = wrapped.args;
|
|
1628
|
+
if (wrapped.env) isoEnv = wrapped.env;
|
|
1629
|
+
if (wrapped.cleanupFiles) isoCleanupFiles = wrapped.cleanupFiles;
|
|
1630
|
+
this.log(`[ACP] Isolation: ${this.options.isolationConfig.method}`);
|
|
1631
|
+
}
|
|
1632
|
+
const spawnEnv = { ...process.env, ...this.options.env, ...isoEnv };
|
|
1353
1633
|
if (process.platform === "win32") {
|
|
1354
|
-
const fullCommand = [
|
|
1634
|
+
const fullCommand = [spawnCommand, ...spawnArgs].join(" ");
|
|
1355
1635
|
this.process = spawn("cmd.exe", ["/c", fullCommand], {
|
|
1356
1636
|
cwd: this.options.cwd,
|
|
1357
|
-
env:
|
|
1637
|
+
env: spawnEnv,
|
|
1358
1638
|
stdio: ["pipe", "pipe", "pipe"],
|
|
1359
1639
|
windowsHide: true
|
|
1360
1640
|
});
|
|
1361
1641
|
} else {
|
|
1362
|
-
this.process = spawn(
|
|
1642
|
+
this.process = spawn(spawnCommand, spawnArgs, {
|
|
1363
1643
|
cwd: this.options.cwd,
|
|
1364
|
-
env:
|
|
1644
|
+
env: spawnEnv,
|
|
1365
1645
|
stdio: ["pipe", "pipe", "pipe"]
|
|
1366
1646
|
});
|
|
1367
1647
|
}
|
|
1648
|
+
if (isoCleanupFiles.length > 0) {
|
|
1649
|
+
this.process.on("exit", async () => {
|
|
1650
|
+
const { rm } = await import('node:fs/promises');
|
|
1651
|
+
for (const f of isoCleanupFiles) rm(f, { force: true }).catch(() => {
|
|
1652
|
+
});
|
|
1653
|
+
});
|
|
1654
|
+
}
|
|
1368
1655
|
if (!this.process.stdin || !this.process.stdout || !this.process.stderr) {
|
|
1369
1656
|
throw new Error("Failed to create stdio pipes");
|
|
1370
1657
|
}
|
|
@@ -1883,6 +2170,9 @@ const KNOWN_ACP_AGENTS = {
|
|
|
1883
2170
|
gemini: { command: "gemini", args: ["--experimental-acp"] },
|
|
1884
2171
|
opencode: { command: "opencode", args: ["acp"] }
|
|
1885
2172
|
};
|
|
2173
|
+
const KNOWN_MCP_AGENTS = {
|
|
2174
|
+
codex: { command: "codex", args: ["mcp-server"] }
|
|
2175
|
+
};
|
|
1886
2176
|
function resolveAcpAgentConfig(cliArgs) {
|
|
1887
2177
|
if (cliArgs.length === 0) {
|
|
1888
2178
|
throw new Error("Usage: svamp agent <agent-name> or svamp agent -- <command> [args]");
|
|
@@ -1899,13 +2189,21 @@ function resolveAcpAgentConfig(cliArgs) {
|
|
|
1899
2189
|
};
|
|
1900
2190
|
}
|
|
1901
2191
|
const agentName = cliArgs[0];
|
|
1902
|
-
const
|
|
1903
|
-
if (
|
|
2192
|
+
const knownAcp = KNOWN_ACP_AGENTS[agentName];
|
|
2193
|
+
if (knownAcp) {
|
|
1904
2194
|
const passthroughArgs = cliArgs.slice(1).filter((arg) => !(agentName === "opencode" && arg === "--acp"));
|
|
1905
2195
|
return {
|
|
1906
2196
|
agentName,
|
|
1907
|
-
command:
|
|
1908
|
-
args: [...
|
|
2197
|
+
command: knownAcp.command,
|
|
2198
|
+
args: [...knownAcp.args, ...passthroughArgs]
|
|
2199
|
+
};
|
|
2200
|
+
}
|
|
2201
|
+
const knownMcp = KNOWN_MCP_AGENTS[agentName];
|
|
2202
|
+
if (knownMcp) {
|
|
2203
|
+
return {
|
|
2204
|
+
agentName,
|
|
2205
|
+
command: knownMcp.command,
|
|
2206
|
+
args: [...knownMcp.args, ...cliArgs.slice(1)]
|
|
1909
2207
|
};
|
|
1910
2208
|
}
|
|
1911
2209
|
return {
|
|
@@ -1918,6 +2216,7 @@ function resolveAcpAgentConfig(cliArgs) {
|
|
|
1918
2216
|
var acpAgentConfig = /*#__PURE__*/Object.freeze({
|
|
1919
2217
|
__proto__: null,
|
|
1920
2218
|
KNOWN_ACP_AGENTS: KNOWN_ACP_AGENTS,
|
|
2219
|
+
KNOWN_MCP_AGENTS: KNOWN_MCP_AGENTS,
|
|
1921
2220
|
resolveAcpAgentConfig: resolveAcpAgentConfig
|
|
1922
2221
|
});
|
|
1923
2222
|
|
|
@@ -1965,15 +2264,15 @@ function bridgeAcpToSession(backend, sessionService, getMetadata, setMetadata, l
|
|
|
1965
2264
|
setMetadata((m) => ({ ...m, lifecycleState: "running" }));
|
|
1966
2265
|
} else if (msg.status === "error") {
|
|
1967
2266
|
flushText();
|
|
1968
|
-
sessionService.pushMessage(
|
|
1969
|
-
type: "
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
sessionService.
|
|
2267
|
+
sessionService.pushMessage(
|
|
2268
|
+
{ type: "message", message: `Agent process exited unexpectedly: ${msg.detail || "Unknown error"}` },
|
|
2269
|
+
"event"
|
|
2270
|
+
);
|
|
2271
|
+
sessionService.sendSessionEnd();
|
|
1973
2272
|
setMetadata((m) => ({ ...m, lifecycleState: "error" }));
|
|
1974
2273
|
} else if (msg.status === "stopped") {
|
|
1975
2274
|
flushText();
|
|
1976
|
-
sessionService.
|
|
2275
|
+
sessionService.sendSessionEnd();
|
|
1977
2276
|
setMetadata((m) => ({ ...m, lifecycleState: "stopped" }));
|
|
1978
2277
|
}
|
|
1979
2278
|
break;
|
|
@@ -2094,6 +2393,426 @@ class HyphaPermissionHandler {
|
|
|
2094
2393
|
}
|
|
2095
2394
|
}
|
|
2096
2395
|
|
|
2396
|
+
const DEFAULT_TIMEOUT = 14 * 24 * 60 * 60 * 1e3;
|
|
2397
|
+
function getCodexMcpCommand() {
|
|
2398
|
+
try {
|
|
2399
|
+
const version = execSync("codex --version", { encoding: "utf8" }).trim();
|
|
2400
|
+
const match = version.match(/codex-cli\s+(\d+\.\d+\.\d+(?:-alpha\.\d+)?)/);
|
|
2401
|
+
if (!match) return "mcp-server";
|
|
2402
|
+
const versionStr = match[1];
|
|
2403
|
+
const [major, minor, patch] = versionStr.split(/[-.]/).map(Number);
|
|
2404
|
+
if (major > 0 || minor > 43) return "mcp-server";
|
|
2405
|
+
if (minor === 43 && patch === 0) {
|
|
2406
|
+
if (versionStr.includes("-alpha.")) {
|
|
2407
|
+
const alphaNum = parseInt(versionStr.split("-alpha.")[1]);
|
|
2408
|
+
return alphaNum >= 5 ? "mcp-server" : "mcp";
|
|
2409
|
+
}
|
|
2410
|
+
return "mcp-server";
|
|
2411
|
+
}
|
|
2412
|
+
return "mcp";
|
|
2413
|
+
} catch {
|
|
2414
|
+
return null;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
class CodexMcpBackend {
|
|
2418
|
+
listeners = [];
|
|
2419
|
+
client;
|
|
2420
|
+
transport = null;
|
|
2421
|
+
disposed = false;
|
|
2422
|
+
codexSessionId = null;
|
|
2423
|
+
conversationId = null;
|
|
2424
|
+
svampSessionId = null;
|
|
2425
|
+
log;
|
|
2426
|
+
options;
|
|
2427
|
+
connected = false;
|
|
2428
|
+
// Pending elicitation approvals
|
|
2429
|
+
pendingApprovals = /* @__PURE__ */ new Map();
|
|
2430
|
+
// Temp files from isolation wrapping (cleaned up on disconnect)
|
|
2431
|
+
_isolationCleanupFiles = [];
|
|
2432
|
+
constructor(options) {
|
|
2433
|
+
this.options = options;
|
|
2434
|
+
this.log = options.log || (() => {
|
|
2435
|
+
});
|
|
2436
|
+
this.client = new Client(
|
|
2437
|
+
{ name: "svamp-codex-client", version: "1.0.0" },
|
|
2438
|
+
{ capabilities: { elicitation: {} } }
|
|
2439
|
+
);
|
|
2440
|
+
this.client.setNotificationHandler(z.object({
|
|
2441
|
+
method: z.literal("codex/event"),
|
|
2442
|
+
params: z.object({ msg: z.any() })
|
|
2443
|
+
}).passthrough(), (data) => {
|
|
2444
|
+
const msg = data.params.msg;
|
|
2445
|
+
this.updateIdentifiersFromEvent(msg);
|
|
2446
|
+
this.handleCodexEvent(msg);
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
// ── AgentBackend interface ──────────────────────────────────────────
|
|
2450
|
+
onMessage(handler) {
|
|
2451
|
+
this.listeners.push(handler);
|
|
2452
|
+
}
|
|
2453
|
+
offMessage(handler) {
|
|
2454
|
+
const idx = this.listeners.indexOf(handler);
|
|
2455
|
+
if (idx !== -1) this.listeners.splice(idx, 1);
|
|
2456
|
+
}
|
|
2457
|
+
async startSession(initialPrompt) {
|
|
2458
|
+
const sessionId = randomUUID();
|
|
2459
|
+
this.svampSessionId = sessionId;
|
|
2460
|
+
this.emit({ type: "status", status: "starting" });
|
|
2461
|
+
await this.connect();
|
|
2462
|
+
this.emit({ type: "status", status: "idle" });
|
|
2463
|
+
if (initialPrompt) {
|
|
2464
|
+
this.sendPrompt(sessionId, initialPrompt).catch((err) => {
|
|
2465
|
+
this.log(`[Codex] Error sending initial prompt: ${err.message}`);
|
|
2466
|
+
this.emit({ type: "status", status: "error", detail: err.message });
|
|
2467
|
+
});
|
|
2468
|
+
}
|
|
2469
|
+
return { sessionId };
|
|
2470
|
+
}
|
|
2471
|
+
async sendPrompt(sessionId, prompt) {
|
|
2472
|
+
if (!this.connected) throw new Error("Codex not connected");
|
|
2473
|
+
this.emit({ type: "status", status: "running" });
|
|
2474
|
+
try {
|
|
2475
|
+
let response;
|
|
2476
|
+
if (this.codexSessionId) {
|
|
2477
|
+
response = await this.continueSession(prompt);
|
|
2478
|
+
} else {
|
|
2479
|
+
const config = {
|
|
2480
|
+
prompt,
|
|
2481
|
+
cwd: this.options.cwd,
|
|
2482
|
+
...this.options.model ? { model: this.options.model } : {}
|
|
2483
|
+
};
|
|
2484
|
+
response = await this.startCodexSession(config);
|
|
2485
|
+
}
|
|
2486
|
+
if (response?.content && Array.isArray(response.content)) {
|
|
2487
|
+
for (const block of response.content) {
|
|
2488
|
+
if (block.type === "text" && block.text) {
|
|
2489
|
+
this.emit({ type: "model-output", fullText: block.text });
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
}
|
|
2493
|
+
} catch (err) {
|
|
2494
|
+
this.log(`[Codex] Error in sendPrompt: ${err.message}`);
|
|
2495
|
+
this.emit({ type: "status", status: "error", detail: err.message });
|
|
2496
|
+
throw err;
|
|
2497
|
+
} finally {
|
|
2498
|
+
this.emit({ type: "status", status: "idle" });
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
async cancel(_sessionId) {
|
|
2502
|
+
this.log("[Codex] Cancel requested");
|
|
2503
|
+
this.emit({ type: "status", status: "idle" });
|
|
2504
|
+
}
|
|
2505
|
+
async respondToPermission(requestId, approved) {
|
|
2506
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
2507
|
+
if (pending) {
|
|
2508
|
+
this.pendingApprovals.delete(requestId);
|
|
2509
|
+
pending.resolve({
|
|
2510
|
+
action: approved ? "accept" : "decline",
|
|
2511
|
+
content: { approval: approved ? "approve" : "deny" }
|
|
2512
|
+
});
|
|
2513
|
+
this.emit({ type: "permission-response", id: requestId, approved });
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
async dispose() {
|
|
2517
|
+
if (this.disposed) return;
|
|
2518
|
+
this.disposed = true;
|
|
2519
|
+
this.log("[Codex] Disposing backend");
|
|
2520
|
+
for (const [, pending] of this.pendingApprovals) {
|
|
2521
|
+
pending.resolve({ action: "decline" });
|
|
2522
|
+
}
|
|
2523
|
+
this.pendingApprovals.clear();
|
|
2524
|
+
await this.disconnect();
|
|
2525
|
+
this.listeners = [];
|
|
2526
|
+
}
|
|
2527
|
+
/** Get the transport's child process PID for tracking */
|
|
2528
|
+
get pid() {
|
|
2529
|
+
return this.transport?.pid ?? null;
|
|
2530
|
+
}
|
|
2531
|
+
/**
|
|
2532
|
+
* Return a process-like object for TrackedSession.childProcess.
|
|
2533
|
+
* We expose a minimal { kill } object that closes the transport.
|
|
2534
|
+
*/
|
|
2535
|
+
getProcess() {
|
|
2536
|
+
if (!this.transport) return null;
|
|
2537
|
+
const pid = this.pid;
|
|
2538
|
+
return {
|
|
2539
|
+
kill: (signal) => {
|
|
2540
|
+
if (pid) {
|
|
2541
|
+
try {
|
|
2542
|
+
process.kill(pid, signal || "SIGTERM");
|
|
2543
|
+
} catch {
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
};
|
|
2548
|
+
}
|
|
2549
|
+
// ── MCP connection ─────────────────────────────────────────────────
|
|
2550
|
+
async connect() {
|
|
2551
|
+
if (this.connected) return;
|
|
2552
|
+
const mcpCommand = getCodexMcpCommand();
|
|
2553
|
+
if (mcpCommand === null) {
|
|
2554
|
+
throw new Error(
|
|
2555
|
+
"Codex CLI not found or not executable.\n\nTo install codex:\n npm install -g @openai/codex\n"
|
|
2556
|
+
);
|
|
2557
|
+
}
|
|
2558
|
+
this.log(`[Codex] Connecting via: codex ${mcpCommand}`);
|
|
2559
|
+
const env = {};
|
|
2560
|
+
for (const key of Object.keys(process.env)) {
|
|
2561
|
+
const value = process.env[key];
|
|
2562
|
+
if (typeof value === "string") env[key] = value;
|
|
2563
|
+
}
|
|
2564
|
+
if (this.options.env) {
|
|
2565
|
+
Object.assign(env, this.options.env);
|
|
2566
|
+
}
|
|
2567
|
+
const rolloutFilter = "codex_core::rollout::list=off";
|
|
2568
|
+
const existingRustLog = env.RUST_LOG?.trim();
|
|
2569
|
+
if (!existingRustLog) {
|
|
2570
|
+
env.RUST_LOG = rolloutFilter;
|
|
2571
|
+
} else if (!existingRustLog.includes("codex_core::rollout::list=")) {
|
|
2572
|
+
env.RUST_LOG = `${existingRustLog},${rolloutFilter}`;
|
|
2573
|
+
}
|
|
2574
|
+
let transportCommand = "codex";
|
|
2575
|
+
let transportArgs = [mcpCommand];
|
|
2576
|
+
if (this.options.isolationConfig) {
|
|
2577
|
+
const wrapped = wrapWithIsolation(transportCommand, transportArgs, this.options.isolationConfig);
|
|
2578
|
+
transportCommand = wrapped.command;
|
|
2579
|
+
transportArgs = wrapped.args;
|
|
2580
|
+
if (wrapped.env) Object.assign(env, wrapped.env);
|
|
2581
|
+
if (wrapped.cleanupFiles) {
|
|
2582
|
+
this._isolationCleanupFiles = wrapped.cleanupFiles;
|
|
2583
|
+
}
|
|
2584
|
+
this.log(`[Codex] Isolation: ${this.options.isolationConfig.method}`);
|
|
2585
|
+
}
|
|
2586
|
+
this.transport = new StdioClientTransport({
|
|
2587
|
+
command: transportCommand,
|
|
2588
|
+
args: transportArgs,
|
|
2589
|
+
env
|
|
2590
|
+
});
|
|
2591
|
+
this.registerPermissionHandlers();
|
|
2592
|
+
try {
|
|
2593
|
+
await this.client.connect(this.transport);
|
|
2594
|
+
this.connected = true;
|
|
2595
|
+
this.log("[Codex] MCP connection established");
|
|
2596
|
+
} catch (err) {
|
|
2597
|
+
this.transport = null;
|
|
2598
|
+
throw err;
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
async disconnect() {
|
|
2602
|
+
if (!this.connected) return;
|
|
2603
|
+
const pid = this.pid;
|
|
2604
|
+
this.log(`[Codex] Disconnecting (pid=${pid ?? "none"})`);
|
|
2605
|
+
try {
|
|
2606
|
+
await this.client.close();
|
|
2607
|
+
} catch {
|
|
2608
|
+
try {
|
|
2609
|
+
await this.transport?.close?.();
|
|
2610
|
+
} catch {
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
if (pid) {
|
|
2614
|
+
try {
|
|
2615
|
+
process.kill(pid, 0);
|
|
2616
|
+
try {
|
|
2617
|
+
process.kill(pid, "SIGKILL");
|
|
2618
|
+
} catch {
|
|
2619
|
+
}
|
|
2620
|
+
} catch {
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
this.transport = null;
|
|
2624
|
+
this.connected = false;
|
|
2625
|
+
if (this._isolationCleanupFiles.length > 0) {
|
|
2626
|
+
const { rm } = await import('node:fs/promises');
|
|
2627
|
+
for (const f of this._isolationCleanupFiles) rm(f, { force: true }).catch(() => {
|
|
2628
|
+
});
|
|
2629
|
+
this._isolationCleanupFiles = [];
|
|
2630
|
+
}
|
|
2631
|
+
}
|
|
2632
|
+
// ── Permission handling ────────────────────────────────────────────
|
|
2633
|
+
registerPermissionHandlers() {
|
|
2634
|
+
this.client.setRequestHandler(
|
|
2635
|
+
ElicitRequestSchema,
|
|
2636
|
+
async (request) => {
|
|
2637
|
+
const params = request.params;
|
|
2638
|
+
const callId = params.codex_call_id || randomUUID();
|
|
2639
|
+
this.log(`[Codex] Elicitation request: ${params.message}`);
|
|
2640
|
+
this.emit({
|
|
2641
|
+
type: "permission-request",
|
|
2642
|
+
id: callId,
|
|
2643
|
+
reason: params.codex_command ? `Execute: ${Array.isArray(params.codex_command) ? params.codex_command.join(" ") : params.codex_command}` : params.message || "Codex requires approval",
|
|
2644
|
+
payload: {
|
|
2645
|
+
toolName: "CodexBash",
|
|
2646
|
+
input: {
|
|
2647
|
+
command: params.codex_command,
|
|
2648
|
+
cwd: params.codex_cwd
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
});
|
|
2652
|
+
return new Promise((resolve) => {
|
|
2653
|
+
this.pendingApprovals.set(callId, { resolve });
|
|
2654
|
+
setTimeout(() => {
|
|
2655
|
+
if (this.pendingApprovals.has(callId)) {
|
|
2656
|
+
this.pendingApprovals.delete(callId);
|
|
2657
|
+
resolve({ action: "decline" });
|
|
2658
|
+
}
|
|
2659
|
+
}, 5 * 60 * 1e3);
|
|
2660
|
+
});
|
|
2661
|
+
}
|
|
2662
|
+
);
|
|
2663
|
+
}
|
|
2664
|
+
// ── Codex MCP tools ────────────────────────────────────────────────
|
|
2665
|
+
async startCodexSession(config) {
|
|
2666
|
+
const response = await this.client.callTool({
|
|
2667
|
+
name: "codex",
|
|
2668
|
+
arguments: config
|
|
2669
|
+
}, void 0, { timeout: DEFAULT_TIMEOUT });
|
|
2670
|
+
this.extractIdentifiers(response);
|
|
2671
|
+
return response;
|
|
2672
|
+
}
|
|
2673
|
+
async continueSession(prompt) {
|
|
2674
|
+
if (!this.conversationId) {
|
|
2675
|
+
this.conversationId = this.codexSessionId;
|
|
2676
|
+
}
|
|
2677
|
+
const response = await this.client.callTool({
|
|
2678
|
+
name: "codex-reply",
|
|
2679
|
+
arguments: {
|
|
2680
|
+
sessionId: this.codexSessionId,
|
|
2681
|
+
conversationId: this.conversationId,
|
|
2682
|
+
prompt
|
|
2683
|
+
}
|
|
2684
|
+
}, void 0, { timeout: DEFAULT_TIMEOUT });
|
|
2685
|
+
this.extractIdentifiers(response);
|
|
2686
|
+
return response;
|
|
2687
|
+
}
|
|
2688
|
+
// ── Event handling ─────────────────────────────────────────────────
|
|
2689
|
+
handleCodexEvent(event) {
|
|
2690
|
+
if (!event || typeof event !== "object") return;
|
|
2691
|
+
const eventType = event.type;
|
|
2692
|
+
this.log(`[Codex] Event: ${eventType}`);
|
|
2693
|
+
switch (eventType) {
|
|
2694
|
+
case "task_started":
|
|
2695
|
+
this.emit({ type: "status", status: "running" });
|
|
2696
|
+
break;
|
|
2697
|
+
case "task_complete":
|
|
2698
|
+
case "turn_aborted":
|
|
2699
|
+
this.emit({ type: "status", status: "idle" });
|
|
2700
|
+
break;
|
|
2701
|
+
case "agent_message": {
|
|
2702
|
+
const content = event.content;
|
|
2703
|
+
if (Array.isArray(content)) {
|
|
2704
|
+
for (const block of content) {
|
|
2705
|
+
if (block.type === "output_text" && block.text) {
|
|
2706
|
+
this.emit({ type: "model-output", fullText: block.text });
|
|
2707
|
+
}
|
|
2708
|
+
}
|
|
2709
|
+
}
|
|
2710
|
+
break;
|
|
2711
|
+
}
|
|
2712
|
+
case "agent_reasoning_delta": {
|
|
2713
|
+
const text = event.delta?.text || event.text;
|
|
2714
|
+
if (text) {
|
|
2715
|
+
this.emit({ type: "event", name: "thinking", payload: { text } });
|
|
2716
|
+
}
|
|
2717
|
+
break;
|
|
2718
|
+
}
|
|
2719
|
+
case "agent_reasoning": {
|
|
2720
|
+
const text = event.text || (Array.isArray(event.content) ? event.content.map((c) => c.text).join("") : "");
|
|
2721
|
+
if (text) {
|
|
2722
|
+
this.emit({ type: "event", name: "thinking", payload: { text } });
|
|
2723
|
+
}
|
|
2724
|
+
break;
|
|
2725
|
+
}
|
|
2726
|
+
case "exec_command_begin": {
|
|
2727
|
+
const callId = event.call_id || event.callId || randomUUID();
|
|
2728
|
+
const command = Array.isArray(event.command) ? event.command.join(" ") : String(event.command || "");
|
|
2729
|
+
this.emit({
|
|
2730
|
+
type: "tool-call",
|
|
2731
|
+
toolName: "CodexBash",
|
|
2732
|
+
callId,
|
|
2733
|
+
args: { command, cwd: event.cwd }
|
|
2734
|
+
});
|
|
2735
|
+
break;
|
|
2736
|
+
}
|
|
2737
|
+
case "exec_command_end": {
|
|
2738
|
+
const callId = event.call_id || event.callId || "";
|
|
2739
|
+
this.emit({
|
|
2740
|
+
type: "tool-result",
|
|
2741
|
+
toolName: "CodexBash",
|
|
2742
|
+
callId,
|
|
2743
|
+
result: {
|
|
2744
|
+
exitCode: event.exit_code ?? event.exitCode,
|
|
2745
|
+
stdout: event.stdout || "",
|
|
2746
|
+
stderr: event.stderr || ""
|
|
2747
|
+
}
|
|
2748
|
+
});
|
|
2749
|
+
break;
|
|
2750
|
+
}
|
|
2751
|
+
case "patch_apply_begin": {
|
|
2752
|
+
const callId = event.call_id || event.callId || randomUUID();
|
|
2753
|
+
this.emit({
|
|
2754
|
+
type: "tool-call",
|
|
2755
|
+
toolName: "CodexPatch",
|
|
2756
|
+
callId,
|
|
2757
|
+
args: { filePath: event.file_path || event.filePath, patch: event.patch }
|
|
2758
|
+
});
|
|
2759
|
+
break;
|
|
2760
|
+
}
|
|
2761
|
+
case "patch_apply_end": {
|
|
2762
|
+
const callId = event.call_id || event.callId || "";
|
|
2763
|
+
this.emit({
|
|
2764
|
+
type: "tool-result",
|
|
2765
|
+
toolName: "CodexPatch",
|
|
2766
|
+
callId,
|
|
2767
|
+
result: {
|
|
2768
|
+
filePath: event.file_path || event.filePath,
|
|
2769
|
+
applied: event.applied,
|
|
2770
|
+
error: event.error
|
|
2771
|
+
}
|
|
2772
|
+
});
|
|
2773
|
+
break;
|
|
2774
|
+
}
|
|
2775
|
+
default:
|
|
2776
|
+
this.log(`[Codex] Unhandled event: ${eventType}`);
|
|
2777
|
+
break;
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
// ── Identifier extraction ──────────────────────────────────────────
|
|
2781
|
+
updateIdentifiersFromEvent(event) {
|
|
2782
|
+
if (!event || typeof event !== "object") return;
|
|
2783
|
+
const candidates = [event];
|
|
2784
|
+
if (event.data && typeof event.data === "object") candidates.push(event.data);
|
|
2785
|
+
for (const c of candidates) {
|
|
2786
|
+
const sid = c.session_id ?? c.sessionId;
|
|
2787
|
+
if (sid) this.codexSessionId = sid;
|
|
2788
|
+
const cid = c.conversation_id ?? c.conversationId;
|
|
2789
|
+
if (cid) this.conversationId = cid;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
extractIdentifiers(response) {
|
|
2793
|
+
const meta = response?.meta || {};
|
|
2794
|
+
this.codexSessionId = meta.sessionId || response?.sessionId || this.codexSessionId;
|
|
2795
|
+
this.conversationId = meta.conversationId || response?.conversationId || this.conversationId;
|
|
2796
|
+
const content = response?.content;
|
|
2797
|
+
if (Array.isArray(content)) {
|
|
2798
|
+
for (const item of content) {
|
|
2799
|
+
if (!this.codexSessionId && item?.sessionId) this.codexSessionId = item.sessionId;
|
|
2800
|
+
if (!this.conversationId && item?.conversationId) this.conversationId = item.conversationId;
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
}
|
|
2804
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
2805
|
+
emit(msg) {
|
|
2806
|
+
if (this.disposed) return;
|
|
2807
|
+
for (const h of this.listeners) {
|
|
2808
|
+
try {
|
|
2809
|
+
h(msg);
|
|
2810
|
+
} catch {
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2097
2816
|
const GEMINI_TIMEOUTS = {
|
|
2098
2817
|
init: 12e4,
|
|
2099
2818
|
toolCall: 12e4,
|
|
@@ -2229,6 +2948,264 @@ var GeminiTransport$1 = /*#__PURE__*/Object.freeze({
|
|
|
2229
2948
|
GeminiTransport: GeminiTransport
|
|
2230
2949
|
});
|
|
2231
2950
|
|
|
2951
|
+
const execFileAsync = promisify(execFile);
|
|
2952
|
+
const SVAMP_TOOLS_DIR = join(homedir(), ".svamp", "tools");
|
|
2953
|
+
const SVAMP_TOOLS_BIN = join(SVAMP_TOOLS_DIR, "node_modules", ".bin");
|
|
2954
|
+
const SVAMP_TOOLS_RG_BIN = join(SVAMP_TOOLS_DIR, "node_modules", "@vscode", "ripgrep", "bin");
|
|
2955
|
+
function getToolsPath() {
|
|
2956
|
+
const parts = [SVAMP_TOOLS_BIN, SVAMP_TOOLS_RG_BIN];
|
|
2957
|
+
if (process.env.PATH) parts.push(process.env.PATH);
|
|
2958
|
+
return parts.join(":");
|
|
2959
|
+
}
|
|
2960
|
+
async function checkCommand(command, versionArgs, extraEnv) {
|
|
2961
|
+
const envWithPath = extraEnv ? { ...process.env, ...extraEnv } : void 0;
|
|
2962
|
+
try {
|
|
2963
|
+
const { stdout } = await execFileAsync(command, versionArgs, {
|
|
2964
|
+
timeout: 5e3,
|
|
2965
|
+
env: envWithPath
|
|
2966
|
+
});
|
|
2967
|
+
const version = stdout.trim().split("\n")[0];
|
|
2968
|
+
return { found: true, version, path: command };
|
|
2969
|
+
} catch {
|
|
2970
|
+
}
|
|
2971
|
+
const localPath = join(SVAMP_TOOLS_BIN, command);
|
|
2972
|
+
try {
|
|
2973
|
+
const { stdout } = await execFileAsync(localPath, versionArgs, {
|
|
2974
|
+
timeout: 5e3,
|
|
2975
|
+
env: envWithPath
|
|
2976
|
+
});
|
|
2977
|
+
const version = stdout.trim().split("\n")[0];
|
|
2978
|
+
return { found: true, version, path: localPath };
|
|
2979
|
+
} catch {
|
|
2980
|
+
}
|
|
2981
|
+
return { found: false };
|
|
2982
|
+
}
|
|
2983
|
+
async function installSrt() {
|
|
2984
|
+
const platform = process.platform;
|
|
2985
|
+
if (platform !== "darwin" && platform !== "linux") {
|
|
2986
|
+
return false;
|
|
2987
|
+
}
|
|
2988
|
+
console.log("[isolation] srt not found. Installing @anthropic-ai/sandbox-runtime...");
|
|
2989
|
+
try {
|
|
2990
|
+
await mkdir(SVAMP_TOOLS_DIR, { recursive: true });
|
|
2991
|
+
await execFileAsync("npm", [
|
|
2992
|
+
"install",
|
|
2993
|
+
"--prefix",
|
|
2994
|
+
SVAMP_TOOLS_DIR,
|
|
2995
|
+
"@anthropic-ai/sandbox-runtime@latest",
|
|
2996
|
+
"@vscode/ripgrep"
|
|
2997
|
+
], { timeout: 12e4 });
|
|
2998
|
+
const srtPath = join(SVAMP_TOOLS_BIN, "srt");
|
|
2999
|
+
try {
|
|
3000
|
+
await access(srtPath);
|
|
3001
|
+
console.log(`[isolation] srt installed successfully at ${srtPath}`);
|
|
3002
|
+
return true;
|
|
3003
|
+
} catch {
|
|
3004
|
+
console.warn("[isolation] npm install completed but srt binary not found");
|
|
3005
|
+
return false;
|
|
3006
|
+
}
|
|
3007
|
+
} catch (e) {
|
|
3008
|
+
console.warn(`[isolation] Failed to install srt: ${e.message}`);
|
|
3009
|
+
return false;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
async function parseIsolationTestOutput(stdout, probeFile) {
|
|
3013
|
+
const output = stdout.trim();
|
|
3014
|
+
const wMatch = output.match(/W=(\d+)/);
|
|
3015
|
+
const pMatch = output.match(/P=(\d+)/);
|
|
3016
|
+
if (!wMatch || !pMatch) {
|
|
3017
|
+
return { passed: false, error: `unexpected test output: ${output}` };
|
|
3018
|
+
}
|
|
3019
|
+
const workspaceWriteOk = wMatch[1] === "0";
|
|
3020
|
+
const probeExitCode = parseInt(pMatch[1], 10);
|
|
3021
|
+
if (!workspaceWriteOk) {
|
|
3022
|
+
return { passed: false, error: "sandbox blocked writes to allowed workspace path" };
|
|
3023
|
+
}
|
|
3024
|
+
if (probeExitCode === 0) {
|
|
3025
|
+
try {
|
|
3026
|
+
await access(probeFile);
|
|
3027
|
+
return {
|
|
3028
|
+
passed: false,
|
|
3029
|
+
error: "writes outside workspace were NOT blocked \u2014 file leaked to host filesystem"
|
|
3030
|
+
};
|
|
3031
|
+
} catch {
|
|
3032
|
+
return { passed: true };
|
|
3033
|
+
}
|
|
3034
|
+
}
|
|
3035
|
+
return { passed: true };
|
|
3036
|
+
}
|
|
3037
|
+
async function verifySrtIsolation(binaryPath) {
|
|
3038
|
+
const testBase = "/tmp";
|
|
3039
|
+
const workDir = await mkdtemp(join(testBase, "svamp-iso-work-"));
|
|
3040
|
+
const probeDir = await mkdtemp(join(testBase, "svamp-iso-probe-"));
|
|
3041
|
+
const probeFile = join(probeDir, "leak-test");
|
|
3042
|
+
const settingsFile = join(workDir, "srt-settings.json");
|
|
3043
|
+
try {
|
|
3044
|
+
await writeFile(settingsFile, JSON.stringify({
|
|
3045
|
+
filesystem: {
|
|
3046
|
+
denyRead: [],
|
|
3047
|
+
allowWrite: [workDir],
|
|
3048
|
+
denyWrite: []
|
|
3049
|
+
},
|
|
3050
|
+
network: {
|
|
3051
|
+
allowedDomains: [],
|
|
3052
|
+
deniedDomains: []
|
|
3053
|
+
}
|
|
3054
|
+
}));
|
|
3055
|
+
const testScript = [
|
|
3056
|
+
`echo ok > "${workDir}/test" 2>/dev/null; W=$?`,
|
|
3057
|
+
`echo leak > "${probeFile}" 2>/dev/null; P=$?`,
|
|
3058
|
+
`echo "W=$W P=$P"`
|
|
3059
|
+
].join("; ");
|
|
3060
|
+
const { stdout } = await execFileAsync(binaryPath, [
|
|
3061
|
+
"--settings",
|
|
3062
|
+
settingsFile,
|
|
3063
|
+
"sh",
|
|
3064
|
+
"-c",
|
|
3065
|
+
testScript
|
|
3066
|
+
], {
|
|
3067
|
+
timeout: 15e3,
|
|
3068
|
+
env: { ...process.env, PATH: getToolsPath() }
|
|
3069
|
+
});
|
|
3070
|
+
return parseIsolationTestOutput(stdout, probeFile);
|
|
3071
|
+
} catch (e) {
|
|
3072
|
+
return { passed: false, error: e.message };
|
|
3073
|
+
} finally {
|
|
3074
|
+
await rm(workDir, { recursive: true, force: true }).catch(() => {
|
|
3075
|
+
});
|
|
3076
|
+
await rm(probeDir, { recursive: true, force: true }).catch(() => {
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
}
|
|
3080
|
+
async function verifyBwrapIsolation(binaryPath) {
|
|
3081
|
+
const testBase = "/tmp";
|
|
3082
|
+
const workDir = await mkdtemp(join(testBase, "svamp-iso-work-"));
|
|
3083
|
+
const probeDir = await mkdtemp(join(testBase, "svamp-iso-probe-"));
|
|
3084
|
+
const probeFile = join(probeDir, "leak-test");
|
|
3085
|
+
try {
|
|
3086
|
+
const testScript = [
|
|
3087
|
+
`echo ok > "${workDir}/test" 2>/dev/null; W=$?`,
|
|
3088
|
+
`echo leak > "${probeFile}" 2>/dev/null; P=$?`,
|
|
3089
|
+
`echo "W=$W P=$P"`
|
|
3090
|
+
].join("; ");
|
|
3091
|
+
const { stdout } = await execFileAsync(binaryPath, [
|
|
3092
|
+
"--ro-bind",
|
|
3093
|
+
"/",
|
|
3094
|
+
"/",
|
|
3095
|
+
"--bind",
|
|
3096
|
+
workDir,
|
|
3097
|
+
workDir,
|
|
3098
|
+
"--dev",
|
|
3099
|
+
"/dev",
|
|
3100
|
+
"--proc",
|
|
3101
|
+
"/proc",
|
|
3102
|
+
"--unshare-pid",
|
|
3103
|
+
"--die-with-parent",
|
|
3104
|
+
"--",
|
|
3105
|
+
"sh",
|
|
3106
|
+
"-c",
|
|
3107
|
+
testScript
|
|
3108
|
+
], { timeout: 15e3 });
|
|
3109
|
+
return parseIsolationTestOutput(stdout, probeFile);
|
|
3110
|
+
} catch (e) {
|
|
3111
|
+
return { passed: false, error: e.message };
|
|
3112
|
+
} finally {
|
|
3113
|
+
await rm(workDir, { recursive: true, force: true }).catch(() => {
|
|
3114
|
+
});
|
|
3115
|
+
await rm(probeDir, { recursive: true, force: true }).catch(() => {
|
|
3116
|
+
});
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
async function verifyContainerIsolation(method, binaryPath) {
|
|
3120
|
+
try {
|
|
3121
|
+
await execFileAsync(binaryPath, ["info"], { timeout: 15e3 });
|
|
3122
|
+
} catch (e) {
|
|
3123
|
+
return { passed: false, error: `${method} daemon not accessible: ${e.message}` };
|
|
3124
|
+
}
|
|
3125
|
+
try {
|
|
3126
|
+
const { stdout } = await execFileAsync(
|
|
3127
|
+
binaryPath,
|
|
3128
|
+
["run", "--rm", "--network=none", "alpine", "echo", "isolation-ok"],
|
|
3129
|
+
{ timeout: 3e4 }
|
|
3130
|
+
);
|
|
3131
|
+
if (!stdout.includes("isolation-ok")) {
|
|
3132
|
+
return { passed: false, error: `container test returned unexpected output: ${stdout.trim()}` };
|
|
3133
|
+
}
|
|
3134
|
+
return { passed: true };
|
|
3135
|
+
} catch (e) {
|
|
3136
|
+
if (e.message?.includes("pull access denied") || e.message?.includes("not found")) {
|
|
3137
|
+
return { passed: true };
|
|
3138
|
+
}
|
|
3139
|
+
return { passed: false, error: `container test failed: ${e.message}` };
|
|
3140
|
+
}
|
|
3141
|
+
}
|
|
3142
|
+
async function verifyIsolation(method, binaryPath) {
|
|
3143
|
+
switch (method) {
|
|
3144
|
+
case "srt":
|
|
3145
|
+
return verifySrtIsolation(binaryPath);
|
|
3146
|
+
case "bwrap":
|
|
3147
|
+
return verifyBwrapIsolation(binaryPath);
|
|
3148
|
+
case "docker":
|
|
3149
|
+
case "podman":
|
|
3150
|
+
return verifyContainerIsolation(method, binaryPath);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
async function detectIsolationCapabilities() {
|
|
3154
|
+
const srtEnv = { PATH: getToolsPath() };
|
|
3155
|
+
const checks = {
|
|
3156
|
+
srt: checkCommand("srt", ["--version"], srtEnv),
|
|
3157
|
+
bwrap: checkCommand("bwrap", ["--version"]),
|
|
3158
|
+
docker: checkCommand("docker", ["--version"]),
|
|
3159
|
+
podman: checkCommand("podman", ["--version"])
|
|
3160
|
+
};
|
|
3161
|
+
const checkResults = await Promise.all(
|
|
3162
|
+
Object.entries(checks).map(async ([method, promise]) => {
|
|
3163
|
+
const detail = await promise;
|
|
3164
|
+
return [method, detail];
|
|
3165
|
+
})
|
|
3166
|
+
);
|
|
3167
|
+
const details = Object.fromEntries(checkResults);
|
|
3168
|
+
if (!details.srt.found) {
|
|
3169
|
+
const installed = await installSrt();
|
|
3170
|
+
if (installed) {
|
|
3171
|
+
details.srt = await checkCommand("srt", ["--version"], srtEnv);
|
|
3172
|
+
}
|
|
3173
|
+
}
|
|
3174
|
+
const foundMethods = ISOLATION_PREFERENCE.filter((m) => details[m].found);
|
|
3175
|
+
if (foundMethods.length > 0) {
|
|
3176
|
+
console.log(`[isolation] Found runtimes: ${foundMethods.join(", ")}. Verifying isolation...`);
|
|
3177
|
+
await Promise.all(
|
|
3178
|
+
foundMethods.map(async (method) => {
|
|
3179
|
+
const binaryPath = details[method].path || method;
|
|
3180
|
+
const result = await verifyIsolation(method, binaryPath);
|
|
3181
|
+
details[method].verified = result.passed;
|
|
3182
|
+
if (!result.passed) {
|
|
3183
|
+
details[method].verificationError = result.error;
|
|
3184
|
+
console.warn(
|
|
3185
|
+
`[isolation] WARNING: ${method} found but verification FAILED: ${result.error}. This runtime will NOT be used for isolation.`
|
|
3186
|
+
);
|
|
3187
|
+
} else {
|
|
3188
|
+
console.log(`[isolation] ${method}: verified OK`);
|
|
3189
|
+
}
|
|
3190
|
+
})
|
|
3191
|
+
);
|
|
3192
|
+
}
|
|
3193
|
+
const available = ISOLATION_PREFERENCE.filter(
|
|
3194
|
+
(m) => details[m].found && details[m].verified === true
|
|
3195
|
+
);
|
|
3196
|
+
const preferred = available.length > 0 ? available[0] : null;
|
|
3197
|
+
if (available.length === 0 && foundMethods.length > 0) {
|
|
3198
|
+
console.warn(
|
|
3199
|
+
`[isolation] No isolation runtime passed verification (found: ${foundMethods.join(", ")}). Session sharing will be DISABLED.`
|
|
3200
|
+
);
|
|
3201
|
+
} else if (available.length === 0) {
|
|
3202
|
+
console.log("[isolation] No isolation runtimes found. Session sharing will be unavailable.");
|
|
3203
|
+
} else {
|
|
3204
|
+
console.log(`[isolation] Preferred isolation method: ${preferred}`);
|
|
3205
|
+
}
|
|
3206
|
+
return { available, preferred, details };
|
|
3207
|
+
}
|
|
3208
|
+
|
|
2232
3209
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
2233
3210
|
const __dirname$1 = dirname(__filename$1);
|
|
2234
3211
|
function loadDotEnv() {
|
|
@@ -2399,22 +3376,24 @@ function deletePersistedSession(sessionId) {
|
|
|
2399
3376
|
function loadPersistedSessions() {
|
|
2400
3377
|
const sessions = [];
|
|
2401
3378
|
const index = loadSessionIndex();
|
|
3379
|
+
let indexChanged = false;
|
|
2402
3380
|
for (const [sessionId, entry] of Object.entries(index)) {
|
|
2403
3381
|
const filePath = getSessionFilePath(entry.directory, sessionId);
|
|
2404
3382
|
try {
|
|
2405
3383
|
const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
|
|
2406
3384
|
if (data.sessionId && data.directory) {
|
|
2407
|
-
if (data.stopped) {
|
|
2408
|
-
delete index[sessionId];
|
|
2409
|
-
} else {
|
|
3385
|
+
if (!data.stopped) {
|
|
2410
3386
|
sessions.push(data);
|
|
2411
3387
|
}
|
|
2412
3388
|
}
|
|
2413
3389
|
} catch {
|
|
2414
3390
|
delete index[sessionId];
|
|
3391
|
+
indexChanged = true;
|
|
2415
3392
|
}
|
|
2416
3393
|
}
|
|
2417
|
-
|
|
3394
|
+
if (indexChanged) {
|
|
3395
|
+
saveSessionIndex(index);
|
|
3396
|
+
}
|
|
2418
3397
|
return sessions;
|
|
2419
3398
|
}
|
|
2420
3399
|
function ensureHomeDir() {
|
|
@@ -2506,6 +3485,7 @@ async function startDaemon() {
|
|
|
2506
3485
|
let isReconnecting = false;
|
|
2507
3486
|
process.on("SIGINT", () => requestShutdown("os-signal"));
|
|
2508
3487
|
process.on("SIGTERM", () => requestShutdown("os-signal"));
|
|
3488
|
+
process.on("SIGUSR1", () => requestShutdown("os-signal-cleanup"));
|
|
2509
3489
|
process.on("uncaughtException", (error) => {
|
|
2510
3490
|
if (shutdownRequested) return;
|
|
2511
3491
|
logger.error("Uncaught exception:", error);
|
|
@@ -2657,8 +3637,8 @@ async function startDaemon() {
|
|
|
2657
3637
|
}
|
|
2658
3638
|
const sessionId = options.sessionId || randomUUID$1();
|
|
2659
3639
|
const agentName = options.agent || agentConfig.agent_type || "claude";
|
|
2660
|
-
if (agentName !== "claude" && KNOWN_ACP_AGENTS[agentName]) {
|
|
2661
|
-
return await
|
|
3640
|
+
if (agentName !== "claude" && (KNOWN_ACP_AGENTS[agentName] || KNOWN_MCP_AGENTS[agentName])) {
|
|
3641
|
+
return await spawnAgentSession(sessionId, directory, agentName, options, resumeSessionId);
|
|
2662
3642
|
}
|
|
2663
3643
|
try {
|
|
2664
3644
|
let parseBashPermission2 = function(permission) {
|
|
@@ -2707,8 +3687,19 @@ async function startDaemon() {
|
|
|
2707
3687
|
proc.kill(signal);
|
|
2708
3688
|
}
|
|
2709
3689
|
});
|
|
3690
|
+
}, buildIsolationConfig2 = function(dir) {
|
|
3691
|
+
if (!sessionMetadata.sharing?.enabled) return null;
|
|
3692
|
+
const method = isolationCapabilities.preferred;
|
|
3693
|
+
if (!method) return null;
|
|
3694
|
+
const detail = isolationCapabilities.details[method];
|
|
3695
|
+
if (!detail.found || detail.verified === false) return null;
|
|
3696
|
+
return {
|
|
3697
|
+
method,
|
|
3698
|
+
binaryPath: detail.path || method,
|
|
3699
|
+
workspacePath: dir
|
|
3700
|
+
};
|
|
2710
3701
|
};
|
|
2711
|
-
var parseBashPermission = parseBashPermission2, shouldAutoAllow = shouldAutoAllow2, killAndWaitForExit = killAndWaitForExit2;
|
|
3702
|
+
var parseBashPermission = parseBashPermission2, shouldAutoAllow = shouldAutoAllow2, killAndWaitForExit = killAndWaitForExit2, buildIsolationConfig = buildIsolationConfig2;
|
|
2712
3703
|
let sessionMetadata = {
|
|
2713
3704
|
path: directory,
|
|
2714
3705
|
host: os.hostname(),
|
|
@@ -2720,7 +3711,8 @@ async function startDaemon() {
|
|
|
2720
3711
|
svampToolsDir: join$1(__dirname$1, "..", "tools"),
|
|
2721
3712
|
startedFromDaemon: true,
|
|
2722
3713
|
startedBy: "daemon",
|
|
2723
|
-
lifecycleState: resumeSessionId ? "idle" : "starting"
|
|
3714
|
+
lifecycleState: resumeSessionId ? "idle" : "starting",
|
|
3715
|
+
sharing: options.sharing
|
|
2724
3716
|
};
|
|
2725
3717
|
let claudeProcess = null;
|
|
2726
3718
|
const allPersisted = loadPersistedSessions();
|
|
@@ -2756,8 +3748,14 @@ async function startDaemon() {
|
|
|
2756
3748
|
let turnInitiatedByUser = true;
|
|
2757
3749
|
let isKillingClaude = false;
|
|
2758
3750
|
let checkSvampConfig;
|
|
3751
|
+
const CLAUDE_PERMISSION_MODE_MAP = {
|
|
3752
|
+
"auto-approve-all": "bypassPermissions"
|
|
3753
|
+
};
|
|
3754
|
+
const toClaudePermissionMode = (mode) => CLAUDE_PERMISSION_MODE_MAP[mode] || mode;
|
|
3755
|
+
let isolationCleanupFiles = [];
|
|
2759
3756
|
const spawnClaude = (initialMessage, meta) => {
|
|
2760
|
-
const
|
|
3757
|
+
const rawPermissionMode = meta?.permissionMode || agentConfig.default_permission_mode || currentPermissionMode;
|
|
3758
|
+
const permissionMode = toClaudePermissionMode(rawPermissionMode);
|
|
2761
3759
|
currentPermissionMode = permissionMode;
|
|
2762
3760
|
const model = meta?.model || agentConfig.default_model || void 0;
|
|
2763
3761
|
const appendSystemPrompt = meta?.appendSystemPrompt || agentConfig.append_system_prompt || void 0;
|
|
@@ -2775,10 +3773,26 @@ async function startDaemon() {
|
|
|
2775
3773
|
if (model) args.push("--model", model);
|
|
2776
3774
|
if (appendSystemPrompt) args.push("--append-system-prompt", appendSystemPrompt);
|
|
2777
3775
|
if (claudeResumeId) args.push("--resume", claudeResumeId);
|
|
2778
|
-
|
|
2779
|
-
|
|
3776
|
+
let spawnCommand = "claude";
|
|
3777
|
+
let spawnArgs = args;
|
|
3778
|
+
let extraEnv = {};
|
|
3779
|
+
isolationCleanupFiles = [];
|
|
3780
|
+
const isoConfig = buildIsolationConfig2(directory);
|
|
3781
|
+
if (isoConfig) {
|
|
3782
|
+
const wrapped = wrapWithIsolation(spawnCommand, spawnArgs, isoConfig);
|
|
3783
|
+
spawnCommand = wrapped.command;
|
|
3784
|
+
spawnArgs = wrapped.args;
|
|
3785
|
+
if (wrapped.env) extraEnv = wrapped.env;
|
|
3786
|
+
if (wrapped.cleanupFiles) isolationCleanupFiles = wrapped.cleanupFiles;
|
|
3787
|
+
sessionMetadata = { ...sessionMetadata, isolationMethod: isoConfig.method };
|
|
3788
|
+
logger.log(`[Session ${sessionId}] Isolation: ${isoConfig.method} (binary: ${isoConfig.binaryPath})`);
|
|
3789
|
+
} else {
|
|
3790
|
+
sessionMetadata = { ...sessionMetadata, isolationMethod: void 0 };
|
|
3791
|
+
}
|
|
3792
|
+
logger.log(`[Session ${sessionId}] Spawning Claude: ${spawnCommand} ${spawnArgs.join(" ")} (cwd: ${directory})`);
|
|
3793
|
+
const spawnEnv = { ...process.env, ...extraEnv };
|
|
2780
3794
|
delete spawnEnv.CLAUDECODE;
|
|
2781
|
-
const child = spawn$1(
|
|
3795
|
+
const child = spawn$1(spawnCommand, spawnArgs, {
|
|
2782
3796
|
cwd: directory,
|
|
2783
3797
|
stdio: ["pipe", "pipe", "pipe"],
|
|
2784
3798
|
env: spawnEnv,
|
|
@@ -2788,17 +3802,11 @@ async function startDaemon() {
|
|
|
2788
3802
|
logger.log(`[Session ${sessionId}] Claude PID: ${child.pid}, stdin: ${!!child.stdin}, stdout: ${!!child.stdout}, stderr: ${!!child.stderr}`);
|
|
2789
3803
|
child.on("error", (err) => {
|
|
2790
3804
|
logger.log(`[Session ${sessionId}] Claude process error: ${err.message}`);
|
|
2791
|
-
sessionService.pushMessage(
|
|
2792
|
-
type: "
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
Please ensure Claude Code CLI is installed on this machine. You can install it with:
|
|
2798
|
-
\`npm install -g @anthropic-ai/claude-code\``
|
|
2799
|
-
}]
|
|
2800
|
-
}, "agent");
|
|
2801
|
-
sessionService.sendKeepAlive(false);
|
|
3805
|
+
sessionService.pushMessage(
|
|
3806
|
+
{ type: "message", message: `Agent process exited unexpectedly: ${err.message}. Please ensure Claude Code CLI is installed.` },
|
|
3807
|
+
"event"
|
|
3808
|
+
);
|
|
3809
|
+
sessionService.sendSessionEnd();
|
|
2802
3810
|
});
|
|
2803
3811
|
let stdoutBuffer = "";
|
|
2804
3812
|
let lastErrorMessagePushed = false;
|
|
@@ -2900,15 +3908,24 @@ Please ensure Claude Code CLI is installed on this machine. You can install it w
|
|
|
2900
3908
|
}
|
|
2901
3909
|
}
|
|
2902
3910
|
if (msg.type === "result") {
|
|
2903
|
-
if (msg.is_error
|
|
3911
|
+
if (msg.is_error) {
|
|
2904
3912
|
const resultText = msg.result || "";
|
|
2905
|
-
logger.error(`[Session ${sessionId}] Claude
|
|
2906
|
-
const
|
|
2907
|
-
const
|
|
2908
|
-
const
|
|
3913
|
+
logger.error(`[Session ${sessionId}] Claude error (is_error=true, api_ms=${msg.duration_api_ms}): "${resultText}"`);
|
|
3914
|
+
const lower = resultText.toLowerCase();
|
|
3915
|
+
const isLoginIssue = lower.includes("login") || lower.includes("logged in") || lower.includes("auth") || lower.includes("api key") || lower.includes("unauthorized");
|
|
3916
|
+
const isResumeIssue = lower.includes("tool_use.name") || lower.includes("invalid_request") || lower.includes("messages.");
|
|
3917
|
+
let hint = "";
|
|
3918
|
+
if (isLoginIssue) {
|
|
3919
|
+
hint = "\n\nRun `claude login` in your terminal on the machine running the daemon to re-authenticate.";
|
|
3920
|
+
} else if (isResumeIssue) {
|
|
3921
|
+
hint = "\n\nThe conversation history may be corrupted. Try starting a fresh session.";
|
|
3922
|
+
} else {
|
|
3923
|
+
hint = "\n\nCheck that the Claude Code CLI is properly installed and configured.";
|
|
3924
|
+
}
|
|
3925
|
+
const displayMsg = resultText || "Claude Code exited with an error.";
|
|
2909
3926
|
sessionService.pushMessage({
|
|
2910
3927
|
type: "assistant",
|
|
2911
|
-
content: [{ type: "text", text:
|
|
3928
|
+
content: [{ type: "text", text: `**Error:** ${displayMsg}${hint}` }]
|
|
2912
3929
|
}, "agent");
|
|
2913
3930
|
lastErrorMessagePushed = true;
|
|
2914
3931
|
}
|
|
@@ -2983,25 +4000,25 @@ Please ensure Claude Code CLI is installed on this machine. You can install it w
|
|
|
2983
4000
|
child.on("exit", (code, signal) => {
|
|
2984
4001
|
logger.log(`[Session ${sessionId}] Claude exited: code=${code}, signal=${signal}`);
|
|
2985
4002
|
claudeProcess = null;
|
|
4003
|
+
for (const f of isolationCleanupFiles) {
|
|
4004
|
+
fs.rm(f, { force: true }).catch(() => {
|
|
4005
|
+
});
|
|
4006
|
+
}
|
|
4007
|
+
isolationCleanupFiles = [];
|
|
2986
4008
|
for (const [id, pending] of pendingPermissions) {
|
|
2987
4009
|
pending.resolve({ behavior: "deny", message: "Claude process exited" });
|
|
2988
4010
|
}
|
|
2989
4011
|
pendingPermissions.clear();
|
|
2990
4012
|
if (code !== 0 && code !== null && !lastErrorMessagePushed) {
|
|
2991
|
-
sessionService.pushMessage(
|
|
2992
|
-
type: "
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
text: `Error: Claude process exited with code ${code}${signal ? ` (signal: ${signal})` : ""}.
|
|
2996
|
-
|
|
2997
|
-
This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
2998
|
-
}]
|
|
2999
|
-
}, "agent");
|
|
4013
|
+
sessionService.pushMessage(
|
|
4014
|
+
{ type: "message", message: `Agent process exited unexpectedly (code ${code}${signal ? `, signal: ${signal}` : ""})` },
|
|
4015
|
+
"event"
|
|
4016
|
+
);
|
|
3000
4017
|
}
|
|
3001
4018
|
lastErrorMessagePushed = false;
|
|
3002
4019
|
sessionMetadata = { ...sessionMetadata, lifecycleState: claudeResumeId ? "idle" : "stopped" };
|
|
3003
4020
|
sessionService.updateMetadata(sessionMetadata);
|
|
3004
|
-
sessionService.
|
|
4021
|
+
sessionService.sendSessionEnd();
|
|
3005
4022
|
if (claudeResumeId && !trackedSession.stopped) {
|
|
3006
4023
|
saveSession({
|
|
3007
4024
|
sessionId,
|
|
@@ -3052,7 +4069,7 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3052
4069
|
text = typeof content === "string" ? content : JSON.stringify(content);
|
|
3053
4070
|
}
|
|
3054
4071
|
if (msgMeta?.permissionMode) {
|
|
3055
|
-
currentPermissionMode = msgMeta.permissionMode;
|
|
4072
|
+
currentPermissionMode = toClaudePermissionMode(msgMeta.permissionMode);
|
|
3056
4073
|
logger.log(`[Session ${sessionId}] Permission mode updated to: ${currentPermissionMode}`);
|
|
3057
4074
|
}
|
|
3058
4075
|
if (isKillingClaude) {
|
|
@@ -3082,8 +4099,8 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3082
4099
|
const requestId = params.id;
|
|
3083
4100
|
const pending = pendingPermissions.get(requestId);
|
|
3084
4101
|
if (params.mode) {
|
|
3085
|
-
|
|
3086
|
-
|
|
4102
|
+
currentPermissionMode = toClaudePermissionMode(params.mode);
|
|
4103
|
+
logger.log(`[Session ${sessionId}] Permission mode changed to: ${currentPermissionMode}`);
|
|
3087
4104
|
}
|
|
3088
4105
|
if (params.allowTools && Array.isArray(params.allowTools)) {
|
|
3089
4106
|
for (const tool of params.allowTools) {
|
|
@@ -3279,8 +4296,8 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3279
4296
|
};
|
|
3280
4297
|
}
|
|
3281
4298
|
};
|
|
3282
|
-
const
|
|
3283
|
-
logger.log(`[
|
|
4299
|
+
const spawnAgentSession = async (sessionId, directory, agentName, options, resumeSessionId) => {
|
|
4300
|
+
logger.log(`[Agent] Spawning ${agentName} session: ${sessionId}`);
|
|
3284
4301
|
try {
|
|
3285
4302
|
let parseBashPermission2 = function(permission) {
|
|
3286
4303
|
if (permission === "Bash") return;
|
|
@@ -3321,7 +4338,8 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3321
4338
|
startedFromDaemon: true,
|
|
3322
4339
|
startedBy: "daemon",
|
|
3323
4340
|
lifecycleState: "starting",
|
|
3324
|
-
flavor: agentName
|
|
4341
|
+
flavor: agentName,
|
|
4342
|
+
sharing: options.sharing
|
|
3325
4343
|
};
|
|
3326
4344
|
let currentPermissionMode = "default";
|
|
3327
4345
|
const allowedTools = /* @__PURE__ */ new Set();
|
|
@@ -3335,7 +4353,7 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3335
4353
|
{ controlledByUser: false },
|
|
3336
4354
|
{
|
|
3337
4355
|
onUserMessage: (content, meta) => {
|
|
3338
|
-
logger.log(`[
|
|
4356
|
+
logger.log(`[${agentName} Session ${sessionId}] User message received`);
|
|
3339
4357
|
let text;
|
|
3340
4358
|
let msgMeta = meta;
|
|
3341
4359
|
try {
|
|
@@ -3355,17 +4373,17 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3355
4373
|
if (msgMeta?.permissionMode) {
|
|
3356
4374
|
currentPermissionMode = msgMeta.permissionMode;
|
|
3357
4375
|
}
|
|
3358
|
-
|
|
3359
|
-
logger.error(`[
|
|
4376
|
+
agentBackend.sendPrompt(sessionId, text).catch((err) => {
|
|
4377
|
+
logger.error(`[${agentName} Session ${sessionId}] Error sending prompt:`, err);
|
|
3360
4378
|
});
|
|
3361
4379
|
},
|
|
3362
4380
|
onAbort: () => {
|
|
3363
|
-
logger.log(`[
|
|
3364
|
-
|
|
4381
|
+
logger.log(`[${agentName} Session ${sessionId}] Abort requested`);
|
|
4382
|
+
agentBackend.cancel(sessionId).catch(() => {
|
|
3365
4383
|
});
|
|
3366
4384
|
},
|
|
3367
4385
|
onPermissionResponse: (params) => {
|
|
3368
|
-
logger.log(`[
|
|
4386
|
+
logger.log(`[${agentName} Session ${sessionId}] Permission response:`, JSON.stringify(params));
|
|
3369
4387
|
const requestId = params.id;
|
|
3370
4388
|
if (params.mode) currentPermissionMode = params.mode;
|
|
3371
4389
|
if (params.allowTools && Array.isArray(params.allowTools)) {
|
|
@@ -3378,17 +4396,18 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3378
4396
|
}
|
|
3379
4397
|
}
|
|
3380
4398
|
permissionHandler.resolvePermission(requestId, params.approved);
|
|
4399
|
+
agentBackend.respondToPermission?.(requestId, params.approved);
|
|
3381
4400
|
},
|
|
3382
4401
|
onSwitchMode: (mode) => {
|
|
3383
|
-
logger.log(`[
|
|
4402
|
+
logger.log(`[${agentName} Session ${sessionId}] Switch mode: ${mode}`);
|
|
3384
4403
|
currentPermissionMode = mode;
|
|
3385
4404
|
},
|
|
3386
4405
|
onRestartClaude: async () => {
|
|
3387
|
-
logger.log(`[
|
|
4406
|
+
logger.log(`[${agentName} Session ${sessionId}] Restart agent requested`);
|
|
3388
4407
|
return { success: false, message: "Restart is not supported for this agent type." };
|
|
3389
4408
|
},
|
|
3390
4409
|
onKillSession: () => {
|
|
3391
|
-
logger.log(`[
|
|
4410
|
+
logger.log(`[${agentName} Session ${sessionId}] Kill session requested`);
|
|
3392
4411
|
stopSession(sessionId);
|
|
3393
4412
|
},
|
|
3394
4413
|
onBash: async (command, cwd) => {
|
|
@@ -3490,21 +4509,46 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3490
4509
|
sessionService,
|
|
3491
4510
|
logger
|
|
3492
4511
|
);
|
|
3493
|
-
const transportHandler = agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(agentName);
|
|
3494
4512
|
const permissionHandler = new HyphaPermissionHandler(shouldAutoAllow2, logger.log);
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
|
|
3505
|
-
|
|
4513
|
+
let agentIsoConfig;
|
|
4514
|
+
if (sessionMetadata.sharing?.enabled && isolationCapabilities.preferred) {
|
|
4515
|
+
const method = isolationCapabilities.preferred;
|
|
4516
|
+
const detail = isolationCapabilities.details[method];
|
|
4517
|
+
if (detail.found && detail.verified !== false) {
|
|
4518
|
+
agentIsoConfig = {
|
|
4519
|
+
method,
|
|
4520
|
+
binaryPath: detail.path || method,
|
|
4521
|
+
workspacePath: directory
|
|
4522
|
+
};
|
|
4523
|
+
sessionMetadata = { ...sessionMetadata, isolationMethod: method };
|
|
4524
|
+
logger.log(`[Agent Session ${sessionId}] Isolation: ${method}`);
|
|
4525
|
+
}
|
|
4526
|
+
}
|
|
4527
|
+
let agentBackend;
|
|
4528
|
+
if (KNOWN_MCP_AGENTS[agentName]) {
|
|
4529
|
+
agentBackend = new CodexMcpBackend({
|
|
4530
|
+
cwd: directory,
|
|
4531
|
+
env: options.environmentVariables,
|
|
4532
|
+
log: logger.log,
|
|
4533
|
+
isolationConfig: agentIsoConfig
|
|
4534
|
+
});
|
|
4535
|
+
} else {
|
|
4536
|
+
const transportHandler = agentName === "gemini" ? new GeminiTransport() : new DefaultTransport(agentName);
|
|
4537
|
+
const acpConfig = KNOWN_ACP_AGENTS[agentName];
|
|
4538
|
+
agentBackend = new AcpBackend({
|
|
4539
|
+
agentName,
|
|
4540
|
+
cwd: directory,
|
|
4541
|
+
command: acpConfig.command,
|
|
4542
|
+
args: acpConfig.args,
|
|
4543
|
+
env: options.environmentVariables,
|
|
4544
|
+
permissionHandler,
|
|
4545
|
+
transportHandler,
|
|
4546
|
+
log: logger.log,
|
|
4547
|
+
isolationConfig: agentIsoConfig
|
|
4548
|
+
});
|
|
4549
|
+
}
|
|
3506
4550
|
bridgeAcpToSession(
|
|
3507
|
-
|
|
4551
|
+
agentBackend,
|
|
3508
4552
|
sessionService,
|
|
3509
4553
|
() => sessionMetadata,
|
|
3510
4554
|
(updater) => {
|
|
@@ -3523,33 +4567,28 @@ This may indicate that Claude Code CLI is not properly installed or configured.`
|
|
|
3523
4567
|
directory,
|
|
3524
4568
|
resumeSessionId,
|
|
3525
4569
|
get childProcess() {
|
|
3526
|
-
return
|
|
4570
|
+
return agentBackend.getProcess?.() || void 0;
|
|
3527
4571
|
}
|
|
3528
4572
|
};
|
|
3529
4573
|
pidToTrackedSession.set(process.pid + Math.floor(Math.random() * 1e5), trackedSession);
|
|
3530
|
-
logger.log(`[
|
|
3531
|
-
|
|
3532
|
-
logger.log(`[
|
|
4574
|
+
logger.log(`[Agent Session ${sessionId}] Starting ${agentName} backend...`);
|
|
4575
|
+
agentBackend.startSession().then(() => {
|
|
4576
|
+
logger.log(`[Agent Session ${sessionId}] ${agentName} backend started, waiting for first message`);
|
|
3533
4577
|
}).catch((err) => {
|
|
3534
|
-
logger.error(`[
|
|
3535
|
-
sessionService.pushMessage(
|
|
3536
|
-
type: "
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
Please ensure the ${agentName} CLI is installed.`
|
|
3542
|
-
}]
|
|
3543
|
-
}, "agent");
|
|
3544
|
-
sessionService.sendKeepAlive(false);
|
|
4578
|
+
logger.error(`[Agent Session ${sessionId}] Failed to start ${agentName}:`, err);
|
|
4579
|
+
sessionService.pushMessage(
|
|
4580
|
+
{ type: "message", message: `Agent process exited unexpectedly: ${err.message}. Please ensure the ${agentName} CLI is installed.` },
|
|
4581
|
+
"event"
|
|
4582
|
+
);
|
|
4583
|
+
sessionService.sendSessionEnd();
|
|
3545
4584
|
});
|
|
3546
4585
|
return {
|
|
3547
4586
|
type: "success",
|
|
3548
4587
|
sessionId,
|
|
3549
|
-
message: `
|
|
4588
|
+
message: `Agent session (${agentName}) registered as svamp-session-${sessionId}`
|
|
3550
4589
|
};
|
|
3551
4590
|
} catch (err) {
|
|
3552
|
-
logger.error(`[
|
|
4591
|
+
logger.error(`[Agent] Failed to spawn ${agentName} session:`, err);
|
|
3553
4592
|
return {
|
|
3554
4593
|
type: "error",
|
|
3555
4594
|
errorMessage: `Failed to spawn ${agentName} session: ${err.message}`
|
|
@@ -3578,6 +4617,14 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3578
4617
|
logger.log(`Session ${sessionId} not found`);
|
|
3579
4618
|
return false;
|
|
3580
4619
|
};
|
|
4620
|
+
let isolationCapabilities;
|
|
4621
|
+
try {
|
|
4622
|
+
isolationCapabilities = await detectIsolationCapabilities();
|
|
4623
|
+
logger.log(`Isolation capabilities: ${isolationCapabilities.available.join(", ") || "none"} (preferred: ${isolationCapabilities.preferred || "none"})`);
|
|
4624
|
+
} catch (err) {
|
|
4625
|
+
logger.log(`Failed to detect isolation capabilities: ${err}`);
|
|
4626
|
+
isolationCapabilities = { available: [], preferred: null, details: { srt: { found: false }, bwrap: { found: false }, docker: { found: false }, podman: { found: false } } };
|
|
4627
|
+
}
|
|
3581
4628
|
const defaultHomeDir = existsSync$1("/data") ? "/data" : os.homedir();
|
|
3582
4629
|
const machineMetadata = {
|
|
3583
4630
|
host: os.hostname(),
|
|
@@ -3586,7 +4633,8 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3586
4633
|
homeDir: defaultHomeDir,
|
|
3587
4634
|
svampHomeDir: SVAMP_HOME,
|
|
3588
4635
|
svampLibDir: join$1(__dirname$1, ".."),
|
|
3589
|
-
displayName: process.env.SVAMP_DISPLAY_NAME || void 0
|
|
4636
|
+
displayName: process.env.SVAMP_DISPLAY_NAME || void 0,
|
|
4637
|
+
isolationCapabilities
|
|
3590
4638
|
};
|
|
3591
4639
|
const initialDaemonState = {
|
|
3592
4640
|
status: "running",
|
|
@@ -3695,7 +4743,7 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3695
4743
|
console.log(` Service: svamp-machine-${machineId}`);
|
|
3696
4744
|
console.log(` Log file: ${logger.logFilePath}`);
|
|
3697
4745
|
let consecutiveHeartbeatFailures = 0;
|
|
3698
|
-
const MAX_HEARTBEAT_FAILURES =
|
|
4746
|
+
const MAX_HEARTBEAT_FAILURES = 60;
|
|
3699
4747
|
const heartbeatInterval = setInterval(async () => {
|
|
3700
4748
|
try {
|
|
3701
4749
|
const state = readDaemonStateFile();
|
|
@@ -3796,16 +4844,22 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3796
4844
|
}
|
|
3797
4845
|
}
|
|
3798
4846
|
}
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
const
|
|
3803
|
-
|
|
3804
|
-
const
|
|
3805
|
-
|
|
4847
|
+
const shouldMarkStopped = source === "os-signal-cleanup";
|
|
4848
|
+
if (shouldMarkStopped) {
|
|
4849
|
+
try {
|
|
4850
|
+
const index = loadSessionIndex();
|
|
4851
|
+
for (const [sessionId, entry] of Object.entries(index)) {
|
|
4852
|
+
const filePath = getSessionFilePath(entry.directory, sessionId);
|
|
4853
|
+
if (existsSync$1(filePath)) {
|
|
4854
|
+
const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
|
|
4855
|
+
writeFileSync$1(filePath, JSON.stringify({ ...data, stopped: true }, null, 2), "utf-8");
|
|
4856
|
+
}
|
|
3806
4857
|
}
|
|
4858
|
+
logger.log("Marked all sessions as stopped (--cleanup mode)");
|
|
4859
|
+
} catch {
|
|
3807
4860
|
}
|
|
3808
|
-
}
|
|
4861
|
+
} else {
|
|
4862
|
+
logger.log("Sessions preserved for auto-restore on next start");
|
|
3809
4863
|
}
|
|
3810
4864
|
try {
|
|
3811
4865
|
await machineService.disconnect();
|
|
@@ -3841,16 +4895,18 @@ Please ensure the ${agentName} CLI is installed.`
|
|
|
3841
4895
|
process.exit(1);
|
|
3842
4896
|
}
|
|
3843
4897
|
}
|
|
3844
|
-
async function stopDaemon() {
|
|
4898
|
+
async function stopDaemon(options) {
|
|
3845
4899
|
const state = readDaemonStateFile();
|
|
3846
4900
|
if (!state) {
|
|
3847
4901
|
console.log("No daemon running");
|
|
3848
4902
|
return;
|
|
3849
4903
|
}
|
|
4904
|
+
const signal = options?.cleanup ? "SIGUSR1" : "SIGTERM";
|
|
4905
|
+
const mode = options?.cleanup ? "cleanup (sessions will be stopped)" : "quick (sessions preserved for auto-restore)";
|
|
3850
4906
|
try {
|
|
3851
4907
|
process.kill(state.pid, 0);
|
|
3852
|
-
process.kill(state.pid,
|
|
3853
|
-
console.log(`Sent
|
|
4908
|
+
process.kill(state.pid, signal);
|
|
4909
|
+
console.log(`Sent ${signal} to daemon PID ${state.pid} \u2014 ${mode}`);
|
|
3854
4910
|
for (let i = 0; i < 30; i++) {
|
|
3855
4911
|
await new Promise((r) => setTimeout(r, 100));
|
|
3856
4912
|
try {
|
|
@@ -3899,4 +4955,4 @@ function daemonStatus() {
|
|
|
3899
4955
|
}
|
|
3900
4956
|
}
|
|
3901
4957
|
|
|
3902
|
-
export { DefaultTransport$1 as D, GeminiTransport$1 as G, registerSessionService as a, stopDaemon as b,
|
|
4958
|
+
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 };
|