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.
Files changed (80) hide show
  1. package/dist/cli.mjs +114 -42
  2. package/dist/commands-B2xQb9u7.mjs +862 -0
  3. package/dist/commands-C5pW2VmI.mjs +862 -0
  4. package/dist/commands-CtO1WRt4.mjs +862 -0
  5. package/dist/commands-TZkNivgV.mjs +862 -0
  6. package/dist/index.mjs +8 -2
  7. package/dist/{package-CgBD49cA.mjs → package-CKOQ5lA7.mjs} +2 -2
  8. package/dist/{run-DjfPjgOb.mjs → run-BCSNMhiz.mjs} +1200 -132
  9. package/dist/{run-Cmostc0S.mjs → run-CtCTd6if.mjs} +1225 -169
  10. package/dist/{run-DYhBROuo.mjs → run-Cxdc5Zmw.mjs} +1253 -211
  11. package/dist/{run-CL-FS4Yc.mjs → run-dBWhjQRf.mjs} +1255 -211
  12. package/package.json +3 -6
  13. package/bin/svamp-agent.mjs +0 -34
  14. package/dist/agent-cli.mjs +0 -453
  15. package/dist/commands-1CYZC6Xh.mjs +0 -481
  16. package/dist/commands-B1DcpgLW.mjs +0 -481
  17. package/dist/commands-BGmdgMAC.mjs +0 -485
  18. package/dist/commands-BOeSil-P.mjs +0 -459
  19. package/dist/commands-BU4GZQuH.mjs +0 -481
  20. package/dist/commands-Ba66PxtQ.mjs +0 -481
  21. package/dist/commands-C0-xqIIc.mjs +0 -481
  22. package/dist/commands-C7Qy5n6d.mjs +0 -481
  23. package/dist/commands-CKpC8R9T.mjs +0 -481
  24. package/dist/commands-CNqOjR1y.mjs +0 -481
  25. package/dist/commands-CVKh1tWr.mjs +0 -485
  26. package/dist/commands-CYBblX73.mjs +0 -485
  27. package/dist/commands-CZBYmj16.mjs +0 -485
  28. package/dist/commands-CcWIvCA4.mjs +0 -481
  29. package/dist/commands-Cfwf-cQG.mjs +0 -481
  30. package/dist/commands-DCNO2m66.mjs +0 -471
  31. package/dist/commands-DRIFvhmC.mjs +0 -481
  32. package/dist/commands-DXmw2dzy.mjs +0 -481
  33. package/dist/commands-DkSvlKFF.mjs +0 -485
  34. package/dist/commands-DnDd4Sew.mjs +0 -481
  35. package/dist/commands-DnpnAFQW.mjs +0 -485
  36. package/dist/commands-Do-TVYFm.mjs +0 -481
  37. package/dist/commands-Kzm0_XNH.mjs +0 -481
  38. package/dist/commands-MQvNbIid.mjs +0 -481
  39. package/dist/commands-_uCC3U1U.mjs +0 -481
  40. package/dist/commands-y2WG29W9.mjs +0 -485
  41. package/dist/hyphaClient-DLkclazm.mjs +0 -39
  42. package/dist/package-B2FOzHaM.mjs +0 -57
  43. package/dist/package-Bk_PFVA0.mjs +0 -57
  44. package/dist/package-Bnij-ZtR.mjs +0 -57
  45. package/dist/package-BtRbHfjz.mjs +0 -57
  46. package/dist/package-C5B0twb8.mjs +0 -57
  47. package/dist/package-CC5d8_0L.mjs +0 -57
  48. package/dist/package-CCJ045H0.mjs +0 -60
  49. package/dist/package-CS219SXn.mjs +0 -57
  50. package/dist/package-Cd-9ktpd.mjs +0 -60
  51. package/dist/package-CvnNnsm7.mjs +0 -60
  52. package/dist/package-DpqWz9Cr.mjs +0 -60
  53. package/dist/package-JqEt5Ib4.mjs +0 -57
  54. package/dist/package-nzkXV1aM.mjs +0 -57
  55. package/dist/package-pNo6GC3a.mjs +0 -60
  56. package/dist/package-pZp14zKI.mjs +0 -57
  57. package/dist/run-4fyJcaRE.mjs +0 -3856
  58. package/dist/run-BI32lPRK.mjs +0 -3870
  59. package/dist/run-BQHneHfW.mjs +0 -3834
  60. package/dist/run-Bb4fyIWZ.mjs +0 -3812
  61. package/dist/run-BglwnB-A.mjs +0 -3889
  62. package/dist/run-BjVWuitO.mjs +0 -3919
  63. package/dist/run-BzUE-JUT.mjs +0 -3708
  64. package/dist/run-BzqS97Sx.mjs +0 -3666
  65. package/dist/run-C6snRxyh.mjs +0 -3826
  66. package/dist/run-C8CI8Ujj.mjs +0 -3693
  67. package/dist/run-CS1Z4GcM.mjs +0 -3786
  68. package/dist/run-CW26vPqj.mjs +0 -3919
  69. package/dist/run-CkTufc0D.mjs +0 -3875
  70. package/dist/run-Cp3kKdzm.mjs +0 -3865
  71. package/dist/run-D0bCTY72.mjs +0 -3816
  72. package/dist/run-DMW8ibIw.mjs +0 -3958
  73. package/dist/run-DO52unxE.mjs +0 -3950
  74. package/dist/run-Dptna3Je.mjs +0 -3867
  75. package/dist/run-DwK3dfHd.mjs +0 -3875
  76. package/dist/run-M_SMt96j.mjs +0 -3913
  77. package/dist/run-MlpxQUPN.mjs +0 -3869
  78. package/dist/run-PuTIelbv.mjs +0 -3706
  79. package/dist/run-h37iSCUB.mjs +0 -3934
  80. 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
