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