- machineId,
59
- metadata: currentMetadata,
60
- metadataVersion,
61
- daemonState: currentDaemonState,
62
- daemonStateVersion
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
- metadata: currentMetadata,
107
- version: metadataVersion
108
- }),
109
- updateMetadata: async (newMetadata, expectedVersion) => {
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
- daemonState: currentDaemonState,
133
- version: daemonStateVersion
134
- }),
135
- updateDaemonState: async (newState, expectedVersion) => {
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
- version: metadataVersion
400
- }),
401
- updateMetadata: async (newMetadata, expectedVersion) => {
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
- agentState,
425
- version: agentStateVersion
426
- }),
427
- updateAgentState: async (newState, expectedVersion) => {
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 = [this.options.command, ...args].join(" ");
1634
+ const fullCommand = [spawnCommand, ...spawnArgs].join(" ");
1355
1635
  this.process = spawn("cmd.exe", ["/c", fullCommand], {
1356
1636
  cwd: this.options.cwd,
1357
- env: { ...process.env, ...this.options.env },
1637
+ env: spawnEnv,
1358
1638
  stdio: ["pipe", "pipe", "pipe"],
1359
1639
  windowsHide: true
1360
1640
  });
1361
1641
  } else {
1362
- this.process = spawn(this.options.command, args, {
1642
+ this.process = spawn(spawnCommand, spawnArgs, {
1363
1643
  cwd: this.options.cwd,
1364
- env: { ...process.env, ...this.options.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 known = KNOWN_ACP_AGENTS[agentName];
1903
- if (known) {
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: known.command,
1908
- args: [...known.args, ...passthroughArgs]
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: "assistant",
1970
- content: [{ type: "text", text: `Error: ${msg.detail || "Unknown error"}` }]
1971
- }, "agent");
1972
- sessionService.sendKeepAlive(false);
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.sendKeepAlive(false);
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
- saveSessionIndex(index);
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 spawnAcpSession(sessionId, directory, agentName, options, resumeSessionId);
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 permissionMode = meta?.permissionMode || agentConfig.default_permission_mode || currentPermissionMode;
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
- logger.log(`[Session ${sessionId}] Spawning Claude: claude ${args.join(" ")} (cwd: ${directory})`);
2779
- const spawnEnv = { ...process.env };
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("claude", args, {
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: "assistant",
2793
- content: [{
2794
- type: "text",
2795
- text: `Error: Failed to start Claude Code CLI: ${err.message}
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 && msg.duration_api_ms === 0) {
3911
+ if (msg.is_error) {
2904
3912
  const resultText = msg.result || "";
2905
- logger.error(`[Session ${sessionId}] Claude startup failure (no API calls made): "${resultText}"`);
2906
- const isLoginIssue = resultText.toLowerCase().includes("login") || resultText.toLowerCase().includes("logged in") || resultText.toLowerCase().includes("auth") || resultText.toLowerCase().includes("api key") || resultText.toLowerCase().includes("unauthorized");
2907
- const hint = isLoginIssue ? "\n\nRun `claude login` in your terminal on the machine running the daemon to re-authenticate." : "\n\nCheck that the Claude Code CLI is properly installed and configured.";
2908
- const displayMsg = resultText || "Claude Code failed to start without making any API calls.";
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: `\u26A0\uFE0F **Claude Code error:** ${displayMsg}${hint}` }]
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: "assistant",
2993
- content: [{
2994
- type: "text",
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.sendKeepAlive(false);
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
- logger.log(`[Session ${sessionId}] Permission mode changed to: ${params.mode}`);
3086
- currentPermissionMode = params.mode;
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 spawnAcpSession = async (sessionId, directory, agentName, options, resumeSessionId) => {
3283
- logger.log(`[ACP] Spawning ${agentName} session: ${sessionId}`);
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(`[ACP Session ${sessionId}] User message received`);
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
- acpBackend.sendPrompt(sessionId, text).catch((err) => {
3359
- logger.error(`[ACP Session ${sessionId}] Error sending prompt:`, err);
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(`[ACP Session ${sessionId}] Abort requested`);
3364
- acpBackend.cancel(sessionId).catch(() => {
4381
+ logger.log(`[${agentName} Session ${sessionId}] Abort requested`);
4382
+ agentBackend.cancel(sessionId).catch(() => {
3365
4383
  });
3366
4384
  },
3367
4385
  onPermissionResponse: (params) => {
3368
- logger.log(`[ACP Session ${sessionId}] Permission response:`, JSON.stringify(params));
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(`[ACP Session ${sessionId}] Switch mode: ${mode}`);
4402
+ logger.log(`[${agentName} Session ${sessionId}] Switch mode: ${mode}`);
3384
4403
  currentPermissionMode = mode;
3385
4404
  },
3386
4405
  onRestartClaude: async () => {
3387
- logger.log(`[ACP Session ${sessionId}] Restart agent requested`);
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(`[ACP Session ${sessionId}] Kill session requested`);
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
- const acpConfig = KNOWN_ACP_AGENTS[agentName];
3496
- const acpBackend = new AcpBackend({
3497
- agentName,
3498
- cwd: directory,
3499
- command: acpConfig.command,
3500
- args: acpConfig.args,
3501
- env: options.environmentVariables,
3502
- permissionHandler,
3503
- transportHandler,
3504
- log: logger.log
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
- acpBackend,
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 acpBackend.getProcess() || void 0;
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(`[ACP Session ${sessionId}] Starting ${agentName} backend...`);
3531
- acpBackend.startSession().then(() => {
3532
- logger.log(`[ACP Session ${sessionId}] ${agentName} backend started, waiting for first message`);
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(`[ACP Session ${sessionId}] Failed to start ${agentName}:`, err);
3535
- sessionService.pushMessage({
3536
- type: "assistant",
3537
- content: [{
3538
- type: "text",
3539
- text: `Error: Failed to start ${agentName} agent: ${err.message}
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: `ACP session (${agentName}) registered as svamp-session-${sessionId}`
4588
+ message: `Agent session (${agentName}) registered as svamp-session-${sessionId}`
3550
4589
  };
3551
4590
  } catch (err) {
3552
- logger.error(`[ACP] Failed to spawn ${agentName} session:`, err);
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 = 10;
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
- try {
3800
- const index = loadSessionIndex();
3801
- for (const [sessionId, entry] of Object.entries(index)) {
3802
- const filePath = getSessionFilePath(entry.directory, sessionId);
3803
- if (existsSync$1(filePath)) {
3804
- const data = JSON.parse(readFileSync$1(filePath, "utf-8"));
3805
- writeFileSync$1(filePath, JSON.stringify({ ...data, stopped: true }, null, 2), "utf-8");
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
- } catch {
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, "SIGTERM");
3853
- console.log(`Sent SIGTERM to daemon PID ${state.pid}`);
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, acpBackend as c, daemonStatus as d, acpAgentConfig as e, registerMachineService as r, startDaemon as s };
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 };